[
  {
    "path": ".cursorrules",
    "content": "## 🧑‍💻 Development Guidelines\n\nThis project follows **Next.js (App Router)** and is structured using **Feature-Sliced Design (FSD)** for modularity, scalability, and clear\nseparation of concerns.\n\nUse this prompt and coding standards to ensure consistency across the codebase:\n\n---\n\n### 🔧 Code Style and Structure\n\n- Write concise, expressive, and idiomatic **TypeScript**\n- Use **functional programming** patterns (avoid classes and side effects)\n- Prefer **composition** over inheritance, and modularization over duplication\n- Organize each `feature/`, `entity/`, or `widget/` with:\n\n  - model/ → logic (React Query, actions, hooks)\n  - schema/ → Zod schemas for validation ui/ → client components (TSX)\n  - lib/ → pure helper functions\n  - types/ → interfaces & TS types\n\n- All external dependencies (**API**, `localStorage`, `Date`) must be **abstracted** in `shared/lib/`\n- Avoid direct calls to:\n- `fetch` → use actions or `shared/api/`\n- `new Date()` → use `shared/lib/date` abstraction\n- `localStorage` → wrap in `shared/lib/storage`\n\n---\n\n### 🧠 Naming Conventions\n\n- Use `kebab-case` for **directories** (e.g. `features/auth/signup`)\n- Use **named exports** (no default exports for components)\n- Use descriptive names with **auxiliary verbs** (e.g. `isLoading`, `hasError`, `canSubmit`)\n- Components:\n- Pure UI: `src/components/ui/`\n- Shared logic: `src/shared/lib/`\n- Composition: `src/widgets/`\n\n---\n\n### 📐 TypeScript Usage\n\n- Use `interface` over `type` for objects\n- Avoid `enum`; use `as const` object maps instead\n- Use `infer` and `z.infer<typeof schema>` for accurate form types\n- Types live in `types/` or colocated with usage\n\n---\n\n### 📦 Feature Architecture\n\n**Keep React component logic inside the relevant feature:**\n\nfeatures/auth/signup/ ├── model/ → useSignUp.ts, signup.action.ts ├── schema/ → signup.schema.ts ├── ui/ → signup-form.tsx\n\nIf reusable between many features (e.g. `User`, `Link`, `Session`), move logic to `entities/`.\n\n---\n\n### 🧪 Error Handling & Validation\n\n- Use **Zod** for schema validation\n- Prefer early returns & guard clauses\n- Use `ActionError` in server actions and handle them with `next-safe-action`\n- Wrap React components in `ErrorBoundary` (or `shared/ui/ErrorBoundaries.tsx`)\n- Display user-friendly errors via `toast()` or `<Alert />`\n\n---\n\n### 💅 UI & Styling\n\n- Use **Shadcn UI**, **Radix**, and **Tailwind CSS** with **mobile-first** responsive design\n- Design theme:\n\n  - **Minimal**, professional with a **slightly playful touch**\n  - Inspired by **Apple**, tailored to fitness coaches\n  - Emphasize visuals: badges, progress bars, illustrations\n  - Use `lucide-react` icons, subtle borders, hover feedback\n  - Avoid drop shadows; prefer light borders and soft hover effects\n\n- Animations:\n\n  - Elegant and performant (use `framer-motion` if needed)\n  - Use `transition`, `duration-xxx`, and `ease-xxx` from Tailwind\n\n- UX Principles:\n\n  - Clear hierarchy\n  - Responsive: no overflow, no overlap\n  - All buttons and interactive elements should provide feedback\n  - Use @tailwind.config.ts for the theme.\n\n- **UI Stack**:\n\n  - **Shadcn UI**, **Radix UI**, and **Tailwind CSS** (mobile-first approach)\n  - Icons: **lucide-react**\n\n- **Design Language**:\n\n  - 🎨 **Modern & minimalist**, inspired by **Apple’s design system**, with a **slightly more colorful palette**\n  - Interface should be **clean**, **cohesive**, and **functional** without sacrificing features\n  - Avoid drop shadows; prefer **subtle borders** where relevant\n  - Ensure a **clear visual hierarchy** and **intuitive navigation**\n\n- **Interactive Components**:\n\n  - Buttons and inputs must be **elegant**, with **subtle visual feedback** (hover, click, validation)\n  - Use **addictive micro-interactions** sparingly to enhance engagement without clutter\n\n- **Animations**:\n\n  - Use Tailwind’s built-in utilities: `transition`, `duration-xxx`, `ease-xxx` for basic transitions\n  - Use `framer-motion` for advanced animations only if necessary\n  - ✅ **Performance comes first**: animations must be smooth and lightweight\n\n- **Responsiveness**:\n\n  - Fully responsive layout: **no overlapping**, **no overflow**\n  - Consistent behavior across all devices, from mobile to desktop\n\n- **User Experience**:\n  - All interactive elements must provide **clear visual feedback**\n  - Interfaces should remain **simple to navigate**, even when **feature-rich**\n\n---\n\n### 🧱 Rendering & Performance\n\n- Favor **Server Components** (`RSC`) and SSR for pages and logic\n- Limit `'use client'` usage — only where needed:\n  - form states, event listeners, animations\n- Wrap all client components in `<Suspense />` with fallback\n- Use dynamic import for non-critical UI (e.g. `Dialog`, `Chart`)\n- Optimize media:\n  - Use **WebP** images with width/height\n  - Enable lazy loading where possible\n\n---\n\n### 🔍 Data, Forms, Actions\n\n- Use `@tanstack/react-query` for client state\n- Use `next-safe-action` for server mutations and queries\n- All actions should:\n  - Have clear schema (`schema/`)\n  - Model expected errors with `ActionError`\n  - Return typed output\n  - Use the clientAction from `@/shared/api/safe-actions`\n- Use `Form`, `FormField`, `FormMessage` from Shadcn for all forms\n\n---\n\n### 🧭 Routing & Navigation\n\n- All routes defined in `app/`, avoid logic here\n- Use constants in `shared/constants/paths.ts`\n- For search parameters, use `nuqs` (`useQueryState`) — never manipulate `router.query` directly\n- Follow Next.js App Router standards for layouts and segments\n\n---\n\n- [Feature-Sliced Design](https://feature-sliced.design/)\n- [Shadcn UI](https://ui.shadcn.com/)\n- [Zod](https://zod.dev/)\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: snouzy\nko_fi: workoutcool\n# buy_me_a_coffee: workout_cool\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Describe the bug** A clear and concise description of what the bug is.\n\n**To Reproduce** Steps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior** A clear and concise description of what you expected to happen.\n\n**Screenshots** If applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n\n- OS: [e.g. iOS]\n- Browser [e.g. chrome, safari]\n- Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n\n- Device: [e.g. iPhone6]\n- OS: [e.g. iOS8.1]\n- Browser [e.g. stock browser, safari]\n- Version [e.g. 22]\n\n**Additional context** Add any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 💬 Workout Cool Discord\n    url: https://discord.gg/NtrsUBuHUB\n    about: Please use our Discord server for all questions, discussions, and support."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest a feature for this project\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always\nfrustrated when [...]\n\n**Describe the solution you'd like** A clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context** Add any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## 📝 Description\n\n<!-- Briefly describe the changes made -->\n\n## 📋 Checklist\n\n- [ ] My code follows the project conventions\n- [ ] This PR includes breaking changes\n- [ ] I have updated documentation if necessary\n\n## 🗃️ Prisma Migrations (if applicable)\n\n- [ ] I have created a migration\n- [ ] I have tested the migration locally\n\n## 📸 Screenshots (if applicable)\n\n<!-- Add screenshots for visual changes -->\n\n## 🔗 Related Issues\n\n<!-- Reference issues: Closes #123, Fixes #456 -->\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: ci\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: \"20\"\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Get pnpm store directory\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n      - name: Setup pnpm cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ env.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Generate Prisma client\n        run: pnpm prisma generate\n\n      - name: Run linting\n        run: pnpm lint\n\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: \"20\"\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Get pnpm store directory\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n      - name: Setup pnpm cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ env.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Generate Prisma client\n        run: pnpm prisma generate\n\n      - name: Build project\n        run: pnpm build\n        env:\n          BETTER_AUTH_URL: http://localhost:3000\n          DATABASE_URL: postgresql://user:password@localhost:5432/test_db\n          GOOGLE_CLIENT_ID: test_client_id\n          GOOGLE_CLIENT_SECRET: test_client_secret\n          RESEND_API_KEY: re_test_key\n          BETTER_AUTH_SECRET: test_secret_key_32_chars_minimum\n          OPENPANEL_SECRET_KEY: test_secret\n          NEXT_PUBLIC_OPENPANEL_CLIENT_ID: test_client_id\n          NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: test_client_id\n          NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_EU: test_price_monthly\n          NEXT_PUBLIC_STRIPE_PRICE_YEARLY_EU: test_price_yearly\n          NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_US: test_price_monthly\n          NEXT_PUBLIC_STRIPE_PRICE_YEARLY_US: test_price_yearly\n          NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_LATAM: test_price_monthly\n          NEXT_PUBLIC_STRIPE_PRICE_YEARLY_LATAM: test_price_yearly\n          NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_BR: test_price_monthly\n          NEXT_PUBLIC_STRIPE_PRICE_YEARLY_BR: test_price_yearly\n          NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_RU: test_price_monthly\n          NEXT_PUBLIC_STRIPE_PRICE_YEARLY_RU: test_price_yearly\n          NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_CN: test_price_monthly\n          NEXT_PUBLIC_STRIPE_PRICE_YEARLY_CN: test_price_yearly\n          NEXT_PUBLIC_APP_URL: http://localhost:3000\n          STRIPE_SECRET_KEY: test_secret_key\n          REVENUECAT_SECRET_KEY: test_secret_key\n          REVENUECAT_WEBHOOK_SECRET: test_webhook_secret\n          STRIPE_WEBHOOK_SECRET: test_webhook_secret\n          NEXT_PUBLIC_SHOW_ADS: false\n          NEXT_PUBLIC_AD_CLIENT: test_client_id\n          NEXT_PUBLIC_VERTICAL_LEFT_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_VERTICAL_RIGHT_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_EQUIPMENT_SELECTION_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_EXERCISE_SELECTION_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_MUSCLE_SELECTION_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_WORKOUT_SESSION_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_BOTTOM_WORKOUT_SESSION_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_STEPPER_STEP_1_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_STEPPER_STEP_2_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_STEPPER_STEP_3_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_PROGRAMS_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_BOTTOM_PROGRAMS_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_TOOLS_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_BOTTOM_TOOLS_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_CALCULATOR_HUB_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_BOTTOM_CALCULATOR_HUB_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_PROGRAM_DETAILS_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_BOTTOM_PROGRAM_DETAILS_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_PROFILE_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_IN_ARTICLE_BMI_1_AD_SLOT: 1234567890\n          NEXT_PUBLIC_IN_ARTICLE_BMI_2_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_BMI_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_BOTTOM_BMI_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_HEART_ZONES_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_BOTTOM_HEART_ZONES_BANNER_AD_SLOT: 1234567890\n          NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_1: 1234567890\n          NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_2: 1234567890\n          NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_3: 1234567890\n          NEXT_PUBLIC_TOP_MIFFLIN_ST_JEOR_CALCULATOR_AD_SLOT: 1234567890\n          NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_OXFORD_CALCULATOR_AD_SLOT: 1234567890\n          NEXT_PUBLIC_BOTTOM_OXFORD_CALCULATOR_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_HARRIS_BENEDICT_CALCULATOR_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_KATCH_MCARDLE_CALCULATOR_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_CUNNINGHAM_CALCULATOR_AD_SLOT: 1234567890\n          NEXT_PUBLIC_TOP_CALORIE_CALCULATOR_COMPARISON_AD_SLOT: 1234567890\n          NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_COMPARISON_AD_SLOT: 1234567890\n"
  },
  {
    "path": ".github/workflows/notify-discord-issues.yml",
    "content": "name: Discord Issue Notification\n\non:\n  issues:\n    types: [opened, reopened, closed]\n  workflow_dispatch:\n    inputs:\n      issue_number:\n        description: \"Issue number\"\n        required: true\n        type: string\n\njobs:\n  Discord:\n    runs-on: ubuntu-latest\n    name: Discord Issue Notifier\n    steps:\n      - uses: actions/checkout@v4\n        if: github.event_name == 'workflow_dispatch'\n\n      - name: Get issue info for manual trigger\n        id: issue-info\n        if: github.event_name == 'workflow_dispatch'\n        run: |\n          ISSUE_INFO=$(gh issue view ${{ github.event.inputs.issue_number }} --json number,title,url,author,state,labels,createdAt)\n          echo \"number=$(echo \"$ISSUE_INFO\" | jq -r '.number')\" >> $GITHUB_OUTPUT\n          echo \"title=$(echo \"$ISSUE_INFO\" | jq -r '.title')\" >> $GITHUB_OUTPUT\n          echo \"html_url=$(echo \"$ISSUE_INFO\" | jq -r '.url')\" >> $GITHUB_OUTPUT\n          echo \"author_login=$(echo \"$ISSUE_INFO\" | jq -r '.author.login')\" >> $GITHUB_OUTPUT\n          echo \"author_html_url=https://github.com/$(echo \"$ISSUE_INFO\" | jq -r '.author.login')\" >> $GITHUB_OUTPUT\n          echo \"state=$(echo \"$ISSUE_INFO\" | jq -r '.state')\" >> $GITHUB_OUTPUT\n          echo \"created_at=$(echo \"$ISSUE_INFO\" | jq -r '.createdAt')\" >> $GITHUB_OUTPUT\n          echo \"labels=$(echo \"$ISSUE_INFO\" | jq -r '.labels | map(.name) | join(\", \") // \"None\"')\" >> $GITHUB_OUTPUT\n        env:\n          GH_TOKEN: ${{ github.token }}\n\n      - name: Determine action color and emoji\n        id: action-info\n        run: |\n          if [ \"${{ github.event_name }}\" = \"workflow_dispatch\" ]; then\n            # For manual trigger, use the current state\n            case \"${{ steps.issue-info.outputs.state }}\" in\n              \"OPEN\")\n                echo \"color=15158332\" >> $GITHUB_OUTPUT  # Red\n                echo \"emoji=🔴\" >> $GITHUB_OUTPUT\n                echo \"action_text=Open\" >> $GITHUB_OUTPUT\n                ;;\n              \"CLOSED\")\n                echo \"color=5763719\" >> $GITHUB_OUTPUT   # Green\n                echo \"emoji=🟢\" >> $GITHUB_OUTPUT\n                echo \"action_text=Closed\" >> $GITHUB_OUTPUT\n                ;;\n            esac\n          else\n            # For automatic trigger, use the action\n            case \"${{ github.event.action }}\" in\n              \"opened\")\n                echo \"color=15158332\" >> $GITHUB_OUTPUT  # Red\n                echo \"emoji=🔴\" >> $GITHUB_OUTPUT\n                echo \"action_text=Opened\" >> $GITHUB_OUTPUT\n                ;;\n              \"reopened\")\n                echo \"color=16776960\" >> $GITHUB_OUTPUT  # Yellow\n                echo \"emoji=🟡\" >> $GITHUB_OUTPUT\n                echo \"action_text=Reopened\" >> $GITHUB_OUTPUT\n                ;;\n              \"closed\")\n                echo \"color=5763719\" >> $GITHUB_OUTPUT   # Green\n                echo \"emoji=🟢\" >> $GITHUB_OUTPUT\n                echo \"action_text=Closed\" >> $GITHUB_OUTPUT\n                ;;\n            esac\n          fi\n\n      - name: Create Discord webhook payload\n        run: |\n          # Determine data source based on trigger type\n          if [ \"${{ github.event_name }}\" = \"workflow_dispatch\" ]; then\n            ISSUE_NUMBER=\"${{ steps.issue-info.outputs.number }}\"\n            ISSUE_TITLE=\"${{ steps.issue-info.outputs.title }}\"\n            ISSUE_URL=\"${{ steps.issue-info.outputs.html_url }}\"\n            AUTHOR_LOGIN=\"${{ steps.issue-info.outputs.author_login }}\"\n            AUTHOR_URL=\"${{ steps.issue-info.outputs.author_html_url }}\"\n            ISSUE_STATE=\"${{ steps.issue-info.outputs.state }}\"\n            ISSUE_LABELS=\"${{ steps.issue-info.outputs.labels }}\"\n            CREATED_AT=\"${{ steps.issue-info.outputs.created_at }}\"\n          else\n            ISSUE_NUMBER=\"${{ github.event.issue.number }}\"\n            ISSUE_TITLE=\"${{ github.event.issue.title }}\"\n            ISSUE_URL=\"${{ github.event.issue.html_url }}\"\n            AUTHOR_LOGIN=\"${{ github.event.issue.user.login }}\"\n            AUTHOR_URL=\"${{ github.event.issue.user.html_url }}\"\n            ISSUE_STATE=\"${{ github.event.issue.state }}\"\n            ISSUE_LABELS=\"${{ github.event.issue.labels[0].name && join(github.event.issue.labels.*.name, ', ') || 'None' }}\"\n            CREATED_AT=\"${{ github.event.issue.created_at }}\"\n          fi\n\n          # Create a temporary JSON file\n          cat > discord_payload.json << EOF\n          {\n            \"avatar_url\": \"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png\",\n            \"embeds\": [\n              {\n                \"title\": \"${{ steps.action-info.outputs.emoji }} Issue ${{ steps.action-info.outputs.action_text }}: #${ISSUE_NUMBER}\",\n                \"description\": \"${ISSUE_TITLE}\",\n                \"url\": \"${ISSUE_URL}\",\n                \"color\": ${{ steps.action-info.outputs.color }},\n                \"thumbnail\": {\n                  \"url\": \"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png\"\n                },\n                \"fields\": [\n                  {\n                    \"name\": \"📋 Issue #\",\n                    \"value\": \"\\`#${ISSUE_NUMBER}\\`\",\n                    \"inline\": true\n                  },\n                  {\n                    \"name\": \"👤 Author\",\n                    \"value\": \"[${AUTHOR_LOGIN}](${AUTHOR_URL})\",\n                    \"inline\": true\n                  },\n                  {\n                    \"name\": \"📁 Repository\",\n                    \"value\": \"[${{ github.event.repository.name }}](${{ github.event.repository.html_url }})\",\n                    \"inline\": true\n                  },\n                  {\n                    \"name\": \"🏷️ Labels\",\n                    \"value\": \"${ISSUE_LABELS}\",\n                    \"inline\": true\n                  },\n                  {\n                    \"name\": \"📊 State\",\n                    \"value\": \"\\`${ISSUE_STATE}\\`\",\n                    \"inline\": true\n                  },\n                  {\n                    \"name\": \"🔗 View Issue\",\n                    \"value\": \"[Issue Page](${ISSUE_URL})\",\n                    \"inline\": true\n                  }\n                ],\n                \"timestamp\": \"${CREATED_AT}\",\n                \"footer\": {\n                  \"text\": \"Workout Cool • Issue ${{ steps.action-info.outputs.action_text }}\",\n                  \"icon_url\": \"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png\"\n                }\n              }\n            ]\n          }\n          EOF\n\n      - name: Send Discord notification\n        run: |\n          curl -H \"Content-Type: application/json\" \\\n               -d @discord_payload.json \\\n               \"${{ secrets.DISCORD_ISSUES_WEBHOOK }}\"\n"
  },
  {
    "path": ".github/workflows/notify-discord-pr.yml",
    "content": "name: Discord PR Notification\n\non:\n  pull_request:\n    types: [opened]\n  workflow_dispatch:\n    inputs:\n      pr_number:\n        description: \"Pull Request number\"\n        required: true\n        type: string\n\njobs:\n  Discord:\n    runs-on: ubuntu-latest\n    name: Discord PR Notifier\n    if: github.event.pull_request.head.repo.full_name == github.repository\n    steps:\n      - uses: actions/checkout@v4\n        if: github.event_name == 'workflow_dispatch'\n\n      - name: Get PR info for manual trigger\n        id: pr-info\n        if: github.event_name == 'workflow_dispatch'\n        run: |\n          PR_INFO=$(gh pr view ${{ github.event.inputs.pr_number }} --json number,title,url,author,state,labels,createdAt,headRefName,baseRefName,isDraft,mergeable)\n          echo \"number=$(echo \"$PR_INFO\" | jq -r '.number')\" >> $GITHUB_OUTPUT\n          echo \"title=$(echo \"$PR_INFO\" | jq -r '.title')\" >> $GITHUB_OUTPUT\n          echo \"html_url=$(echo \"$PR_INFO\" | jq -r '.url')\" >> $GITHUB_OUTPUT\n          echo \"author_login=$(echo \"$PR_INFO\" | jq -r '.author.login')\" >> $GITHUB_OUTPUT\n          echo \"author_html_url=https://github.com/$(echo \"$PR_INFO\" | jq -r '.author.login')\" >> $GITHUB_OUTPUT\n          echo \"state=$(echo \"$PR_INFO\" | jq -r '.state')\" >> $GITHUB_OUTPUT\n          echo \"created_at=$(echo \"$PR_INFO\" | jq -r '.createdAt')\" >> $GITHUB_OUTPUT\n          echo \"labels=$(echo \"$PR_INFO\" | jq -r '.labels | map(.name) | join(\", \") // \"None\"')\" >> $GITHUB_OUTPUT\n          echo \"head_ref=$(echo \"$PR_INFO\" | jq -r '.headRefName')\" >> $GITHUB_OUTPUT\n          echo \"base_ref=$(echo \"$PR_INFO\" | jq -r '.baseRefName')\" >> $GITHUB_OUTPUT\n          echo \"is_draft=$(echo \"$PR_INFO\" | jq -r '.isDraft')\" >> $GITHUB_OUTPUT\n          echo \"mergeable=$(echo \"$PR_INFO\" | jq -r '.mergeable // \"UNKNOWN\"')\" >> $GITHUB_OUTPUT\n        env:\n          GH_TOKEN: ${{ github.token }}\n\n      - name: Determine action color and emoji\n        id: action-info\n        run: |\n          if [ \"${{ github.event_name }}\" = \"workflow_dispatch\" ]; then\n            # For manual trigger, use the current state\n            case \"${{ steps.pr-info.outputs.state }}\" in\n              \"OPEN\")\n                if [ \"${{ steps.pr-info.outputs.is_draft }}\" = \"true\" ]; then\n                  echo \"color=8421504\" >> $GITHUB_OUTPUT   # Gray\n                  echo \"emoji=📝\" >> $GITHUB_OUTPUT\n                  echo \"action_text=Draft\" >> $GITHUB_OUTPUT\n                else\n                  echo \"color=5763719\" >> $GITHUB_OUTPUT   # Green\n                  echo \"emoji=🔄\" >> $GITHUB_OUTPUT\n                  echo \"action_text=Open\" >> $GITHUB_OUTPUT\n                fi\n                ;;\n              \"CLOSED\")\n                echo \"color=15158332\" >> $GITHUB_OUTPUT    # Red\n                echo \"emoji=❌\" >> $GITHUB_OUTPUT\n                echo \"action_text=Closed\" >> $GITHUB_OUTPUT\n                ;;\n              \"MERGED\")\n                echo \"color=6559689\" >> $GITHUB_OUTPUT     # Purple\n                echo \"emoji=🎉\" >> $GITHUB_OUTPUT\n                echo \"action_text=Merged\" >> $GITHUB_OUTPUT\n                ;;\n            esac\n          else\n            # For automatic trigger, use the action\n            case \"${{ github.event.action }}\" in\n              \"opened\")\n                echo \"color=5763719\" >> $GITHUB_OUTPUT     # Green\n                echo \"emoji=🔄\" >> $GITHUB_OUTPUT\n                echo \"action_text=Opened\" >> $GITHUB_OUTPUT\n                ;;\n              \"reopened\")\n                echo \"color=16776960\" >> $GITHUB_OUTPUT    # Yellow\n                echo \"emoji=🔄\" >> $GITHUB_OUTPUT\n                echo \"action_text=Reopened\" >> $GITHUB_OUTPUT\n                ;;\n              \"closed\")\n                if [ \"${{ github.event.pull_request.merged }}\" = \"true\" ]; then\n                  echo \"color=6559689\" >> $GITHUB_OUTPUT   # Purple\n                  echo \"emoji=🎉\" >> $GITHUB_OUTPUT\n                  echo \"action_text=Merged\" >> $GITHUB_OUTPUT\n                else\n                  echo \"color=15158332\" >> $GITHUB_OUTPUT  # Red\n                  echo \"emoji=❌\" >> $GITHUB_OUTPUT\n                  echo \"action_text=Closed\" >> $GITHUB_OUTPUT\n                fi\n                ;;\n              \"ready_for_review\")\n                echo \"color=5763719\" >> $GITHUB_OUTPUT     # Green\n                echo \"emoji=👀\" >> $GITHUB_OUTPUT\n                echo \"action_text=Ready for Review\" >> $GITHUB_OUTPUT\n                ;;\n              \"converted_to_draft\")\n                echo \"color=8421504\" >> $GITHUB_OUTPUT     # Gray\n                echo \"emoji=📝\" >> $GITHUB_OUTPUT\n                echo \"action_text=Converted to Draft\" >> $GITHUB_OUTPUT\n                ;;\n            esac\n          fi\n\n      - name: Create Discord webhook payload\n        run: |\n          # Determine data source based on trigger type\n          if [ \"${{ github.event_name }}\" = \"workflow_dispatch\" ]; then\n            PR_NUMBER=\"${{ steps.pr-info.outputs.number }}\"\n            PR_TITLE=\"${{ steps.pr-info.outputs.title }}\"\n            PR_URL=\"${{ steps.pr-info.outputs.html_url }}\"\n            AUTHOR_LOGIN=\"${{ steps.pr-info.outputs.author_login }}\"\n            AUTHOR_URL=\"${{ steps.pr-info.outputs.author_html_url }}\"\n            PR_STATE=\"${{ steps.pr-info.outputs.state }}\"\n            PR_LABELS=\"${{ steps.pr-info.outputs.labels }}\"\n            CREATED_AT=\"${{ steps.pr-info.outputs.created_at }}\"\n            HEAD_REF=\"${{ steps.pr-info.outputs.head_ref }}\"\n            BASE_REF=\"${{ steps.pr-info.outputs.base_ref }}\"\n            IS_DRAFT=\"${{ steps.pr-info.outputs.is_draft }}\"\n          else\n            PR_NUMBER=\"${{ github.event.pull_request.number }}\"\n            PR_TITLE=\"${{ github.event.pull_request.title }}\"\n            PR_URL=\"${{ github.event.pull_request.html_url }}\"\n            AUTHOR_LOGIN=\"${{ github.event.pull_request.user.login }}\"\n            AUTHOR_URL=\"${{ github.event.pull_request.user.html_url }}\"\n            PR_STATE=\"${{ github.event.pull_request.state }}\"\n            PR_LABELS=\"${{ github.event.pull_request.labels[0].name && join(github.event.pull_request.labels.*.name, ', ') || 'None' }}\"\n            CREATED_AT=\"${{ github.event.pull_request.created_at }}\"\n            HEAD_REF=\"${{ github.event.pull_request.head.ref }}\"\n            BASE_REF=\"${{ github.event.pull_request.base.ref }}\"\n            IS_DRAFT=\"${{ github.event.pull_request.draft }}\"\n          fi\n\n          # Escape special characters for JSON\n          PR_TITLE_ESCAPED=$(echo \"$PR_TITLE\" | sed 's/\"/\\\\\"/g' | sed \"s/'/\\\\'/g\")\n\n          # Create a temporary JSON file using jq to ensure valid JSON\n          jq -n \\\n            --arg avatar_url \"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png\" \\\n            --arg title \"${{ steps.action-info.outputs.emoji }} Pull Request ${{ steps.action-info.outputs.action_text }}: #${PR_NUMBER}\" \\\n            --arg description \"$PR_TITLE_ESCAPED\" \\\n            --arg url \"$PR_URL\" \\\n            --argjson color ${{ steps.action-info.outputs.color }} \\\n            --arg thumbnail_url \"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png\" \\\n            --arg pr_number \"#${PR_NUMBER}\" \\\n            --arg author_name \"$AUTHOR_LOGIN\" \\\n            --arg author_url \"$AUTHOR_URL\" \\\n            --arg repo_name \"${{ github.event.repository.name }}\" \\\n            --arg repo_url \"${{ github.event.repository.html_url }}\" \\\n            --arg branch \"${HEAD_REF} → ${BASE_REF}\" \\\n            --arg labels \"$PR_LABELS\" \\\n            --arg status \"$PR_STATE\" \\\n            --arg pr_url \"$PR_URL\" \\\n            --arg timestamp \"$CREATED_AT\" \\\n            --arg footer_text \"Workout Cool • PR ${{ steps.action-info.outputs.action_text }}\" \\\n            --arg footer_icon \"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png\" \\\n            '{\n              \"avatar_url\": $avatar_url,\n              \"embeds\": [\n                {\n                  \"title\": $title,\n                  \"description\": $description,\n                  \"url\": $url,\n                  \"color\": $color,\n                  \"thumbnail\": {\n                    \"url\": $thumbnail_url\n                  },\n                  \"fields\": [\n                    {\n                      \"name\": \"📋 PR #\",\n                      \"value\": $pr_number,\n                      \"inline\": true\n                    },\n                    {\n                      \"name\": \"👤 Author\",\n                      \"value\": (\"[\\($author_name)](\\($author_url))\"),\n                      \"inline\": true\n                    },\n                    {\n                      \"name\": \"📁 Repository\",\n                      \"value\": (\"[\\($repo_name)](\\($repo_url))\"),\n                      \"inline\": true\n                    },\n                    {\n                      \"name\": \"🌿 Branch\",\n                      \"value\": $branch,\n                      \"inline\": true\n                    },\n                    {\n                      \"name\": \"🏷️ Labels\",\n                      \"value\": $labels,\n                      \"inline\": true\n                    },\n                    {\n                      \"name\": \"📊 Status\",\n                      \"value\": $status,\n                      \"inline\": true\n                    },\n                    {\n                      \"name\": \"🔗 View PR\",\n                      \"value\": (\"[Pull Request](\\($pr_url))\"),\n                      \"inline\": true\n                    }\n                  ],\n                  \"timestamp\": $timestamp,\n                  \"footer\": {\n                    \"text\": $footer_text,\n                    \"icon_url\": $footer_icon\n                  }\n                }\n              ]\n            }' > discord_payload.json\n\n      - name: Send Discord notification\n        run: |\n          curl -H \"Content-Type: application/json\" \\\n               -d @discord_payload.json \\\n               \"${{ secrets.DISCORD_PR_WEBHOOK }}\"\n"
  },
  {
    "path": ".github/workflows/notify-discord.yml",
    "content": "name: Discord Notification\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n    inputs:\n      tag_name:\n        description: \"Tag name (leave empty for latest release)\"\n        required: false\n        type: string\n      custom_title:\n        description: \"Custom title for the release notification\"\n        required: false\n        type: string\n\njobs:\n  Discord:\n    runs-on: ubuntu-latest\n    name: Discord Notifier\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Get release info\n        id: release-info\n        run: |\n          if [ \"${{ github.event_name }}\" = \"workflow_dispatch\" ]; then\n            # For manual trigger, get the latest release or use the input\n            if [ -n \"${{ github.event.inputs.tag_name }}\" ]; then\n              TAG_NAME=\"${{ github.event.inputs.tag_name }}\"\n            else\n              TAG_NAME=$(gh release list --limit 1 --json tagName --jq '.[0].tagName')\n            fi\n            \n            # Get release info via GitHub CLI\n            RELEASE_INFO=$(gh release view \"$TAG_NAME\" --json name,body,url,author,publishedAt,tagName)\n            \n            echo \"tag_name=$(echo \"$RELEASE_INFO\" | jq -r '.tagName')\" >> $GITHUB_OUTPUT\n            echo \"name=$(echo \"$RELEASE_INFO\" | jq -r '.name')\" >> $GITHUB_OUTPUT\n            \n            # Use EOF for the body which may contain special characters\n            echo \"body<<EOF\" >> $GITHUB_OUTPUT\n            echo \"$RELEASE_INFO\" | jq -r '.body' >> $GITHUB_OUTPUT\n            echo \"EOF\" >> $GITHUB_OUTPUT\n            \n            echo \"html_url=$(echo \"$RELEASE_INFO\" | jq -r '.url')\" >> $GITHUB_OUTPUT\n            echo \"author_login=$(echo \"$RELEASE_INFO\" | jq -r '.author.login')\" >> $GITHUB_OUTPUT\n            echo \"author_html_url=https://github.com/$(echo \"$RELEASE_INFO\" | jq -r '.author.login')\" >> $GITHUB_OUTPUT\n            echo \"published_at=$(echo \"$RELEASE_INFO\" | jq -r '.publishedAt')\" >> $GITHUB_OUTPUT\n          else\n            # For automatic trigger, use event data\n            echo \"tag_name=${{ github.event.release.tag_name }}\" >> $GITHUB_OUTPUT\n            echo \"name=${{ github.event.release.name }}\" >> $GITHUB_OUTPUT\n            \n            # Use EOF for the automatic release body as well\n            echo \"body<<EOF\" >> $GITHUB_OUTPUT\n            echo \"${{ github.event.release.body }}\" >> $GITHUB_OUTPUT\n            echo \"EOF\" >> $GITHUB_OUTPUT\n            \n            echo \"html_url=${{ github.event.release.html_url }}\" >> $GITHUB_OUTPUT\n            echo \"author_login=${{ github.event.release.author.login }}\" >> $GITHUB_OUTPUT\n            echo \"author_html_url=${{ github.event.release.author.html_url }}\" >> $GITHUB_OUTPUT\n            echo \"published_at=${{ github.event.release.published_at }}\" >> $GITHUB_OUTPUT\n          fi\n        env:\n          GH_TOKEN: ${{ github.token }}\n\n      - name: Get previous release\n        id: previous-release\n        run: |\n          PREV_TAG=$(git tag --sort=-version:refname | grep -v '${{ steps.release-info.outputs.tag_name }}' | head -n1)\n          echo \"previous_tag=${PREV_TAG}\" >> $GITHUB_OUTPUT\n\n      - name: Get changed files since last release\n        id: changed-files\n        uses: tj-actions/changed-files@v44\n        with:\n          base_sha: ${{ steps.previous-release.outputs.previous_tag }}\n          separator: \"\\n• \"\n\n      - name: Create Discord webhook payload\n        run: |\n          # Create a temporary JSON file\n          cat > discord_payload.json << 'EOF'\n          {\n            \"avatar_url\": \"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png\",\n            \"embeds\": [\n              {\n                \"title\": \"🚀 New Release: ${{ steps.release-info.outputs.tag_name }}${{ github.event.inputs.custom_title && ' - ' || '' }}${{ github.event.inputs.custom_title || '' }}\",\n                \"description\": \"${{ steps.release-info.outputs.name }}\",\n                \"url\": \"${{ steps.release-info.outputs.html_url }}\",\n                \"color\": 5763719,\n                \"thumbnail\": {\n                  \"url\": \"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png\"\n                },\n                \"fields\": [\n                  {\n                    \"name\": \"📦 Version\",\n                    \"value\": \"`${{ steps.release-info.outputs.tag_name }}`\",\n                    \"inline\": true\n                  },\n                  {\n                    \"name\": \"👤 Released by\",\n                    \"value\": \"[${{ steps.release-info.outputs.author_login }}](${{ steps.release-info.outputs.author_html_url }})\",\n                    \"inline\": true\n                  },\n                  {\n                    \"name\": \"📁 Repository\",\n                    \"value\": \"[${{ github.event.repository.name }}](${{ github.event.repository.html_url }})\",\n                    \"inline\": true\n                  },\n                  {\n                    \"name\": \"🔗 Download\",\n                    \"value\": \"[Release Page](${{ steps.release-info.outputs.html_url }})\",\n                    \"inline\": true\n                  }\n                ],\n                \"timestamp\": \"${{ steps.release-info.outputs.published_at }}\",\n                \"footer\": {\n                  \"text\": \"Workout Cool • Release\",\n                  \"icon_url\": \"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png\"\n                }\n              }\n            ]\n          }\n          EOF\n\n      - name: Send Discord notification\n        run: |\n          curl -H \"Content-Type: application/json\" \\\n               -d @discord_payload.json \\\n               \"${{ secrets.DISCORD_RELEASE_WEBHOOK }}\"\n\n# https://stackoverflow.com/a/68068674/19395252\n# https://birdie0.github.io/discord-webhooks-guide/structure/embeds.html\n# https://github.com/marketplace/actions/changed-files\n"
  },
  {
    "path": ".github/workflows/publish-ghcr-image.yml",
    "content": "name: Publish Docker GHCR Image\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n    inputs:\n      version:\n        description: \"Version tag for the Docker image\"\n        required: true\n        default: \"1.2.5\"\n        type: string\npermissions:\n  contents: read\n  packages: write\n\njobs:\n  build_and_publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Log in to GHCR\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Set image tag\n        id: vars\n        run: |\n          if [ \"${{ github.event_name }}\" = \"release\" ]; then\n            # Get the release tag name\n            RELEASE_TAG=\"${{ github.event.release.tag_name }}\"\n            # Remove 'v' prefix if present for Docker tag\n            tag=\"${RELEASE_TAG#v}\"\n            echo \"tag=$tag\" >> $GITHUB_OUTPUT\n          else\n            echo \"tag=${{ github.event.inputs.version }}\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: ./Dockerfile\n          push: true\n          tags: |\n            ghcr.io/snouzy/workout-cool:${{ steps.vars.outputs.tag }}\n            ghcr.io/snouzy/workout-cool:latest\n          platforms: linux/amd64,linux/arm64\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/_next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env\n.env.local\n.env.test\n.env.development\n.env.production\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n\n# editors\n.idea\n.zed\nscripts/private/\nscripts/personal/\nproduct-development/\n.claude/\n.cache/\n"
  },
  {
    "path": ".npmrc",
    "content": "public-hoist-pattern[]=*import-in-the-middle*\npublic-hoist-pattern[]=*require-in-the-middle*"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"plugins\": [\"prettier-plugin-sort-json\"],\n  \"printWidth\": 140,\n  \"proseWrap\": \"always\",\n  \"singleQuote\": false\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  },\n  \"typescript.tsdk\": \"node_modules/typescript/lib\"\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md - Development Guide for AI Coding Agents\n\n## Build/Test Commands\n\n- `pnpm dev` - Start development server with Turbopack\n- `pnpm build` - Build production bundle\n- `pnpm lint` - Run ESLint (no test framework detected)\n- `pnpm db:seed` - Seed database with sample data\n- `tsx scripts/[script-name].ts` - Run TypeScript scripts directly\n\n## Architecture & Structure\n\n- **Next.js 15** with App Router, **Feature-Sliced Design (FSD)**\n- Structure: `features/[feature]/[model|schema|ui|lib]/` for business logic\n- `src/components/ui/` for reusable UI, `src/shared/` for cross-cutting concerns\n- Use **Server Components** by default, `'use client'` only when needed\n\n## Code Style & Conventions\n\n- **TypeScript** with strict mode, functional programming patterns\n- **Named exports only** (no default exports for components)\n- **kebab-case** for directories, **camelCase** for variables, **PascalCase** for components\n- Use `interface` over `type`, avoid `enum` (use `as const` objects)\n- Double quotes (`\"`) enforced, 140 char line limit, no trailing commas\n\n## Import Organization (enforced by ESLint)\n\n```typescript\n// External libraries (alphabetical desc)\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useForm } from \"react-hook-form\";\n\n// Internal modules\nimport { useI18n } from \"locales/client\";\nimport { Button } from \"@/components/ui/button\";\nimport { paths } from \"@/shared/constants/paths\";\n```\n\n## Key Patterns\n\n- **Zod schemas** for validation in `schema/` directories\n- **React Hook Form** with zodResolver for forms\n- **next-safe-action** for server actions with typed errors\n- **@tanstack/react-query** for client state management\n- **Shadcn UI + Radix + Tailwind** for styling (mobile-first)\n- Abstract external deps in `shared/lib/` (no direct fetch, Date, localStorage)\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\n## Project Overview\n\nWorkout.cool is a fitness app with two main components:\n\n- **Website** Next.js (App Router) web client with Server Actions Location: `/Users/mathiasbradiceanu/dev/perso/workout-cool-web`\n\n- **Mobile App** React Native app for iOS and Android Consumes the Workout.cool Next.js API Location:\n  `/Users/mathiasbradiceanu/dev/perso/workout-cool-mobile`\n\n## Architecture\n\n### System Components\n\n1. **Web Client (Next.js)**\n\n   - Uses App Router and Server Actions for data mutations\n   - Provides REST/JSON API endpoints consumed by the mobile app\n   - TailwindCSS for styling\n   - Contain the schema of the prisma database in `/Users/mathiasbradiceanu/dev/perso/workout-cool-web/prisma/schema.prisma`\n\n2. **Mobile App (React Native / Expo)**\n\n   - Communicates with the Next.js API for workouts\n   - Push notifications and offline support for session data\n\n3. **Both projects are using the FSD design system**.\n\n### Data Flow\n\n1. Mobile app and browser make API requests to the Next.js server\n2. Next.js Server Actions handle form submissions, data mutations, and fetches\n3. Data is stored/retrieved from the database via Next.js backend logic\n4. Web client renders pages and exposes JSON endpoints\n5. Mobile app syncs progress and displays workout sessions\n\n## Key Features\n\n- **3-Step Session Builder**: Equipment → Target Muscles → Generated Exercises\n- **Embedded Videos**: Guide users through each exercise\n- **In-Session Tracking**: Add sets with Reps, Weight, Time, or Bodyweight\n- **Session History**: “Commit-style” log of past workouts on user profile\n- **Repeat & Share**: Re-run past sessions or share summaries with others\n\n## External Integrations\n\n- **Database**: PostgreSQL via Next.js data layer\n- **ORM**: Prisma, the schema is under `/Users/mathiasbradiceanu/dev/perso/workout-cool-web/prisma/schema.prisma`\n- **Authentication**: BetterAuth (email/password, OAuth)\n- **Video Hosting**: YouTube\n\n## Linting\n\n- ESLint and Prettier configured in both web and mobile workspaces\n\n## Deployment\n\n- **Website**: Vercel (Next.js)\n- **Mobile App**: Expo EAS Build & Updates\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "### ✅ Review & Contribution Flow\n\n- Before starting, **create an issue** for the task you want to work on.\n- **Assign yourself** to the issue so it’s clear who’s working on it.\n- Keep PRs focused: one issue = one PR (preferably small and scoped).\n- All PRs need **at least one maintainer review**.\n- We use **\"Squash and merge\"** to keep history clean.\n- Address review comments quickly and respectfully.\n\n---\n\n### 🤔 Need Help?\n\n- **General questions** → use GitHub Discussions\n- **Bug reports or features** → open an Issue\n- **Live chat** → [Join our Discord](https://discord.gg/NtrsUBuHUB)\n\n---\n\n### 📚 Useful Links\n\n- [Feature-Sliced Design](https://feature-sliced.design/)\n- [Next.js Docs](https://nextjs.org/docs)\n- [Prisma Docs](https://www.prisma.io/docs/)\n\n---\n\n### 🌟 Recognition\n\nWe credit contributors in:\n\n- the GitHub contributors list\n- release notes (for impactful work)\n- internal documentation if relevant\n\nThanks again for contributing to Workout Cool! 💪\n\nQuestions? Just open an issue or ping a maintainer.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:20-alpine AS base\n\nWORKDIR /app\nRUN npm install -g pnpm\n\n# Install dependencies\nFROM base AS deps\nCOPY package.json pnpm-lock.yaml ./\nCOPY prisma ./prisma\nRUN pnpm install --frozen-lockfile\n\n# Build the app\nFROM base AS builder\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY --from=deps /app/prisma ./prisma\nCOPY . .\nCOPY .env.example .env\n\nRUN pnpm run build\n\n# Production image, copy only necessary files\nFROM base AS runner\nWORKDIR /app\n\nCOPY --from=builder /app/public ./public\nCOPY --from=builder /app/.next ./.next\nCOPY --from=builder /app/node_modules ./node_modules\nCOPY --from=builder /app/package.json ./package.json\nCOPY --from=builder /app/prisma ./prisma\nCOPY --from=builder /app/data ./data\n\nCOPY scripts /app/scripts\nRUN chmod +x /app/scripts/setup.sh\n\nENTRYPOINT [\"/app/scripts/setup.sh\"]\n\nEXPOSE 3000\nENV PORT=3000\n\nCMD [\"pnpm\", \"start\"]"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Mathias Bradiceanu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the\nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: dev setup up down help\n\nhelp:\n\t@echo \"🚀 Workout Cool Development Commands\"\n\t@echo \"\"\n\t@echo \"  dev     - Start development server (automatically sets up everything)\"\n\t@echo \"  setup   - One-time setup: database, schema, and sample data\"\n\t@echo \"  db      - Start PostgreSQL database only\"\n\t@echo \"  down    - Stop all services\"\n\t@echo \"\"\n\n\ndb:\n\t@echo \"🐘 Starting PostgreSQL database...\"\n\tdocker compose up -d postgres\n\n\nsetup: db\n\t@echo \"📦 Installing dependencies...\"\n\tpnpm install --frozen-lockfile\n\t@echo \"🔄 Applying database migrations...\"\n\tnpx prisma migrate deploy\n\tnpx prisma generate\n\t@echo \"🌱 Seeding database with sample data...\"\n\tpnpm run import:exercises-full ./data/sample-exercises.csv\n\tpnpm run db:seed-leaderboard\n\t@echo \"✅ Setup complete!\"\n\n\ndev: setup\n\t@echo \"🚀 Starting Next.js development server...\"\n\tpnpm dev\n\ndown:\n\t@echo \"🛑 Stopping all services...\"\n\tdocker compose down\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n<img src=\"public/logo.png\" alt=\"Workout.cool Logo\" width=\"120\" height=\"120\">\n<h1>Workout.cool</h1>\n<h3><em>Modern fitness coaching platform with comprehensive exercise database</em></h3>\n<p>\n<a href=\"https://github.com/Snouzy/workout-cool/graphs/contributors\"><img src=\"https://img.shields.io/github/contributors/Snouzy/workout-cool?style=plastic\" alt=\"Contributors\">\n<a href=\"https://github.com/Snouzy/workout-cool/network/members\">  \n<img src=\"https://img.shields.io/github/forks/Snouzy/workout-cool\" alt=\"Forks\">\n<a href=\"https://github.com/Snouzy/workout-cool/stargazers\">\n<img src=\"https://img.shields.io/github/stars/Snouzy/workout-cool\" alt=\"Stars\">\n<a href=\"https://github.com/Snouzy/workout-cool/issues\">  \n<img src=\"https://img.shields.io/github/issues/Snouzy/workout-cool\" alt=\"Issues\">\n<img src=\"https://img.shields.io/github/repo-size/Snouzy/workout-cool\" alt=\"Repository Size\">\n<a href=\"LICENSE\">\n  <img src=\"https://img.shields.io/badge/License-MIT-green.svg\" alt=\"MIT License\">\n</a>\n\n<p>\n    <a href=\"https://discord.gg/NtrsUBuHUB\">\n      <img src=\"https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white\" alt=\"Discord\">\n    </a>\n    <a href=\"https://ko-fi.com/workoutcool\">\n      <img src=\"https://img.shields.io/badge/Ko--fi-Support%20Project-FF5E5B?style=for-the-badge&logo=ko-fi&logoColor=white\" alt=\"Ko-fi\">\n    </a>\n  </p>\n  <!-- Keep these links. Translations will automatically update with the README. -->\n  <a href=\"https://readme-i18n.com/Snouzy/workout-cool?lang=de\">Deutsch</a> |\n  <a href=\"https://readme-i18n.com/Snouzy/workout-cool?lang=es\">Español</a> |\n  <a href=\"https://readme-i18n.com/Snouzy/workout-cool?lang=fr\">français</a> |\n  <a href=\"https://readme-i18n.com/Snouzy/workout-cool?lang=ja\">日本語</a> |\n  <a href=\"https://readme-i18n.com/Snouzy/workout-cool?lang=ko\">한국어</a> |\n  <a href=\"https://readme-i18n.com/Snouzy/workout-cool?lang=pt\">Português</a> |\n  <a href=\"https://readme-i18n.com/Snouzy/workout-cool?lang=ru\">Русский</a> |\n  <a href=\"https://readme-i18n.com/Snouzy/workout-cool?lang=zh\">中文</a>\n</p>\n</div>\n\n## Table of Contents\n\n- [About](#about)\n- [Project Origin & Motivation](#-project-origin--motivation)\n- [Quick Start](#quick-start)\n- [Exercise Database Import](#exercise-database-import)\n- [Project Architecture](#project-architecture)\n- [Contributing](#contributing)\n- [Self-hosting](#deployment--self-hosting)\n- [Resources](#resources)\n- [License](#license)\n- [Sponsor This Project](#-sponsor-this-project)\n\n## Contributors\n\n<a href=\"https://github.com/Snouzy/workout-cool/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=Snouzy/workout-cool&nocache=1\" />\n</a>\n\n## Sponsors\n\n<div>\n  <h4>They are helping making workout.cool free and open-source for everyone :</h4>\n\n<a href=\"https://vercel.com/oss\">\n  <img alt=\"Vercel OSS Program\" src=\"https://vercel.com/oss/program-badge.svg\" />\n</a>\n<br/>\n<br/>\n\n  <table>\n    <tr>\n      <td align=\"center\">\n        <a href=\"https://github.com/lj020326\">\n          <img src=\"https://github.com/lj020326.png\" width=\"50px;\" alt=\"lj020326\"/>\n          <br />\n          <sub><b>lj020326</b></sub>\n          <br />\n        </a>\n      </td>\n      <td align=\"center\">\n        <a href=\"https://github.com/lucasnevespereira\">\n          <img src=\"https://github.com/lucasnevespereira.png\" width=\"50px;\" alt=\"lucasnevespereira\"/>\n          <br />\n          <sub><b>lucasnevespereira</b></sub>\n          <br />\n        </a>\n      </td>\n    </tr>\n  </table>\n\n</div>\n\n## About\n\nA comprehensive fitness coaching platform that allows create workout plans for you, track progress, and access a vast exercise database with\ndetailed instructions and video demonstrations.\n\n## 🎯 Project Origin & Motivation\n\nThis project was born from a personal mission to revive and improve upon a previous fitness platform. As the **primary contributor** to the\noriginal [workout.lol](https://github.com/workout-lol/workout-lol) project, I witnessed its journey and abandonment. 🥹\n\n### The Story Behind **_workout.cool_**\n\n- 🏗️ **Original Contributor**: I was the main contributor to workout.lol\n- 💼 **Business Challenges**: The original project faced major hurdles with exercise video partnerships (no reliable video provider) could\n  be established\n- 💰 **Project Sale**: Due to these partnership issues, the project was sold to another party\n- 📉 **Abandonment**: The new owner quickly realized that **exercise video licensing costs were prohibitively expensive**, began to be sick\n  and abandoned the entire project\n- 🔄 **Revival Attempts**: For the past **9 months**, I've been trying to reconnect with the new stakeholder\n- 📧 **Radio Silence**: Despite multiple (15) attempts, there has been no response\n- 🚀 **New Beginning**: Rather than let this valuable work disappear, I decided to create a fresh, modern implementation\n\n### Why **_workout.cool_** Exists\n\n**Someone had to step up.**\n\nThe opensource fitness community deserves better than broken promises and abandoned platforms.\n\nI'm not building this for profit.\n\nThis isn't just a revival : it's an evolution. **workout.cool** represents everything the original project could have been, with the\nreliability, modern approach, and **maintenance** that the fitness open source community deserves.\n\n## 👥 From the Community, For the Community\n\n**I'm not just a developer : I'm a user who refused to let our community down.**\n\nI experienced firsthand the frustration of watching a beloved tool slowly disappear. Like many of you, I had workouts saved, progress\ntracked, and a routine built around the platform.\n\n### My Mission: Rescue & Revive.\n\n_If you were part of the original workout.lol community, welcome back! If you're new here, welcome to the future of fitness platform\nmanagement._\n\n## Quick Start\n\n### Prerequisites\n\n- [Node.js](https://nodejs.org/) (v18+)\n- [pnpm](https://pnpm.io/) (v8+)\n- [Docker](https://www.docker.com/)\n\n### Installation\n\n1. **Clone the repository**\n\n   ```bash\n   git clone https://github.com/Snouzy/workout-cool.git\n   cd workout-cool\n   ```\n\n2. **Choose your installation method:**\n\n<details>\n<summary><b>🐳 With Docker</b></summary>\n\n### Docker Installation\n\n1. **Copy environment variables**\n\n   ```bash\n   cp .env.example .env\n   ```\n\n2. **Start everything for development:**\n\n   ```sh\n   make dev\n   ```\n\n   - This will start the database in Docker, run migrations, seed the DB, and start the Next.js dev server.\n   - To stop services run `make down`\n\n3. **Open your browser** Navigate to [http://localhost:3000](http://localhost:3000)\n\n</details>\n\n<details>\n<summary><b>💻 Without Docker</b></summary>\n\n### Manual Installation\n\n1. **Install dependencies**\n\n   ```bash\n   pnpm install\n   ```\n\n2. **Copy environment variables**\n\n   ```bash\n   cp .env.example .env\n   ```\n\n3. **Set up PostgreSQL database**\n\n   - If you don't already have it, install PostgreSQL locally\n   - Create a database named `workout_cool` : `createdb -h localhost -p 5432 -U postgres workout_cool`\n\n4. **Run database migrations**\n\n   ```bash\n   npx prisma migrate dev\n   ```\n\n5. **Seed the database (optional)**\n\nSee the - [Exercise database import section](#exercise-database-import)\n\n6. **Start the development server**\n\n   ```bash\n   pnpm dev\n   ```\n\n7. **Open your browser** Navigate to [http://localhost:3000](http://localhost:3000)\n\n</details>\n\n## Exercise Database Import\n\nThe project includes a comprehensive exercise database. To import a sample of exercises:\n\n### Prerequisites for Import\n\n1. **Prepare your CSV file**\n\nYour CSV should have these columns:\n\n```\nid,name,name_en,description,description_en,full_video_url,full_video_image_url,introduction,introduction_en,slug,slug_en,attribute_name,attribute_value\n```\n\nYou can use the provided example.\n\n### Import Commands\n\n```bash\n# Import exercises from a CSV file\npnpm run import:exercises-full /path/to/your/exercises.csv\n\n# Example with the provided sample data\npnpm run import:exercises-full ./data/sample-exercises.csv\n```\n\n### CSV Format Example\n\n```csv\nid,name,name_en,description,description_en,full_video_url,full_video_image_url,introduction,introduction_en,slug,slug_en,attribute_name,attribute_value\n157,\"Fentes arrières à la barre\",\"Barbell Reverse Lunges\",\"<p>Stand upright...</p>\",\"<p>Stand upright...</p>\",https://youtube.com/...,https://img.youtube.com/...,slug-fr,slug-en,TYPE,STRENGTH\n157,\"Fentes arrières à la barre\",\"Barbell Reverse Lunges\",\"<p>Stand upright...</p>\",\"<p>Stand upright...</p>\",https://youtube.com/...,https://img.youtube.com/...,slug-fr,slug-en,PRIMARY_MUSCLE,QUADRICEPS\n```\n\nWant unlimited exercise for local development ?\n\nJust ask chatGPT with the prompt from `./scripts/import-exercises-with-attributes.prompt.md`\n\n## Project Architecture\n\nThis project follows **Feature-Sliced Design (FSD)** principles with Next.js App Router:\n\n```\nsrc/\n├── app/ # Next.js pages, routes and layouts\n├── processes/ # Business flows (multi-feature)\n├── widgets/ # Composable UI with logic (Sidebar, Header)\n├── features/ # Business units (auth, exercise-management)\n├── entities/ # Domain entities (user, exercise, workout)\n├── shared/ # Shared code (UI, lib, config, types)\n└── styles/ # Global CSS, themes\n```\n\n### Architecture Principles\n\n- **Feature-driven**: Each feature is independent and reusable\n- **Clear domain isolation**: `shared` → `entities` → `features` → `widgets` → `app`\n- **Consistency**: Between business logic, UI, and data layers\n\n### Example Feature Structure\n\n```\nfeatures/\n└── exercise-management/\n├── ui/ # UI components (ExerciseForm, ExerciseCard)\n├── model/ # Hooks, state management (useExercises)\n├── lib/ # Utilities (exercise-helpers)\n└── api/ # Server actions or API calls\n```\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.\n\n### Development Workflow\n\n1. **Create an issue** for the feature/bug you want to work on. Say that you will work on it (or no)\n2. Fork the repository\n3. Create your feature|fix|chore|refactor branch (`git checkout -b feature/amazing-feature`)\n4. Make your changes following our [code standards](#code-style)\n5. Commit your changes (`git commit -m 'feat: add amazing feature'`)\n6. Push to the branch (`git push origin feature/amazing-feature`)\n7. Open a Pull Request (one issue = one PR)\n\n**📋 For complete contribution guidelines, see our [Contributing Guide](CONTRIBUTING.md)**\n\n### Code Style\n\n- Follow TypeScript best practices\n- Use Feature-Sliced Design architecture\n- Write meaningful commit messages\n\n## Deployment / Self-hosting\n\n> 📖 **For detailed self-hosting instructions, see our [Complete Self-hosting Guide](docs/SELF-HOSTING.md)**\n>\n> 📺 **You can also watch a [3-minute video guide on self-hosting Workout.Cool](https://www.youtube.com/watch?v=HQecjb0CfAo).**\n\n\nTo seed the database with the sample exercises, set the `SEED_SAMPLE_DATA` env variable to `true`.\n\n### Using Docker\n\n```bash\n# Build the Docker image\ndocker build -t yourusername/workout-cool .\n\n# Run the container\ndocker run -p 3000:3000 --env-file .env.production yourusername/workout-cool\n```\n\n### Using Docker Compose\n\n#### DATABASE_URL\n\nUpdate the `host` to point to the `postgres` service instead of `localhost`\n`DATABASE_URL=postgresql://username:password@postgres:5432/workout_cool`\n\n```bash\ndocker compose up -d\n```\n\n### Manual Deployment\n\n```bash\n# Build the application\npnpm build\n\n# Run database migrations\nexport DATABASE_URL=\"your-production-db-url\"\nnpx prisma migrate deploy\n\n# Start the production server\npnpm start\n```\n\n## Resources\n\n- [Feature-Sliced Design](https://feature-sliced.design/)\n- [Next.js Documentation](https://nextjs.org/docs)\n- [Prisma Documentation](https://www.prisma.io/docs/)\n- [Better Auth](https://github.com/better-auth/better-auth)\n\n## License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n\n[![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)\n\n## 🤝 Join the Rescue Mission\n\n**This is about rebuilding what we lost, together.**\n\n### How You Can Help\n\n- 🌟 **Star this repo** to show the world our community is alive and thriving\n- 💬 **Join our Discord** to connect with other fitness enthusiasts and developers\n- 🐛 **Report issues** you find. I'm listening to every single one\n- 💡 **Share your feature requests** finally, someone who will actually implement them !\n- 🔄 **Spread the word** to fellow fitness enthusiasts who lost hope\n- 🤝 **Contribute code** if you're a developer : let's build this together\n\n<div align=\"center\">\n  <a href=\"https://discord.gg/NtrsUBuHUB\" target=\"_blank\">\n    <img src=\"https://img.shields.io/badge/Discord-Join%20Our%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n  <br><br>\n  <a href=\"https://www.producthunt.com/products/workout-cool?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-workout&#0045;cool\" target=\"_blank\">\n    <img src=\"https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=980519&theme=light&t=1750436372984\" alt=\"Product Hunt\" width=\"180\">\n  </a>\n</div>\n\n## 💖 Sponsor This Project\n\nAppear in the README and on the website as supporter by donating:\n\n<div align=\"center\">\n  <a href=\"https://ko-fi.com/workoutcool\" target=\"_blank\">\n    <img src=\"https://ko-fi.com/img/githubbutton_sm.svg\" alt=\"Sponsor on Ko-fi\" />\n  </a>\n  &nbsp;&nbsp;&nbsp;\n  <!-- TODO: setup -->\n  <!-- <a href=\"https://buymeacoffee.com/workout_cool\" target=\"_blank\">\n    <img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png\" alt=\"Buy Me A Coffee\" height=\"41\" width=\"174\" />\n  </a> -->\n</div>\n\n<p align=\"center\" style=\"margin-top:20px;\">\n  <em>If you believe in open-source fitness tools and want to help this project thrive,<br>\n  consider buying me a coffee ☕ or sponsoring the continued development.</em>\n</p>\n\n<p align=\"center\">\n  Your support helps cover hosting costs, exercise database updates, and continuous improvement.<br>\n  Thank you for keeping <strong>workout.cool</strong> alive and evolving 💪\n</p>\n\n<br />\n<br />\n<a href=\"https://vercel.com/oss\">\n<img alt=\"Vercel OSS Program\" src=\"https://vercel.com/oss/program-badge.svg\" />\n</a>\n"
  },
  {
    "path": "app/[locale]/(admin)/admin/[...catchAll]/not-found.tsx",
    "content": "import { Page404 } from \"@/widgets/404\";\n\nexport default function NotFoundPage() {\n  return <Page404 />;\n}\n"
  },
  {
    "path": "app/[locale]/(admin)/admin/[...catchAll]/page.tsx",
    "content": "import { Page404 } from \"@/widgets/404\";\n\nexport default function AdminCatchAll() {\n  return <Page404 />;\n}\n"
  },
  {
    "path": "app/[locale]/(admin)/admin/dashboard/page.tsx",
    "content": "import { Suspense } from \"react\";\nimport Image from \"next/image\";\nimport { Users, Target } from \"lucide-react\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nasync function getDashboardStats() {\n  const [totalUsers, totalWorkoutSessions, totalExercises, activeSubscriptions, recentUsers, recentWorkouts, totalPrograms] =\n    await Promise.all([\n      // Total users\n      prisma.user.count(),\n\n      // Total workout sessions\n      prisma.workoutSession.count(),\n\n      // Total exercises\n      prisma.exercise.count(),\n\n      // Active subscriptions\n      prisma.subscription.count({\n        where: {\n          status: \"ACTIVE\",\n        },\n      }),\n\n      // Users created in last 7 days\n      prisma.user.count({\n        where: {\n          createdAt: {\n            gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),\n          },\n        },\n      }),\n\n      // Workout sessions in last 7 days\n      prisma.workoutSession.count({\n        where: {\n          startedAt: {\n            gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),\n          },\n        },\n      }),\n\n      // Total programs\n      prisma.program.count(),\n    ]);\n\n  return {\n    totalUsers,\n    totalWorkoutSessions,\n    totalExercises,\n    activeSubscriptions,\n    recentUsers,\n    recentWorkouts,\n    totalPrograms,\n  };\n}\n\nasync function DashboardStats() {\n  const stats = await getDashboardStats();\n\n  return (\n    <div className=\"grid gap-4 md:gap-6\">\n      <div className=\"grid grid-cols-2 gap-4 md:grid-cols-4 md:gap-6\">\n        <div className=\"group col-span-3\">\n          <div className=\"relative overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-blue-50 to-blue-100 p-5 transition-all duration-200 ease-in-out hover:scale-[1.02] hover:border-blue-300 dark:border-gray-700 dark:from-blue-950/50 dark:to-blue-900/30 dark:hover:border-blue-600\">\n            <div className=\"flex items-start justify-between\">\n              <div>\n                <div className=\"mb-2 flex items-center space-x-2\">\n                  <div className=\"rounded-xl bg-blue-500 p-2\">\n                    <Users className=\"h-5 w-5 text-white\" />\n                  </div>\n                  <span className=\"text-xs font-medium uppercase tracking-wider text-blue-600 dark:text-blue-400\">Communauté</span>\n                </div>\n                <h3 className=\"text-3xl font-bold text-gray-900 dark:text-white\">{stats.totalUsers.toLocaleString()}</h3>\n                <p className=\"text-sm text-gray-600 dark:text-gray-300\">\n                  <p className=\"text-xs text-gray-600 dark:text-gray-300\">Utilisateurs</p>\n                  <span className=\"font-semibold text-green-600 dark:text-green-400\">+{stats.recentUsers}</span> cette semaine\n                </p>\n              </div>\n              <div className=\"transition-transform duration-200 group-hover:rotate-6\">\n                <Image alt=\"Happy mascot\" className=\"h-12 w-12\" height={48} src=\"/images/emojis/WorkoutCoolHappy.png\" width={48} />\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Workout Sessions Card */}\n        <div className=\"group col-span-2 md:col-span-1\">\n          <div className=\"relative overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-green-50 to-emerald-100 p-4 transition-all duration-200 ease-in-out hover:scale-[1.02] hover:border-green-300 dark:border-gray-700 dark:from-green-950/50 dark:to-emerald-900/30 dark:hover:border-green-600\">\n            <div className=\"mb-3 flex items-center justify-between\">\n              <div className=\"rounded-xl bg-green-500 p-2\">\n                <Image\n                  alt=\"Swag mascot\"\n                  className=\"h-8 w-8 transition-transform duration-200 group-hover:scale-110\"\n                  height={32}\n                  src=\"/images/emojis/WorkoutCoolSwag.png\"\n                  width={32}\n                />\n              </div>\n            </div>\n            <h3 className=\"text-2xl font-bold text-gray-900 dark:text-white\">{stats.totalWorkoutSessions.toLocaleString()}</h3>\n            <p className=\"text-xs text-gray-600 dark:text-gray-300\">Sessions</p>\n            <p className=\"text-xs text-green-600 dark:text-green-400\">+{stats.recentWorkouts} cette semaine</p>\n          </div>\n        </div>\n      </div>\n\n      {/* Row 2 */}\n      <div className=\"grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6\">\n        {/* Programs Card */}\n        <div className=\"group\">\n          <div className=\"relative overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-amber-50 to-yellow-100 p-4 transition-all duration-200 ease-in-out hover:scale-[1.02] hover:border-amber-300 dark:border-gray-700 dark:from-amber-950/50 dark:to-yellow-900/30 dark:hover:border-amber-600\">\n            <div className=\"mb-3 flex items-center justify-between\">\n              <div className=\"rounded-xl bg-amber-500 p-2\">\n                <Image\n                  alt=\"Wooow mascot\"\n                  className=\"h-8 w-8 transition-transform duration-200 group-hover:scale-110\"\n                  height={32}\n                  src=\"/images/emojis/WorkoutCoolWooow.png\"\n                  width={32}\n                />\n              </div>\n            </div>\n            <h3 className=\"text-xl font-bold text-gray-900 dark:text-white\">{stats.totalPrograms.toLocaleString()}</h3>\n            <p className=\"text-xs text-gray-600 dark:text-gray-300\">Programmes</p>\n          </div>\n        </div>\n\n        <div className=\"group\">\n          <div className=\"relative overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-purple-50 to-violet-100 p-4 transition-all duration-200 ease-in-out hover:scale-[1.02] hover:border-purple-300 dark:border-gray-700 dark:from-purple-950/50 dark:to-violet-900/30 dark:hover:border-purple-600\">\n            <div className=\"mb-3 flex items-center justify-between\">\n              <div className=\"rounded-xl bg-purple-500 p-2\">\n                <Image\n                  alt=\"Love mascot\"\n                  className=\"h-8 w-8 transition-transform duration-200 group-hover:scale-110\"\n                  height={32}\n                  src=\"/images/emojis/WorkoutCoolBiceps.png\"\n                  width={32}\n                />\n              </div>\n            </div>\n            <h3 className=\"text-2xl font-bold text-gray-900 dark:text-white\">{stats.totalExercises.toLocaleString()}</h3>\n            <p className=\"text-xs text-gray-600 dark:text-gray-300\">Exercices</p>\n          </div>\n        </div>\n\n        {/* Growth Card */}\n        <div className=\"group\">\n          <div className=\"relative overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-cyan-50 to-blue-100 p-4 transition-all duration-200 ease-in-out hover:scale-[1.02] hover:border-cyan-300 dark:border-gray-700 dark:from-cyan-950/50 dark:to-blue-900/30 dark:hover:border-cyan-600\">\n            <div className=\"mb-3 flex items-center justify-between\">\n              <div className=\"rounded-xl bg-cyan-500 p-2\">\n                <Image\n                  alt=\"Teeth mascot\"\n                  className=\"h-8 w-8 transition-transform duration-200 group-hover:scale-110\"\n                  height={32}\n                  src=\"/images/emojis/WorkoutCoolTeeths.png\"\n                  width={32}\n                />\n              </div>\n            </div>\n            <h3 className=\"text-xl font-bold text-gray-900 dark:text-white\">{stats.activeSubscriptions}</h3>\n            <p className=\"text-xs text-gray-600 dark:text-gray-300\">Abonnés</p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction DashboardStatsLoading() {\n  return (\n    <div className=\"grid gap-4 md:gap-6\">\n      <div className=\"grid grid-cols-2 gap-4 md:grid-cols-4 md:gap-6\">\n        <div className=\"col-span-2 md:col-span-2\">\n          <div className=\"rounded-2xl border border-gray-200 p-6 dark:border-gray-700\">\n            <Skeleton className=\"mb-4 h-6 w-24\" />\n            <Skeleton className=\"mb-2 h-8 w-20\" />\n            <Skeleton className=\"h-4 w-32\" />\n          </div>\n        </div>\n        <div className=\"rounded-2xl border border-gray-200 p-4 dark:border-gray-700\">\n          <Skeleton className=\"mb-3 h-6 w-full\" />\n          <Skeleton className=\"mb-2 h-6 w-16\" />\n          <Skeleton className=\"h-3 w-20\" />\n        </div>\n        <div className=\"rounded-2xl border border-gray-200 p-4 dark:border-gray-700\">\n          <Skeleton className=\"mb-3 h-6 w-full\" />\n          <Skeleton className=\"mb-2 h-6 w-16\" />\n          <Skeleton className=\"h-3 w-20\" />\n        </div>\n      </div>\n      <div className=\"grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6\">\n        {Array.from({ length: 3 }).map((_, i) => (\n          <div className=\"rounded-2xl border border-gray-200 p-4 dark:border-gray-700\" key={i}>\n            <Skeleton className=\"mb-3 h-6 w-full\" />\n            <Skeleton className=\"mb-2 h-6 w-12\" />\n            <Skeleton className=\"h-3 w-16\" />\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport default function AdminDashboard() {\n  return (\n    <div className=\"space-y-6\">\n      <div className=\"flex items-center space-x-4\">\n        <div className=\"rounded-2xl bg-gradient-to-r from-blue-500 to-purple-600 p-3\">\n          <Target className=\"h-6 w-6 text-white\" />\n        </div>\n        <div>\n          <h1 className=\"text-3xl font-bold tracking-tight text-gray-900 dark:text-white\">Dashboard Admin</h1>\n          <p className=\"text-gray-600 dark:text-gray-300\">WorkoutCool Admin</p>\n        </div>\n      </div>\n\n      <Suspense fallback={<DashboardStatsLoading />}>\n        <DashboardStats />\n      </Suspense>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(admin)/admin/layout.tsx",
    "content": "import { ReactElement } from \"react\";\nimport { redirect } from \"next/navigation\";\nimport { UserRole } from \"@prisma/client\";\n\nimport { AdminSidebar } from \"@/features/admin/layout/admin-sidebar/ui/admin-sidebar\";\nimport { AdminHeader } from \"@/features/admin/layout/admin-sidebar/ui/admin-header\";\nimport { serverRequiredUser } from \"@/entities/user/model/get-server-session-user\";\n\ninterface AdminLayoutProps {\n  params: Promise<{ locale: string }>;\n  children: ReactElement;\n}\n\nexport default async function AdminLayout({ children }: AdminLayoutProps) {\n  const user = await serverRequiredUser();\n\n  if (user.role !== UserRole.admin) {\n    redirect(\"/\");\n  }\n\n  return (\n    <div className=\"flex bg-gray-50 dark:bg-gray-900\">\n      {/* Sidebar */}\n      <AdminSidebar />\n\n      {/* Main content */}\n      <div className=\"flex flex-1 flex-col overflow-hidden\">\n        {/* Header */}\n        <AdminHeader user={user} />\n\n        {/* Page content */}\n        <main className=\"flex-1 overflow-y-auto bg-white p-6 dark:bg-gray-800\">\n          <div className=\"mx-auto max-w-7xl w-full\">{children}</div>\n        </main>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(admin)/admin/not-found.tsx",
    "content": "import { Page404 } from \"@/widgets/404\";\n\nexport default function NotFoundPage() {\n  return <Page404 />;\n}\n"
  },
  {
    "path": "app/[locale]/(admin)/admin/programs/[id]/edit/page.tsx",
    "content": "import { notFound } from \"next/navigation\";\n\nimport { ProgramBuilder } from \"@/features/admin/programs/ui/program-builder\";\nimport { getProgramById } from \"@/features/admin/programs/actions/get-programs.action\";\n\ninterface ProgramEditPageProps {\n  params: Promise<{ id: string }>;\n}\n\nexport default async function ProgramEditPage({ params }: ProgramEditPageProps) {\n  const { id } = await params;\n\n  const program = await getProgramById(id);\n\n  if (!program) {\n    notFound();\n  }\n\n  return <ProgramBuilder program={program} />;\n}\n"
  },
  {
    "path": "app/[locale]/(admin)/admin/programs/page.tsx",
    "content": "import { Suspense } from \"react\";\n\nimport { ProgramsList } from \"@/features/admin/programs/ui/programs-list\";\nimport { CreateProgramButton } from \"@/features/admin/programs/ui/create-program-button\";\n\nexport default function AdminPrograms() {\n  return (\n    <div className=\"space-y-6\">\n      <div className=\"flex items-center justify-between\">\n        <div>\n          <h1 className=\"text-3xl font-bold tracking-tight\">Programs</h1>\n          <p className=\"text-muted-foreground\">Create, edit, view and delete programs.</p>\n        </div>\n        <CreateProgramButton />\n      </div>\n\n      <Suspense fallback={<div>Loading programs...</div>}>\n        <ProgramsList />\n      </Suspense>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(admin)/admin/settings/page.tsx",
    "content": "export default function AdminSettings() {\n  return (\n    <div className=\"space-y-6\">\n      <div>\n        <h1 className=\"text-3xl font-bold tracking-tight\">Settings</h1>\n        <p className=\"text-muted-foreground\">Configuration and administration of the system.</p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(admin)/admin/users/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport { UserRole } from \"@prisma/client\";\n\nimport { UsersTable } from \"@/features/admin/users/list/ui/users-table\";\nimport { getUsersAction } from \"@/entities/user/model/get-users.actions\";\nimport { serverRequiredUser } from \"@/entities/user/model/get-server-session-user\";\n\nexport default async function AdminUsersPage() {\n  try {\n    const user = await serverRequiredUser();\n\n    if (user.role !== UserRole.admin) {\n      redirect(\"/\");\n    }\n\n    // Call the action with proper error handling\n    const result = await getUsersAction({\n      page: 1,\n      limit: 10,\n    });\n\n    // Check if the action was successful\n    if (!result || !result.data) {\n      throw new Error(\"Impossible de charger les utilisateurs\");\n    }\n\n    return (\n      <div className=\"space-y-6\">\n        <div>\n          <h1 className=\"text-3xl font-bold tracking-tight\">Utilisateurs</h1>\n          <p className=\"text-muted-foreground\">Gestion et administration des comptes utilisateurs</p>\n        </div>\n        <UsersTable initialUsers={result} />\n      </div>\n    );\n  } catch (error) {\n    console.error(\"Error in admin users page:\", error);\n\n    return (\n      <div className=\"space-y-6\">\n        <div>\n          <h1 className=\"text-3xl font-bold tracking-tight\">Utilisateurs</h1>\n          <p className=\"text-muted-foreground\">Gestion et administration des comptes utilisateurs</p>\n        </div>\n        <div className=\"rounded-lg border border-red-200 bg-red-50 p-6 dark:border-red-800 dark:bg-red-950\">\n          <h2 className=\"text-lg font-semibold text-red-800 dark:text-red-200\">Erreur de chargement</h2>\n          <p className=\"mt-2 text-sm text-red-700 dark:text-red-300\">\n            Impossible de charger la liste des utilisateurs. Veuillez réessayer plus tard.\n          </p>\n          <p className=\"mt-1 text-xs text-red-600 dark:text-red-400\">{error instanceof Error ? error.message : \"Erreur inconnue\"}</p>\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "app/[locale]/(app)/(legal-and-payment)/layout.tsx",
    "content": "import { LayoutParams } from \"@/shared/types/next\";\n\ntype LocaleParams = Record<string, string> & {\n  locale: string;\n};\n\nexport default function RouteLayout({ children, params: _ }: LayoutParams<LocaleParams>) {\n  return (\n    <div className=\"bg-muted/30 text-foreground flex min-h-screen flex-col\">\n      {/* Fixe l'espace sous le header flottant */}\n      <div className=\"h-16\" />\n\n      {/* Contenu principal centré avec marge */}\n      <main className=\"flex-1 px-4 py-12\">\n        <div className=\"mx-auto w-full max-w-4xl\">{children}</div>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/(legal-and-payment)/legal/privacy/page.tsx",
    "content": "import { getLocalizedMdx } from \"@/shared/lib/mdx/load-mdx\";\nimport { Typography } from \"@/components/ui/typography\";\n\ntype PageProps = {\n  params: Promise<{ locale: string }>;\n};\n\nexport default async function PrivacyPolicyPage({ params }: PageProps) {\n  const { locale } = await params;\n  const content = await getLocalizedMdx(\"privacy-policy\", locale);\n\n  return (\n    <div className=\"bg-muted/50 py-12\">\n      <div className=\"container mx-auto max-w-4xl px-4\">\n        <header className=\"mb-10 text-center\">\n          <Typography className=\"mb-2 text-3xl md:text-4xl\" variant=\"h1\">\n            {locale === \"fr\" ? \"Politique de Confidentialité\" : \"Privacy Policy\"}\n          </Typography>\n          <p className=\"text-muted-foreground text-base md:text-lg\">\n            {locale === \"fr\"\n              ? \"Voici comment nous traitons vos données personnelles.\"\n              : \"How we handle your personal data at Workout Cool.\"}\n          </p>\n        </header>\n\n        <div className=\"prose prose-neutral max-w-none dark:prose-invert\">{content}</div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/(legal-and-payment)/legal/sales-terms/page.tsx",
    "content": "import { getLocalizedMdx } from \"@/shared/lib/mdx/load-mdx\";\nimport { Layout, LayoutContent } from \"@/features/page/layout\";\nimport { Typography } from \"@/components/ui/typography\";\n\ntype PageProps = {\n  params: Promise<{ locale: string }>;\n};\n\nexport default async function SalesTermsPage({ params }: PageProps) {\n  const { locale } = await params;\n  const content = await getLocalizedMdx(\"sales-terms\", locale);\n\n  return (\n    <div className=\"bg-muted/50 py-12\">\n      <div className=\"container mx-auto max-w-4xl px-4\">\n        <header className=\"mb-10 text-center\">\n          <Typography className=\"mb-2 text-3xl md:text-4xl\" variant=\"h1\">\n            {locale === \"fr\" ? \"Conditions Générales de Vente\" : \"General Terms of Sale\"}\n          </Typography>\n          <p className=\"text-muted-foreground text-base md:text-lg\">\n            {locale === \"fr\"\n              ? \"Les conditions qui régissent l’achat d’un abonnement Workout Cool.\"\n              : \"The terms governing the purchase of a Workout Cool subscription.\"}\n          </p>\n        </header>\n\n        <Layout>\n          <LayoutContent className=\"prose prose-neutral max-w-none dark:prose-invert\">{content}</LayoutContent>\n        </Layout>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/(legal-and-payment)/legal/terms/page.tsx",
    "content": "import { getLocalizedMdx } from \"@/shared/lib/mdx/load-mdx\";\nimport { Layout, LayoutContent } from \"@/features/page/layout\";\nimport { Typography } from \"@/components/ui/typography\";\n\ntype PageProps = {\n  params: Promise<{ locale: string }>;\n};\n\nexport default async function TermsPage({ params }: PageProps) {\n  const { locale } = await params;\n  const content = await getLocalizedMdx(\"terms\", locale);\n\n  return (\n    <div className=\"bg-muted/50 py-12\">\n      <div className=\"container mx-auto max-w-4xl px-4\">\n        <header className=\"mb-10 text-center\">\n          <Typography className=\"mb-2 text-3xl md:text-4xl\" variant=\"h1\">\n            {locale === \"fr\" ? \"Conditions Générales d’Utilisation\" : \"Terms of Use\"}\n          </Typography>\n          <p className=\"text-muted-foreground text-base md:text-lg\">\n            {locale === \"fr\"\n              ? \"Merci de lire attentivement ces conditions avant d’utiliser nos services.\"\n              : \"Please read these terms carefully before using our services.\"}\n          </p>\n        </header>\n\n        <Layout>\n          <LayoutContent className=\"prose prose-neutral max-w-none dark:prose-invert\">{content}</LayoutContent>\n        </Layout>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/[slug]/layout.tsx",
    "content": "import { ReactElement } from \"react\";\n\ninterface RootLayoutProps {\n    params: Promise<{ locale: string }>;\n    children: ReactElement;\n  }\n\nexport default async function RootLayout({ children }: RootLayoutProps) {\n\n    return (\n        <div>\n            {children}\n        </div>\n    )\n}"
  },
  {
    "path": "app/[locale]/(app)/about/page.tsx",
    "content": "import { getLocalizedMdx } from \"@/shared/lib/mdx/load-mdx\";\n\ntype PageProps = {\n  params: Promise<{ locale: string }>;\n};\n\nexport default async function AboutPage({ params }: PageProps) {\n  const { locale } = await params;\n  const content = await getLocalizedMdx(\"about\", locale);\n\n  return (\n    <div className=\"bg-muted/50 py-12 min-h-screen\">\n      <div className=\"container mx-auto max-w-3xl px-4\">\n        <div className=\"prose prose-neutral max-w-none dark:prose-invert\">{content}</div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/(auth-layout)/forgot-password/page.tsx",
    "content": "import { ForgotPasswordForm } from \"@/features/auth/forgot-password/ui/forgot-password-form\";\n\nexport default async function ForgotPasswordPage() {\n  return <ForgotPasswordForm />;\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/(auth-layout)/layout.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport { headers } from \"next/headers\";\n\nimport { getI18n } from \"locales/server\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\n\nimport type { LayoutParams } from \"@/shared/types/next\";\n\nexport default async function AuthLayout(props: LayoutParams<{}>) {\n  const t = await getI18n();\n\n  const headerStore = await headers();\n  const searchParams = Object.fromEntries(new URLSearchParams(headerStore.get(\"searchParams\") || \"\"));\n  const translatedError = t(`next_auth_errors.${searchParams.error}` as keyof typeof t);\n\n  const user = await auth.api.getSession({ headers: headerStore });\n\n  if (user) {\n    redirect(`/${paths.root}`);\n  }\n\n  return (\n    <>\n      <div className=\"h-full flex\">\n        {searchParams.error && (\n          <Alert className=\"mb-4\" variant=\"error\">\n            <AlertTitle>{translatedError}</AlertTitle>\n            <AlertDescription>{t(\"signin_error_subtitle\")}</AlertDescription>\n          </Alert>\n        )}\n\n        <div className=\"flex flex-1 items-center justify-center\">\n          <div className=\"w-full max-w-md\">{props.children}</div>\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/(auth-layout)/reset-password/page.tsx",
    "content": "import { ResetPasswordForm } from \"@/features/auth/reset-password/ui/reset-password-form\";\n\nexport default function ResetPasswordPage() {\n  return <ResetPasswordForm />;\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/(auth-layout)/signin/page.tsx",
    "content": "import { CredentialsLoginForm } from \"@/features/auth/signin/ui/CredentialsLoginForm\";\n\nexport default async function AuthSignInPage() {\n  return <CredentialsLoginForm />;\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/(auth-layout)/signup/page.tsx",
    "content": "import Link from \"next/link\";\n\nimport { getI18n } from \"locales/server\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { SignUpForm } from \"@/features/auth/signup/ui/signup-form\";\n\nexport const metadata = {\n  title: \"Sign Up - Workout.cool\",\n  description: \"Créez votre compte pour commencer\",\n};\n\nexport default async function AuthSignUpPage() {\n  const t = await getI18n();\n\n  return (\n    <div className=\"container mx-auto max-w-lg px-4 py-8\">\n      <div className=\"mb-8 space-y-2\">\n        <h1 className=\"text-3xl font-bold tracking-tight\">{t(\"register_title\")}</h1>\n        <p className=\"text-muted-foreground\">{t(\"register_description\")}</p>\n      </div>\n\n      <SignUpForm />\n\n      <div className=\"text-muted-foreground mt-6 text-center text-sm\">\n        <p>\n          {t(\"register_terms\")}{\" \"}\n          <Link className=\"font-medium text-primary underline-offset-4 hover:underline\" href={paths.privacy}>\n            {t(\"register_privacy\")}\n          </Link>{\" \"}\n          .\n        </p>\n      </div>\n    </div>\n  );\n}"
  },
  {
    "path": "app/[locale]/(app)/auth/error/page.tsx",
    "content": "import Link from \"next/link\";\n\nimport { Card, CardDescription, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\nexport default async function AuthErrorPage({ params }: { params: Promise<{ error: string }> }) {\n  const result = await params;\n\n  return (\n    <div className=\"flex h-full flex-col\">\n      <Card>\n        <CardHeader>\n          <CardTitle>Error</CardTitle>\n          <CardDescription>{result.error}</CardDescription>\n        </CardHeader>\n        <CardFooter className=\"flex items-center gap-2\">\n          <Link className={buttonVariants({ size: \"small\" })} href=\"/\">\n            Home\n          </Link>\n        </CardFooter>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/error.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\n\nimport { logger } from \"@/shared/lib/logger\";\nimport { Card, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\n\nimport type { ErrorParams } from \"@/shared/types/next\";\n\nexport default function RouteError({ error, reset }: ErrorParams) {\n  useEffect(() => {\n    // Log the error to an error reporting service\n    logger.error(error);\n  }, [error]);\n\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>Sorry, something went wrong. Please try again later.</CardTitle>\n      </CardHeader>\n      <CardFooter>\n        <Button onClick={reset}>Try again</Button>\n      </CardFooter>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/layout.tsx",
    "content": "export default function AuthLayout({ children }: { children: React.ReactNode }) {\n  return <>{children}</>;\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/signout/page.tsx",
    "content": "export default function AuthSignOutPage() {\n  return <div>AuthSignOutPage</div>;\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/verify-email/layout.tsx",
    "content": "import { ReactElement } from \"react\";\nimport { redirect } from \"next/navigation\";\n\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { serverRequiredUser } from \"@/entities/user/model/get-server-session-user\";\n\ninterface RootLayoutProps {\n  params: Promise<{ locale: string }>;\n  children: ReactElement;\n}\n\nexport default async function RootLayout({ children }: RootLayoutProps) {\n  const auth = await serverRequiredUser();\n\n  if (auth.emailVerified) {\n    redirect(`${getServerUrl()}/${paths.root}`);\n  }\n\n  return children;\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/verify-email/page.tsx",
    "content": "\"use client\";\n\nimport { VerifyEmailPage } from \"@/features/auth/verify-email/ui/verify-email-page\";\n\nexport default function VerifyEmailRootPage() {\n  return <VerifyEmailPage />;\n}\n"
  },
  {
    "path": "app/[locale]/(app)/auth/verify-request/page.tsx",
    "content": "import Image from \"next/image\";\n\nimport { SiteConfig } from \"@/shared/config/site-config\";\nimport { Typography } from \"@/components/ui/typography\";\nimport { Card, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\n\ninterface VerifyRequestPageParams {\n  params: Promise<Record<string, string>>;\n  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;\n}\n\nexport default async function AuthVerifyRequestPage({ params: _p, searchParams: _s }: VerifyRequestPageParams) {\n  return (\n    <div className=\"h-full\">\n      <header className=\"flex items-center gap-2 px-4 pt-4\">\n        <Image alt=\"app icon\" height={32} src={SiteConfig.appIcon} width={32} />\n        <Typography variant=\"h2\">{SiteConfig.title}</Typography>\n      </header>\n      <div className=\"flex h-full items-center justify-center\">\n        <Card className=\"w-full max-w-md\">\n          <CardHeader>\n            <CardTitle>Almost There!</CardTitle>\n            <CardDescription>\n              {\n                \"To complete the verification, head over to your email inbox. You'll find a magic link from us. Click on it, and you're all set!\"\n              }\n            </CardDescription>\n          </CardHeader>\n        </Card>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/layout.tsx",
    "content": "import { ReactElement } from \"react\";\n\nimport { Header } from \"@/features/layout/Header\";\nimport { Footer } from \"@/features/layout/Footer\";\nimport { BottomNavigation } from \"@/features/layout/BottomNavigation\";\n\ninterface RootLayoutProps {\n  params: Promise<{ locale: string }>;\n  children: ReactElement;\n}\n\nexport default async function RootLayout({ children }: RootLayoutProps) {\n  return (\n    <div className=\"card w-full max-w-3xl min-h-[500px] max-h-[90vh] bg-white dark:bg-[#232324] shadow-xl border border-base-200 dark:border-slate-700 flex flex-col justify-between overflow-hidden max-sm:rounded-none max-sm:h-full rounded-lg\">\n      <Header />\n      <div className=\"flex-1 overflow-auto flex flex-col\">{children}</div>\n      <BottomNavigation />\n      <Footer />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/leaderboard/page.tsx",
    "content": "import { Metadata } from \"next\";\n\nimport { Locale } from \"locales/types\";\nimport { getI18n } from \"locales/server\";\nimport LeaderboardPage from \"@/features/leaderboard/ui/leaderboard-page\";\nimport { Breadcrumbs } from \"@/components/seo/breadcrumbs\";\n\nexport const metadata: Metadata = {\n  title: \"🏆 Workout Streak Leaderboard\",\n  description: \"See who's dominating their fitness journey with the longest workout streaks! Join the leaderboard and track your progress.\",\n};\n\nexport default async function LeaderboardRootPage({ params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = (await params) as { locale: Locale };\n  const t = await getI18n();\n\n  const breadcrumbItems = [\n    {\n      label: t(\"breadcrumbs.home\"),\n      href: `/${locale}`,\n    },\n    {\n      label: t(\"bottom_navigation.leaderboard\"),\n      current: true,\n    },\n  ];\n\n  return (\n    <>\n      <Breadcrumbs items={breadcrumbItems} />\n      <LeaderboardPage />\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/onboarding/layout.tsx",
    "content": "import { LayoutParams } from \"@/shared/types/next\";\n\nexport default async function OnboardingLayout(props: LayoutParams<{}>) {\n  // TODO: add onboarding logic\n\n  return props.children;\n}\n"
  },
  {
    "path": "app/[locale]/(app)/onboarding/page.tsx",
    "content": "export default async function OnboardingPage() {\n  return (\n    <main className=\"bg-muted flex min-h-screen flex-col items-center justify-center\">\n      <div>Onboarding</div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/page.tsx",
    "content": "import React from \"react\";\n\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { SiteConfig } from \"@/shared/config/site-config\";\nimport { WorkoutStepper } from \"@/features/workout-builder\";\n\nimport type { Metadata } from \"next\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {\n  const { locale } = await params;\n\n  const isEnglish = locale === \"en\";\n  const title = isEnglish ? \"Build Your Perfect Workout\" : \"Créez Votre Entraînement Parfait\";\n  const description = isEnglish\n    ? \"Create free workout routines with our comprehensive exercise database. Track your progress and achieve your fitness goals. 🏋️\"\n    : \"Créez des routines d'entraînement gratuites avec notre base de données d'exercices complète. Suivez vos progrès et atteignez vos objectifs fitness. 🏋️\";\n\n  return {\n    title,\n    description,\n    keywords: isEnglish\n      ? [\"workout builder\", \"exercise planner\", \"fitness routine\", \"personalized training\", \"muscle targeting\", \"free workout\"]\n      : [\n          \"créateur d'entraînement\",\n          \"planificateur d'exercices\",\n          \"routine fitness\",\n          \"entraînement personnalisé\",\n          \"ciblage musculaire\",\n          \"entraînement gratuit\",\n        ],\n    openGraph: {\n      title: `${title} | ${SiteConfig.title}`,\n      description,\n      images: [\n        {\n          url: `${getServerUrl()}/images/default-og-image_${locale}.jpg`,\n          width: SiteConfig.seo.ogImage.width,\n          height: SiteConfig.seo.ogImage.height,\n          alt: title,\n        },\n      ],\n    },\n    twitter: {\n      title: `${title} | ${SiteConfig.title}`,\n      description,\n      images: [`${getServerUrl()}/images/default-og-image_${locale}.jpg`],\n    },\n  };\n}\n\nexport default async function HomePage() {\n  return (\n    <div className=\"bg-background text-foreground relative flex flex-col h-full\">\n      <WorkoutStepper />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/premium/page.tsx",
    "content": "import { Metadata } from \"next\";\n\nimport { PremiumUpgradeCard } from \"@/features/premium/ui/premium-upgrade-card\";\n\nexport const metadata: Metadata = {\n  title: \"Premium Plans - Train freely, support the mission\",\n  description:\n    \"Join thousands of fitness enthusiasts who believe in open-source training freedom. Support our mission while unlocking advanced features.\",\n  keywords: [\"premium\", \"fitness\", \"workout\", \"open-source\", \"subscription\", \"training\"],\n  openGraph: {\n    title: \"Premium Plans - Support the Workout.cool Mission 💪\",\n    description: \"For passionate fitness enthusiasts who believe in open-source and training freedom. Core features always free!\",\n    type: \"website\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: \"Premium Plans - Workout.cool\",\n    description: \"Train freely, support the mission. Join the passionate fitness community!\",\n  },\n};\n\nexport default function PremiumPage() {\n  return (\n    <div className=\"bg-white dark:bg-gray-950\">\n      {/* Main Content */}\n      <div className=\"relative\" data-section=\"pricing\">\n        <PremiumUpgradeCard />\n      </div>\n\n      {/* Mobile Sticky CTA */}\n      {/* <MobileStickyCard /> */}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/profile/page.tsx",
    "content": "\"use client\";\nimport { useRouter } from \"next/navigation\";\n\nimport { useI18n } from \"locales/client\";\nimport { WorkoutSessionList } from \"@/features/workout-session/ui/workout-session-list\";\nimport { WorkoutSessionHeatmap } from \"@/features/workout-session/ui/workout-session-heatmap\";\nimport { useWorkoutSessions } from \"@/features/workout-session/model/use-workout-sessions\";\nimport { env } from \"@/env\";\nimport { useCurrentSession } from \"@/entities/user/model/useCurrentSession\";\nimport { LocalAlert } from \"@/components/ui/local-alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { HorizontalTopBanner } from \"@/components/ads\";\n\nexport default function ProfilePage() {\n  const router = useRouter();\n  const t = useI18n();\n  const { data: sessions = [] } = useWorkoutSessions();\n  const session = useCurrentSession();\n\n  const values: Record<string, number> = {};\n  sessions.forEach((session) => {\n    const date = session.startedAt.slice(0, 10);\n    values[date] = (values[date] || 0) + 1;\n  });\n\n  const until =\n    sessions.length > 0\n      ? sessions.reduce((max, s) => (s.startedAt > max ? s.startedAt : max), sessions[0].startedAt).slice(0, 10)\n      : new Date().toISOString().slice(0, 10);\n\n  return (\n    <div className=\"px-2 sm:px-6\">\n      {env.NEXT_PUBLIC_TOP_PROFILE_BANNER_AD_SLOT && <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_PROFILE_BANNER_AD_SLOT} />}\n      {!session && <LocalAlert className=\"my-4\" />}\n      {session && (\n        <div className=\"mt-4\">\n          <div>\n            <h2 className=\"text-2xl font-bold\">Hello, {session.user?.name} 👋</h2>\n          </div>\n        </div>\n      )}\n\n      <div className=\"mt-4\">\n        <WorkoutSessionHeatmap until={until} values={values} />\n      </div>\n      <WorkoutSessionList />\n      <div className=\"mt-8 flex justify-center\">\n        <Button onClick={() => router.push(\"/\")} size=\"large\">\n          {t(\"profile.new_workout\")}\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/programs/[slug]/page.tsx",
    "content": "import { notFound } from \"next/navigation\";\nimport { headers } from \"next/headers\";\nimport { Metadata } from \"next\";\n\nimport { Locale } from \"locales/types\";\nimport { getI18n } from \"locales/server\";\nimport { generateStructuredData, StructuredDataScript } from \"@/shared/lib/structured-data\";\nimport { getLocalizedMetadata } from \"@/shared/config/localized-metadata\";\nimport { ProgramDetailPage } from \"@/features/programs/ui/program-detail-page\";\nimport { getProgramDescription, getProgramTitle } from \"@/features/programs/lib/translations-mapper\";\nimport { generateProgramSEOKeywords } from \"@/features/programs/lib/program-metadata\";\nimport { getProgramBySlug } from \"@/features/programs/actions/get-program-by-slug.action\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\ninterface ProgramDetailPageProps {\n  params: Promise<{ slug: string; locale: Locale }>;\n}\n\nexport async function generateMetadata({ params }: ProgramDetailPageProps): Promise<Metadata> {\n  const { slug, locale } = await params;\n  const t = await getI18n();\n  const program = await getProgramBySlug(slug);\n  const localizedData = getLocalizedMetadata(locale);\n\n  if (!program) {\n    return { title: t(\"programs.not_found\") };\n  }\n\n  const localizedTitle = getProgramTitle(program, locale);\n  const localizedDescription = getProgramDescription(program, locale);\n  const seoKeywords = generateProgramSEOKeywords(program, locale, t);\n\n  return {\n    title: `${localizedTitle} - ${localizedData.title}`,\n    description: localizedDescription,\n    keywords: seoKeywords,\n    openGraph: {\n      title: `${localizedTitle} - ${localizedData.title}`,\n      description: localizedDescription,\n      images: [\n        {\n          url: program.image, // TODO: specific opengraph image for each program (upload admin side)\n          width: 400,\n          height: 600,\n          alt: localizedTitle,\n        },\n      ],\n    },\n    twitter: {\n      card: \"summary_large_image\",\n      title: `${localizedTitle} - ${localizedData.title}`,\n      description: localizedDescription,\n      images: [program.image],\n    },\n  };\n}\n\nexport default async function ProgramDetailPageRoute({ params }: ProgramDetailPageProps) {\n  const { slug, locale } = await params;\n  const program = await getProgramBySlug(slug);\n\n  if (!program) {\n    notFound();\n  }\n\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  // Generate Course structured data\n  const localizedTitle = getProgramTitle(program, locale);\n  const localizedDescription = getProgramDescription(program, locale);\n\n  const courseStructuredData = generateStructuredData({\n    type: \"Course\",\n    locale,\n    title: localizedTitle,\n    description: localizedDescription,\n    courseData: {\n      id: program.id,\n      level: program.level,\n      category: program.category,\n      durationWeeks: program.durationWeeks,\n      sessionsPerWeek: program.sessionsPerWeek,\n      sessionDurationMin: program.sessionDurationMin,\n      equipment: program.equipment,\n      isPremium: program.isPremium,\n      participantCount: program.participantCount,\n      totalSessions: program.weeks.reduce((acc, week) => acc + week.sessions.length, 0),\n      totalExercises: program.weeks.reduce(\n        (acc, week) => acc + week.sessions.reduce((sessAcc, session) => sessAcc + session.totalExercises, 0),\n        0,\n      ),\n      coaches: program.coaches,\n    },\n  });\n\n  // Breadcrumbs\n\n  return (\n    <>\n      <StructuredDataScript data={courseStructuredData} />\n      <ProgramDetailPage isAuthenticated={!!session} program={program} />\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/ProgramSessionClient.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { ArrowLeft, Play } from \"lucide-react\";\nimport { ExerciseAttributeNameEnum, ProgramWeek } from \"@prisma/client\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport { canStartSession } from \"@/shared/lib/access-control\";\nimport { WorkoutSessionSets } from \"@/features/workout-session/ui/workout-session-sets\";\nimport { WorkoutSessionHeader } from \"@/features/workout-session/ui/workout-session-header\";\nimport { useWorkoutSession } from \"@/features/workout-session/model/use-workout-session\";\nimport { SessionAccessGuard } from \"@/features/programs/ui/session-access-guard\";\nimport { getSessionDescription, getSessionTitle, getSessionSlug, getProgramTitle } from \"@/features/programs/lib/translations-mapper\";\nimport { startProgramSession } from \"@/features/programs/actions/start-program-session.action\";\nimport { enrollInProgram } from \"@/features/programs/actions/enroll-program.action\";\nimport { completeProgramSession } from \"@/features/programs/actions/complete-program-session.action\";\nimport { ProgramSessionWithExercises } from \"@/entities/program-session/types/program-session.types\";\nimport { ProgramI18nReference } from \"@/entities/program/types/program.types\";\nimport { Button } from \"@/components/ui/button\";\nimport { SessionRichSnippets } from \"@/components/seo/session-rich-snippets\";\n\ninterface ProgramSessionClientProps {\n  program: ProgramI18nReference;\n  week: ProgramWeek;\n  session: ProgramSessionWithExercises;\n  isAuthenticated: boolean;\n  isPremium: boolean;\n}\n\nexport function ProgramSessionClient({ program, week, session, isAuthenticated, isPremium }: ProgramSessionClientProps) {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n  const router = useRouter();\n  const { startWorkout, session: workoutSession, completeWorkout, isWorkoutActive, quitWorkout } = useWorkoutSession();\n  const [isLoading, setIsLoading] = useState(false);\n  const [_enrollmentId, setEnrollmentId] = useState<string | null>(null);\n  const [sessionProgressId, setSessionProgressId] = useState<string | null>(null);\n  const [showCongrats, setShowCongrats] = useState(false);\n  const [hasStartedWorkout, setHasStartedWorkout] = useState(false);\n\n  const programTitle = getProgramTitle(program, locale);\n  const programSessionTitle = getSessionTitle(session, locale);\n  const programSessionDescription = getSessionDescription(session, locale);\n  const programSlug = getSessionSlug(program, locale);\n  const sessionSlug = getSessionSlug(session, locale);\n\n  // Access control context\n  const accessContext = {\n    isAuthenticated,\n    isPremium,\n    isSessionPremium: session.isPremium,\n  };\n\n  const handleStartWorkout = async () => {\n    if (!canStartSession(accessContext)) return;\n\n    setIsLoading(true);\n    try {\n      // Ensure user is enrolled\n      const { enrollment } = await enrollInProgram(program.id);\n      setEnrollmentId(enrollment.id);\n\n      // Start or resume session\n      const { sessionProgress } = await startProgramSession(enrollment.id, session.id);\n      setSessionProgressId(sessionProgress.id);\n\n      // Convert program exercises to workout format\n      const exercises = session.exercises.map((ex) => ({\n        id: ex.exercise.id,\n        name: ex.exercise.name,\n        nameEn: ex.exercise.nameEn || null,\n        description: ex.exercise.description || \"\",\n        descriptionEn: ex.exercise.descriptionEn || \"\",\n        fullVideoUrl: ex.exercise.fullVideoUrl || null,\n        fullVideoImageUrl: ex.exercise.fullVideoImageUrl || null,\n        introduction: null,\n        introductionEn: null,\n        slug: null,\n        slugEn: null,\n        createdAt: new Date(),\n        updatedAt: new Date(),\n        order: ex.order,\n        attributes: ex.exercise.attributes.map((attr) => ({\n          id: attr.id,\n          createdAt: new Date(),\n          updatedAt: new Date(),\n          exerciseId: ex.exercise.id,\n          attributeNameId: attr.attributeNameId,\n          attributeValueId: attr.attributeValueId,\n          attributeName: attr.attributeName,\n          attributeValue: attr.attributeValue,\n        })),\n      }));\n\n      // Extract equipment and muscles from session exercises\n      const equipment = session.exercises.flatMap((ex) =>\n        ex.exercise.attributes\n          .filter((attr) => attr.attributeName === ExerciseAttributeNameEnum.EQUIPMENT)\n          .map((attr) => attr.attributeValue),\n      );\n\n      const muscles = session.exercises.flatMap((ex) =>\n        ex.exercise.attributes\n          .filter((attr) => attr.attributeName === ExerciseAttributeNameEnum.PRIMARY_MUSCLE)\n          .map((attr) => attr.attributeValue),\n      );\n\n      // Convert suggestedSets to workout format\n      const exercisesWithSets = exercises.map((exercise, idx) => {\n        const programExercise = session.exercises[idx];\n        const suggestedSets = programExercise?.suggestedSets || [];\n\n        const workoutSets = suggestedSets.map((suggestedSet, setIndex) => ({\n          id: `${exercise.id}-set-${setIndex + 1}`,\n          setIndex,\n          types: suggestedSet.types || [],\n          valuesInt: suggestedSet.valuesInt || [],\n          valuesSec: suggestedSet.valuesSec || [],\n          units: suggestedSet.units || [],\n          completed: false,\n        }));\n\n        return {\n          ...exercise,\n          sets:\n            workoutSets.length > 0\n              ? workoutSets\n              : [\n                  {\n                    id: `${exercise.id}-set-1`,\n                    setIndex: 0,\n                    types: [\"REPS\"],\n                    valuesInt: [],\n                    valuesSec: [],\n                    units: [],\n                    completed: false,\n                  },\n                ],\n        };\n      });\n\n      startWorkout(exercisesWithSets, equipment, muscles);\n      setHasStartedWorkout(true);\n    } catch (error) {\n      console.error(\"Failed to start session:\", error);\n      alert(t(\"programs.error_starting_session\"));\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleCompleteSession = async () => {\n    if (!workoutSession || !sessionProgressId) return;\n\n    try {\n      // Complete the workout\n      completeWorkout();\n\n      // Save to database and mark session as complete\n      const { isCompleted, nextWeek, nextSession } = await completeProgramSession(sessionProgressId, workoutSession.id);\n\n      setShowCongrats(true);\n\n      if (isCompleted) {\n        router.push(`/programs/${programSlug}?completed=true&refresh=${Date.now()}`);\n      } else {\n        router.push(`/programs/${programSlug}?week=${nextWeek}&session=${nextSession}&refresh=${Date.now()}`);\n      }\n    } catch (error) {\n      console.error(\"Failed to complete session:\", error);\n    }\n  };\n\n  const handleQuitWorkout = () => {\n    quitWorkout();\n    setHasStartedWorkout(false);\n  };\n\n  // Show workout interface if user has started the workout\n  if (hasStartedWorkout && isWorkoutActive && workoutSession) {\n    return (\n      <div className=\"w-full max-w-6xl mx-auto\">\n        <WorkoutSessionHeader onQuitWorkout={handleQuitWorkout} />\n        <WorkoutSessionSets isWorkoutActive={isWorkoutActive} onCongrats={handleCompleteSession} showCongrats={showCongrats} />\n      </div>\n    );\n  }\n\n  const totalSets = session.exercises.reduce((total, ex) => total + ex.suggestedSets.length, 0);\n\n  // Use access guard to handle authentication and premium restrictions\n  return (\n    <SessionAccessGuard\n      context={accessContext}\n      programSlug={programSlug}\n      sessionDescription={programSessionDescription}\n      sessionSlug={sessionSlug}\n      sessionTitle={programSessionTitle}\n    >\n      <div className=\"flex flex-col h-screen\">\n        {/* Header */}\n        <div className=\"bg-gradient-to-r from-[#4F8EF7] to-[#25CB78] text-white p-4\">\n          <div className=\"flex items-center gap-4 mb-2\">\n            <Button\n              className=\"text-white hover:bg-white/20\"\n              onClick={() => router.push(`/programs/${programSlug}`)}\n              size=\"icon\"\n              variant=\"ghost\"\n            >\n              <ArrowLeft size={20} />\n            </Button>\n            <div className=\"flex-1\">\n              <p className=\"text-sm opacity-90\">\n                {programTitle} - {t(\"programs.week\")} {week.weekNumber}\n              </p>\n              <h1 className=\"text-xl font-bold\">{programSessionTitle}</h1>\n            </div>\n          </div>\n        </div>\n\n        {/* Session preview content */}\n        <main className=\"flex-1 bg-gray-50 dark:bg-gray-900 p-2 md:p-6\">\n          <div className=\"max-w-4xl mx-auto\">\n            <article className=\"bg-white dark:bg-gray-800 rounded-lg p-2 sm:p-6 mb-6\">\n              {/* Session info */}\n              <header className=\"flex items-center justify-between mb-6 flex-col text-center\">\n                <div className=\"mt-5 sm:mt-0 flex flex-col gap-2\">\n                  <h1 className=\"text-2xl font-bold text-gray-900 dark:text-white\">{programSessionTitle}</h1>\n                  <SessionRichSnippets\n                    duration={Math.round(session.exercises.length * 3)}\n                    exerciseCount={session.exercises.length}\n                    totalSets={totalSets}\n                  />\n                </div>\n                {programSessionDescription && <p className=\"text-gray-600 dark:text-gray-400 mt-2\">{programSessionDescription}</p>}\n              </header>\n\n              {/* Exercise list */}\n              <section className=\"space-y-4 mb-8\">\n                <h2 className=\"text-lg font-semibold text-gray-900 dark:text-white\">{t(\"programs.exercises_in_session\")}</h2>\n                <div className=\"grid gap-3\">\n                  {session.exercises.map((exercise, index) => {\n                    const exerciseName = locale === \"fr\" ? exercise.exercise.name : exercise.exercise.nameEn;\n                    return (\n                      <div className=\"flex items-center p-4 bg-gray-50 dark:bg-gray-700 rounded-lg\" key={exercise.id}>\n                        <div className=\"w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-4\">\n                          {index + 1}\n                        </div>\n                        <div className=\"flex-1\">\n                          <h3 className=\"font-medium text-gray-900 dark:text-white\">{exerciseName}</h3>\n                        </div>\n                        <div className=\"text-right\">\n                          <span className=\"text-sm text-gray-500 dark:text-gray-400\">\n                            {exercise.suggestedSets.length} {t(\"programs.set\", { count: exercise.suggestedSets.length })}\n                          </span>\n                        </div>\n                      </div>\n                    );\n                  })}\n                </div>\n              </section>\n\n              {/* Start workout button */}\n              <footer className=\"flex justify-center\">\n                <Button\n                  className=\"bg-gradient-to-r from-[#4F8EF7] to-[#25CB78] hover:from-[#4F8EF7]/80 hover:to-[#25CB78]/80 text-white px-8 py-4 text-lg font-bold rounded-xl flex items-center gap-3\"\n                  disabled={isLoading}\n                  onClick={handleStartWorkout}\n                >\n                  {isLoading ? (\n                    <>\n                      <div className=\"animate-spin rounded-full h-5 w-5 border-b-2 border-white\"></div>\n                      {t(\"programs.starting_session\")}\n                    </>\n                  ) : (\n                    <>\n                      <Play size={20} />\n                      {t(\"programs.start_session\")}\n                    </>\n                  )}\n                </Button>\n              </footer>\n            </article>\n          </div>\n        </main>\n      </div>\n    </SessionAccessGuard>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/page.tsx",
    "content": "import { notFound } from \"next/navigation\";\nimport { headers } from \"next/headers\";\nimport { Metadata } from \"next\";\n\nimport { Locale } from \"locales/types\";\nimport { getI18n } from \"locales/server\";\nimport { generateStructuredData, StructuredDataScript } from \"@/shared/lib/structured-data\";\nimport { getSessionTitle, getProgramTitle } from \"@/features/programs/lib/translations-mapper\";\nimport { generateSessionMetadata } from \"@/features/programs/lib/session-metadata\";\nimport { getSessionBySlug } from \"@/features/programs/actions/get-session-by-slug.action\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\nimport { Breadcrumbs } from \"@/components/seo/breadcrumbs\";\n\n// Import the existing session client component\nimport { ProgramSessionClient } from \"./ProgramSessionClient\";\n\ninterface SessionDetailPageProps {\n  params: Promise<{ slug: string; sessionSlug: string; locale: Locale }>;\n}\n\nexport async function generateMetadata({ params }: SessionDetailPageProps): Promise<Metadata> {\n  const { slug, sessionSlug, locale } = await params;\n  const t = await getI18n();\n  const response = await getSessionBySlug(slug, sessionSlug, locale);\n\n  if (!response) {\n    return { title: t(\"programs.not_found\") };\n  }\n\n  const sessionMetadata = generateSessionMetadata(response.session, response.program, locale);\n  const imageUrl = response.session.exercises[0]?.exercise.fullVideoImageUrl || \"/images/default-workout.jpg\";\n\n  return {\n    title: sessionMetadata.title,\n    description: sessionMetadata.description,\n    keywords: sessionMetadata.keywords,\n    openGraph: {\n      title: sessionMetadata.title,\n      description: sessionMetadata.description,\n      url: `https://www.workout.cool/${locale}/programs/${slug}/session/${sessionSlug}`,\n      siteName: \"Workout Cool\",\n      images: [\n        {\n          url: imageUrl,\n          width: 800,\n          height: 600,\n          alt: sessionMetadata.title,\n        },\n      ],\n      locale: locale === \"zh-CN\" ? \"zh_CN\" : locale.replace(\"-\", \"_\"),\n      type: \"website\",\n    },\n    twitter: {\n      card: \"summary_large_image\",\n      title: sessionMetadata.title,\n      description: sessionMetadata.description,\n      images: [imageUrl],\n      creator: \"@WorkoutCool\",\n    },\n    alternates: {\n      canonical: `https://www.workout.cool/${locale}/programs/${slug}/session/${sessionSlug}`,\n      languages: {\n        \"fr-FR\": `https://www.workout.cool/fr/programs/${slug}/session/${sessionSlug}`,\n        \"en-US\": `https://www.workout.cool/en/programs/${slug}/session/${sessionSlug}`,\n        \"es-ES\": `https://www.workout.cool/es/programs/${slug}/session/${sessionSlug}`,\n        \"pt-PT\": `https://www.workout.cool/pt/programs/${slug}/session/${sessionSlug}`,\n        \"ru-RU\": `https://www.workout.cool/ru/programs/${slug}/session/${sessionSlug}`,\n        \"zh-CN\": `https://www.workout.cool/zh-CN/programs/${slug}/session/${sessionSlug}`,\n        \"x-default\": `https://www.workout.cool/programs/${slug}/session/${sessionSlug}`,\n      },\n    },\n    robots: {\n      index: true,\n      follow: true,\n      googleBot: {\n        index: true,\n        follow: true,\n        \"max-video-preview\": -1,\n        \"max-image-preview\": \"large\",\n        \"max-snippet\": -1,\n      },\n    },\n  };\n}\n\nexport default async function SessionDetailPage({ params }: SessionDetailPageProps) {\n  const { slug, sessionSlug, locale } = await params;\n  const response = await getSessionBySlug(slug, sessionSlug, locale);\n\n  if (!response) {\n    notFound();\n  }\n\n  const authSession = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  // Pass authentication and premium status\n  const isAuthenticated = !!authSession?.user;\n  const isPremium = authSession?.user?.isPremium || false;\n\n  const t = await getI18n();\n  const sessionTitle = getSessionTitle(response.session, locale);\n  const programTitle = getProgramTitle(response.program, locale);\n\n  // Generate breadcrumb items\n  const breadcrumbItems = [\n    {\n      label: t(\"breadcrumbs.home\"),\n      href: `/${locale}`,\n    },\n    {\n      label: t(\"programs.workout_programs\"),\n      href: `/${locale}/programs`,\n    },\n    {\n      label: programTitle,\n      href: `/${locale}/programs/${slug}`,\n    },\n    {\n      label: sessionTitle,\n      current: true,\n    },\n  ];\n\n  // Generate VideoObject structured data\n  const sessionStructuredData = generateStructuredData({\n    type: \"VideoObject\",\n    locale,\n    title: `${sessionTitle} - ${programTitle}`,\n    description: response.session.description || `${sessionTitle} workout session`,\n    url: `https://www.workout.cool/${locale}/programs/${slug}/session/${sessionSlug}`,\n    image: response.session.exercises[0]?.exercise.fullVideoImageUrl || undefined,\n    sessionData: {\n      duration: Math.round(response.session.exercises.length * 3), // Estimate 3 min per exercise\n      exercises: response.session.exercises.map((ex) => ({\n        name: ex.exercise.name,\n        sets: ex.suggestedSets.length,\n      })),\n      thumbnailUrl: response.session.exercises[0]?.exercise.fullVideoImageUrl || undefined,\n      videoUrl: response.session.exercises[0]?.exercise.fullVideoUrl || undefined,\n    },\n  });\n\n  return (\n    <>\n      <StructuredDataScript data={sessionStructuredData} />\n      <Breadcrumbs items={breadcrumbItems} />\n      <ProgramSessionClient\n        isAuthenticated={isAuthenticated}\n        isPremium={isPremium}\n        program={response.program}\n        session={response.session}\n        week={response.week}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/programs/page.tsx",
    "content": "import { Metadata } from \"next\";\n\nimport { Locale } from \"locales/types\";\nimport { getI18n } from \"locales/server\";\nimport { ProgramsPage } from \"@/features/programs/ui/programs-page\";\nimport { Breadcrumbs } from \"@/components/seo/breadcrumbs\";\n\nexport const metadata: Metadata = {\n  title: \"Programmes\",\n  description: \"Découvrez nos programmes d'entraînement gamifiés pour tous les niveaux - Rejoins la communauté WorkoutCool !\",\n};\n\nexport default async function ProgramsRootPage({ params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = (await params) as { locale: Locale };\n  const t = await getI18n();\n\n  const breadcrumbItems = [\n    {\n      label: t(\"breadcrumbs.home\"),\n      href: `/${locale}`,\n    },\n    {\n      label: t(\"programs.workout_programs\"),\n      current: true,\n    },\n  ];\n\n  return (\n    <>\n      <Breadcrumbs items={breadcrumbItems} />\n      <ProgramsPage locale={locale} />\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/statistics/page.tsx",
    "content": "import React from \"react\";\n\nimport { getI18n } from \"locales/server\";\nimport { ExercisesBrowser } from \"@/features/statistics/components/ExercisesBrowser\";\nimport { PremiumGate } from \"@/components/ui/premium-gate\";\n\nexport default async function StatisticsPage() {\n  const t = await getI18n();\n  return (\n    <div className=\"container mx-auto px-2 sm:px-4 py-8 max-w-7xl\">\n      <div className=\"text-center mb-12\">\n        <h1 className=\"text-4xl sm:text-6xl font-black mb-4 bg-gradient-to-r from-[#4F8EF7] via-[#8B5CF6] to-[#25CB78] bg-clip-text text-transparent\">\n          {t(\"statistics.title\")}\n        </h1>\n        <p className=\"text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto\">{t(\"statistics.page_subtitle\")}</p>\n\n        {/* Stats hero social proof */}\n        <PremiumGate\n          fallback={\n            <div className=\"flex justify-center gap-8 mb-8\">\n              <div className=\"text-center\">\n                <p className=\"text-3xl font-bold text-[#4F8EF7]\">15.4K+</p>\n                <p className=\"text-sm text-gray-500 dark:text-gray-400\">{t(\"statistics.active_daily_users\")}</p>\n              </div>\n              <div className=\"text-center\">\n                <p className=\"text-3xl font-bold text-[#25CB78]\">89%</p>\n                <p className=\"text-sm text-gray-500 dark:text-gray-400\">{t(\"statistics.success_rate\")}</p>\n              </div>\n              <div className=\"text-center\">\n                <p className=\"text-3xl font-bold text-[#8B5CF6]\">4.8★</p>\n                <p className=\"text-sm text-gray-500 dark:text-gray-400\">{t(\"statistics.user_rating\")}</p>\n              </div>\n            </div>\n          }\n          feature=\"Statistics\"\n        >\n          {/* this is the premium content ↓ */}\n          <div />\n        </PremiumGate>\n      </div>\n\n      {/* Main Content */}\n      <ExercisesBrowser />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/bmi-calculator/bmi-calculator.utils.ts",
    "content": "import { TFunction } from \"locales/client\";\n\nexport type UnitSystem = \"metric\" | \"imperial\";\n\nexport interface BmiData {\n  height: number; // cm for metric, inches for imperial\n  weight: number; // kg for metric, lbs for imperial\n  unit: UnitSystem;\n}\n\nexport interface BmiResult {\n  bmi: number;\n  bmiPrime: number;\n  ponderalIndex: number;\n  category: BmiCategory;\n  healthRisk: HealthRisk;\n  recommendations: string[];\n  detailedInfo: {\n    bmiRange: { min: number; max: number };\n    idealWeight: { min: number; max: number };\n    weightToLose?: number;\n    weightToGain?: number;\n  };\n}\n\nexport type BmiCategory = \n  | \"severe_thinness\" \n  | \"moderate_thinness\" \n  | \"mild_thinness\" \n  | \"normal\" \n  | \"overweight\" \n  | \"obese_class_1\" \n  | \"obese_class_2\" \n  | \"obese_class_3\";\n\nexport type HealthRisk = \"low\" | \"normal\" | \"increased\" | \"high\" | \"very_high\" | \"extremely_high\";\n\nexport function calculateBmi(data: BmiData, t: TFunction): BmiResult {\n  const { height: initialHeight, weight: initialWeight, unit } = data;\n  let height = initialHeight;\n  let weight = initialWeight;\n\n  // Convert to metric if needed\n  if (unit === \"imperial\") {\n    height = height * 2.54; // inches to cm\n    weight = weight * 0.453592; // lbs to kg\n  }\n\n  // Convert height from cm to meters\n  const heightInMeters = height / 100;\n\n  // Calculate BMI\n  const bmi = weight / (heightInMeters * heightInMeters);\n\n  // Calculate BMI Prime\n  const bmiPrime = bmi / 25;\n\n  // Calculate Ponderal Index\n  const ponderalIndex = weight / (heightInMeters * heightInMeters * heightInMeters);\n\n  // Determine category and health risk\n  const category = getBmiCategory(bmi);\n  const healthRisk = getHealthRisk(category);\n  const recommendations = getRecommendations(category, t);\n\n  // Calculate detailed info\n  const bmiRange = getBmiRange(category);\n  const idealWeight = calculateIdealWeight(heightInMeters);\n  const weightToLose = (category === \"overweight\" || category === \"obese_class_1\" || category === \"obese_class_2\" || category === \"obese_class_3\")\n    ? Math.max(0, weight - idealWeight.max)\n    : undefined;\n  const weightToGain = (category === \"severe_thinness\" || category === \"moderate_thinness\" || category === \"mild_thinness\")\n    ? Math.max(0, idealWeight.min - weight)\n    : undefined;\n\n  return {\n    bmi: Math.round(bmi * 10) / 10,\n    bmiPrime: Math.round(bmiPrime * 100) / 100,\n    ponderalIndex: Math.round(ponderalIndex * 10) / 10,\n    category,\n    healthRisk,\n    recommendations,\n    detailedInfo: {\n      bmiRange,\n      idealWeight,\n      weightToLose,\n      weightToGain,\n    },\n  };\n}\n\nexport function getBmiCategory(bmi: number): BmiCategory {\n  if (bmi < 16) return \"severe_thinness\";\n  if (bmi < 17) return \"moderate_thinness\";\n  if (bmi < 18.5) return \"mild_thinness\";\n  if (bmi < 25) return \"normal\";\n  if (bmi < 30) return \"overweight\";\n  if (bmi < 35) return \"obese_class_1\";\n  if (bmi < 40) return \"obese_class_2\";\n  return \"obese_class_3\";\n}\n\nexport function getHealthRisk(category: BmiCategory): HealthRisk {\n  switch (category) {\n    case \"severe_thinness\":\n      return \"very_high\";\n    case \"moderate_thinness\":\n      return \"high\";\n    case \"mild_thinness\":\n      return \"increased\";\n    case \"normal\":\n      return \"normal\";\n    case \"overweight\":\n      return \"increased\";\n    case \"obese_class_1\":\n      return \"high\";\n    case \"obese_class_2\":\n      return \"very_high\";\n    case \"obese_class_3\":\n      return \"extremely_high\";\n    default:\n      return \"normal\";\n  }\n}\n\nexport function getRecommendations(category: BmiCategory, t: TFunction): string[] {\n  switch (category) {\n    case \"severe_thinness\":\n      return [\n        t(\"bmi-calculator.recommendations.severe_thinness.medical_consultation\"),\n        t(\"bmi-calculator.recommendations.severe_thinness.nutritional_assessment\"),\n        t(\"bmi-calculator.recommendations.severe_thinness.weight_gain_program\"),\n        t(\"bmi-calculator.recommendations.severe_thinness.screen_conditions\"),\n        t(\"bmi-calculator.recommendations.severe_thinness.psychological_evaluation\"),\n      ];\n    case \"moderate_thinness\":\n      return [\n        t(\"bmi-calculator.recommendations.moderate_thinness.healthcare_provider\"),\n        t(\"bmi-calculator.recommendations.moderate_thinness.nutrient_dense_foods\"),\n        t(\"bmi-calculator.recommendations.moderate_thinness.registered_dietitian\"),\n        t(\"bmi-calculator.recommendations.moderate_thinness.monitor_malnutrition\"),\n        t(\"bmi-calculator.recommendations.moderate_thinness.gradual_weight_gain\"),\n      ];\n    case \"mild_thinness\":\n      return [\n        t(\"bmi-calculator.recommendations.mild_thinness.consider_healthcare\"),\n        t(\"bmi-calculator.recommendations.mild_thinness.nutrient_dense_foods\"),\n        t(\"bmi-calculator.recommendations.mild_thinness.strength_training\"),\n        t(\"bmi-calculator.recommendations.mild_thinness.monitor_health\"),\n        t(\"bmi-calculator.recommendations.mild_thinness.gradual_weight_gain\"),\n      ];\n    case \"normal\":\n      return [\n        t(\"bmi-calculator.recommendations.normal.maintain_weight\"),\n        t(\"bmi-calculator.recommendations.normal.physical_activity\"),\n        t(\"bmi-calculator.recommendations.normal.balanced_diet\"),\n        t(\"bmi-calculator.recommendations.normal.health_checkups\"),\n        t(\"bmi-calculator.recommendations.normal.overall_wellness\"),\n      ];\n    case \"overweight\":\n      return [\n        t(\"bmi-calculator.recommendations.overweight.gradual_weight_loss\"),\n        t(\"bmi-calculator.recommendations.overweight.increase_activity\"),\n        t(\"bmi-calculator.recommendations.overweight.portion_control\"),\n        t(\"bmi-calculator.recommendations.overweight.healthcare_provider\"),\n        t(\"bmi-calculator.recommendations.overweight.lifestyle_goals\"),\n      ];\n    case \"obese_class_1\":\n      return [\n        t(\"bmi-calculator.recommendations.obese_class_1.healthcare_provider\"),\n        t(\"bmi-calculator.recommendations.obese_class_1.weight_loss_target\"),\n        t(\"bmi-calculator.recommendations.obese_class_1.diet_exercise\"),\n        t(\"bmi-calculator.recommendations.obese_class_1.nutritional_counseling\"),\n        t(\"bmi-calculator.recommendations.obese_class_1.screen_conditions\"),\n      ];\n    case \"obese_class_2\":\n      return [\n        t(\"bmi-calculator.recommendations.obese_class_2.medical_supervision\"),\n        t(\"bmi-calculator.recommendations.obese_class_2.lifestyle_programs\"),\n        t(\"bmi-calculator.recommendations.obese_class_2.evaluate_conditions\"),\n        t(\"bmi-calculator.recommendations.obese_class_2.medical_treatments\"),\n        t(\"bmi-calculator.recommendations.obese_class_2.bariatric_surgery\"),\n      ];\n    case \"obese_class_3\":\n      return [\n        t(\"bmi-calculator.recommendations.obese_class_3.medical_consultation\"),\n        t(\"bmi-calculator.recommendations.obese_class_3.bariatric_surgery\"),\n        t(\"bmi-calculator.recommendations.obese_class_3.weight_management\"),\n        t(\"bmi-calculator.recommendations.obese_class_3.health_complications\"),\n        t(\"bmi-calculator.recommendations.obese_class_3.multidisciplinary\"),\n      ];\n    default:\n      return [];\n  }\n}\n\nexport function getBmiRange(category: BmiCategory): { min: number; max: number } {\n  switch (category) {\n    case \"severe_thinness\":\n      return { min: 0, max: 15.9 };\n    case \"moderate_thinness\":\n      return { min: 16, max: 16.9 };\n    case \"mild_thinness\":\n      return { min: 17, max: 18.4 };\n    case \"normal\":\n      return { min: 18.5, max: 24.9 };\n    case \"overweight\":\n      return { min: 25, max: 29.9 };\n    case \"obese_class_1\":\n      return { min: 30, max: 34.9 };\n    case \"obese_class_2\":\n      return { min: 35, max: 39.9 };\n    case \"obese_class_3\":\n      return { min: 40, max: 100 };\n    default:\n      return { min: 0, max: 100 };\n  }\n}\n\nexport function calculateIdealWeight(heightInMeters: number): { min: number; max: number } {\n  // Calculate ideal weight range based on normal BMI (18.5-24.9)\n  const minWeight = 18.5 * heightInMeters * heightInMeters;\n  const maxWeight = 24.9 * heightInMeters * heightInMeters;\n  \n  return {\n    min: Math.round(minWeight * 10) / 10,\n    max: Math.round(maxWeight * 10) / 10,\n  };\n}\n\nexport function convertHeight(height: number, fromUnit: UnitSystem, toUnit: UnitSystem): number {\n  if (fromUnit === toUnit) return height;\n  \n  if (fromUnit === \"imperial\" && toUnit === \"metric\") {\n    return height * 2.54; // inches to cm\n  } else {\n    return height / 2.54; // cm to inches\n  }\n}\n\nexport function convertWeight(weight: number, fromUnit: UnitSystem, toUnit: UnitSystem): number {\n  if (fromUnit === toUnit) return weight;\n  \n  if (fromUnit === \"imperial\" && toUnit === \"metric\") {\n    return weight * 0.453592; // lbs to kg\n  } else {\n    return weight / 0.453592; // kg to lbs\n  }\n}\n\n// Additional utility functions for enhanced BMI analysis\n\nexport function getBmiPrimeCategory(bmiPrime: number): string {\n  if (bmiPrime < 0.64) return \"severe_thinness\";\n  if (bmiPrime < 0.68) return \"moderate_thinness\";\n  if (bmiPrime < 0.74) return \"mild_thinness\";\n  if (bmiPrime <= 1) return \"normal\";\n  if (bmiPrime <= 1.2) return \"overweight\";\n  if (bmiPrime <= 1.4) return \"obese_class_1\";\n  if (bmiPrime <= 1.6) return \"obese_class_2\";\n  return \"obese_class_3\";\n}\n\nexport function getPonderalIndexCategory(pi: number): string {\n  // Ponderal Index normal range is typically 11-14 kg/m³\n  if (pi < 11) return \"low\";\n  if (pi <= 14) return \"normal\";\n  return \"high\";\n}\n\nexport function getHealthRisks(_category: BmiCategory, t: TFunction): { overweight: string[]; underweight: string[] } {\n  const overweightRisks = [\n    t(\"bmi-calculator.health_risks.overweight.high_blood_pressure\"),\n    t(\"bmi-calculator.health_risks.overweight.ldl_cholesterol\"),\n    t(\"bmi-calculator.health_risks.overweight.hdl_cholesterol\"),\n    t(\"bmi-calculator.health_risks.overweight.triglycerides\"),\n    t(\"bmi-calculator.health_risks.overweight.type_2_diabetes\"),\n    t(\"bmi-calculator.health_risks.overweight.coronary_heart_disease\"),\n    t(\"bmi-calculator.health_risks.overweight.stroke\"),\n    t(\"bmi-calculator.health_risks.overweight.gallbladder_disease\"),\n    t(\"bmi-calculator.health_risks.overweight.osteoarthritis\"),\n    t(\"bmi-calculator.health_risks.overweight.sleep_apnea\"),\n    t(\"bmi-calculator.health_risks.overweight.certain_cancers\"),\n    t(\"bmi-calculator.health_risks.overweight.low_quality_life\"),\n    t(\"bmi-calculator.health_risks.overweight.mental_illnesses\"),\n    t(\"bmi-calculator.health_risks.overweight.body_pains\"),\n    t(\"bmi-calculator.health_risks.overweight.increased_mortality\"),\n  ];\n\n  const underweightRisks = [\n    t(\"bmi-calculator.health_risks.underweight.malnutrition\"),\n    t(\"bmi-calculator.health_risks.underweight.anemia\"),\n    t(\"bmi-calculator.health_risks.underweight.osteoporosis\"),\n    t(\"bmi-calculator.health_risks.underweight.immune_function\"),\n    t(\"bmi-calculator.health_risks.underweight.growth_development\"),\n    t(\"bmi-calculator.health_risks.underweight.reproductive_issues\"),\n    t(\"bmi-calculator.health_risks.underweight.miscarriage_risk\"),\n    t(\"bmi-calculator.health_risks.underweight.surgery_complications\"),\n    t(\"bmi-calculator.health_risks.underweight.increased_mortality\"),\n    t(\"bmi-calculator.health_risks.underweight.underlying_conditions\"),\n  ];\n\n  return { overweight: overweightRisks, underweight: underweightRisks };\n}"
  },
  {
    "path": "app/[locale]/(app)/tools/bmi-calculator/page.tsx",
    "content": "import React from \"react\";\nimport { Metadata } from \"next\";\n\nimport { getI18n } from \"locales/server\";\nimport { BmiEducationalContent } from \"app/[locale]/(app)/tools/bmi-calculator/shared/components/BmiEducationalContent\";\nimport { BmiCalculatorClient } from \"app/[locale]/(app)/tools/bmi-calculator/shared/BmiCalculatorClient\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { env } from \"@/env\";\nimport { generateSEOMetadata, SEOScripts } from \"@/components/seo/SEOHead\";\nimport { HorizontalBottomBanner, HorizontalTopBanner } from \"@/components/ads\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return generateSEOMetadata({\n    title: t(\"tools.bmi-calculator-hub.meta.title\"),\n    description: t(\"tools.bmi-calculator-hub.meta.description\"),\n    keywords: [\n      ...t(\"tools.bmi-calculator-hub.meta.keywords\").split(\", \"),\n      \"BMI formula\",\n      \"BMI prime\",\n      \"ponderal index\",\n      \"WHO BMI classification\",\n      \"CDC BMI percentiles\",\n      \"BMI health risks\",\n      \"BMI limitations\",\n      \"body mass index calculator\",\n      \"BMI chart\",\n      \"BMI table\",\n      \"overweight risks\",\n      \"underweight risks\",\n      \"BMI for adults\",\n      \"BMI for children\",\n      \"BMI accuracy\",\n    ],\n    locale,\n    canonical: `${getServerUrl()}/${locale}/tools/bmi-calculator`,\n    structuredData: {\n      type: \"Calculator\",\n      calculatorData: {\n        calculatorType: \"bmi\",\n        inputFields: [\"height\", \"weight\", \"age\", \"gender\"],\n        outputFields: [\n          \"BMI\",\n          \"BMI Prime\",\n          \"Ponderal Index\",\n          \"BMI category\",\n          \"health risk assessment\",\n          \"ideal weight range\",\n          \"health recommendations\",\n        ],\n        formula: \"BMI = weight (kg) / height (m)²\",\n        accuracy: \"Standard WHO classification with detailed health risk assessment\",\n        targetAudience: [\n          \"health conscious individuals\",\n          \"fitness enthusiasts\",\n          \"medical professionals\",\n          \"general public\",\n          \"parents\",\n          \"athletes\",\n        ],\n        relatedCalculators: [\"standard-calculator\", \"adjusted-calculator\", \"pediatric-calculator\", \"bmi-comparison\"],\n      },\n    },\n  });\n}\n\nexport default async function BmiCalculatorPage({ params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return (\n    <>\n      <SEOScripts\n        canonical={`${getServerUrl()}/${locale}/tools/bmi-calculator`}\n        description={t(\"tools.bmi-calculator-hub.meta.description\")}\n        locale={locale}\n        structuredData={{\n          type: \"Calculator\",\n          calculatorData: {\n            calculatorType: \"bmi\",\n            inputFields: [\"height\", \"weight\", \"age\", \"gender\"],\n            outputFields: [\"BMI\", \"BMI category\", \"health recommendations\"],\n            formula: \"BMI = weight (kg) / height (m)²\",\n            accuracy: \"Standard WHO classification\",\n            targetAudience: [\"health conscious individuals\", \"fitness enthusiasts\", \"medical professionals\", \"general public\"],\n            relatedCalculators: [\"standard-calculator\", \"adjusted-calculator\", \"pediatric-calculator\", \"bmi-comparison\"],\n          },\n        }}\n        title={t(\"tools.bmi-calculator-hub.meta.title\")}\n      />\n      <div className=\"light:bg-white dark:bg-base-200/20\">\n        {env.NEXT_PUBLIC_TOP_BMI_BANNER_AD_SLOT && <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_BMI_BANNER_AD_SLOT} />}\n        <div className=\"container mx-auto px-2 sm:px-4 py-4 sm:py-8 max-w-4xl\">\n          {/* Header */}\n          <div className=\"text-center max-w-3xl mx-auto mb-8\">\n            <h1 className=\"text-3xl sm:text-4xl font-bold mb-4 text-base-content dark:text-base-content/90\">\n              {t(\"tools.bmi-calculator-hub.standard.page_title\")}\n            </h1>\n            <p className=\"text-lg text-base-content/70 dark:text-base-content/60\">\n              {t(\"tools.bmi-calculator-hub.standard.page_description\")}\n            </p>\n          </div>\n\n          {/* Calculator */}\n          <BmiCalculatorClient />\n\n          {/* Educational Content */}\n          <div className=\"mt-16\">\n            <BmiEducationalContent />\n          </div>\n        </div>\n        {env.NEXT_PUBLIC_BOTTOM_BMI_BANNER_AD_SLOT && <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_BOTTOM_BMI_BANNER_AD_SLOT} />}\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/bmi-calculator/shared/BmiCalculatorClient.tsx",
    "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport {\n  BmiData,\n  BmiResult,\n  UnitSystem,\n  calculateBmi,\n  convertHeight,\n  convertWeight,\n} from \"app/[locale]/(app)/tools/bmi-calculator/bmi-calculator.utils\";\n\nimport { BmiWeightInput } from \"./components/BmiWeightInput\";\nimport { BmiUnitSelector } from \"./components/BmiUnitSelector\";\nimport { BmiResultsDisplay } from \"./components/BmiResultsDisplay\";\nimport { BmiHeightInput } from \"./components/BmiHeightInput\";\n\nexport function BmiCalculatorClient() {\n  const t = useI18n();\n  const [unit, setUnit] = useState<UnitSystem>(\"metric\");\n  const [height, setHeight] = useState<number>(170); // cm for metric, inches for imperial\n  const [weight, setWeight] = useState<number>(70); // kg for metric, lbs for imperial\n  const [result, setResult] = useState<BmiResult | null>(null);\n  const [isInitialized, setIsInitialized] = useState<boolean>(false);\n\n  // Convert values when unit system changes (but not on first render)\n  useEffect(() => {\n    if (!isInitialized) {\n      setIsInitialized(true);\n      return;\n    }\n\n    if (unit === \"imperial\") {\n      // Convert from metric to imperial\n      setHeight(Math.round(convertHeight(height, \"metric\", \"imperial\")));\n      setWeight(Math.round(convertWeight(weight, \"metric\", \"imperial\") * 10) / 10);\n    } else {\n      // Convert from imperial to metric\n      setHeight(Math.round(convertHeight(height, \"imperial\", \"metric\")));\n      setWeight(Math.round(convertWeight(weight, \"imperial\", \"metric\") * 10) / 10);\n    }\n  }, [unit]);\n\n  // Calculate BMI whenever inputs change\n  useEffect(() => {\n    if (height > 0 && weight > 0) {\n      const bmiData: BmiData = { height, weight, unit };\n      const bmiResult = calculateBmi(bmiData, t);\n      setResult(bmiResult);\n    } else {\n      setResult(null);\n    }\n  }, [height, weight, unit]);\n\n  return (\n    <div className=\"space-y-8\">\n      {/* Input Form */}\n      <div className=\"bg-base-100 dark:bg-base-200/30 rounded-2xl p-4 sm:p-8 border border-base-content/10\">\n        <div className=\"space-y-6\">\n          {/* Unit Selector */}\n          <BmiUnitSelector onChange={setUnit} value={unit} />\n\n          {/* Height and Weight Inputs */}\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n            <BmiHeightInput onChange={setHeight} unit={unit} value={height} />\n            <BmiWeightInput onChange={setWeight} unit={unit} value={weight} />\n          </div>\n        </div>\n      </div>\n\n      {/* Results */}\n      {result && (\n        <div className=\"bg-base-100 dark:bg-base-200/30 rounded-2xl p-4 sm:p-8 border border-base-content/10\">\n          <BmiResultsDisplay result={result} />\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/bmi-calculator/shared/components/BmiEducationalContent.tsx",
    "content": "\"use client\";\n\nimport { useI18n } from \"locales/client\";\nimport { env } from \"@/env\";\nimport { InArticle } from \"@/components/ads\";\n\nimport { FormulaCard, createFraction, createSuperscript } from \"./MathEquation\";\n\nexport function BmiEducationalContent() {\n  const t = useI18n();\n\n  return (\n    <div className=\"space-y-12 max-w-4xl mx-auto\">\n      {/* BMI Introduction */}\n      <section className=\"space-y-6\">\n        <h2 className=\"text-3xl font-bold text-base-content\">{t(\"bmi-calculator.educational.introduction_title\")}</h2>\n        <div className=\"prose prose-lg max-w-none text-base-content/80\">\n          <p>{t(\"bmi-calculator.educational.introduction_text\")}</p>\n          <p>{t(\"bmi-calculator.educational.introduction_usage\")}</p>\n        </div>\n      </section>\n\n      {/* BMI Tables */}\n      <section className=\"space-y-8\">\n        <div className=\"space-y-6\">\n          {env.NEXT_PUBLIC_IN_ARTICLE_BMI_1_AD_SLOT && <InArticle adSlot={env.NEXT_PUBLIC_IN_ARTICLE_BMI_1_AD_SLOT} />}\n          <h2 className=\"text-3xl font-bold text-base-content\">{t(\"bmi-calculator.educational.adult_table_title\")}</h2>\n\n          <p className=\"text-base-content/80\">{t(\"bmi-calculator.educational.adult_table_description\")}</p>\n\n          {/* WHO Adult BMI Table */}\n          <div className=\"overflow-x-auto\">\n            <table className=\"table table-zebra w-full bg-base-100\">\n              <thead>\n                <tr className=\"bg-primary text-primary-content\">\n                  <th className=\"text-left\">{t(\"bmi-calculator.educational.classification\")}</th>\n                  <th className=\"text-center\">{t(\"bmi-calculator.educational.bmi_range\")}</th>\n                </tr>\n              </thead>\n              <tbody>\n                <tr>\n                  <td className=\"font-medium\">{t(\"bmi-calculator.category_severe_thinness\")}</td>\n                  <td className=\"text-center font-mono\">&lt; 16</td>\n                </tr>\n                <tr>\n                  <td className=\"font-medium\">{t(\"bmi-calculator.category_moderate_thinness\")}</td>\n                  <td className=\"text-center font-mono\">16 - 17</td>\n                </tr>\n                <tr>\n                  <td className=\"font-medium\">{t(\"bmi-calculator.category_mild_thinness\")}</td>\n                  <td className=\"text-center font-mono\">17 - 18.5</td>\n                </tr>\n                <tr className=\"bg-success/20\">\n                  <td className=\"font-medium\">{t(\"bmi-calculator.category_normal\")}</td>\n                  <td className=\"text-center font-mono font-bold\">18.5 - 25</td>\n                </tr>\n                <tr>\n                  <td className=\"font-medium\">{t(\"bmi-calculator.category_overweight\")}</td>\n                  <td className=\"text-center font-mono\">25 - 30</td>\n                </tr>\n                <tr>\n                  <td className=\"font-medium\">{t(\"bmi-calculator.category_obese_class_1\")}</td>\n                  <td className=\"text-center font-mono\">30 - 35</td>\n                </tr>\n                <tr>\n                  <td className=\"font-medium\">{t(\"bmi-calculator.category_obese_class_2\")}</td>\n                  <td className=\"text-center font-mono\">35 - 40</td>\n                </tr>\n                <tr>\n                  <td className=\"font-medium\">{t(\"bmi-calculator.category_obese_class_3\")}</td>\n                  <td className=\"text-center font-mono\">&gt; 40</td>\n                </tr>\n              </tbody>\n            </table>\n          </div>\n        </div>\n\n        {/* Children BMI Table */}\n        <div className=\"space-y-6\">\n          <h2 className=\"text-3xl font-bold text-base-content\">{t(\"bmi-calculator.educational.children_table_title\")}</h2>\n          <p className=\"text-base-content/80\">{t(\"bmi-calculator.educational.children_table_description\")}</p>\n\n          <div className=\"overflow-x-auto\">\n            <table className=\"table table-zebra w-full bg-base-100\">\n              <thead>\n                <tr className=\"bg-primary text-primary-content\">\n                  <th className=\"text-left\">{t(\"bmi-calculator.educational.category\")}</th>\n                  <th className=\"text-center\">{t(\"bmi-calculator.educational.percentile_range\")}</th>\n                </tr>\n              </thead>\n              <tbody>\n                <tr>\n                  <td className=\"font-medium\">{t(\"bmi-calculator.educational.underweight\")}</td>\n                  <td className=\"text-center font-mono\">&lt; 5%</td>\n                </tr>\n                <tr className=\"bg-success/20\">\n                  <td className=\"font-medium\">{t(\"bmi-calculator.educational.healthy_weight\")}</td>\n                  <td className=\"text-center font-mono font-bold\">5% - 85%</td>\n                </tr>\n                <tr>\n                  <td className=\"font-medium\">{t(\"bmi-calculator.educational.at_risk_overweight\")}</td>\n                  <td className=\"text-center font-mono\">85% - 95%</td>\n                </tr>\n                <tr>\n                  <td className=\"font-medium\">{t(\"bmi-calculator.educational.overweight\")}</td>\n                  <td className=\"text-center font-mono\">&gt; 95%</td>\n                </tr>\n              </tbody>\n            </table>\n          </div>\n        </div>\n      </section>\n\n      {/* Health Risks */}\n      <section className=\"space-y-8\">\n        <div className=\"space-y-6\">\n          <h2 className=\"text-3xl font-bold text-base-content\">{t(\"bmi-calculator.educational.overweight_risks_title\")}</h2>\n          <p className=\"text-base-content/80\">{t(\"bmi-calculator.educational.overweight_risks_intro\")}</p>\n\n          <div className=\"grid md:grid-cols-2 gap-4\">\n            <div className=\"space-y-3\">\n              <h4 className=\"text-lg font-semibold text-base-content\">{t(\"bmi-calculator.educational.cardiovascular_risks\")}</h4>\n              <ul className=\"space-y-2 text-base-content/80\">\n                <li className=\"flex items-start gap-2\">\n                  <span className=\"text-error\">•</span>\n                  {t(\"bmi-calculator.educational.high_blood_pressure\")}\n                </li>\n                <li className=\"flex items-start gap-2\">\n                  <span className=\"text-error\">•</span>\n                  {t(\"bmi-calculator.educational.cholesterol_issues\")}\n                </li>\n                <li className=\"flex items-start gap-2\">\n                  <span className=\"text-error\">•</span>\n                  {t(\"bmi-calculator.educational.coronary_heart_disease\")}\n                </li>\n                <li className=\"flex items-start gap-2\">\n                  <span className=\"text-error\">•</span>\n                  {t(\"bmi-calculator.educational.stroke\")}\n                </li>\n              </ul>\n            </div>\n\n            <div className=\"space-y-3\">\n              <h4 className=\"text-lg font-semibold text-base-content\">{t(\"bmi-calculator.educational.metabolic_risks\")}</h4>\n              <ul className=\"space-y-2 text-base-content/80\">\n                <li className=\"flex items-start gap-2\">\n                  <span className=\"text-error\">•</span>\n                  {t(\"bmi-calculator.educational.type_2_diabetes\")}\n                </li>\n                <li className=\"flex items-start gap-2\">\n                  <span className=\"text-error\">•</span>\n                  {t(\"bmi-calculator.educational.gallbladder_disease\")}\n                </li>\n                <li className=\"flex items-start gap-2\">\n                  <span className=\"text-error\">•</span>\n                  {t(\"bmi-calculator.educational.sleep_apnea\")}\n                </li>\n                <li className=\"flex items-start gap-2\">\n                  <span className=\"text-error\">•</span>\n                  {t(\"bmi-calculator.educational.osteoarthritis\")}\n                </li>\n              </ul>\n            </div>\n          </div>\n\n          <div className=\"space-y-3\">\n            <h4 className=\"text-lg font-semibold text-base-content\">{t(\"bmi-calculator.educational.other_risks\")}</h4>\n            <ul className=\"grid md:grid-cols-2 gap-2 text-base-content/80\">\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-error\">•</span>\n                {t(\"bmi-calculator.educational.certain_cancers\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-error\">•</span>\n                {t(\"bmi-calculator.educational.mental_health_issues\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-error\">•</span>\n                {t(\"bmi-calculator.educational.reduced_quality_life\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-error\">•</span>\n                {t(\"bmi-calculator.educational.increased_mortality\")}\n              </li>\n            </ul>\n          </div>\n        </div>\n\n        {/* Underweight Risks */}\n        <div className=\"space-y-6\">\n          <h2 className=\"text-3xl font-bold text-base-content\">{t(\"bmi-calculator.educational.underweight_risks_title\")}</h2>\n          <p className=\"text-base-content/80\">{t(\"bmi-calculator.educational.underweight_risks_intro\")}</p>\n\n          <div className=\"grid md:grid-cols-2 gap-4\">\n            <ul className=\"space-y-2 text-base-content/80\">\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-warning mt-1\">•</span>\n                {t(\"bmi-calculator.educational.malnutrition\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-warning mt-1\">•</span>\n                {t(\"bmi-calculator.educational.osteoporosis\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-warning mt-1\">•</span>\n                {t(\"bmi-calculator.educational.immune_function_decrease\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-warning mt-1\">•</span>\n                {t(\"bmi-calculator.educational.growth_development_issues\")}\n              </li>\n            </ul>\n            <ul className=\"space-y-2 text-base-content/80\">\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-warning mt-1\">•</span>\n                {t(\"bmi-calculator.educational.reproductive_issues\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-warning mt-1\">•</span>\n                {t(\"bmi-calculator.educational.surgery_complications\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-warning mt-1\">•</span>\n                {t(\"bmi-calculator.educational.increased_mortality_underweight\")}\n              </li>\n            </ul>\n          </div>\n        </div>\n      </section>\n\n      {/* BMI Limitations */}\n      <section className=\"space-y-6\">\n        <h2 className=\"text-3xl font-bold text-base-content\">{t(\"bmi-calculator.limitations_title\")}</h2>\n        <div className=\"prose prose-lg max-w-none text-base-content/80\">\n          <p>{t(\"bmi-calculator.limitations_text\")}</p>\n        </div>\n\n        <div className=\"grid md:grid-cols-2 gap-6\">\n          <div className=\"space-y-4\">\n            <h4 className=\"text-lg font-semibold text-base-content\">{t(\"bmi-calculator.educational.adults_limitations\")}</h4>\n            <ul className=\"space-y-2 text-base-content/80\">\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-info mt-1\">•</span>\n                {t(\"bmi-calculator.educational.older_adults_fat\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-info mt-1\">•</span>\n                {t(\"bmi-calculator.educational.women_fat_difference\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-info mt-1\">•</span>\n                {t(\"bmi-calculator.educational.athletes_muscle_mass\")}\n              </li>\n            </ul>\n          </div>\n\n          <div className=\"space-y-4\">\n            <h4 className=\"text-lg font-semibold text-base-content\">{t(\"bmi-calculator.educational.children_limitations\")}</h4>\n            <ul className=\"space-y-2 text-base-content/80\">\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-info mt-1\">•</span>\n                {t(\"bmi-calculator.educational.height_maturation_influence\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-info mt-1\">•</span>\n                {t(\"bmi-calculator.educational.fat_free_mass_difference\")}\n              </li>\n              <li className=\"flex items-start gap-2\">\n                <span className=\"text-info mt-1\">•</span>\n                {t(\"bmi-calculator.educational.population_accuracy\")}\n              </li>\n            </ul>\n          </div>\n        </div>\n      </section>\n\n      {/* BMI Formulas */}\n      <section className=\"space-y-6\">\n        <h2 className=\"text-3xl font-bold text-base-content\">{t(\"bmi-calculator.educational.formulas_title\")}</h2>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          {/* Metric Formula */}\n          <FormulaCard\n            description=\"Standard international formula using kilograms and meters\"\n            equation={`BMI = ${createFraction(\"weight (kg)\", createSuperscript(\"height\", \"2\") + \" (m)\")}`}\n            example={`${createFraction(\"70\", createSuperscript(\"1.75\", \"2\"))} = 22.9`}\n            title={t(\"bmi-calculator.educational.metric_formula\")}\n          />\n\n          {/* Imperial Formula */}\n          <FormulaCard\n            description=\"US customary units formula with conversion factor\"\n            equation={`BMI = 703 × ${createFraction(\"weight (lbs)\", createSuperscript(\"height\", \"2\") + \" (in)\")}`}\n            example={`703 × ${createFraction(\"154\", createSuperscript(\"69\", \"2\"))} = 22.9`}\n            title={t(\"bmi-calculator.educational.imperial_formula\")}\n          />\n        </div>\n      </section>\n\n      {env.NEXT_PUBLIC_IN_ARTICLE_BMI_2_AD_SLOT && <InArticle adSlot={env.NEXT_PUBLIC_IN_ARTICLE_BMI_2_AD_SLOT} />}\n\n      {/* BMI Prime Section */}\n      <section className=\"space-y-6\">\n        <h2 className=\"text-3xl font-bold text-base-content\">{t(\"bmi-calculator.about_bmi_prime\")}</h2>\n        <div className=\"prose prose-lg max-w-none text-base-content/80\">\n          <p>{t(\"bmi-calculator.bmi_prime_explanation\")}</p>\n        </div>\n\n        <FormulaCard\n          description={t(\"bmi-calculator.educational.bmi_prime_description\")}\n          equation={`BMI Prime = ${createFraction(\"BMI\", \"25\")}`}\n          title={t(\"bmi-calculator.educational.bmi_prime_formula\")}\n        />\n\n        {/* BMI Prime Table */}\n        <div className=\"overflow-x-auto\">\n          <table className=\"table table-zebra w-full bg-base-100\">\n            <thead>\n              <tr className=\"bg-primary text-primary-content\">\n                <th className=\"text-left\">Classification</th>\n                <th className=\"text-center\">BMI</th>\n                <th className=\"text-center\">BMI Prime</th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr>\n                <td className=\"font-medium\">{t(\"bmi-calculator.category_severe_thinness\")}</td>\n                <td className=\"text-center font-mono\">&lt; 16</td>\n                <td className=\"text-center font-mono\">&lt; 0.64</td>\n              </tr>\n              <tr>\n                <td className=\"font-medium\">{t(\"bmi-calculator.category_moderate_thinness\")}</td>\n                <td className=\"text-center font-mono\">16 - 17</td>\n                <td className=\"text-center font-mono\">0.64 - 0.68</td>\n              </tr>\n              <tr>\n                <td className=\"font-medium\">{t(\"bmi-calculator.category_mild_thinness\")}</td>\n                <td className=\"text-center font-mono\">17 - 18.5</td>\n                <td className=\"text-center font-mono\">0.68 - 0.74</td>\n              </tr>\n              <tr className=\"bg-success/20\">\n                <td className=\"font-medium\">{t(\"bmi-calculator.category_normal\")}</td>\n                <td className=\"text-center font-mono font-bold\">18.5 - 25</td>\n                <td className=\"text-center font-mono font-bold\">0.74 - 1.0</td>\n              </tr>\n              <tr>\n                <td className=\"font-medium\">{t(\"bmi-calculator.category_overweight\")}</td>\n                <td className=\"text-center font-mono\">25 - 30</td>\n                <td className=\"text-center font-mono\">1.0 - 1.2</td>\n              </tr>\n              <tr>\n                <td className=\"font-medium\">{t(\"bmi-calculator.category_obese_class_1\")}</td>\n                <td className=\"text-center font-mono\">30 - 35</td>\n                <td className=\"text-center font-mono\">1.2 - 1.4</td>\n              </tr>\n              <tr>\n                <td className=\"font-medium\">{t(\"bmi-calculator.category_obese_class_2\")}</td>\n                <td className=\"text-center font-mono\">35 - 40</td>\n                <td className=\"text-center font-mono\">1.4 - 1.6</td>\n              </tr>\n              <tr>\n                <td className=\"font-medium\">{t(\"bmi-calculator.category_obese_class_3\")}</td>\n                <td className=\"text-center font-mono\">&gt; 40</td>\n                <td className=\"text-center font-mono\">&gt; 1.6</td>\n              </tr>\n            </tbody>\n          </table>\n        </div>\n      </section>\n\n      {/* Ponderal Index */}\n      <section className=\"space-y-6\">\n        <h2 className=\"text-3xl font-bold text-base-content\">{t(\"bmi-calculator.educational.ponderal_index_title\")}</h2>\n        <div className=\"prose prose-lg max-w-none text-base-content/80\">\n          <p>{t(\"bmi-calculator.educational.ponderal_index_explanation\")}</p>\n        </div>\n\n        <div className=\"grid md:grid-cols-2 gap-8\">\n          {/* Metric PI Formula */}\n          <FormulaCard\n            description={t(\"bmi-calculator.educational.ponderal_index_metric_description\")}\n            equation={`PI = ${createFraction(\"weight (kg)\", createSuperscript(\"height\", \"3\") + \" (m)\")}`}\n            title={t(\"bmi-calculator.educational.metric_formula\")}\n          />\n\n          {/* Imperial PI Formula */}\n          <FormulaCard\n            description={t(\"bmi-calculator.educational.ponderal_index_imperial_description\")}\n            equation={`PI = ${createFraction(createSuperscript(\"height\", \"3\") + \" (in)\", \"∛weight (lbs)\")}`}\n            title={t(\"bmi-calculator.educational.imperial_formula\")}\n          />\n        </div>\n      </section>\n\n      {/* Disclaimer */}\n      <section className=\"bg-warning/10 border border-warning/20 rounded-lg p-6\">\n        <div className=\"flex items-start gap-3\">\n          <div className=\"text-warning text-xl\">⚠️</div>\n          <div className=\"space-y-2\">\n            <h4 className=\"font-semibold text-base-content\">{t(\"bmi-calculator.educational.medical_disclaimer_title\")}</h4>\n            <p className=\"text-base-content/80\">{t(\"bmi-calculator.disclaimer\")}</p>\n          </div>\n        </div>\n      </section>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/bmi-calculator/shared/components/BmiHeightInput.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { UnitSystem } from \"app/[locale]/(app)/tools/bmi-calculator/bmi-calculator.utils\";\n\ninterface BmiHeightInputProps {\n  value: number;\n  unit: UnitSystem;\n  onChange: (height: number) => void;\n}\n\nexport function BmiHeightInput({ value, unit, onChange }: BmiHeightInputProps) {\n  const t = useI18n();\n\n  // For imperial, we need to handle feet and inches\n  if (unit === \"imperial\") {\n    const totalInches = value;\n    const feet = Math.floor(totalInches / 12);\n    const inches = totalInches % 12;\n\n    const handleFeetChange = (newFeet: number) => {\n      onChange(newFeet * 12 + inches);\n    };\n\n    const handleInchesChange = (newInches: number) => {\n      onChange(feet * 12 + newInches);\n    };\n\n    return (\n      <div>\n        <label className=\"text-sm font-medium text-base-content/70 uppercase tracking-wider\">{t(\"bmi-calculator.height\")}</label>\n        <div className=\"mt-2 grid grid-cols-2 gap-2\">\n          <div>\n            <input\n              className=\"w-full px-4 py-3 rounded-xl border-2 border-base-content/15 dark:border-base-content/10 light:bg-white dark:bg-base-200/30 text-base-content focus:border-primary focus:outline-none transition-all duration-300 hover:border-primary/30 text-center font-semibold\"\n              max=\"7\"\n              min=\"4\"\n              onChange={(e) => handleFeetChange(Number(e.target.value))}\n              type=\"number\"\n              value={feet}\n            />\n            <span className=\"text-xs text-base-content/60 dark:text-base-content/50 mt-1 block text-center font-medium\">\n              {t(\"bmi-calculator.feet\")}\n            </span>\n          </div>\n          <div>\n            <input\n              className=\"w-full px-4 py-3 rounded-xl border-2 border-base-content/15 dark:border-base-content/10 light:bg-white dark:bg-base-200/30 text-base-content focus:border-primary focus:outline-none transition-all duration-300 hover:border-primary/30 text-center font-semibold\"\n              max=\"11\"\n              min=\"0\"\n              onChange={(e) => handleInchesChange(Number(e.target.value))}\n              type=\"number\"\n              value={inches}\n            />\n            <span className=\"text-xs text-base-content/60 dark:text-base-content/50 mt-1 block text-center font-medium\">\n              {t(\"bmi-calculator.inches\")}\n            </span>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  // Metric - simple cm input\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 block\">\n        {t(\"bmi-calculator.height\")}\n      </label>\n      <div className=\"mt-2\">\n        <div className=\"relative\">\n          <input\n            className=\"w-full px-4 py-3 pr-12 rounded-xl border-2 border-base-content/15 dark:border-base-content/10 light:bg-white dark:bg-base-200/30 text-base-content focus:border-primary focus:outline-none transition-all duration-300 hover:border-primary/30 font-semibold\"\n            max=\"250\"\n            min=\"100\"\n            onChange={(e) => onChange(Number(e.target.value))}\n            placeholder={t(\"bmi-calculator.height_placeholder\")}\n            type=\"number\"\n            value={value}\n          />\n          <span className=\"absolute right-4 top-1/2 -translate-y-1/2 text-sm text-base-content/60 dark:text-base-content/50 font-medium\">\n            {t(\"bmi-calculator.cm\")}\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/bmi-calculator/shared/components/BmiResultsDisplay.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { CheckCircleIcon, AlertTriangleIcon, XCircleIcon, InfoIcon, TrendingUpIcon, TrendingDownIcon } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { BmiResult, BmiCategory, HealthRisk } from \"app/[locale]/(app)/tools/bmi-calculator/bmi-calculator.utils\";\n\ninterface BmiResultsDisplayProps {\n  result: BmiResult;\n}\n\nexport function BmiResultsDisplay({ result }: BmiResultsDisplayProps) {\n  const t = useI18n();\n\n  const getCategoryColor = (category: BmiCategory) => {\n    switch (category) {\n      case \"severe_thinness\":\n        return \"text-red-700 dark:text-red-500\";\n      case \"moderate_thinness\":\n        return \"text-red-600 dark:text-red-400\";\n      case \"mild_thinness\":\n        return \"text-blue-600 dark:text-blue-400\";\n      case \"normal\":\n        return \"text-green-600 dark:text-green-400\";\n      case \"overweight\":\n        return \"text-yellow-600 dark:text-yellow-400\";\n      case \"obese_class_1\":\n        return \"text-orange-600 dark:text-orange-400\";\n      case \"obese_class_2\":\n        return \"text-red-600 dark:text-red-400\";\n      case \"obese_class_3\":\n        return \"text-red-700 dark:text-red-500\";\n      default:\n        return \"text-gray-600 dark:text-gray-400\";\n    }\n  };\n\n  const getRiskColor = (risk: HealthRisk) => {\n    switch (risk) {\n      case \"low\":\n      case \"normal\":\n        return \"text-green-600 dark:text-green-400\";\n      case \"increased\":\n        return \"text-yellow-600 dark:text-yellow-400\";\n      case \"high\":\n        return \"text-orange-600 dark:text-orange-400\";\n      case \"very_high\":\n        return \"text-red-600 dark:text-red-400\";\n      case \"extremely_high\":\n        return \"text-red-700 dark:text-red-500\";\n      default:\n        return \"text-gray-600 dark:text-gray-400\";\n    }\n  };\n\n  const getRiskIcon = (risk: HealthRisk) => {\n    switch (risk) {\n      case \"low\":\n      case \"normal\":\n        return <CheckCircleIcon className=\"w-5 h-5\" />;\n      case \"increased\":\n      case \"high\":\n        return <AlertTriangleIcon className=\"w-5 h-5\" />;\n      case \"very_high\":\n      case \"extremely_high\":\n        return <XCircleIcon className=\"w-5 h-5\" />;\n      default:\n        return <CheckCircleIcon className=\"w-5 h-5\" />;\n    }\n  };\n\n  const getBmiGradient = (category: BmiCategory) => {\n    switch (category) {\n      case \"severe_thinness\":\n        return \"from-red-600 to-red-700\";\n      case \"moderate_thinness\":\n        return \"from-red-500 to-red-600\";\n      case \"mild_thinness\":\n        return \"from-blue-500 to-blue-600\";\n      case \"normal\":\n        return \"from-green-500 to-green-600\";\n      case \"overweight\":\n        return \"from-yellow-500 to-yellow-600\";\n      case \"obese_class_1\":\n        return \"from-orange-500 to-orange-600\";\n      case \"obese_class_2\":\n        return \"from-red-500 to-red-600\";\n      case \"obese_class_3\":\n        return \"from-red-600 to-red-700\";\n      default:\n        return \"from-gray-500 to-gray-600\";\n    }\n  };\n\n  return (\n    <div className=\"space-y-6\">\n      {/* Main Results Grid */}\n      <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n        {/* BMI Value */}\n        <div className=\"text-center\">\n          <div\n            className={`inline-flex items-center justify-center w-32 h-32 rounded-full bg-gradient-to-br ${getBmiGradient(result.category)} text-white shadow-lg`}\n          >\n            <div className=\"text-center\">\n              <div className=\"text-3xl font-bold\">{result.bmi}</div>\n              <div className=\"text-sm opacity-90\">{t(\"bmi-calculator.your_bmi\")}</div>\n            </div>\n          </div>\n        </div>\n\n        {/* BMI Prime */}\n        <div className=\"text-center\">\n          <div className=\"inline-flex items-center justify-center w-32 h-32 rounded-full bg-gradient-to-br from-purple-500 to-purple-600 text-white shadow-lg\">\n            <div className=\"text-center\">\n              <div className=\"text-3xl font-bold\">{result.bmiPrime}</div>\n              <div className=\"text-sm opacity-90\">{t(\"bmi-calculator.bmi_prime\")}</div>\n            </div>\n          </div>\n        </div>\n\n        {/* Ponderal Index */}\n        <div className=\"text-center\">\n          <div className=\"inline-flex items-center justify-center w-32 h-32 rounded-full bg-gradient-to-br from-indigo-500 to-indigo-600 text-white shadow-lg\">\n            <div className=\"text-center\">\n              <div className=\"text-3xl font-bold\">{result.ponderalIndex}</div>\n              <div className=\"text-sm opacity-90\">{t(\"bmi-calculator.ponderal_index\")}</div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Category and Risk */}\n      <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n        <div className=\"bg-base-100 dark:bg-base-200/30 rounded-2xl p-6 border border-base-content/10\">\n          <h3 className=\"text-lg font-semibold mb-3 text-base-content dark:text-base-content/90\">{t(\"bmi-calculator.bmi_category\")}</h3>\n          <div className={`text-xl font-bold ${getCategoryColor(result.category)}`}>\n            {t(`bmi-calculator.category_${result.category}` as keyof typeof t)}\n          </div>\n          <div className=\"text-sm text-base-content/60 mt-2\">\n            BMI: {result.detailedInfo.bmiRange.min} - {result.detailedInfo.bmiRange.max}\n          </div>\n        </div>\n\n        <div className=\"bg-base-100 dark:bg-base-200/30 rounded-2xl p-6 border border-base-content/10\">\n          <h3 className=\"text-lg font-semibold mb-3 text-base-content dark:text-base-content/90\">{t(\"bmi-calculator.health_risk\")}</h3>\n          <div className={`flex items-center gap-2 text-xl font-bold ${getRiskColor(result.healthRisk)}`}>\n            {getRiskIcon(result.healthRisk)}\n            {t(`bmi-calculator.risk_${result.healthRisk}` as keyof typeof t)}\n          </div>\n        </div>\n      </div>\n\n      {/* Ideal Weight and Weight Goals */}\n      <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n        <div className=\"bg-base-100 dark:bg-base-200/30 rounded-2xl p-6 border border-base-content/10\">\n          <h3 className=\"text-lg font-semibold mb-3 text-base-content dark:text-base-content/90\">{t(\"bmi-calculator.ideal_weight\")}</h3>\n          <div className=\"text-lg font-bold text-green-600 dark:text-green-400\">\n            {result.detailedInfo.idealWeight.min} - {result.detailedInfo.idealWeight.max} kg\n          </div>\n          <div className=\"text-sm text-base-content/60 mt-1\">{t(\"bmi-calculator.normal_range\")}</div>\n        </div>\n\n        {(result.detailedInfo.weightToLose || result.detailedInfo.weightToGain) && (\n          <div className=\"bg-base-100 dark:bg-base-200/30 rounded-2xl p-6 border border-base-content/10\">\n            <h3 className=\"text-lg font-semibold mb-3 text-base-content dark:text-base-content/90\">\n              {result.detailedInfo.weightToLose ? t(\"bmi-calculator.weight_to_lose\") : t(\"bmi-calculator.weight_to_gain\")}\n            </h3>\n            <div\n              className={`flex items-center gap-2 text-lg font-bold ${result.detailedInfo.weightToLose ? \"text-red-600 dark:text-red-400\" : \"text-blue-600 dark:text-blue-400\"}`}\n            >\n              {result.detailedInfo.weightToLose ? <TrendingDownIcon className=\"w-5 h-5\" /> : <TrendingUpIcon className=\"w-5 h-5\" />}\n              {result.detailedInfo.weightToLose || result.detailedInfo.weightToGain} kg\n            </div>\n          </div>\n        )}\n      </div>\n\n      {/* Detailed BMI Range Reference */}\n      <div className=\"bg-gradient-to-br from-primary/5 to-primary/10 dark:from-primary/10 dark:to-primary/5 rounded-2xl p-6 border border-primary/20\">\n        <h3 className=\"text-lg font-semibold mb-3 text-base-content dark:text-base-content/90\">\n          {t(\"bmi-calculator.bmi_range\")} (WHO Classification)\n        </h3>\n        <div className=\"space-y-2 text-sm\">\n          <div className=\"flex justify-between\">\n            <span className=\"text-red-700 dark:text-red-500\">{t(\"bmi-calculator.category_severe_thinness\")}</span>\n            <span className=\"text-base-content/70\">{\"< 16\"}</span>\n          </div>\n          <div className=\"flex justify-between\">\n            <span className=\"text-red-600 dark:text-red-400\">{t(\"bmi-calculator.category_moderate_thinness\")}</span>\n            <span className=\"text-base-content/70\">16.0 - 16.9</span>\n          </div>\n          <div className=\"flex justify-between\">\n            <span className=\"text-blue-600 dark:text-blue-400\">{t(\"bmi-calculator.category_mild_thinness\")}</span>\n            <span className=\"text-base-content/70\">17.0 - 18.4</span>\n          </div>\n          <div className=\"flex justify-between\">\n            <span className=\"text-green-600 dark:text-green-400\">{t(\"bmi-calculator.category_normal\")}</span>\n            <span className=\"text-base-content/70\">18.5 - 24.9</span>\n          </div>\n          <div className=\"flex justify-between\">\n            <span className=\"text-yellow-600 dark:text-yellow-400\">{t(\"bmi-calculator.category_overweight\")}</span>\n            <span className=\"text-base-content/70\">25.0 - 29.9</span>\n          </div>\n          <div className=\"flex justify-between\">\n            <span className=\"text-orange-600 dark:text-orange-400\">{t(\"bmi-calculator.category_obese_class_1\")}</span>\n            <span className=\"text-base-content/70\">30.0 - 34.9</span>\n          </div>\n          <div className=\"flex justify-between\">\n            <span className=\"text-red-600 dark:text-red-400\">{t(\"bmi-calculator.category_obese_class_2\")}</span>\n            <span className=\"text-base-content/70\">35.0 - 39.9</span>\n          </div>\n          <div className=\"flex justify-between\">\n            <span className=\"text-red-700 dark:text-red-500\">{t(\"bmi-calculator.category_obese_class_3\")}</span>\n            <span className=\"text-base-content/70\">{\"≥ 40.0\"}</span>\n          </div>\n        </div>\n      </div>\n\n      {/* BMI Prime Information */}\n      <div className=\"bg-base-100 dark:bg-base-200/30 rounded-2xl p-6 border border-base-content/10\">\n        <h3 className=\"text-lg font-semibold mb-3 text-base-content dark:text-base-content/90\">{t(\"bmi-calculator.about_bmi_prime\")}</h3>\n        <p className=\"text-sm text-base-content/70 dark:text-base-content/60 mb-3\">{t(\"bmi-calculator.bmi_prime_explanation\")}</p>\n        <div className=\"grid grid-cols-2 md:grid-cols-4 gap-2 text-xs\">\n          <div className=\"text-center p-2 bg-base-200 dark:bg-base-300/20 rounded\">\n            <div className=\"font-semibold\">{\"< 0.74\"}</div>\n            <div className=\"text-blue-600\">{t(\"bmi-calculator.underweight\")}</div>\n          </div>\n          <div className=\"text-center p-2 bg-base-200 dark:bg-base-300/20 rounded\">\n            <div className=\"font-semibold\">0.74 - 1.0</div>\n            <div className=\"text-green-600\">{t(\"bmi-calculator.normal\")}</div>\n          </div>\n          <div className=\"text-center p-2 bg-base-200 dark:bg-base-300/20 rounded\">\n            <div className=\"font-semibold\">1.0 - 1.2</div>\n            <div className=\"text-yellow-600\">{t(\"bmi-calculator.overweight\")}</div>\n          </div>\n          <div className=\"text-center p-2 bg-base-200 dark:bg-base-300/20 rounded\">\n            <div className=\"font-semibold\">{\"> 1.2\"}</div>\n            <div className=\"text-red-600\">{t(\"bmi-calculator.obese\")}</div>\n          </div>\n        </div>\n      </div>\n\n      {/* Recommendations */}\n      <div className=\"bg-base-100 dark:bg-base-200/30 rounded-2xl p-6 border border-base-content/10\">\n        <h3 className=\"text-lg font-semibold mb-4 text-base-content dark:text-base-content/90\">\n          {t(\"bmi-calculator.recommendations_label\")}\n        </h3>\n        <ul className=\"space-y-2\">\n          {result.recommendations.map((recommendation, index) => (\n            <li className=\"flex items-start gap-3 text-sm text-base-content/70 dark:text-base-content/60\" key={index}>\n              <CheckCircleIcon className=\"w-4 h-4 text-primary mt-0.5 flex-shrink-0\" />\n              <span>{recommendation}</span>\n            </li>\n          ))}\n        </ul>\n      </div>\n\n      {/* BMI Limitations */}\n      <div className=\"bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-2xl p-4\">\n        <div className=\"flex items-start gap-3\">\n          <InfoIcon className=\"w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0\" />\n          <div>\n            <h4 className=\"font-semibold text-blue-800 dark:text-blue-200 mb-2\">{t(\"bmi-calculator.limitations_title\")}</h4>\n            <p className=\"text-sm text-blue-800 dark:text-blue-200\">{t(\"bmi-calculator.limitations_text\")}</p>\n          </div>\n        </div>\n      </div>\n\n      {/* Disclaimer */}\n      <div className=\"bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-2xl p-4\">\n        <div className=\"flex items-start gap-3\">\n          <AlertTriangleIcon className=\"w-5 h-5 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0\" />\n          <p className=\"text-sm text-yellow-800 dark:text-yellow-200\">{t(\"bmi-calculator.disclaimer\")}</p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/bmi-calculator/shared/components/BmiUnitSelector.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { UnitSystem } from \"app/[locale]/(app)/tools/bmi-calculator/bmi-calculator.utils\";\n\ninterface BmiUnitSelectorProps {\n  value: UnitSystem;\n  onChange: (unit: UnitSystem) => void;\n}\n\nexport function BmiUnitSelector({ value, onChange }: BmiUnitSelectorProps) {\n  const t = useI18n();\n\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 block\">\n        {t(\"bmi-calculator.units\")}\n      </label>\n      <div className=\"grid grid-cols-2 gap-3\">\n        <button\n          className={`group flex items-center justify-center gap-2 sm:gap-3 py-3 sm:py-4 px-3 sm:px-4 rounded-2xl border-2 transition-all duration-300 touch-manipulation ${\n            value === \"metric\"\n              ? \"border-[#4F8EF7] bg-gradient-to-br from-[#4F8EF7]/20 to-[#238BE6]/10 text-[#4F8EF7] dark:from-[#4F8EF7]/15 dark:to-[#238BE6]/5 scale-[1.02]\"\n              : \"border-base-content/15 dark:border-base-content/10 hover:border-[#4F8EF7]/50 light:bg-white dark:bg-base-200/30 hover:bg-base-200/50 active:bg-base-200/70\"\n          }`}\n          onClick={() => onChange(\"metric\")}\n        >\n          <span className=\"text-sm font-semibold\">{t(\"bmi-calculator.metric\")}</span>\n        </button>\n        <button\n          className={`group flex items-center justify-center gap-2 sm:gap-3 py-3 sm:py-4 px-3 sm:px-4 rounded-2xl border-2 transition-all duration-300 touch-manipulation ${\n            value === \"imperial\"\n              ? \"border-[#4F8EF7] bg-gradient-to-br from-[#4F8EF7]/20 to-[#238BE6]/10 text-[#4F8EF7] dark:from-[#4F8EF7]/15 dark:to-[#238BE6]/5 scale-[1.02]\"\n              : \"border-base-content/15 dark:border-base-content/10 hover:border-[#4F8EF7]/50 light:bg-white dark:bg-base-200/30 hover:bg-base-200/50 active:bg-base-200/70\"\n          }`}\n          onClick={() => onChange(\"imperial\")}\n        >\n          <span className=\"text-sm font-semibold\">{t(\"bmi-calculator.imperial\")}</span>\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/bmi-calculator/shared/components/BmiWeightInput.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { UnitSystem } from \"app/[locale]/(app)/tools/bmi-calculator/bmi-calculator.utils\";\n\ninterface BmiWeightInputProps {\n  value: number;\n  unit: UnitSystem;\n  onChange: (weight: number) => void;\n}\n\nexport function BmiWeightInput({ value, unit, onChange }: BmiWeightInputProps) {\n  const t = useI18n();\n  const unitLabel = unit === \"metric\" ? t(\"bmi-calculator.kg\") : t(\"bmi-calculator.lbs\");\n  const min = unit === \"metric\" ? 30 : 66;\n  const max = unit === \"metric\" ? 300 : 660;\n\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 block\">\n        {t(\"bmi-calculator.weight\")}\n      </label>\n      <div className=\"relative\">\n        <input\n          className=\"w-full px-4 py-3 pr-12 rounded-xl border-2 border-base-content/15 dark:border-base-content/10 light:bg-white dark:bg-base-200/30 text-base-content focus:border-primary focus:outline-none transition-all duration-300 hover:border-primary/30 font-semibold\"\n          max={max}\n          min={min}\n          onChange={(e) => onChange(Number(e.target.value))}\n          placeholder={t(\"bmi-calculator.weight_placeholder\")}\n          step=\"0.1\"\n          type=\"number\"\n          value={value}\n        />\n        <span className=\"absolute right-4 top-1/2 -translate-y-1/2 text-sm text-base-content/60 dark:text-base-content/50 font-medium\">\n          {unitLabel}\n        </span>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/bmi-calculator/shared/components/MathEquation.tsx",
    "content": "\"use client\";\n\ninterface MathEquationProps {\n  equation: string;\n  display?: boolean;\n  className?: string;\n}\n\nexport function MathEquation({ equation, display = false, className = \"\" }: MathEquationProps) {\n  return (\n    <div className={`math-equation ${display ? \"block\" : \"inline-block\"} ${className}`}>\n      <span\n        className=\"font-mono text-lg\"\n        dangerouslySetInnerHTML={{ __html: equation }}\n        style={{ fontFamily: \"KaTeX_Math, Times New Roman, serif\" }}\n      />\n    </div>\n  );\n}\n\ninterface FormulaCardProps {\n  title: string;\n  equation: string;\n  example?: string;\n  description?: string;\n  className?: string;\n}\n\nexport function FormulaCard({ title, equation, example, description, className = \"\" }: FormulaCardProps) {\n  return (\n    <div className={`bg-base-200 p-6 rounded-lg space-y-4 ${className}`}>\n      <h4 className=\"text-lg font-semibold text-base-content text-center\">{title}</h4>\n\n      <div className=\"text-center py-4\">\n        <div className=\"text-xl font-mono\" style={{ fontFamily: \"KaTeX_Math, Times New Roman, serif\" }}>\n          <div dangerouslySetInnerHTML={{ __html: equation }} />\n        </div>\n      </div>\n\n      {description && <p className=\"text-sm text-base-content/70 text-center\">{description}</p>}\n\n      {example && (\n        <div className=\"text-center border-t border-base-content/10 pt-4\">\n          <p className=\"text-sm text-base-content/60 mb-2\">Example:</p>\n          <div className=\"text-lg font-mono\" style={{ fontFamily: \"KaTeX_Math, Times New Roman, serif\" }}>\n            <div dangerouslySetInnerHTML={{ __html: example }} />\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\n// Helper function to create fraction notation\nexport function createFraction(numerator: string, denominator: string): string {\n  return `\n    <div style=\"display: inline-block; text-align: center; vertical-align: middle;\">\n      <div style=\"border-bottom: 1px solid currentColor; padding: 0 4px;\">${numerator}</div>\n      <div style=\"padding: 0 4px;\">${denominator}</div>\n    </div>\n  `;\n}\n\n// Helper function for superscript\nexport function createSuperscript(base: string, exponent: string): string {\n  return `${base}<sup style=\"font-size: 0.8em;\">${exponent}</sup>`;\n}\n\n// Helper function for subscript\nexport function createSubscript(base: string, subscript: string): string {\n  return `${base}<sub style=\"font-size: 0.8em;\">${subscript}</sub>`;\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/CalorieCalculatorHub.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport Link from \"next/link\";\nimport { TrendingUpIcon, AwardIcon, TargetIcon, BrainIcon, GlobeIcon, ChartBarIcon } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { env } from \"@/env\";\nimport { HorizontalBottomBanner, HorizontalTopBanner } from \"@/components/ads\";\n\ninterface CalculatorFormula {\n  id: string;\n  href: string;\n  icon: React.ReactNode;\n  year: string;\n  popularity: number; // 1-5\n  accuracy: \"high\" | \"medium\" | \"good\";\n  bestFor: string;\n  gradient: {\n    from: string;\n    to: string;\n  };\n}\n\nconst calculatorFormulas: CalculatorFormula[] = [\n  {\n    id: \"mifflin-st-jeor\",\n    href: \"/tools/calorie-calculator/mifflin-st-jeor-calculator\",\n    icon: <AwardIcon className=\"w-6 h-6\" />,\n    year: \"1990\",\n    popularity: 5,\n    accuracy: \"high\",\n    bestFor: \"general\",\n    gradient: {\n      from: \"from-[#4F8EF7]\",\n      to: \"to-[#238BE6]\",\n    },\n  },\n  {\n    id: \"harris-benedict\",\n    href: \"/tools/calorie-calculator/harris-benedict-calculator\",\n    icon: <TrendingUpIcon className=\"w-6 h-6\" />,\n    year: \"1984\",\n    popularity: 5,\n    accuracy: \"good\",\n    bestFor: \"traditional\",\n    gradient: {\n      from: \"from-[#25CB78]\",\n      to: \"to-[#22C55E]\",\n    },\n  },\n  {\n    id: \"katch-mcardle\",\n    href: \"/tools/calorie-calculator/katch-mcardle-calculator\",\n    icon: <TargetIcon className=\"w-6 h-6\" />,\n    year: \"1996\",\n    popularity: 3,\n    accuracy: \"high\",\n    bestFor: \"athletes\",\n    gradient: {\n      from: \"from-[#FF5722]\",\n      to: \"to-[#EF4444]\",\n    },\n  },\n  {\n    id: \"cunningham\",\n    href: \"/tools/calorie-calculator/cunningham-calculator\",\n    icon: <BrainIcon className=\"w-6 h-6\" />,\n    year: \"1980\",\n    popularity: 2,\n    accuracy: \"high\",\n    bestFor: \"bodybuilders\",\n    gradient: {\n      from: \"from-[#8B5CF6]\",\n      to: \"to-[#7C3AED]\",\n    },\n  },\n  {\n    id: \"oxford\",\n    href: \"/tools/calorie-calculator/oxford-calculator\",\n    icon: <GlobeIcon className=\"w-6 h-6\" />,\n    year: \"2005\",\n    popularity: 3,\n    accuracy: \"good\",\n    bestFor: \"european\",\n    gradient: {\n      from: \"from-[#F59E0B]\",\n      to: \"to-[#EF4444]\",\n    },\n  },\n  {\n    id: \"comparison\",\n    href: \"/tools/calorie-calculator/calorie-calculator-comparison\",\n    icon: <ChartBarIcon className=\"w-6 h-6\" />,\n    year: \"all\",\n    popularity: 4,\n    accuracy: \"high\",\n    bestFor: \"comparison\",\n    gradient: {\n      from: \"from-[#06B6D4]\",\n      to: \"to-[#3B82F6]\",\n    },\n  },\n];\n\nexport function CalorieCalculatorHub() {\n  const t = useI18n();\n\n  const renderStars = (count: number) => {\n    return Array.from({ length: 5 }, (_, i) => (\n      <span className={i < count ? \"text-yellow-500\" : \"text-base-content/20\"} key={i}>\n        ★\n      </span>\n    ));\n  };\n\n  const getAccuracyColor = (accuracy: string) => {\n    switch (accuracy) {\n      case \"high\":\n        return \"text-green-600 dark:text-green-400\";\n      case \"good\":\n        return \"text-blue-600 dark:text-blue-400\";\n      default:\n        return \"text-orange-600 dark:text-orange-400\";\n    }\n  };\n\n  return (\n    <div className=\"space-y-8\">\n      {/* Introduction */}\n      {env.NEXT_PUBLIC_TOP_CALCULATOR_HUB_BANNER_AD_SLOT && (\n        <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_CALCULATOR_HUB_BANNER_AD_SLOT} />\n      )}\n      <div className=\"text-center max-w-3xl mx-auto\">\n        <h1 className=\"text-3xl sm:text-4xl font-bold mb-4 text-base-content dark:text-base-content/90\">\n          {t(\"tools.calorie-calculator-hub.title\")}\n        </h1>\n        <p className=\"text-lg text-base-content/70 dark:text-base-content/60\">{t(\"tools.calorie-calculator-hub.subtitle\")}</p>\n      </div>\n\n      {/* Calculator Cards */}\n      <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n        {calculatorFormulas.map((formula) => (\n          <Link\n            className=\"group relative overflow-hidden rounded-2xl border border-base-content/10 bg-base-100 dark:bg-base-200/30 transition-all duration-300 hover:scale-[1.02] hover:border-primary/50\"\n            href={formula.href}\n            key={formula.id}\n          >\n            <div\n              className={`absolute inset-0 bg-gradient-to-br ${formula.gradient.from} ${formula.gradient.to} opacity-0 transition-opacity duration-300 group-hover:opacity-5`}\n            />\n\n            <div className=\"relative p-6\">\n              {/* Header */}\n              <div className=\"flex items-start justify-between mb-4\">\n                <div className={`p-3 rounded-xl bg-gradient-to-br ${formula.gradient.from} ${formula.gradient.to} text-white`}>\n                  {formula.icon}\n                </div>\n              </div>\n\n              {/* Title */}\n              <h3 className=\"text-xl font-bold mb-2 text-base-content dark:text-base-content/90\">\n                {t(`tools.calorie-calculator-hub.${formula.id}.title` as keyof typeof t)}\n              </h3>\n\n              {/* Year Badge */}\n              <div className=\"inline-flex items-center gap-2 text-xs mb-3\">\n                <span className=\"px-2 py-1 rounded-full bg-base-200 dark:bg-base-300/50 text-base-content/60\">\n                  {formula.year === \"all\"\n                    ? t(\"tools.calorie-calculator-hub.all_formulas\")\n                    : `${t(\"tools.calorie-calculator-hub.since\")} ${formula.year}`}\n                </span>\n              </div>\n\n              {/* Description */}\n              <p className=\"text-sm text-base-content/70 dark:text-base-content/60 mb-4\">\n                {t(`tools.calorie-calculator-hub.${formula.id}.description` as keyof typeof t)}\n              </p>\n\n              {/* Stats */}\n              <div className=\"space-y-2\">\n                <div className=\"flex items-center justify-between text-sm\">\n                  <span className=\"text-base-content/60\">{t(\"tools.calorie-calculator-hub.popularity\")}</span>\n                  <span>{renderStars(formula.popularity)}</span>\n                </div>\n                <div className=\"flex items-center justify-between text-sm\">\n                  <span className=\"text-base-content/60\">{t(\"tools.calorie-calculator-hub.accuracy\")}</span>\n                  <span className={`font-medium ${getAccuracyColor(formula.accuracy)}`}>\n                    {t(`tools.calorie-calculator-hub.accuracy_${formula.accuracy}`)}\n                  </span>\n                </div>\n                <div className=\"flex items-center justify-between text-sm\">\n                  <span className=\"text-base-content/60\">{t(\"tools.calorie-calculator-hub.best_for\")}</span>\n                  <span className=\"text-xs font-medium text-primary\">\n                    {t(`tools.calorie-calculator-hub.best_for_${formula.bestFor}` as keyof typeof t)}\n                  </span>\n                </div>\n              </div>\n\n              {/* Best For Badge */}\n            </div>\n          </Link>\n        ))}\n      </div>\n\n      {env.NEXT_PUBLIC_BOTTOM_CALCULATOR_HUB_BANNER_AD_SLOT && (\n        <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_BOTTOM_CALCULATOR_HUB_BANNER_AD_SLOT} />\n      )}\n\n      {/* Info Section */}\n      <div className=\"mt-12 bg-gradient-to-br from-primary/5 to-primary/10 dark:from-primary/10 dark:to-primary/5 rounded-2xl p-6 sm:p-8 border border-primary/20\">\n        <h2 className=\"text-2xl font-bold mb-4 text-base-content dark:text-base-content/90\">\n          {t(\"tools.calorie-calculator-hub.which_formula\")}\n        </h2>\n        <div className=\"prose prose-sm dark:prose-invert max-w-none\">\n          <p className=\"text-base-content/70 dark:text-base-content/60\">{t(\"tools.calorie-calculator-hub.formula_explanation\")}</p>\n          <ul className=\"mt-4 space-y-2 text-base-content/70 dark:text-base-content/60\">\n            <li>\n              <strong className=\"text-base-content dark:text-base-content/90\">\n                {t(\"tools.calorie-calculator-hub.mifflin-st-jeor.title\")}:\n              </strong>{\" \"}\n              {t(\"tools.calorie-calculator-hub.recommendation_general\")}\n            </li>\n            <li>\n              <strong className=\"text-base-content dark:text-base-content/90\">\n                {t(\"tools.calorie-calculator-hub.harris-benedict.title\")}:\n              </strong>{\" \"}\n              {t(\"tools.calorie-calculator-hub.recommendation_traditional\")}\n            </li>\n            <li>\n              <strong className=\"text-base-content dark:text-base-content/90\">\n                {t(\"tools.calorie-calculator-hub.katch-mcardle.title\")}:\n              </strong>{\" \"}\n              {t(\"tools.calorie-calculator-hub.recommendation_bodyfat\")}\n            </li>\n          </ul>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/calorie-calculator-comparison/CalorieCalculatorComparison.tsx",
    "content": "\"use client\";\n\nimport React, { useState } from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { BodyFatInput } from \"app/[locale]/(app)/tools/calorie-calculator/shared/components/BodyFatInput\";\nimport {\n  ActivityLevelSelector,\n  AgeInput,\n  GenderSelector,\n  GoalSelector,\n  HeightInput,\n  UnitSelector,\n  WeightInput,\n} from \"app/[locale]/(app)/tools/calorie-calculator/shared/components\";\nimport {\n  calculateCalories,\n  CalorieCalculatorInputs,\n  CalorieResults,\n} from \"app/[locale]/(app)/tools/calorie-calculator/shared/calorie-formulas.utils\";\nimport { env } from \"@/env\";\nimport { HorizontalBottomBanner } from \"@/components/ads\";\n\ninterface FormulaResult {\n  name: string;\n  formula: \"mifflin\" | \"harris\" | \"katch\" | \"cunningham\" | \"oxford\";\n  results: CalorieResults | null;\n  error?: string;\n  gradient: {\n    from: string;\n    to: string;\n  };\n}\n\nexport function CalorieCalculatorComparison() {\n  const t = useI18n();\n\n  const [inputs, setInputs] = useState<CalorieCalculatorInputs>({\n    gender: \"male\",\n    unit: \"metric\",\n    age: 25,\n    height: 170,\n    weight: 70,\n    activityLevel: \"moderate\",\n    goal: \"maintain\",\n    bodyFatPercentage: 15,\n  });\n\n  const [isCalculating, setIsCalculating] = useState(false);\n  const [formulaResults, setFormulaResults] = useState<FormulaResult[]>([]);\n\n  const formulas: FormulaResult[] = [\n    {\n      name: t(\"tools.calorie-calculator-hub.mifflin-st-jeor.title\"),\n      formula: \"mifflin\",\n      results: null,\n      gradient: { from: \"from-[#4F8EF7]\", to: \"to-[#238BE6]\" },\n    },\n    {\n      name: t(\"tools.calorie-calculator-hub.harris-benedict.title\"),\n      formula: \"harris\",\n      results: null,\n      gradient: { from: \"from-[#25CB78]\", to: \"to-[#22C55E]\" },\n    },\n    {\n      name: t(\"tools.calorie-calculator-hub.katch-mcardle.title\"),\n      formula: \"katch\",\n      results: null,\n      gradient: { from: \"from-[#FF5722]\", to: \"to-[#EF4444]\" },\n    },\n    {\n      name: t(\"tools.calorie-calculator-hub.cunningham.title\"),\n      formula: \"cunningham\",\n      results: null,\n      gradient: { from: \"from-[#8B5CF6]\", to: \"to-[#7C3AED]\" },\n    },\n    {\n      name: t(\"tools.calorie-calculator-hub.oxford.title\"),\n      formula: \"oxford\",\n      results: null,\n      gradient: { from: \"from-[#F59E0B]\", to: \"to-[#EF4444]\" },\n    },\n  ];\n\n  const handleCalculate = () => {\n    setIsCalculating(true);\n\n    setTimeout(() => {\n      const results = formulas.map((formula) => {\n        try {\n          const calculatedResults = calculateCalories(inputs, formula.formula);\n          return {\n            ...formula,\n            results: calculatedResults,\n            error: undefined,\n          };\n        } catch (error) {\n          return {\n            ...formula,\n            results: null,\n            error: error instanceof Error ? error.message : \"Calculation error\",\n          };\n        }\n      });\n\n      setFormulaResults(results);\n      setIsCalculating(false);\n    }, 500);\n  };\n\n  const updateInput = <K extends keyof CalorieCalculatorInputs>(key: K, value: CalorieCalculatorInputs[K]) => {\n    setInputs((prev) => ({ ...prev, [key]: value }));\n  };\n\n  const getResultDifference = (result: CalorieResults, baseline: CalorieResults) => {\n    const diff = result.targetCalories - baseline.targetCalories;\n    const percentage = (diff / baseline.targetCalories) * 100;\n    return { diff, percentage };\n  };\n\n  const baseline = formulaResults.find((r) => r.formula === \"mifflin\")?.results;\n\n  return (\n    <div className=\"space-y-8\">\n      {/* Input Form */}\n      <div className=\"light:bg-white dark:bg-base-200/20 backdrop-blur-sm rounded-2xl border border-base-content/10 dark:border-base-content/5 p-6 sm:p-8\">\n        <h2 className=\"text-2xl font-bold mb-6 text-base-content dark:text-base-content/90\">\n          {t(\"tools.calorie-calculator-comparison.input_details\")}\n        </h2>\n\n        <div className=\"space-y-6\">\n          <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-6\">\n            <GenderSelector onChange={(gender) => updateInput(\"gender\", gender)} value={inputs.gender} />\n            <UnitSelector onChange={(unit) => updateInput(\"unit\", unit)} value={inputs.unit} />\n          </div>\n\n          <AgeInput onChange={(age) => updateInput(\"age\", age)} value={inputs.age} />\n\n          <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-6\">\n            <HeightInput onChange={(height) => updateInput(\"height\", height)} unit={inputs.unit} value={inputs.height} />\n            <WeightInput onChange={(weight) => updateInput(\"weight\", weight)} unit={inputs.unit} value={inputs.weight} />\n          </div>\n\n          <BodyFatInput onChange={(bodyFat) => updateInput(\"bodyFatPercentage\", bodyFat)} value={inputs.bodyFatPercentage || 15} />\n\n          <ActivityLevelSelector onChange={(level) => updateInput(\"activityLevel\", level)} value={inputs.activityLevel} />\n\n          <GoalSelector onChange={(goal) => updateInput(\"goal\", goal)} value={inputs.goal} />\n\n          {env.NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_COMPARISON_AD_SLOT && (\n            <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_COMPARISON_AD_SLOT} />\n          )}\n          {/* Calculate Button */}\n          <button\n            aria-label={t(\"tools.calorie-calculator.calculate\")}\n            className={\n              \"w-full py-4 px-6 rounded-xl bg-gradient-to-r from-[#06B6D4] to-[#3B82F6] text-white font-bold text-lg transition-all duration-300 hover:scale-[1.02] hover:shadow-lg hover:shadow-primary/25 active:scale-[0.98] touch-manipulation\"\n            }\n            disabled={isCalculating}\n            onClick={handleCalculate}\n          >\n            {isCalculating ? (\n              <span className=\"flex items-center justify-center gap-3\">\n                <svg className=\"animate-spin h-5 w-5\" viewBox=\"0 0 24 24\">\n                  <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" fill=\"none\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n                  <path\n                    className=\"opacity-75\"\n                    d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n                    fill=\"currentColor\"\n                  />\n                </svg>\n                {t(\"tools.calorie-calculator.calculating\")}\n              </span>\n            ) : (\n              `${t(\"tools.calorie-calculator.calculate\")} & ${t(\"tools.calorie-calculator-comparison.compare\")}`\n            )}\n          </button>\n        </div>\n      </div>\n\n      {/* Results Comparison */}\n      {formulaResults.length > 0 && (\n        <div className=\"space-y-6\">\n          <h2 className=\"text-2xl font-bold text-base-content dark:text-base-content/90\">\n            {t(\"tools.calorie-calculator-comparison.results_comparison\")}\n          </h2>\n\n          {/* Results Grid */}\n          <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n            {formulaResults.map((formula, index) => (\n              <div\n                className=\"relative bg-base-100 dark:bg-base-200/30 rounded-2xl border border-base-content/10 p-6 transition-all duration-300 hover:scale-[1.02]\"\n                key={formula.formula}\n              >\n                <div\n                  className={`absolute inset-0 bg-gradient-to-br ${formula.gradient.from} ${formula.gradient.to} opacity-5 rounded-2xl`}\n                />\n\n                <div className=\"relative\">\n                  <div className=\"flex items-center gap-3 mb-4\">\n                    <div\n                      className={`p-2 rounded-lg bg-gradient-to-br ${formula.gradient.from} ${formula.gradient.to} text-white text-sm font-bold`}\n                    >\n                      #{index + 1}\n                    </div>\n                    <h3 className=\"font-bold text-base-content dark:text-base-content/90\">{formula.name}</h3>\n                  </div>\n\n                  {formula.error ? (\n                    <div className=\"text-red-500 text-sm\">{formula.error}</div>\n                  ) : formula.results ? (\n                    <div className=\"space-y-3\">\n                      <div>\n                        <div className=\"text-2xl font-bold text-base-content dark:text-base-content/90\">\n                          {formula.results.targetCalories} cal\n                        </div>\n                        <div className=\"text-sm text-base-content/70\">{t(\"tools.calorie-calculator.results.target\")}</div>\n                      </div>\n\n                      <div className=\"grid grid-cols-2 gap-4 text-sm\">\n                        <div>\n                          <div className=\"font-semibold text-base-content/90\">{formula.results.bmr}</div>\n                          <div className=\"text-base-content/70\">BMR</div>\n                        </div>\n                        <div>\n                          <div className=\"font-semibold text-base-content/90\">{formula.results.tdee}</div>\n                          <div className=\"text-base-content/70\">TDEE</div>\n                        </div>\n                      </div>\n\n                      {baseline && formula.formula !== \"mifflin\" && (\n                        <div className=\"pt-3 border-t border-base-content/10\">\n                          <div className=\"text-xs text-base-content/60\">{t(\"tools.calorie-calculator-comparison.vs_mifflin\")}</div>\n                          {(() => {\n                            const { diff, percentage } = getResultDifference(formula.results, baseline);\n                            return (\n                              <div\n                                className={`text-sm font-semibold ${diff > 0 ? \"text-orange-600\" : diff < 0 ? \"text-blue-600\" : \"text-gray-600\"}`}\n                              >\n                                {diff > 0 ? \"+\" : \"\"}\n                                {diff} cal ({percentage > 0 ? \"+\" : \"\"}\n                                {percentage.toFixed(1)}%)\n                              </div>\n                            );\n                          })()}\n                        </div>\n                      )}\n                    </div>\n                  ) : null}\n                </div>\n              </div>\n            ))}\n          </div>\n\n          {/* Summary */}\n          {baseline && (\n            <div className=\"bg-gradient-to-br from-primary/5 to-primary/10 dark:from-primary/10 dark:to-primary/5 rounded-2xl p-6 border border-primary/20\">\n              <h3 className=\"text-lg font-bold mb-3 text-base-content dark:text-base-content/90\">\n                {t(\"tools.calorie-calculator-comparison.summary\")}\n              </h3>\n              <div className=\"space-y-2 text-sm text-base-content/70 dark:text-base-content/60\">\n                <p>{t(\"tools.calorie-calculator-comparison.summary_explanation\")}</p>\n                <p className=\"font-medium text-base-content/80\">{t(\"tools.calorie-calculator-comparison.recommendation\")}</p>\n              </div>\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/calorie-calculator-comparison/page.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\nimport { Metadata } from \"next\";\nimport { ChevronLeftIcon } from \"lucide-react\";\n\nimport { getI18n } from \"locales/server\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { env } from \"@/env\";\nimport { generateSEOMetadata, SEOScripts } from \"@/components/seo/SEOHead\";\nimport { HorizontalTopBanner } from \"@/components/ads\";\n\nimport { CalorieCalculatorComparison } from \"./CalorieCalculatorComparison\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return generateSEOMetadata({\n    title: t(\"tools.calorie-calculator-comparison.meta.title\"),\n    description: t(\"tools.calorie-calculator-comparison.meta.description\"),\n    keywords: t(\"tools.calorie-calculator-comparison.meta.keywords\").split(\", \"),\n    locale,\n    canonical: `${getServerUrl()}/${locale}/tools/calorie-calculator-comparison`,\n    structuredData: {\n      type: \"Calculator\",\n      calculatorData: {\n        calculatorType: \"calorie\",\n        inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"body fat percentage\", \"activity level\", \"goal\"],\n        outputFields: [\"BMR comparison\", \"TDEE comparison\", \"accuracy analysis\", \"formula recommendations\"],\n        formula: \"Multi-Formula Comparison Tool\",\n        accuracy: \"Comprehensive accuracy analysis across 5 formulas\",\n        targetAudience: [\"fitness professionals\", \"researchers\", \"health enthusiasts\", \"Cal the Chef users\"],\n        relatedCalculators: [\n          \"calorie-calculator\",\n          \"mifflin-st-jeor-calculator\",\n          \"harris-benedict-calculator\",\n          \"katch-mcardle-calculator\",\n          \"cunningham-calculator\",\n          \"oxford-calculator\",\n        ],\n      },\n    },\n  });\n}\n\nexport default async function CalorieCalculatorComparisonPage({ params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return (\n    <>\n      <SEOScripts\n        canonical={`${getServerUrl()}/${locale}/tools/calorie-calculator-comparison`}\n        description={t(\"tools.calorie-calculator-comparison.meta.description\")}\n        locale={locale}\n        structuredData={{\n          type: \"Calculator\",\n          calculatorData: {\n            calculatorType: \"calorie\",\n            inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"body fat percentage\", \"activity level\", \"goal\"],\n            outputFields: [\"BMR comparison\", \"TDEE comparison\", \"accuracy analysis\", \"formula recommendations\"],\n            formula: \"Multi-Formula Comparison Tool\",\n            accuracy: \"Comprehensive accuracy analysis across 5 formulas\",\n            targetAudience: [\"fitness professionals\", \"researchers\", \"health enthusiasts\", \"Cal the Chef users\"],\n            relatedCalculators: [\n              \"calorie-calculator\",\n              \"mifflin-st-jeor-calculator\",\n              \"harris-benedict-calculator\",\n              \"katch-mcardle-calculator\",\n              \"cunningham-calculator\",\n              \"oxford-calculator\",\n            ],\n          },\n        }}\n        title={t(\"tools.calorie-calculator-comparison.meta.title\")}\n      />\n      <div className=\"light:bg-white dark:bg-base-200/20\">\n        <div className=\"container mx-auto px-2 sm:px-4 py-8 sm:py-12 max-w-6xl\">\n          {env.NEXT_PUBLIC_TOP_CALORIE_CALCULATOR_COMPARISON_AD_SLOT && (\n            <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_CALORIE_CALCULATOR_COMPARISON_AD_SLOT} />\n          )}\n          {/* Back to hub */}\n          <Link\n            className=\"inline-flex items-center gap-2 text-sm text-base-content/60 hover:text-primary transition-colors mb-6\"\n            href=\"/tools/calorie-calculator\"\n          >\n            <ChevronLeftIcon className=\"w-4 h-4\" />\n            {t(\"tools.back_to_calculators\")}\n          </Link>\n\n          <div className=\"mb-8 text-center\">\n            <h1 className=\"text-4xl sm:text-5xl font-bold mb-4 bg-gradient-to-r from-[#06B6D4] to-[#3B82F6] bg-clip-text text-transparent animate-fadeIn\">\n              {t(\"tools.calorie-calculator-comparison.title\")}\n            </h1>\n            <p\n              className=\"text-lg text-base-content/70 dark:text-base-content/60 max-w-3xl mx-auto animate-fadeIn\"\n              style={{ animationDelay: \"0.1s\" }}\n            >\n              {t(\"tools.calorie-calculator-comparison.subtitle\")}\n            </p>\n          </div>\n\n          {/* Educational Section */}\n          <div\n            className=\"mb-8 bg-gradient-to-br from-[#06B6D4]/5 to-[#3B82F6]/5 dark:from-[#06B6D4]/10 dark:to-[#3B82F6]/10 rounded-2xl border border-[#06B6D4]/20 dark:border-[#06B6D4]/30 p-6 animate-fadeIn\"\n            style={{ animationDelay: \"0.2s\" }}\n          >\n            <h2 className=\"text-xl font-bold mb-3 text-base-content dark:text-base-content/90\">\n              {t(\"tools.calorie-calculator-comparison.how_it_works\")}\n            </h2>\n            <div className=\"space-y-2 text-base-content/70 dark:text-base-content/60\">\n              <p className=\"text-sm leading-relaxed\">{t(\"tools.calorie-calculator-comparison.how_it_works_description\")}</p>\n            </div>\n          </div>\n\n          <CalorieCalculatorComparison />\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/calorie-calculator.utils.ts",
    "content": "// Types for the calorie calculator\nexport type Gender = \"male\" | \"female\";\nexport type UnitSystem = \"metric\" | \"imperial\";\nexport type ActivityLevel = \"sedentary\" | \"light\" | \"moderate\" | \"active\" | \"very_active\";\nexport type Goal = \"lose_fast\" | \"lose_slow\" | \"maintain\" | \"gain_slow\" | \"gain_fast\";\n\nexport interface CalorieCalculatorInputs {\n  gender: Gender;\n  unit: UnitSystem;\n  age: number;\n  height: number; // cm for metric, inches for imperial\n  weight: number; // kg for metric, lbs for imperial\n  activityLevel: ActivityLevel;\n  goal: Goal;\n}\n\nexport interface CalorieResults {\n  bmr: number; // Basal Metabolic Rate\n  tdee: number; // Total Daily Energy Expenditure\n  targetCalories: number; // Based on goal\n  proteinGrams: number;\n  carbsGrams: number;\n  fatGrams: number;\n}\n\n// Activity level multipliers\nconst ACTIVITY_MULTIPLIERS: Record<ActivityLevel, number> = {\n  sedentary: 1.2, // Little to no exercise\n  light: 1.375, // Light exercise 1-3 days/week\n  moderate: 1.55, // Moderate exercise 3-5 days/week\n  active: 1.725, // Heavy exercise 6-7 days/week\n  very_active: 1.9, // Very heavy physical job or training\n};\n\n// Goal adjustments (calories per day)\nconst GOAL_ADJUSTMENTS: Record<Goal, number> = {\n  lose_fast: -1000, // Lose 2 lbs/week\n  lose_slow: -500, // Lose 1 lb/week\n  maintain: 0,\n  gain_slow: 500, // Gain 1 lb/week\n  gain_fast: 1000, // Gain 2 lbs/week\n};\n\n/**\n * Convert imperial units to metric for calculation\n */\nfunction convertToMetric(inputs: CalorieCalculatorInputs): {\n  weight: number;\n  height: number;\n} {\n  if (inputs.unit === \"metric\") {\n    return {\n      weight: inputs.weight,\n      height: inputs.height,\n    };\n  }\n\n  // Convert lbs to kg and inches to cm\n  return {\n    weight: inputs.weight * 0.453592,\n    height: inputs.height * 2.54,\n  };\n}\n\n/**\n * Calculate BMR using Mifflin-St Jeor Equation\n * Men: BMR = 10 × weight(kg) + 6.25 × height(cm) - 5 × age(years) + 5\n * Women: BMR = 10 × weight(kg) + 6.25 × height(cm) - 5 × age(years) - 161\n */\nfunction calculateBMR(inputs: CalorieCalculatorInputs): number {\n  const { weight, height } = convertToMetric(inputs);\n\n  const baseBMR = 10 * weight + 6.25 * height - 5 * inputs.age;\n\n  if (inputs.gender === \"male\") {\n    return baseBMR + 5;\n  } else {\n    return baseBMR - 161;\n  }\n}\n\n/**\n * Calculate macros based on target calories\n * Using a balanced approach: 30% protein, 40% carbs, 30% fat\n */\nfunction calculateMacros(targetCalories: number): {\n  proteinGrams: number;\n  carbsGrams: number;\n  fatGrams: number;\n} {\n  const proteinCalories = targetCalories * 0.3;\n  const carbsCalories = targetCalories * 0.4;\n  const fatCalories = targetCalories * 0.3;\n\n  // Protein and carbs = 4 calories per gram, fat = 9 calories per gram\n  return {\n    proteinGrams: Math.round(proteinCalories / 4),\n    carbsGrams: Math.round(carbsCalories / 4),\n    fatGrams: Math.round(fatCalories / 9),\n  };\n}\n\n/**\n * Main calculation function\n */\nexport function calculateTDEE(inputs: CalorieCalculatorInputs): CalorieResults {\n  const bmr = calculateBMR(inputs);\n  const tdee = bmr * ACTIVITY_MULTIPLIERS[inputs.activityLevel];\n  const targetCalories = tdee + GOAL_ADJUSTMENTS[inputs.goal];\n\n  // Ensure minimum calories (never go below 1200 for safety)\n  const safeTargetCalories = Math.max(1200, targetCalories);\n\n  const macros = calculateMacros(safeTargetCalories);\n\n  return {\n    bmr: Math.round(bmr),\n    tdee: Math.round(tdee),\n    targetCalories: Math.round(safeTargetCalories),\n    ...macros,\n  };\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/cunningham-calculator/page.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\nimport { Metadata } from \"next\";\nimport { ChevronLeftIcon } from \"lucide-react\";\n\nimport { getI18n } from \"locales/server\";\nimport { CalorieCalculatorClient } from \"app/[locale]/(app)/tools/calorie-calculator/shared/CalorieCalculatorClient\";\nimport { calculatorConfigs } from \"app/[locale]/(app)/tools/calorie-calculator/shared/calculator-configs\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { env } from \"@/env\";\nimport { generateSEOMetadata, SEOScripts } from \"@/components/seo/SEOHead\";\nimport { HorizontalTopBanner } from \"@/components/ads\";\nimport \"../styles.css\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return generateSEOMetadata({\n    title: t(\"tools.cunningham.meta.title\"),\n    description: t(\"tools.cunningham.meta.description\"),\n    keywords: t(\"tools.cunningham.meta.keywords\").split(\", \"),\n    locale,\n    canonical: `${getServerUrl()}/${locale}/tools/cunningham-calculator`,\n    structuredData: {\n      type: \"Calculator\",\n      calculatorData: {\n        calculatorType: \"calorie\",\n        inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"body fat percentage\", \"activity level\", \"goal\"],\n        outputFields: [\"BMR (Cunningham)\", \"lean body mass\", \"TDEE\", \"target calories\", \"recommended macros\"],\n        formula: \"Cunningham Equation - Active Individual Formula\",\n        accuracy: \"Excellent accuracy for active individuals with known body fat (±5% error rate)\",\n        targetAudience: [\"active athletes\", \"trained individuals\", \"bodybuilders\", \"fitness enthusiasts\", \"Cal the Chef users\"],\n        relatedCalculators: [\n          \"calorie-calculator\",\n          \"mifflin-st-jeor-calculator\",\n          \"harris-benedict-calculator\",\n          \"katch-mcardle-calculator\",\n          \"calorie-calculator-comparison\",\n        ],\n      },\n    },\n  });\n}\n\nexport default async function CunninghamCalculatorPage({ params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return (\n    <>\n      <SEOScripts\n        canonical={`${getServerUrl()}/${locale}/tools/cunningham-calculator`}\n        description={t(\"tools.cunningham.meta.description\")}\n        locale={locale}\n        structuredData={{\n          type: \"Calculator\",\n          calculatorData: {\n            calculatorType: \"calorie\",\n            inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"body fat percentage\", \"activity level\", \"goal\"],\n            outputFields: [\"BMR (Cunningham)\", \"lean body mass\", \"TDEE\", \"target calories\", \"recommended macros\"],\n            formula: \"Cunningham Equation - Active Individual Formula\",\n            accuracy: \"Excellent accuracy for active individuals with known body fat (±5% error rate)\",\n            targetAudience: [\"active athletes\", \"trained individuals\", \"bodybuilders\", \"fitness enthusiasts\", \"Cal the Chef users\"],\n            relatedCalculators: [\n              \"calorie-calculator\",\n              \"mifflin-st-jeor-calculator\",\n              \"harris-benedict-calculator\",\n              \"katch-mcardle-calculator\",\n              \"calorie-calculator-comparison\",\n            ],\n          },\n        }}\n        title={t(\"tools.cunningham.meta.title\")}\n      />\n      <div className=\"light:bg-white dark:bg-base-200/20\">\n        {env.NEXT_PUBLIC_TOP_CUNNINGHAM_CALCULATOR_AD_SLOT && (\n          <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_CUNNINGHAM_CALCULATOR_AD_SLOT} />\n        )}\n        <div className=\"container mx-auto px-2 sm:px-4 py-4 sm:py-8  max-w-4xl\">\n          {/* Back to hub */}\n          <Link\n            className=\"inline-flex items-center gap-2 text-sm text-base-content/60 hover:text-primary transition-colors mb-6\"\n            href=\"/tools/calorie-calculator\"\n          >\n            <ChevronLeftIcon className=\"w-4 h-4\" />\n            {t(\"tools.back_to_calculators\")}\n          </Link>\n\n          <div className=\"mb-8 text-center\">\n            <h1 className=\"text-4xl sm:text-5xl font-bold mb-4 bg-gradient-to-r from-[#8B5CF6] to-[#7C3AED] bg-clip-text text-transparent animate-fadeIn\">\n              {t(\"tools.cunningham.title\")}\n            </h1>\n            <p\n              className=\"text-lg text-base-content/70 dark:text-base-content/60 max-w-2xl mx-auto animate-fadeIn\"\n              style={{ animationDelay: \"0.1s\" }}\n            >\n              {t(\"tools.cunningham.subtitle\")}\n            </p>\n          </div>\n\n          {/* Educational Section */}\n          <div\n            className=\"mb-8 bg-gradient-to-br from-[#8B5CF6]/5 to-[#7C3AED]/5 dark:from-[#8B5CF6]/10 dark:to-[#7C3AED]/10 rounded-2xl border border-[#8B5CF6]/20 dark:border-[#8B5CF6]/30 p-6 animate-fadeIn\"\n            style={{ animationDelay: \"0.2s\" }}\n          >\n            <h2 className=\"text-xl font-bold mb-3 text-base-content dark:text-base-content/90\">{t(\"tools.cunningham.how_it_works\")}</h2>\n            <div className=\"space-y-2 text-base-content/70 dark:text-base-content/60\">\n              <p className=\"text-sm leading-relaxed\">{t(\"tools.cunningham.how_it_works_description\")}</p>\n              <div className=\"mt-4 p-3 bg-base-100/50 dark:bg-base-100/20 rounded-lg\">\n                <p className=\"text-xs font-mono text-base-content/70\">\n                  <strong>Cunningham:</strong> BMR = 500 + (22 × lean body mass)\n                  <br />\n                  <strong>Lean Body Mass:</strong> Weight(kg) × (1 - body fat %/100)\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <CalorieCalculatorClient config={calculatorConfigs.cunningham} />\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/harris-benedict-calculator/page.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\nimport { Metadata } from \"next\";\nimport { ChevronLeftIcon } from \"lucide-react\";\n\nimport { getI18n } from \"locales/server\";\nimport { CalorieCalculatorClient } from \"app/[locale]/(app)/tools/calorie-calculator/shared/CalorieCalculatorClient\";\nimport { calculatorConfigs } from \"app/[locale]/(app)/tools/calorie-calculator/shared/calculator-configs\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { env } from \"@/env\";\nimport { generateSEOMetadata, SEOScripts } from \"@/components/seo/SEOHead\";\nimport { HorizontalTopBanner } from \"@/components/ads\";\nimport \"../styles.css\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return generateSEOMetadata({\n    title: t(\"tools.harris-benedict.meta.title\"),\n    description: t(\"tools.harris-benedict.meta.description\"),\n    keywords: t(\"tools.harris-benedict.meta.keywords\").split(\", \"),\n    locale,\n    canonical: `${getServerUrl()}/${locale}/tools/harris-benedict-calculator`,\n    structuredData: {\n      type: \"Calculator\",\n      calculatorData: {\n        calculatorType: \"calorie\",\n        inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"activity level\", \"goal\"],\n        outputFields: [\"BMR (Harris-Benedict)\", \"TDEE\", \"target calories\", \"recommended macros\"],\n        formula: \"Harris-Benedict Equation (1984) - Classic Formula\",\n        accuracy: \"Good accuracy for most adults (±10-15% error rate)\",\n        targetAudience: [\"adults\", \"fitness enthusiasts\", \"health conscious individuals\", \"Cal the Chef users\"],\n        relatedCalculators: [\n          \"calorie-calculator\",\n          \"mifflin-st-jeor-calculator\",\n          \"katch-mcardle-calculator\",\n          \"calorie-calculator-comparison\",\n        ],\n      },\n    },\n  });\n}\n\nexport default async function HarrisBenedictCalculatorPage({ params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return (\n    <>\n      <SEOScripts\n        canonical={`${getServerUrl()}/${locale}/tools/harris-benedict-calculator`}\n        description={t(\"tools.harris-benedict.meta.description\")}\n        locale={locale}\n        structuredData={{\n          type: \"Calculator\",\n          calculatorData: {\n            calculatorType: \"calorie\",\n            inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"activity level\", \"goal\"],\n            outputFields: [\"BMR (Harris-Benedict)\", \"TDEE\", \"target calories\", \"recommended macros\"],\n            formula: \"Harris-Benedict Equation (1984) - Classic Formula\",\n            accuracy: \"Good accuracy for most adults (±10-15% error rate)\",\n            targetAudience: [\"adults\", \"fitness enthusiasts\", \"health conscious individuals\", \"Cal the Chef users\"],\n            relatedCalculators: [\n              \"calorie-calculator\",\n              \"mifflin-st-jeor-calculator\",\n              \"katch-mcardle-calculator\",\n              \"calorie-calculator-comparison\",\n            ],\n          },\n        }}\n        title={t(\"tools.harris-benedict.meta.title\")}\n      />\n      <div className=\"light:bg-white dark:bg-base-200/20\">\n        {env.NEXT_PUBLIC_TOP_HARRIS_BENEDICT_CALCULATOR_AD_SLOT && (\n          <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_HARRIS_BENEDICT_CALCULATOR_AD_SLOT} />\n        )}\n        <div className=\"container mx-auto px-2 sm:px-4 py-4 sm:py-8 max-w-4xl\">\n          {/* Back to hub */}\n          <Link\n            className=\"inline-flex items-center gap-2 text-sm text-base-content/60 hover:text-primary transition-colors mb-6\"\n            href=\"/tools/calorie-calculator\"\n          >\n            <ChevronLeftIcon className=\"w-4 h-4\" />\n            {t(\"tools.back_to_calculators\")}\n          </Link>\n\n          <div className=\"mb-8 text-center\">\n            <h1 className=\"text-4xl sm:text-5xl font-bold mb-4 bg-gradient-to-r from-[#25CB78] to-[#22C55E] bg-clip-text text-transparent animate-fadeIn\">\n              {t(\"tools.harris-benedict.title\")}\n            </h1>\n            <p\n              className=\"text-lg text-base-content/70 dark:text-base-content/60 max-w-2xl mx-auto animate-fadeIn\"\n              style={{ animationDelay: \"0.1s\" }}\n            >\n              {t(\"tools.harris-benedict.subtitle\")}\n            </p>\n          </div>\n\n          {/* Educational Section */}\n          <div\n            className=\"mb-8 bg-gradient-to-br from-[#25CB78]/5 to-[#22C55E]/5 dark:from-[#25CB78]/10 dark:to-[#22C55E]/10 rounded-2xl border border-[#25CB78]/20 dark:border-[#25CB78]/30 p-6 animate-fadeIn\"\n            style={{ animationDelay: \"0.2s\" }}\n          >\n            <h2 className=\"text-xl font-bold mb-3 text-base-content dark:text-base-content/90\">\n              {t(\"tools.harris-benedict.how_it_works\")}\n            </h2>\n            <div className=\"space-y-2 text-base-content/70 dark:text-base-content/60\">\n              <p className=\"text-sm leading-relaxed\">{t(\"tools.harris-benedict.how_it_works_description\")}</p>\n              <div className=\"mt-4 p-3 bg-base-100/50 dark:bg-base-100/20 rounded-lg\">\n                <p className=\"text-xs font-mono text-base-content/70\">\n                  <strong>Men:</strong> BMR = 88.362 + (13.397 × weight) + (4.799 × height) - (5.677 × age)\n                  <br />\n                  <strong>Women:</strong> BMR = 447.593 + (9.247 × weight) + (3.098 × height) - (4.330 × age)\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <CalorieCalculatorClient config={calculatorConfigs[\"harris-benedict\"]} />\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/katch-mcardle-calculator/page.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\nimport { Metadata } from \"next\";\nimport { ChevronLeftIcon } from \"lucide-react\";\n\nimport { getI18n } from \"locales/server\";\nimport { CalorieCalculatorClient } from \"app/[locale]/(app)/tools/calorie-calculator/shared/CalorieCalculatorClient\";\nimport { calculatorConfigs } from \"app/[locale]/(app)/tools/calorie-calculator/shared/calculator-configs\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { env } from \"@/env\";\nimport { generateSEOMetadata, SEOScripts } from \"@/components/seo/SEOHead\";\nimport { HorizontalTopBanner } from \"@/components/ads\";\nimport \"../styles.css\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return generateSEOMetadata({\n    title: t(\"tools.katch-mcardle.meta.title\"),\n    description: t(\"tools.katch-mcardle.meta.description\"),\n    keywords: t(\"tools.katch-mcardle.meta.keywords\").split(\", \"),\n    locale,\n    canonical: `${getServerUrl()}/${locale}/tools/katch-mcardle-calculator`,\n    structuredData: {\n      type: \"Calculator\",\n      calculatorData: {\n        calculatorType: \"calorie\",\n        inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"body fat percentage\", \"activity level\", \"goal\"],\n        outputFields: [\"BMR (Katch-McArdle)\", \"lean body mass\", \"TDEE\", \"target calories\", \"recommended macros\"],\n        formula: \"Katch-McArdle Equation - Body Composition Based\",\n        accuracy: \"Highest accuracy for lean individuals with known body fat (±5% error rate)\",\n        targetAudience: [\"athletes\", \"bodybuilders\", \"fitness professionals\", \"lean individuals\", \"Cal the Chef users\"],\n        relatedCalculators: [\n          \"calorie-calculator\",\n          \"mifflin-st-jeor-calculator\",\n          \"harris-benedict-calculator\",\n          \"cunningham-calculator\",\n          \"calorie-calculator-comparison\",\n        ],\n      },\n    },\n  });\n}\n\nexport default async function KatchMcArdleCalculatorPage({ params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return (\n    <>\n      <SEOScripts\n        canonical={`${getServerUrl()}/${locale}/tools/katch-mcardle-calculator`}\n        description={t(\"tools.katch-mcardle.meta.description\")}\n        locale={locale}\n        structuredData={{\n          type: \"Calculator\",\n          calculatorData: {\n            calculatorType: \"calorie\",\n            inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"body fat percentage\", \"activity level\", \"goal\"],\n            outputFields: [\"BMR (Katch-McArdle)\", \"lean body mass\", \"TDEE\", \"target calories\", \"recommended macros\"],\n            formula: \"Katch-McArdle Equation - Body Composition Based\",\n            accuracy: \"Highest accuracy for lean individuals with known body fat (±5% error rate)\",\n            targetAudience: [\"athletes\", \"bodybuilders\", \"fitness professionals\", \"lean individuals\", \"Cal the Chef users\"],\n            relatedCalculators: [\n              \"calorie-calculator\",\n              \"mifflin-st-jeor-calculator\",\n              \"harris-benedict-calculator\",\n              \"cunningham-calculator\",\n              \"calorie-calculator-comparison\",\n            ],\n          },\n        }}\n        title={t(\"tools.katch-mcardle.meta.title\")}\n      />\n      <div className=\"light:bg-white dark:bg-base-200/20\">\n        {env.NEXT_PUBLIC_TOP_KATCH_MCARDLE_CALCULATOR_AD_SLOT && (\n          <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_KATCH_MCARDLE_CALCULATOR_AD_SLOT} />\n        )}\n        <div className=\"container mx-auto px-2 sm:px-4 py-4 sm:py-8 max-w-4xl\">\n          {/* Back to hub */}\n          <Link\n            className=\"inline-flex items-center gap-2 text-sm text-base-content/60 hover:text-primary transition-colors mb-6\"\n            href=\"/tools/calorie-calculator\"\n          >\n            <ChevronLeftIcon className=\"w-4 h-4\" />\n            {t(\"tools.back_to_calculators\")}\n          </Link>\n\n          <div className=\"mb-8 text-center\">\n            <h1 className=\"text-4xl sm:text-5xl font-bold mb-4 bg-gradient-to-r from-[#FF5722] to-[#EF4444] bg-clip-text text-transparent animate-fadeIn\">\n              {t(\"tools.katch-mcardle.title\")}\n            </h1>\n            <p\n              className=\"text-lg text-base-content/70 dark:text-base-content/60 max-w-2xl mx-auto animate-fadeIn\"\n              style={{ animationDelay: \"0.1s\" }}\n            >\n              {t(\"tools.katch-mcardle.subtitle\")}\n            </p>\n          </div>\n\n          {/* Educational Section */}\n          <div\n            className=\"mb-8 bg-gradient-to-br from-[#FF5722]/5 to-[#EF4444]/5 dark:from-[#FF5722]/10 dark:to-[#EF4444]/10 rounded-2xl border border-[#FF5722]/20 dark:border-[#FF5722]/30 p-6 animate-fadeIn\"\n            style={{ animationDelay: \"0.2s\" }}\n          >\n            <h2 className=\"text-xl font-bold mb-3 text-base-content dark:text-base-content/90\">{t(\"tools.katch-mcardle.how_it_works\")}</h2>\n            <div className=\"space-y-2 text-base-content/70 dark:text-base-content/60\">\n              <p className=\"text-sm leading-relaxed\">{t(\"tools.katch-mcardle.how_it_works_description\")}</p>\n              <div className=\"mt-4 p-3 bg-base-100/50 dark:bg-base-100/20 rounded-lg\">\n                <p className=\"text-xs font-mono text-base-content/70\">\n                  <strong>Katch-McArdle:</strong> BMR = 370 + (21.6 × lean body mass)\n                  <br />\n                  <strong>Lean Body Mass:</strong> Weight(kg) × (1 - body fat %/100)\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <CalorieCalculatorClient config={calculatorConfigs[\"katch-mcardle\"]} />\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/mifflin-st-jeor-calculator/page.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\nimport { Metadata } from \"next\";\nimport { ChevronLeftIcon } from \"lucide-react\";\n\nimport { getI18n } from \"locales/server\";\nimport { CalorieCalculatorClient } from \"app/[locale]/(app)/tools/calorie-calculator/shared/CalorieCalculatorClient\";\nimport { calculatorConfigs } from \"app/[locale]/(app)/tools/calorie-calculator/shared/calculator-configs\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { env } from \"@/env\";\nimport { generateSEOMetadata, SEOScripts } from \"@/components/seo/SEOHead\";\nimport { HorizontalTopBanner } from \"@/components/ads\";\n\nimport \"../styles.css\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return generateSEOMetadata({\n    title: t(\"tools.mifflin-st-jeor.meta.title\"),\n    description: t(\"tools.mifflin-st-jeor.meta.description\"),\n    keywords: t(\"tools.mifflin-st-jeor.meta.keywords\").split(\", \"),\n    locale,\n    canonical: `${getServerUrl()}/${locale}/tools/mifflin-st-jeor-calculator`,\n    structuredData: {\n      type: \"Calculator\",\n      calculatorData: {\n        calculatorType: \"calorie\",\n        inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"activity level\", \"goal\"],\n        outputFields: [\"BMR (Mifflin-St Jeor)\", \"TDEE\", \"target calories\", \"recommended macros\"],\n        formula: \"Mifflin-St Jeor Equation (1990) - Gold Standard\",\n        accuracy: \"Most accurate for general population (±5-10% error rate)\",\n        targetAudience: [\"general population\", \"fitness beginners\", \"health conscious individuals\", \"Cal the Chef users\"],\n        relatedCalculators: [\n          \"calorie-calculator\",\n          \"harris-benedict-calculator\",\n          \"katch-mcardle-calculator\",\n          \"calorie-calculator-comparison\",\n        ],\n      },\n    },\n  });\n}\n\nexport default async function MifflinStJeorCalculatorPage({ params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return (\n    <>\n      <SEOScripts\n        canonical={`${getServerUrl()}/${locale}/tools/mifflin-st-jeor-calculator`}\n        description={t(\"tools.mifflin-st-jeor.meta.description\")}\n        locale={locale}\n        structuredData={{\n          type: \"Calculator\",\n          calculatorData: {\n            calculatorType: \"calorie\",\n            inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"activity level\", \"goal\"],\n            outputFields: [\"BMR (Mifflin-St Jeor)\", \"TDEE\", \"target calories\", \"recommended macros\"],\n            formula: \"Mifflin-St Jeor Equation (1990) - Gold Standard\",\n            accuracy: \"Most accurate for general population (±5-10% error rate)\",\n            targetAudience: [\"general population\", \"fitness beginners\", \"health conscious individuals\", \"Cal the Chef users\"],\n            relatedCalculators: [\n              \"calorie-calculator\",\n              \"harris-benedict-calculator\",\n              \"katch-mcardle-calculator\",\n              \"calorie-calculator-comparison\",\n            ],\n          },\n        }}\n        title={t(\"tools.mifflin-st-jeor.meta.title\")}\n      />\n      <div className=\"light:bg-white dark:bg-base-200/20\">\n        {env.NEXT_PUBLIC_TOP_MIFFLIN_ST_JEOR_CALCULATOR_AD_SLOT && (\n          <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_MIFFLIN_ST_JEOR_CALCULATOR_AD_SLOT} />\n        )}\n        <div className=\"container mx-auto px-2 sm:px-4 py-4 sm:py-8 max-w-4xl\">\n          {/* Back to hub */}\n          <Link\n            className=\"inline-flex items-center gap-2 text-sm text-base-content/60 hover:text-primary transition-colors mb-6\"\n            href=\"/tools/calorie-calculator\"\n          >\n            <ChevronLeftIcon className=\"w-4 h-4\" />\n            {t(\"tools.back_to_calculators\")}\n          </Link>\n\n          <div className=\"mb-8 text-center\">\n            <h1 className=\"text-4xl sm:text-5xl font-bold mb-4 bg-gradient-to-r from-[#4F8EF7] to-[#238BE6] bg-clip-text text-transparent animate-fadeIn\">\n              {t(\"tools.mifflin-st-jeor.title\")}\n            </h1>\n            <p\n              className=\"text-lg text-base-content/70 dark:text-base-content/60 max-w-2xl mx-auto animate-fadeIn\"\n              style={{ animationDelay: \"0.1s\" }}\n            >\n              {t(\"tools.mifflin-st-jeor.subtitle\")}\n            </p>\n          </div>\n\n          {/* Educational Section */}\n          <div\n            className=\"mb-8 bg-gradient-to-br from-[#4F8EF7]/5 to-[#238BE6]/5 dark:from-[#4F8EF7]/10 dark:to-[#238BE6]/10 rounded-2xl border border-[#4F8EF7]/20 dark:border-[#4F8EF7]/30 p-6 animate-fadeIn\"\n            style={{ animationDelay: \"0.2s\" }}\n          >\n            <h2 className=\"text-xl font-bold mb-3 text-base-content dark:text-base-content/90\">\n              {t(\"tools.mifflin-st-jeor.how_it_works\")}\n            </h2>\n            <div className=\"space-y-2 text-base-content/70 dark:text-base-content/60\">\n              <p className=\"text-sm leading-relaxed\">{t(\"tools.mifflin-st-jeor.how_it_works_description\")}</p>\n              <div className=\"mt-4 p-3 bg-base-100/50 dark:bg-base-100/20 rounded-lg\">\n                <p className=\"text-xs font-mono text-base-content/70\">\n                  <strong>Men:</strong> BMR = 10 × weight(kg) + 6.25 × height(cm) - 5 × age + 5<br />\n                  <strong>Women:</strong> BMR = 10 × weight(kg) + 6.25 × height(cm) - 5 × age - 161\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <CalorieCalculatorClient config={calculatorConfigs[\"mifflin-st-jeor\"]} />\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/oxford-calculator/page.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\nimport { Metadata } from \"next\";\nimport { ChevronLeftIcon } from \"lucide-react\";\n\nimport { getI18n } from \"locales/server\";\nimport { CalorieCalculatorClient } from \"app/[locale]/(app)/tools/calorie-calculator/shared/CalorieCalculatorClient\";\nimport { calculatorConfigs } from \"app/[locale]/(app)/tools/calorie-calculator/shared/calculator-configs\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { env } from \"@/env\";\nimport { generateSEOMetadata, SEOScripts } from \"@/components/seo/SEOHead\";\nimport { HorizontalTopBanner } from \"@/components/ads\";\nimport \"../styles.css\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return generateSEOMetadata({\n    title: t(\"tools.oxford.meta.title\"),\n    description: t(\"tools.oxford.meta.description\"),\n    keywords: t(\"tools.oxford.meta.keywords\").split(\", \"),\n    locale,\n    canonical: `${getServerUrl()}/${locale}/tools/oxford-calculator`,\n    structuredData: {\n      type: \"Calculator\",\n      calculatorData: {\n        calculatorType: \"calorie\",\n        inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"physical activity\", \"goal\"],\n        outputFields: [\"BMR (Oxford)\", \"TDEE\", \"target calories\", \"recommended macros\"],\n        formula: \"Oxford Equation (2005) - Physical Activity Based\",\n        accuracy: \"Modern accuracy incorporating physical activity (±8-12% error rate)\",\n        targetAudience: [\"modern populations\", \"diverse ethnicities\", \"health professionals\", \"Cal the Chef users\"],\n        relatedCalculators: [\n          \"calorie-calculator\",\n          \"mifflin-st-jeor-calculator\",\n          \"harris-benedict-calculator\",\n          \"katch-mcardle-calculator\",\n          \"calorie-calculator-comparison\",\n        ],\n      },\n    },\n  });\n}\n\nexport default async function OxfordCalculatorPage({ params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return (\n    <>\n      <SEOScripts\n        canonical={`${getServerUrl()}/${locale}/tools/oxford-calculator`}\n        description={t(\"tools.oxford.meta.description\")}\n        locale={locale}\n        structuredData={{\n          type: \"Calculator\",\n          calculatorData: {\n            calculatorType: \"calorie\",\n            inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"physical activity\", \"goal\"],\n            outputFields: [\"BMR (Oxford)\", \"TDEE\", \"target calories\", \"recommended macros\"],\n            formula: \"Oxford Equation (2005) - Physical Activity Based\",\n            accuracy: \"Modern accuracy incorporating physical activity (±8-12% error rate)\",\n            targetAudience: [\"modern populations\", \"diverse ethnicities\", \"health professionals\", \"Cal the Chef users\"],\n            relatedCalculators: [\n              \"calorie-calculator\",\n              \"mifflin-st-jeor-calculator\",\n              \"harris-benedict-calculator\",\n              \"katch-mcardle-calculator\",\n              \"calorie-calculator-comparison\",\n            ],\n          },\n        }}\n        title={t(\"tools.oxford.meta.title\")}\n      />\n      <div className=\"light:bg-white dark:bg-base-200/20\">\n        {env.NEXT_PUBLIC_TOP_OXFORD_CALCULATOR_AD_SLOT && <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_OXFORD_CALCULATOR_AD_SLOT} />}\n        <div className=\"container mx-auto px-2 sm:px-4 py-4 sm:py-8 max-w-4xl\">\n          {/* Back to hub */}\n          <Link\n            className=\"inline-flex items-center gap-2 text-sm text-base-content/60 hover:text-primary transition-colors mb-6\"\n            href=\"/tools/calorie-calculator\"\n          >\n            <ChevronLeftIcon className=\"w-4 h-4\" />\n            {t(\"tools.back_to_calculators\")}\n          </Link>\n\n          <div className=\"mb-8 text-center\">\n            <h1 className=\"text-4xl sm:text-5xl font-bold mb-4 bg-gradient-to-r from-[#25CB78] to-[#22C55E] bg-clip-text text-transparent animate-fadeIn\">\n              {t(\"tools.oxford.title\")}\n            </h1>\n            <p\n              className=\"text-lg text-base-content/70 dark:text-base-content/60 max-w-2xl mx-auto animate-fadeIn\"\n              style={{ animationDelay: \"0.1s\" }}\n            >\n              {t(\"tools.oxford.subtitle\")}\n            </p>\n          </div>\n\n          {/* Educational Section */}\n          <div\n            className=\"mb-8 bg-gradient-to-br from-[#25CB78]/5 to-[#22C55E]/5 dark:from-[#25CB78]/10 dark:to-[#22C55E]/10 rounded-2xl border border-[#25CB78]/20 dark:border-[#25CB78]/30 p-6 animate-fadeIn\"\n            style={{ animationDelay: \"0.2s\" }}\n          >\n            <h2 className=\"text-xl font-bold mb-3 text-base-content dark:text-base-content/90\">{t(\"tools.oxford.how_it_works\")}</h2>\n            <div className=\"space-y-2 text-base-content/70 dark:text-base-content/60\">\n              <p className=\"text-sm leading-relaxed\">{t(\"tools.oxford.how_it_works_description\")}</p>\n              <div className=\"mt-4 p-3 bg-base-100/50 dark:bg-base-100/20 rounded-lg\">\n                <p className=\"text-xs font-mono text-base-content/70\">\n                  <strong>Men:</strong> BMR = 662 - (9.53 × age) + PA × (15.91 × weight + 539.6 × height)\n                  <br />\n                  <strong>Women:</strong> BMR = 354 - (6.91 × age) + PA × (9.36 × weight + 726 × height)\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <CalorieCalculatorClient config={calculatorConfigs.oxford} />\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/page.tsx",
    "content": "import React from \"react\";\nimport { Metadata } from \"next\";\n\nimport { getI18n } from \"locales/server\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { generateSEOMetadata, SEOScripts } from \"@/components/seo/SEOHead\";\n\nimport { CalorieCalculatorHub } from \"./CalorieCalculatorHub\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return generateSEOMetadata({\n    title: t(\"tools.calorie-calculator-hub.meta.title\"),\n    description: t(\"tools.calorie-calculator-hub.meta.description\"),\n    keywords: t(\"tools.calorie-calculator-hub.meta.keywords\").split(\", \"),\n    locale,\n    canonical: `${getServerUrl()}/${locale}/tools/calorie-calculator`,\n    structuredData: {\n      type: \"Calculator\",\n      calculatorData: {\n        calculatorType: \"calorie\",\n        inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"activity level\", \"goal\"],\n        outputFields: [\"BMR\", \"TDEE\", \"target calories\", \"recommended macros\"],\n        formula: \"Mifflin-St Jeor Equation\",\n        accuracy: \"±100-200 calories (scientifically validated)\",\n        targetAudience: [\"fitness enthusiasts\", \"athletes\", \"weight loss seekers\", \"health conscious individuals\"],\n        relatedCalculators: [\n          \"mifflin-st-jeor-calculator\",\n          \"harris-benedict-calculator\",\n          \"katch-mcardle-calculator\",\n          \"cunningham-calculator\",\n          \"oxford-calculator\",\n        ],\n      },\n    },\n  });\n}\n\nexport default async function CalorieCalculatorPage({ params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  return (\n    <>\n      <SEOScripts\n        canonical={`${getServerUrl()}/${locale}/tools/calorie-calculator`}\n        description={t(\"tools.calorie-calculator-hub.meta.description\")}\n        locale={locale}\n        structuredData={{\n          type: \"Calculator\",\n          calculatorData: {\n            calculatorType: \"calorie\",\n            inputFields: [\"gender\", \"age\", \"height\", \"weight\", \"activity level\", \"goal\"],\n            outputFields: [\"BMR\", \"TDEE\", \"target calories\", \"recommended macros\"],\n            formula: \"Mifflin-St Jeor Equation\",\n            accuracy: \"±100-200 calories (scientifically validated)\",\n            targetAudience: [\"fitness enthusiasts\", \"athletes\", \"weight loss seekers\", \"health conscious individuals\"],\n            relatedCalculators: [\n              \"mifflin-st-jeor-calculator\",\n              \"harris-benedict-calculator\",\n              \"katch-mcardle-calculator\",\n              \"cunningham-calculator\",\n              \"oxford-calculator\",\n            ],\n          },\n        }}\n        title={t(\"tools.calorie-calculator-hub.meta.title\")}\n      />\n      <div className=\"light:bg-white dark:bg-base-200/20\">\n        <div className=\"container mx-auto px-2 sm:px-4 py-4 sm:py-8 max-w-4xl\">\n          <CalorieCalculatorHub />\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/CalorieCalculatorClient.tsx",
    "content": "\"use client\";\n\nimport React, { useState } from \"react\";\nimport { Calculator } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { env } from \"@/env\";\nimport { HorizontalBottomBanner } from \"@/components/ads\";\n\nimport { BodyFatInput } from \"./components/BodyFatInput\";\nimport {\n  GenderSelector,\n  WeightInput,\n  HeightInput,\n  AgeInput,\n  UnitSelector,\n  ActivityLevelSelector,\n  GoalSelector,\n  ResultsDisplay,\n} from \"./components\";\nimport { calculateCalories, type CalorieCalculatorInputs, type CalorieResults } from \"./calorie-formulas.utils\";\n\nimport type { CalculatorConfig } from \"./types\";\n\ninterface CalorieCalculatorClientProps {\n  config: CalculatorConfig;\n}\n\nexport function CalorieCalculatorClient({ config }: CalorieCalculatorClientProps) {\n  const t = useI18n();\n\n  // Form state\n  const [gender, setGender] = useState<CalorieCalculatorInputs[\"gender\"]>(\"male\");\n  const [unit, setUnit] = useState<CalorieCalculatorInputs[\"unit\"]>(\"metric\");\n  const [age, setAge] = useState(25);\n  const [height, setHeight] = useState(unit === \"metric\" ? 175 : 69);\n  const [weight, setWeight] = useState(unit === \"metric\" ? 70 : 154);\n  const [activityLevel, setActivityLevel] = useState<CalorieCalculatorInputs[\"activityLevel\"]>(\"moderate\");\n  const [goal, setGoal] = useState<CalorieCalculatorInputs[\"goal\"]>(\"maintain\");\n  const [bodyFatPercentage, setBodyFatPercentage] = useState(20);\n\n  // Results state\n  const [results, setResults] = useState<CalorieResults | null>(null);\n  const [isCalculating, setIsCalculating] = useState(false);\n\n  // Handle unit change\n  const handleUnitChange = (newUnit: CalorieCalculatorInputs[\"unit\"]) => {\n    setUnit(newUnit);\n    // Convert values\n    if (newUnit === \"imperial\" && unit === \"metric\") {\n      setWeight(Math.round(weight * 2.20462));\n      setHeight(Math.round(height / 2.54));\n    } else if (newUnit === \"metric\" && unit === \"imperial\") {\n      setWeight(Math.round(weight / 2.20462));\n      setHeight(Math.round(height * 2.54));\n    }\n  };\n\n  // Calculate calories\n  const handleCalculate = () => {\n    setIsCalculating(true);\n\n    const inputs: CalorieCalculatorInputs = {\n      gender,\n      unit,\n      age,\n      height,\n      weight,\n      activityLevel,\n      goal,\n      ...(config.requiresBodyFat && { bodyFatPercentage }),\n    };\n\n    try {\n      const calculatedResults = calculateCalories(inputs, config.formula);\n      setResults(calculatedResults);\n    } catch (error) {\n      console.error(\"Error calculating calories:\", error);\n    } finally {\n      setIsCalculating(false);\n    }\n  };\n\n  return (\n    <div className=\"max-w-5xl mx-auto\">\n      <div className=\"grid gap-6\">\n        {/* Input Section */}\n        <div className=\"bg-base-100/50 dark:bg-base-200/20 backdrop-blur-sm rounded-3xl shadow-xl p-6 sm:p-8 border border-base-content/5\">\n          <div className=\"space-y-6\">\n            <div className=\"space-y-6\">\n              <GenderSelector onChange={setGender} value={gender} />\n              <UnitSelector onChange={handleUnitChange} value={unit} />\n              <AgeInput onChange={setAge} value={age} />\n              <HeightInput onChange={setHeight} unit={unit} value={height} />\n              <WeightInput onChange={setWeight} unit={unit} value={weight} />\n              {config.requiresBodyFat && <BodyFatInput onChange={setBodyFatPercentage} value={bodyFatPercentage} />}\n              <ActivityLevelSelector onChange={setActivityLevel} value={activityLevel} />\n              <GoalSelector onChange={setGoal} value={goal} />\n            </div>\n\n            {env.NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_AD_SLOT && (\n              <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_AD_SLOT} />\n            )}\n\n            <button\n              className={`w-full py-4 px-6 rounded-2xl font-bold text-white transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98] shadow-lg flex items-center justify-center gap-3 ${\n                isCalculating ? \"opacity-75 cursor-not-allowed\" : \"\"\n              }`}\n              disabled={isCalculating}\n              onClick={handleCalculate}\n              style={{\n                background: `linear-gradient(135deg, ${config.buttonGradient.from} 0%, ${config.buttonGradient.to} 100%)`,\n              }}\n            >\n              <Calculator className=\"w-6 h-6\" />\n\n              <span className=\"text-lg\">\n                {isCalculating ? t(\"tools.calorie-calculator.calculating\") : t(\"tools.calorie-calculator.calculate\")}\n              </span>\n            </button>\n          </div>\n        </div>\n\n        {/* Results Section */}\n        {results && <ResultsDisplay results={results} />}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/calculator-configs.ts",
    "content": "import type { CalculatorConfig } from \"./types\";\n\n// Calculator configurations\nexport const calculatorConfigs: Record<string, CalculatorConfig> = {\n  oxford: {\n    formula: \"oxford\",\n    requiresBodyFat: false,\n    name: \"Oxford Calculator\",\n    description: \"Uses the Oxford formula for calorie calculation\",\n    buttonGradient: {\n      from: \"#4F8EF7\",\n      to: \"#238BE6\",\n    },\n  },\n  \"mifflin-st-jeor\": {\n    formula: \"mifflin\",\n    requiresBodyFat: false,\n    name: \"Mifflin-St Jeor Calculator\",\n    description: \"Uses the Mifflin-St Jeor formula (most accurate for general population)\",\n    buttonGradient: {\n      from: \"#06B6D4\",\n      to: \"#0891B2\",\n    },\n  },\n  \"harris-benedict\": {\n    formula: \"harris\",\n    requiresBodyFat: false,\n    name: \"Harris-Benedict Calculator\",\n    description: \"Uses the revised Harris-Benedict formula\",\n    buttonGradient: {\n      from: \"#10B981\",\n      to: \"#059669\",\n    },\n  },\n  \"katch-mcardle\": {\n    formula: \"katch\",\n    requiresBodyFat: true,\n    name: \"Katch-McArdle Calculator\",\n    description: \"Most accurate for lean individuals (requires body fat %)\",\n    buttonGradient: {\n      from: \"#F59E0B\",\n      to: \"#D97706\",\n    },\n  },\n  cunningham: {\n    formula: \"cunningham\",\n    requiresBodyFat: true,\n    name: \"Cunningham Calculator\",\n    description: \"Best for athletes with very low body fat (requires body fat %)\",\n    buttonGradient: {\n      from: \"#8B5CF6\",\n      to: \"#7C3AED\",\n    },\n  },\n};\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/calorie-formulas.utils.ts",
    "content": "// Shared types and formulas for all calorie calculators\n\nexport type Gender = \"male\" | \"female\";\nexport type UnitSystem = \"metric\" | \"imperial\";\nexport type ActivityLevel = \"sedentary\" | \"light\" | \"moderate\" | \"active\" | \"very_active\";\nexport type Goal = \"lose_fast\" | \"lose_slow\" | \"maintain\" | \"gain_slow\" | \"gain_fast\";\n\nexport interface CalorieCalculatorInputs {\n  gender: Gender;\n  unit: UnitSystem;\n  age: number;\n  height: number; // cm for metric, inches for imperial\n  weight: number; // kg for metric, lbs for imperial\n  activityLevel: ActivityLevel;\n  goal: Goal;\n  bodyFatPercentage?: number; // Optional for Katch-McArdle\n}\n\nexport interface CalorieResults {\n  bmr: number;\n  tdee: number;\n  targetCalories: number;\n  proteinGrams: number;\n  carbsGrams: number;\n  fatGrams: number;\n}\n\n// Activity level multipliers\nexport const ACTIVITY_MULTIPLIERS: Record<ActivityLevel, number> = {\n  sedentary: 1.2,\n  light: 1.375,\n  moderate: 1.55,\n  active: 1.725,\n  very_active: 1.9,\n};\n\n// Goal adjustments\nexport const GOAL_ADJUSTMENTS: Record<Goal, number> = {\n  lose_fast: -1000,\n  lose_slow: -500,\n  maintain: 0,\n  gain_slow: 500,\n  gain_fast: 1000,\n};\n\n// Convert imperial to metric\nexport function convertToMetric(inputs: CalorieCalculatorInputs): {\n  weight: number;\n  height: number;\n} {\n  if (inputs.unit === \"metric\") {\n    return { weight: inputs.weight, height: inputs.height };\n  }\n  return {\n    weight: inputs.weight * 0.453592,\n    height: inputs.height * 2.54,\n  };\n}\n\n// Calculate macros\nexport function calculateMacros(targetCalories: number): {\n  proteinGrams: number;\n  carbsGrams: number;\n  fatGrams: number;\n} {\n  const proteinCalories = targetCalories * 0.3;\n  const carbsCalories = targetCalories * 0.4;\n  const fatCalories = targetCalories * 0.3;\n\n  return {\n    proteinGrams: Math.round(proteinCalories / 4),\n    carbsGrams: Math.round(carbsCalories / 4),\n    fatGrams: Math.round(fatCalories / 9),\n  };\n}\n\n// Mifflin-St Jeor Formula (1990)\nexport function calculateMifflinStJeor(inputs: CalorieCalculatorInputs): number {\n  const { weight, height } = convertToMetric(inputs);\n  const baseBMR = 10 * weight + 6.25 * height - 5 * inputs.age;\n\n  return inputs.gender === \"male\" ? baseBMR + 5 : baseBMR - 161;\n}\n\n// Harris-Benedict Revised Formula (1984)\nexport function calculateHarrisBenedict(inputs: CalorieCalculatorInputs): number {\n  const { weight, height } = convertToMetric(inputs);\n\n  if (inputs.gender === \"male\") {\n    return 88.362 + 13.397 * weight + 4.799 * height - 5.677 * inputs.age;\n  } else {\n    return 447.593 + 9.247 * weight + 3.098 * height - 4.33 * inputs.age;\n  }\n}\n\n// Katch-McArdle Formula (requires body fat percentage)\nexport function calculateKatchMcArdle(inputs: CalorieCalculatorInputs): number {\n  if (!inputs.bodyFatPercentage) {\n    throw new Error(\"Body fat percentage is required for Katch-McArdle formula\");\n  }\n\n  const { weight } = convertToMetric(inputs);\n  const leanBodyMass = weight * (1 - inputs.bodyFatPercentage / 100);\n\n  return 370 + 21.6 * leanBodyMass;\n}\n\n// Cunningham Formula (for athletes with very low body fat)\nexport function calculateCunningham(inputs: CalorieCalculatorInputs): number {\n  if (!inputs.bodyFatPercentage) {\n    throw new Error(\"Body fat percentage is required for Cunningham formula\");\n  }\n\n  const { weight } = convertToMetric(inputs);\n  const leanBodyMass = weight * (1 - inputs.bodyFatPercentage / 100);\n\n  return 500 + 22 * leanBodyMass;\n}\n\n// Oxford Formula (2005)\nexport function calculateOxford(inputs: CalorieCalculatorInputs): number {\n  const { weight } = convertToMetric(inputs);\n\n  if (inputs.gender === \"male\") {\n    return inputs.age < 30 ? 16.6 * weight + (77 * inputs.height) / 100 + 572 : 14.4 * weight + (313 * inputs.height) / 100 + 113;\n  } else {\n    return inputs.age < 30 ? 13.1 * weight + (558 * inputs.height) / 100 + 184 : 13.4 * weight + (346 * inputs.height) / 100 + 247;\n  }\n}\n\n// Main calculation function\nexport function calculateCalories(\n  inputs: CalorieCalculatorInputs,\n  formula: \"mifflin\" | \"harris\" | \"katch\" | \"cunningham\" | \"oxford\" = \"mifflin\",\n): CalorieResults {\n  let bmr: number;\n\n  switch (formula) {\n    case \"harris\":\n      bmr = calculateHarrisBenedict(inputs);\n      break;\n    case \"katch\":\n      bmr = calculateKatchMcArdle(inputs);\n      break;\n    case \"cunningham\":\n      bmr = calculateCunningham(inputs);\n      break;\n    case \"oxford\":\n      bmr = calculateOxford(inputs);\n      break;\n    default:\n      bmr = calculateMifflinStJeor(inputs);\n  }\n\n  const tdee = bmr * ACTIVITY_MULTIPLIERS[inputs.activityLevel];\n  const targetCalories = Math.max(1200, tdee + GOAL_ADJUSTMENTS[inputs.goal]);\n  const macros = calculateMacros(targetCalories);\n\n  return {\n    bmr: Math.round(bmr),\n    tdee: Math.round(tdee),\n    targetCalories: Math.round(targetCalories),\n    ...macros,\n  };\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/ActivityLevelSelector.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { ActivityIcon, BedIcon, BedDoubleIcon, BikeIcon, ZapIcon } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { ActivityLevel } from \"app/[locale]/(app)/tools/calorie-calculator/calorie-calculator.utils\";\n\ninterface ActivityLevelSelectorProps {\n  value: ActivityLevel;\n  onChange: (level: ActivityLevel) => void;\n}\n\nconst ACTIVITY_LEVELS: ActivityLevel[] = [\"sedentary\", \"light\", \"moderate\", \"active\", \"very_active\"];\n\nexport function ActivityLevelSelector({ value, onChange }: ActivityLevelSelectorProps) {\n  const t = useI18n();\n\n  const ACTIVITY_ICONS: Record<ActivityLevel, React.ReactNode> = {\n    sedentary: <BedIcon className=\"w-5 h-5\" />,\n    light: <BedDoubleIcon className=\"w-5 h-5\" />,\n    moderate: <ActivityIcon className=\"w-5 h-5\" />,\n    active: <BikeIcon className=\"w-5 h-5\" />,\n    very_active: <ZapIcon className=\"w-5 h-5\" />,\n  };\n\n  const ACTIVITY_COLORS: Record<ActivityLevel, { border: string; bg: string; text: string }> = {\n    sedentary: { border: \"border-gray-400\", bg: \"from-gray-400/20 to-gray-500/10\", text: \"text-gray-600\" },\n    light: { border: \"border-blue-400\", bg: \"from-blue-400/20 to-blue-500/10\", text: \"text-blue-600\" },\n    moderate: { border: \"border-green-500\", bg: \"from-green-500/20 to-green-600/10\", text: \"text-green-600\" },\n    active: { border: \"border-orange-500\", bg: \"from-orange-500/20 to-orange-600/10\", text: \"text-orange-600\" },\n    very_active: { border: \"border-red-500\", bg: \"from-red-500/20 to-red-600/10\", text: \"text-red-600\" },\n  };\n\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 block\">\n        {t(\"tools.calorie-calculator.activity_level\")}\n      </label>\n      <div className=\"space-y-3\">\n        {ACTIVITY_LEVELS.map((level) => {\n          const colors = ACTIVITY_COLORS[level];\n          const isSelected = value === level;\n\n          return (\n            <button\n              className={`group w-full text-left p-3 sm:p-4 rounded-2xl border-2 transition-all duration-300 touch-manipulation ${\n                isSelected\n                  ? `${colors.border} bg-gradient-to-br ${colors.bg} scale-[1.02] dark:${colors.bg.replace(\"/20\", \"/15\").replace(\"/10\", \"/5\")}`\n                  : \"border-base-content/15 dark:border-base-content/10 hover:border-base-content/30 bg-base-100 dark:bg-base-200/30 hover:bg-base-200/50 active:bg-base-200/70 active:scale-[0.98]\"\n              }`}\n              key={level}\n              onClick={() => onChange(level)}\n            >\n              <div className=\"flex items-start gap-3 sm:gap-4\">\n                <div\n                  className={`p-2 rounded-lg transition-all duration-300 ${\n                    isSelected\n                      ? `${colors.bg} dark:${colors.bg.replace(\"/20\", \"/15\")}`\n                      : \"bg-base-200 dark:bg-base-300/20 group-hover:bg-base-300/50\"\n                  }`}\n                >\n                  <div\n                    className={`transition-colors duration-300 ${\n                      isSelected ? colors.text : \"text-base-content/60 group-hover:text-base-content/80\"\n                    }`}\n                  >\n                    {ACTIVITY_ICONS[level]}\n                  </div>\n                </div>\n                <div className=\"flex-1\">\n                  <div\n                    className={`font-semibold transition-colors duration-300 ${\n                      isSelected ? colors.text : \"text-base-content/80 dark:text-base-content/70\"\n                    }`}\n                  >\n                    {t(`tools.calorie-calculator.activity.${level}`)}\n                  </div>\n                  <div className=\"text-sm text-base-content/60 dark:text-base-content/50 mt-0.5\">\n                    {t(`tools.calorie-calculator.activity.${level}_desc`)}\n                  </div>\n                </div>\n              </div>\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/AgeInput.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { useI18n } from \"locales/client\";\n\ninterface AgeInputProps {\n  value: number;\n  onChange: (age: number) => void;\n}\n\nexport function AgeInput({ value, onChange }: AgeInputProps) {\n  const t = useI18n();\n\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 block\">\n        {t(\"tools.calorie-calculator.age\")}\n      </label>\n      <div className=\"bg-base-100 dark:bg-base-200/30 rounded-2xl border-2 border-base-content/15 dark:border-base-content/10 p-4 transition-all duration-300 hover:border-primary/30\">\n        <div className=\"flex items-center gap-4\">\n          <input\n            className=\"w-24 px-3 py-2 text-2xl font-bold text-center rounded-xl border-2 border-base-content/10 bg-base-200/50 dark:bg-base-300/20 text-base-content dark:text-base-content/90 focus:border-primary focus:outline-none transition-all duration-300\"\n            max=\"100\"\n            min=\"13\"\n            onChange={(e) => onChange(Number(e.target.value))}\n            placeholder={t(\"tools.calorie-calculator.age_placeholder\")}\n            type=\"number\"\n            value={value}\n          />\n          <div className=\"flex-1\">\n            <input\n              className=\"w-full h-2 bg-base-200 dark:bg-base-300/30 rounded-lg appearance-none cursor-pointer slider\"\n              max=\"100\"\n              min=\"13\"\n              onChange={(e) => onChange(Number(e.target.value))}\n              style={{\n                background: `linear-gradient(to right, #4F8EF7 0%, #4F8EF7 ${((value - 13) / 87) * 100}%, rgb(229 231 235 / 0.3) ${((value - 13) / 87) * 100}%, rgb(229 231 235 / 0.3) 100%)`,\n              }}\n              type=\"range\"\n              value={value}\n            />\n            <div className=\"flex justify-between mt-1 text-xs text-base-content/50\">\n              <span>13</span>\n              <span>100</span>\n            </div>\n          </div>\n          <div className=\"text-center\">\n            <span className=\"text-sm text-base-content/60 dark:text-base-content/50\">{t(\"tools.calorie-calculator.years\")}</span>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/BodyFatInput.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { useI18n } from \"locales/client\";\n\ninterface BodyFatInputProps {\n  value: number;\n  onChange: (bodyFat: number) => void;\n}\n\nexport function BodyFatInput({ value, onChange }: BodyFatInputProps) {\n  const t = useI18n();\n\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 flex items-center gap-2\">\n        {t(\"tools.body_fat_percentage\")}\n      </label>\n      <div className=\"bg-base-100 dark:bg-base-200/30 rounded-2xl border-2 border-base-content/15 dark:border-base-content/10 p-4 transition-all duration-300 hover:border-primary/30\">\n        <div className=\"flex items-center gap-4\">\n          <input\n            className=\"w-24 px-3 py-2 text-2xl font-bold text-center rounded-xl border-2 border-base-content/10 bg-base-200/50 dark:bg-base-300/20 text-base-content dark:text-base-content/90 focus:border-primary focus:outline-none transition-all duration-300\"\n            max=\"50\"\n            min=\"5\"\n            onChange={(e) => onChange(Number(e.target.value))}\n            placeholder=\"15\"\n            step=\"0.5\"\n            type=\"number\"\n            value={value}\n          />\n          <div className=\"flex-1\">\n            <input\n              className=\"w-full h-2 bg-base-200 dark:bg-base-300/30 rounded-lg appearance-none cursor-pointer slider\"\n              max=\"50\"\n              min=\"5\"\n              onChange={(e) => onChange(Number(e.target.value))}\n              step=\"0.5\"\n              style={{\n                background: `linear-gradient(to right, #FF5722 0%, #FF5722 ${((value - 5) / 45) * 100}%, rgb(229 231 235 / 0.3) ${((value - 5) / 45) * 100}%, rgb(229 231 235 / 0.3) 100%)`,\n              }}\n              type=\"range\"\n              value={value}\n            />\n            <div className=\"flex justify-between mt-1 text-xs text-base-content/50\">\n              <span>5%</span>\n              <span>50%</span>\n            </div>\n          </div>\n          <div className=\"text-center\">\n            <span className=\"text-sm text-base-content/60 dark:text-base-content/50\">%</span>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/FAQSection.tsx",
    "content": "\"use client\";\n\nimport React, { useState } from \"react\";\nimport { ChevronDownIcon, HelpCircleIcon } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\n\nexport function FAQSection() {\n  const t = useI18n();\n  const [openIndex, setOpenIndex] = useState<number | null>(null);\n\n  const faqs = [\n    {\n      question: t(\"tools.calorie-calculator.faq.q1\"),\n      answer: t(\"tools.calorie-calculator.faq.a1\"),\n    },\n    {\n      question: t(\"tools.calorie-calculator.faq.q2\"),\n      answer: t(\"tools.calorie-calculator.faq.a2\"),\n    },\n    {\n      question: t(\"tools.calorie-calculator.faq.q3\"),\n      answer: t(\"tools.calorie-calculator.faq.a3\"),\n    },\n    {\n      question: t(\"tools.calorie-calculator.faq.q4\"),\n      answer: t(\"tools.calorie-calculator.faq.a4\"),\n    },\n  ];\n\n  return (\n    <div className=\"mt-12 bg-base-200/30 dark:bg-base-200/20 rounded-2xl border border-base-content/10 dark:border-base-content/5 p-6 sm:p-8\">\n      <div className=\"flex items-center gap-3 mb-6\">\n        <div className=\"p-2 rounded-lg bg-gradient-to-br from-[#4F8EF7]/20 to-[#238BE6]/10\">\n          <HelpCircleIcon className=\"w-6 h-6 text-[#4F8EF7]\" />\n        </div>\n        <h2 className=\"text-2xl font-bold text-base-content dark:text-base-content/90\">{t(\"tools.calorie-calculator.faq.title\")}</h2>\n      </div>\n\n      <div className=\"space-y-4\">\n        {faqs.map((faq, index) => (\n          <div\n            className=\"border border-base-content/10 dark:border-base-content/5 rounded-xl overflow-hidden transition-all duration-300 hover:border-primary/30 bg-base-100 dark:bg-base-400/20\"\n            key={index}\n          >\n            <button\n              className=\"w-full p-4 text-left flex items-center justify-between gap-4 transition-colors hover:bg-base-200/50 dark:hover:bg-base-300/20\"\n              onClick={() => setOpenIndex(openIndex === index ? null : index)}\n            >\n              <h3 className=\"font-semibold text-base-content dark:text-base-content/90\">{faq.question}</h3>\n              <ChevronDownIcon\n                className={`w-5 h-5 text-base-content/60 transition-transform duration-300 ${openIndex === index ? \"rotate-180\" : \"\"}`}\n              />\n            </button>\n            <div className={`px-4 transition-all duration-300 ${openIndex === index ? \"pb-4\" : \"max-h-0 overflow-hidden\"}`}>\n              <p className=\"text-sm text-base-content/70 dark:text-base-content/60 leading-relaxed\">{faq.answer}</p>\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/GenderSelector.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { UserIcon, UsersIcon } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { Gender } from \"app/[locale]/(app)/tools/calorie-calculator/calorie-calculator.utils\";\n\ninterface GenderSelectorProps {\n  value: Gender;\n  onChange: (gender: Gender) => void;\n}\n\nexport function GenderSelector({ value, onChange }: GenderSelectorProps) {\n  const t = useI18n();\n\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 block\">\n        {t(\"tools.calorie-calculator.gender\")}\n      </label>\n      <div className=\"grid grid-cols-2 gap-3\">\n        <button\n          className={`group flex items-center justify-center gap-2 sm:gap-3 py-3 sm:py-4 px-3 sm:px-4 rounded-2xl border-2 transition-all duration-300 touch-manipulation ${\n            value === \"male\"\n              ? \"border-[#4F8EF7] bg-gradient-to-br from-[#4F8EF7]/20 to-[#238BE6]/10 text-[#4F8EF7] dark:from-[#4F8EF7]/15 dark:to-[#238BE6]/5 scale-[1.02]\"\n              : \"border-base-content/15 dark:border-base-content/10 hover:border-[#4F8EF7]/50 bg-base-100 dark:bg-base-200/30 hover:bg-base-200/50 active:bg-base-200/70\"\n          }`}\n          onClick={() => onChange(\"male\")}\n        >\n          <div\n            className={`p-2 rounded-lg transition-all duration-300 ${\n              value === \"male\" ? \"bg-[#4F8EF7]/20 dark:bg-[#4F8EF7]/15\" : \"bg-base-200 dark:bg-base-300/20 group-hover:bg-[#4F8EF7]/10\"\n            }`}\n          >\n            <UserIcon\n              className={`w-5 h-5 transition-colors duration-300 ${\n                value === \"male\" ? \"text-[#4F8EF7]\" : \"text-base-content/60 group-hover:text-[#4F8EF7]/70\"\n              }`}\n            />\n          </div>\n          <span\n            className={`font-semibold transition-colors duration-300 ${\n              value === \"male\" ? \"text-[#4F8EF7]\" : \"text-base-content/70 dark:text-base-content/60\"\n            }`}\n          >\n            {t(\"tools.calorie-calculator.male\")}\n          </span>\n        </button>\n\n        <button\n          className={`group flex items-center justify-center gap-2 sm:gap-3 py-3 sm:py-4 px-3 sm:px-4 rounded-2xl border-2 transition-all duration-300 touch-manipulation ${\n            value === \"female\"\n              ? \"border-[#FF5722] bg-gradient-to-br from-[#FF5722]/20 to-[#F44336]/10 text-[#FF5722] dark:from-[#FF5722]/15 dark:to-[#F44336]/5 scale-[1.02]\"\n              : \"border-base-content/15 dark:border-base-content/10 hover:border-[#FF5722]/50 bg-base-100 dark:bg-base-200/30 hover:bg-base-200/50 active:bg-base-200/70\"\n          }`}\n          onClick={() => onChange(\"female\")}\n        >\n          <div\n            className={`p-2 rounded-lg transition-all duration-300 ${\n              value === \"female\" ? \"bg-[#FF5722]/20 dark:bg-[#FF5722]/15\" : \"bg-base-200 dark:bg-base-300/20 group-hover:bg-[#FF5722]/10\"\n            }`}\n          >\n            <UsersIcon\n              className={`w-5 h-5 transition-colors duration-300 ${\n                value === \"female\" ? \"text-[#FF5722]\" : \"text-base-content/60 group-hover:text-[#FF5722]/70\"\n              }`}\n            />\n          </div>\n          <span\n            className={`font-semibold transition-colors duration-300 ${\n              value === \"female\" ? \"text-[#FF5722]\" : \"text-base-content/70 dark:text-base-content/60\"\n            }`}\n          >\n            {t(\"tools.calorie-calculator.female\")}\n          </span>\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/GoalSelector.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { TrendingDownIcon, TrendingUpIcon, ScaleIcon, RocketIcon, ZapOffIcon } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { Goal } from \"app/[locale]/(app)/tools/calorie-calculator/calorie-calculator.utils\";\n\ninterface GoalSelectorProps {\n  value: Goal;\n  onChange: (goal: Goal) => void;\n}\n\nconst GOALS: Goal[] = [\"lose_fast\", \"lose_slow\", \"maintain\", \"gain_slow\", \"gain_fast\"];\n\nexport function GoalSelector({ value, onChange }: GoalSelectorProps) {\n  const t = useI18n();\n\n  const GOAL_ICONS: Record<Goal, React.ReactNode> = {\n    lose_fast: <ZapOffIcon className=\"w-5 h-5\" />,\n    lose_slow: <TrendingDownIcon className=\"w-5 h-5\" />,\n    maintain: <ScaleIcon className=\"w-5 h-5\" />,\n    gain_slow: <TrendingUpIcon className=\"w-5 h-5\" />,\n    gain_fast: <RocketIcon className=\"w-5 h-5\" />,\n  };\n\n  const GOAL_COLORS: Record<Goal, { border: string; bg: string; text: string }> = {\n    lose_fast: { border: \"border-red-500\", bg: \"from-red-500/20 to-red-600/10\", text: \"text-red-600\" },\n    lose_slow: { border: \"border-orange-500\", bg: \"from-orange-500/20 to-orange-600/10\", text: \"text-orange-600\" },\n    maintain: { border: \"border-blue-500\", bg: \"from-blue-500/20 to-blue-600/10\", text: \"text-blue-600\" },\n    gain_slow: { border: \"border-green-500\", bg: \"from-green-500/20 to-green-600/10\", text: \"text-green-600\" },\n    gain_fast: { border: \"border-purple-500\", bg: \"from-purple-500/20 to-purple-600/10\", text: \"text-purple-600\" },\n  };\n\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 block\">\n        {t(\"tools.calorie-calculator.goal\")}\n      </label>\n      <div className=\"space-y-3\">\n        {GOALS.map((goal) => {\n          const colors = GOAL_COLORS[goal];\n          const isSelected = value === goal;\n\n          return (\n            <button\n              className={`group w-full text-left p-4 rounded-2xl border-2 transition-all duration-300 ${\n                isSelected\n                  ? `${colors.border} bg-gradient-to-br ${colors.bg} scale-[1.02] dark:${colors.bg.replace(\"/20\", \"/15\").replace(\"/10\", \"/5\")}`\n                  : \"border-base-content/15 dark:border-base-content/10 hover:border-base-content/30 bg-base-100 dark:bg-base-200/30 hover:bg-base-200/50\"\n              }`}\n              key={goal}\n              onClick={() => onChange(goal)}\n            >\n              <div className=\"flex items-start gap-4\">\n                <div\n                  className={`p-2 rounded-lg transition-all duration-300 ${\n                    isSelected\n                      ? `${colors.bg} dark:${colors.bg.replace(\"/20\", \"/15\")}`\n                      : \"bg-base-200 dark:bg-base-300/20 group-hover:bg-base-300/50\"\n                  }`}\n                >\n                  <div\n                    className={`transition-colors duration-300 ${\n                      isSelected ? colors.text : \"text-base-content/60 group-hover:text-base-content/80\"\n                    }`}\n                  >\n                    {GOAL_ICONS[goal]}\n                  </div>\n                </div>\n                <div className=\"flex-1\">\n                  <div\n                    className={`font-semibold transition-colors duration-300 ${\n                      isSelected ? colors.text : \"text-base-content/80 dark:text-base-content/70\"\n                    }`}\n                  >\n                    {t(`tools.calorie-calculator.goals.${goal}`)}\n                  </div>\n                  <div className=\"text-sm text-base-content/60 dark:text-base-content/50 mt-0.5\">\n                    {t(`tools.calorie-calculator.goals.${goal}_desc`)}\n                  </div>\n                </div>\n              </div>\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/HeightInput.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { UnitSystem } from \"app/[locale]/(app)/tools/calorie-calculator/calorie-calculator.utils\";\n\ninterface HeightInputProps {\n  value: number;\n  unit: UnitSystem;\n  onChange: (height: number) => void;\n}\n\nexport function HeightInput({ value, unit, onChange }: HeightInputProps) {\n  const t = useI18n();\n\n  // For imperial, we need to handle feet and inches\n  if (unit === \"imperial\") {\n    const totalInches = value;\n    const feet = Math.floor(totalInches / 12);\n    const inches = totalInches % 12;\n\n    const handleFeetChange = (newFeet: number) => {\n      onChange(newFeet * 12 + inches);\n    };\n\n    const handleInchesChange = (newInches: number) => {\n      onChange(feet * 12 + newInches);\n    };\n\n    return (\n      <div>\n        <label className=\"text-sm font-medium text-base-content/70 uppercase tracking-wider\">{t(\"tools.calorie-calculator.height\")}</label>\n        <div className=\"mt-2 grid grid-cols-2 gap-2\">\n          <div>\n            <input\n              className=\"w-full px-4 py-3 rounded-xl border-2 border-base-content/15 dark:border-base-content/10 bg-base-100 dark:bg-base-200/30 text-base-content focus:border-primary focus:outline-none transition-all duration-300 hover:border-primary/30 text-center font-semibold\"\n              max=\"7\"\n              min=\"4\"\n              onChange={(e) => handleFeetChange(Number(e.target.value))}\n              type=\"number\"\n              value={feet}\n            />\n            <span className=\"text-xs text-base-content/60 dark:text-base-content/50 mt-1 block text-center font-medium\">\n              {t(\"tools.calorie-calculator.feet\")}\n            </span>\n          </div>\n          <div>\n            <input\n              className=\"w-full px-4 py-3 rounded-xl border-2 border-base-content/15 dark:border-base-content/10 bg-base-100 dark:bg-base-200/30 text-base-content focus:border-primary focus:outline-none transition-all duration-300 hover:border-primary/30 text-center font-semibold\"\n              max=\"11\"\n              min=\"0\"\n              onChange={(e) => handleInchesChange(Number(e.target.value))}\n              type=\"number\"\n              value={inches}\n            />\n            <span className=\"text-xs text-base-content/60 dark:text-base-content/50 mt-1 block text-center font-medium\">\n              {t(\"tools.calorie-calculator.inches\")}\n            </span>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  // Metric - simple cm input\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 block\">\n        {t(\"tools.calorie-calculator.height\")}\n      </label>\n      <div className=\"mt-2\">\n        <div className=\"relative\">\n          <input\n            className=\"w-full px-4 py-3 pr-12 rounded-xl border-2 border-base-content/15 dark:border-base-content/10 bg-base-100 dark:bg-base-200/30 text-base-content focus:border-primary focus:outline-none transition-all duration-300 hover:border-primary/30 font-semibold\"\n            max=\"250\"\n            min=\"100\"\n            onChange={(e) => onChange(Number(e.target.value))}\n            placeholder={t(\"tools.calorie-calculator.height_placeholder\")}\n            type=\"number\"\n            value={value}\n          />\n          <span className=\"absolute right-4 top-1/2 -translate-y-1/2 text-sm text-base-content/60 dark:text-base-content/50 font-medium\">\n            {t(\"tools.calorie-calculator.cm\")}\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/InfoButton.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { InfoIcon } from \"lucide-react\";\n\nimport { useIsMobile } from \"@/shared/hooks/useIsMobile\";\n\ninterface InfoButtonProps {\n  onClick: () => void;\n  tooltip?: React.ReactNode;\n}\n\nexport function InfoButton({ onClick, tooltip }: InfoButtonProps) {\n  const isMobile = useIsMobile();\n\n  return (\n    <div className=\"relative\">\n      <button aria-label=\"More information\" className=\"touch-manipulation p-1 -m-1\" onClick={onClick}>\n        <InfoIcon\n          className={`w-4 h-4 cursor-help transition-colors ${\n            isMobile ? \"text-primary animate-pulse\" : \"text-base-content/40 hover:text-primary\"\n          }`}\n        />\n      </button>\n      {!isMobile && tooltip && (\n        <div className=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-64 p-3 bg-base-100 dark:bg-base-200 rounded-lg shadow-lg border border-base-content/10 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300 z-10 pointer-events-none\">\n          {tooltip}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/InfoModal.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect } from \"react\";\nimport { XIcon } from \"lucide-react\";\n\ninterface InfoModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  title: string;\n  content: string;\n}\n\nexport function InfoModal({ isOpen, onClose, title, content }: InfoModalProps) {\n  // Close modal on escape key\n  useEffect(() => {\n    const handleEscape = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") onClose();\n    };\n\n    if (isOpen) {\n      document.addEventListener(\"keydown\", handleEscape);\n      // Prevent body scroll when modal is open\n      document.body.style.overflow = \"hidden\";\n    }\n\n    return () => {\n      document.removeEventListener(\"keydown\", handleEscape);\n      document.body.style.overflow = \"unset\";\n    };\n  }, [isOpen, onClose]);\n\n  if (!isOpen) return null;\n\n  return (\n    <>\n      {/* Backdrop */}\n      <div className=\"fixed inset-0 bg-black/50 backdrop-blur-sm z-50 animate-fadeIn\" onClick={onClose} />\n\n      {/* Modal */}\n      <div className=\"fixed inset-x-4 top-1/2 -translate-y-1/2 z-50 max-w-md mx-auto animate-fadeIn\">\n        <div className=\"bg-base-100 dark:bg-base-200 rounded-2xl shadow-2xl border border-base-content/10 p-6\">\n          <div className=\"flex items-start justify-between mb-4\">\n            <h3 className=\"text-lg font-bold text-base-content dark:text-base-content/90 pr-4\">{title}</h3>\n            <button\n              aria-label=\"Close modal\"\n              className=\"p-1 rounded-lg hover:bg-base-200 dark:hover:bg-base-300/50 transition-colors\"\n              onClick={onClose}\n            >\n              <XIcon className=\"w-5 h-5 text-base-content/60\" />\n            </button>\n          </div>\n          <p className=\"text-sm text-base-content/70 dark:text-base-content/60 leading-relaxed\">{content}</p>\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/ResultsDisplay.tsx",
    "content": "\"use client\";\n\nimport React, { useState } from \"react\";\nimport Image from \"next/image\";\nimport { BeefIcon, WheatIcon, DropletIcon, FlameIcon, ActivityIcon } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { CalorieResults } from \"app/[locale]/(app)/tools/calorie-calculator/calorie-calculator.utils\";\n\nimport { InfoModal } from \"./InfoModal\";\nimport { InfoButton } from \"./InfoButton\";\n\ninterface ResultsDisplayProps {\n  results: CalorieResults;\n}\n\nexport function ResultsDisplay({ results }: ResultsDisplayProps) {\n  const t = useI18n();\n  const [activeModal, setActiveModal] = useState<string | null>(null);\n\n  return (\n    <div className=\"bg-gradient-to-br from-[#4F8EF7]/10 to-[#238BE6]/10 dark:from-[#4F8EF7]/5 dark:to-[#238BE6]/5 rounded-2xl border-2 border-[#4F8EF7]/30 dark:border-[#4F8EF7]/20 p-6 sm:p-8 animate-fadeIn\">\n      <div className=\"flex items-center justify-between mb-6\">\n        <h2 className=\"text-2xl font-bold bg-gradient-to-r from-[#4F8EF7] to-[#238BE6] bg-clip-text text-transparent\">\n          {t(\"tools.calorie-calculator.results.title\")}\n        </h2>\n        <Image alt=\"Happy\" className=\"opacity-90 animate-bounce\" height={48} src=\"/images/emojis/WorkoutCoolHappy.png\" width={48} />\n      </div>\n\n      {/* Main Results */}\n      <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4 mb-6 sm:mb-8\">\n        <div className=\"group bg-base-100/70 dark:bg-base-100/30 backdrop-blur-sm rounded-xl p-4 text-center border border-base-content/10 transition-all duration-300 hover:scale-105 relative\">\n          <div className=\"flex items-center justify-center gap-2 mb-1\">\n            <FlameIcon className=\"w-4 h-4 text-orange-500\" />\n            <div className=\"text-sm text-base-content/60 dark:text-base-content/50 font-medium\">\n              {t(\"tools.calorie-calculator.results.bmr\")}\n            </div>\n            <InfoButton\n              onClick={() => setActiveModal(\"bmr\")}\n              tooltip={\n                <p className=\"text-xs text-base-content/80 dark:text-base-content/70 text-left\">\n                  {t(\"tools.calorie-calculator.results.bmr_explanation\")}\n                </p>\n              }\n            />\n          </div>\n          <div className=\"text-2xl font-bold text-base-content dark:text-base-content/90\">{results.bmr.toLocaleString()}</div>\n          <div className=\"text-sm text-base-content/60 dark:text-base-content/50 font-medium\">kcal</div>\n        </div>\n\n        <div className=\"group bg-base-100/70 dark:bg-base-100/30 backdrop-blur-sm rounded-xl p-4 text-center border border-base-content/10 transition-all duration-300 hover:scale-105 relative\">\n          <div className=\"flex items-center justify-center gap-2 mb-1\">\n            <ActivityIcon className=\"w-4 h-4 text-green-500\" />\n            <div className=\"text-sm text-base-content/60 dark:text-base-content/50 font-medium\">\n              {t(\"tools.calorie-calculator.results.tdee\")}\n            </div>\n            <InfoButton\n              onClick={() => setActiveModal(\"tdee\")}\n              tooltip={\n                <p className=\"text-xs text-base-content/80 dark:text-base-content/70 text-left\">\n                  {t(\"tools.calorie-calculator.results.tdee_explanation\")}\n                </p>\n              }\n            />\n          </div>\n          <div className=\"text-2xl font-bold text-base-content dark:text-base-content/90\">{results.tdee.toLocaleString()}</div>\n          <div className=\"text-sm text-base-content/60 dark:text-base-content/50 font-medium\">kcal</div>\n        </div>\n\n        <div className=\"bg-gradient-to-br from-[#4F8EF7]/30 to-[#238BE6]/20 dark:from-[#4F8EF7]/20 dark:to-[#238BE6]/10 rounded-xl p-4 text-center border-2 border-[#4F8EF7]/40 dark:border-[#4F8EF7]/30 transition-all duration-300 hover:scale-105\">\n          <div className=\"text-sm text-[#4F8EF7] dark:text-[#4F8EF7]/90 mb-1 font-bold uppercase tracking-wider\">\n            {t(\"tools.calorie-calculator.results.target\")}\n          </div>\n          <div className=\"text-3xl font-black text-[#4F8EF7] dark:text-[#4F8EF7]/90\">{results.targetCalories.toLocaleString()}</div>\n          <div className=\"text-sm text-[#4F8EF7] dark:text-[#4F8EF7]/90 font-bold\">kcal</div>\n        </div>\n      </div>\n\n      {/* Macros */}\n      <div>\n        <div className=\"flex items-center gap-2 mb-4\">\n          <h3 className=\"text-lg font-bold text-base-content dark:text-base-content/90\">{t(\"tools.calorie-calculator.results.macros\")}</h3>\n          <InfoButton\n            onClick={() => setActiveModal(\"macros\")}\n            tooltip={\n              <p className=\"text-xs text-base-content/80 dark:text-base-content/70\">\n                {t(\"tools.calorie-calculator.results.macros_explanation\")}\n              </p>\n            }\n          />\n        </div>\n        <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4\">\n          <div className=\"group bg-base-100/70 dark:bg-base-100/30 backdrop-blur-sm rounded-xl p-3 sm:p-4 border border-base-content/10 transition-all duration-300 hover:scale-105 hover:border-red-500/30 active:scale-[0.98] relative\">\n            <div className=\"flex items-center gap-3 mb-2\">\n              <div className=\"p-2 rounded-lg bg-gradient-to-br from-red-500/20 to-red-600/10 dark:from-red-500/15 dark:to-red-600/5\">\n                <BeefIcon className=\"w-5 h-5 text-red-500 dark:text-red-400\" />\n              </div>\n              <div className=\"text-sm text-base-content/70 dark:text-base-content/60 font-medium\">\n                {t(\"tools.calorie-calculator.results.protein\")}\n              </div>\n            </div>\n            <div className=\"text-xl font-bold text-base-content dark:text-base-content/90\">{results.proteinGrams}g</div>\n            <div className=\"text-xs text-base-content/60 dark:text-base-content/50 mt-1\">{results.proteinGrams * 4} kcal</div>\n            <div className=\"absolute -top-1 -right-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300\">\n              <div className=\"bg-red-500 text-white text-xs px-2 py-1 rounded-lg font-medium\">30%</div>\n            </div>\n          </div>\n\n          <div className=\"group bg-base-100/70 dark:bg-base-100/30 backdrop-blur-sm rounded-xl p-3 sm:p-4 border border-base-content/10 transition-all duration-300 hover:scale-105 hover:border-yellow-500/30 active:scale-[0.98] relative\">\n            <div className=\"flex items-center gap-3 mb-2\">\n              <div className=\"p-2 rounded-lg bg-gradient-to-br from-yellow-500/20 to-yellow-600/10 dark:from-yellow-500/15 dark:to-yellow-600/5\">\n                <WheatIcon className=\"w-5 h-5 text-yellow-500 dark:text-yellow-400\" />\n              </div>\n              <div className=\"text-sm text-base-content/70 dark:text-base-content/60 font-medium\">\n                {t(\"tools.calorie-calculator.results.carbs\")}\n              </div>\n            </div>\n            <div className=\"text-xl font-bold text-base-content dark:text-base-content/90\">{results.carbsGrams}g</div>\n            <div className=\"text-xs text-base-content/60 dark:text-base-content/50 mt-1\">{results.carbsGrams * 4} kcal</div>\n            <div className=\"absolute -top-1 -right-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300\">\n              <div className=\"bg-yellow-500 text-white text-xs px-2 py-1 rounded-lg font-medium\">40%</div>\n            </div>\n          </div>\n\n          <div className=\"group bg-base-100/70 dark:bg-base-100/30 backdrop-blur-sm rounded-xl p-3 sm:p-4 border border-base-content/10 transition-all duration-300 hover:scale-105 hover:border-blue-500/30 active:scale-[0.98] relative\">\n            <div className=\"flex items-center gap-3 mb-2\">\n              <div className=\"p-2 rounded-lg bg-gradient-to-br from-blue-500/20 to-blue-600/10 dark:from-blue-500/15 dark:to-blue-600/5\">\n                <DropletIcon className=\"w-5 h-5 text-blue-500 dark:text-blue-400\" />\n              </div>\n              <div className=\"text-sm text-base-content/70 dark:text-base-content/60 font-medium\">\n                {t(\"tools.calorie-calculator.results.fat\")}\n              </div>\n            </div>\n            <div className=\"text-xl font-bold text-base-content dark:text-base-content/90\">{results.fatGrams}g</div>\n            <div className=\"text-xs text-base-content/60 dark:text-base-content/50 mt-1\">{results.fatGrams * 9} kcal</div>\n            <div className=\"absolute -top-1 -right-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300\">\n              <div className=\"bg-blue-500 text-white text-xs px-2 py-1 rounded-lg font-medium\">30%</div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Info Message */}\n      <div className=\"mt-6 p-4 bg-gradient-to-r from-[#4F8EF7]/5 to-[#238BE6]/5 dark:from-[#4F8EF7]/10 dark:to-[#238BE6]/10 rounded-xl border border-[#4F8EF7]/20 dark:border-[#4F8EF7]/30\">\n        <p className=\"text-sm text-base-content/70 dark:text-base-content/60 text-center\">\n          {t(\"tools.calorie-calculator.results.disclaimer\")}\n        </p>\n      </div>\n\n      {/* Mobile Modals */}\n      <InfoModal\n        content={t(\"tools.calorie-calculator.results.bmr_explanation\")}\n        isOpen={activeModal === \"bmr\"}\n        onClose={() => setActiveModal(null)}\n        title={t(\"tools.calorie-calculator.results.bmr\")}\n      />\n      <InfoModal\n        content={t(\"tools.calorie-calculator.results.tdee_explanation\")}\n        isOpen={activeModal === \"tdee\"}\n        onClose={() => setActiveModal(null)}\n        title={t(\"tools.calorie-calculator.results.tdee\")}\n      />\n      <InfoModal\n        content={t(\"tools.calorie-calculator.results.macros_explanation\")}\n        isOpen={activeModal === \"macros\"}\n        onClose={() => setActiveModal(null)}\n        title={t(\"tools.calorie-calculator.results.macros\")}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/UnitSelector.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { RulerIcon, GlobeIcon } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { UnitSystem } from \"app/[locale]/(app)/tools/calorie-calculator/calorie-calculator.utils\";\n\ninterface UnitSelectorProps {\n  value: UnitSystem;\n  onChange: (unit: UnitSystem) => void;\n}\n\nexport function UnitSelector({ value, onChange }: UnitSelectorProps) {\n  const t = useI18n();\n\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 block\">\n        {t(\"tools.calorie-calculator.units\")}\n      </label>\n      <div className=\"grid grid-cols-2 gap-3\">\n        <button\n          className={`group flex items-center justify-center gap-3 py-4 px-4 rounded-2xl border-2 transition-all duration-300 ${\n            value === \"metric\"\n              ? \"border-[#25CB78] bg-gradient-to-br from-[#25CB78]/20 to-[#22C55E]/10 text-[#25CB78] dark:from-[#25CB78]/15 dark:to-[#22C55E]/5 scale-[1.02]\"\n              : \"border-base-content/15 dark:border-base-content/10 hover:border-[#25CB78]/50 bg-base-100 dark:bg-base-200/30 hover:bg-base-200/50\"\n          }`}\n          onClick={() => onChange(\"metric\")}\n        >\n          <div\n            className={`p-2 rounded-lg transition-all duration-300 ${\n              value === \"metric\" ? \"bg-[#25CB78]/20 dark:bg-[#25CB78]/15\" : \"bg-base-200 dark:bg-base-300/20 group-hover:bg-[#25CB78]/10\"\n            }`}\n          >\n            <GlobeIcon\n              className={`w-5 h-5 transition-colors duration-300 ${\n                value === \"metric\" ? \"text-[#25CB78]\" : \"text-base-content/60 group-hover:text-[#25CB78]/70\"\n              }`}\n            />\n          </div>\n          <span\n            className={`font-semibold transition-colors duration-300 ${\n              value === \"metric\" ? \"text-[#25CB78]\" : \"text-base-content/70 dark:text-base-content/60\"\n            }`}\n          >\n            {t(\"tools.calorie-calculator.metric\")}\n          </span>\n        </button>\n\n        <button\n          className={`group flex items-center justify-center gap-3 py-4 px-4 rounded-2xl border-2 transition-all duration-300 ${\n            value === \"imperial\"\n              ? \"border-[#F59E0B] bg-gradient-to-br from-[#F59E0B]/20 to-[#EF4444]/10 text-[#F59E0B] dark:from-[#F59E0B]/15 dark:to-[#EF4444]/5 scale-[1.02]\"\n              : \"border-base-content/15 dark:border-base-content/10 hover:border-[#F59E0B]/50 bg-base-100 dark:bg-base-200/30 hover:bg-base-200/50\"\n          }`}\n          onClick={() => onChange(\"imperial\")}\n        >\n          <div\n            className={`p-2 rounded-lg transition-all duration-300 ${\n              value === \"imperial\" ? \"bg-[#F59E0B]/20 dark:bg-[#F59E0B]/15\" : \"bg-base-200 dark:bg-base-300/20 group-hover:bg-[#F59E0B]/10\"\n            }`}\n          >\n            <RulerIcon\n              className={`w-5 h-5 transition-colors duration-300 ${\n                value === \"imperial\" ? \"text-[#F59E0B]\" : \"text-base-content/60 group-hover:text-[#F59E0B]/70\"\n              }`}\n            />\n          </div>\n          <span\n            className={`font-semibold transition-colors duration-300 ${\n              value === \"imperial\" ? \"text-[#F59E0B]\" : \"text-base-content/70 dark:text-base-content/60\"\n            }`}\n          >\n            {t(\"tools.calorie-calculator.imperial\")}\n          </span>\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/WeightInput.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { UnitSystem } from \"app/[locale]/(app)/tools/calorie-calculator/calorie-calculator.utils\";\n\ninterface WeightInputProps {\n  value: number;\n  unit: UnitSystem;\n  onChange: (weight: number) => void;\n}\n\nexport function WeightInput({ value, unit, onChange }: WeightInputProps) {\n  const t = useI18n();\n  const unitLabel = unit === \"metric\" ? t(\"tools.calorie-calculator.kg\") : t(\"tools.calorie-calculator.lbs\");\n  const min = unit === \"metric\" ? 30 : 66;\n  const max = unit === \"metric\" ? 300 : 660;\n\n  return (\n    <div>\n      <label className=\"text-sm font-bold text-base-content/80 dark:text-base-content/70 uppercase tracking-wider mb-3 block\">\n        {t(\"tools.calorie-calculator.weight\")}\n      </label>\n      <div className=\"relative\">\n        <input\n          className=\"w-full px-4 py-3 pr-12 rounded-xl border-2 border-base-content/15 dark:border-base-content/10 bg-base-100 dark:bg-base-200/30 text-base-content focus:border-primary focus:outline-none transition-all duration-300 hover:border-primary/30 font-semibold\"\n          max={max}\n          min={min}\n          onChange={(e) => onChange(Number(e.target.value))}\n          placeholder={t(\"tools.calorie-calculator.weight_placeholder\")}\n          step=\"0.1\"\n          type=\"number\"\n          value={value}\n        />\n        <span className=\"absolute right-4 top-1/2 -translate-y-1/2 text-sm text-base-content/60 dark:text-base-content/50 font-medium\">\n          {unitLabel}\n        </span>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/components/index.ts",
    "content": "// Export all shared components\nexport { GenderSelector } from \"./GenderSelector\";\nexport { WeightInput } from \"./WeightInput\";\nexport { HeightInput } from \"./HeightInput\";\nexport { AgeInput } from \"./AgeInput\";\nexport { UnitSelector } from \"./UnitSelector\";\nexport { ActivityLevelSelector } from \"./ActivityLevelSelector\";\nexport { GoalSelector } from \"./GoalSelector\";\nexport { FAQSection } from \"./FAQSection\";\nexport { InfoButton } from \"./InfoButton\";\nexport { InfoModal } from \"./InfoModal\";\nexport { ResultsDisplay } from \"./ResultsDisplay\";\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/shared/types/index.ts",
    "content": "// Calorie calculator types for better type safety\nexport type CalculatorFormula = \"mifflin\" | \"harris\" | \"katch\" | \"cunningham\" | \"oxford\";\n\nexport interface CalculatorConfig {\n  formula: CalculatorFormula;\n  requiresBodyFat: boolean;\n  name: string;\n  description: string;\n  buttonGradient: {\n    from: string;\n    to: string;\n  };\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/calorie-calculator/styles.css",
    "content": "@keyframes fadeIn {\n  from {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.animate-fadeIn {\n  animation: fadeIn 0.5s ease-out;\n}\n\n/* Custom slider styles */\ninput[type=\"range\"] {\n  -webkit-appearance: none;\n  appearance: none;\n  background: transparent;\n  cursor: pointer;\n}\n\ninput[type=\"range\"]::-webkit-slider-track {\n  background: transparent;\n  height: 8px;\n  border-radius: 4px;\n}\n\ninput[type=\"range\"]::-webkit-slider-thumb {\n  -webkit-appearance: none;\n  appearance: none;\n  background: #4F8EF7;\n  height: 20px;\n  width: 20px;\n  border-radius: 50%;\n  cursor: pointer;\n  margin-top: -6px;\n  transition: all 0.2s ease-in-out;\n}\n\ninput[type=\"range\"]::-webkit-slider-thumb:hover {\n  transform: scale(1.2);\n  background: #238BE6;\n}\n\ninput[type=\"range\"]::-moz-range-track {\n  background: transparent;\n  height: 8px;\n  border-radius: 4px;\n}\n\ninput[type=\"range\"]::-moz-range-thumb {\n  background: #4F8EF7;\n  height: 20px;\n  width: 20px;\n  border-radius: 50%;\n  cursor: pointer;\n  border: none;\n  transition: all 0.2s ease-in-out;\n}\n\ninput[type=\"range\"]::-moz-range-thumb:hover {\n  transform: scale(1.2);\n  background: #238BE6;\n}\n\n/* Dark mode adjustments */\n@media (prefers-color-scheme: dark) {\n  input[type=\"range\"]::-webkit-slider-thumb {\n    background: #4F8EF7;\n  }\n  \n  input[type=\"range\"]::-moz-range-thumb {\n    background: #4F8EF7;\n  }\n}\n\n/* Mobile optimizations */\n@media (max-width: 768px) {\n  /* Larger touch targets */\n  .touch-manipulation {\n    touch-action: manipulation;\n    -webkit-tap-highlight-color: transparent;\n  }\n  \n  /* Remove hover effects on mobile */\n  @media (hover: none) and (pointer: coarse) {\n    .hover\\:scale-105:hover {\n      transform: none;\n    }\n    \n    .hover\\:border-primary\\/30:hover {\n      border-color: inherit;\n    }\n  }\n  \n  /* Active states for better feedback */\n  button:active {\n    transform: scale(0.98);\n  }\n  \n  /* Larger slider thumb for mobile */\n  input[type=\"range\"]::-webkit-slider-thumb {\n    height: 28px;\n    width: 28px;\n  }\n  \n  input[type=\"range\"]::-moz-range-thumb {\n    height: 28px;\n    width: 28px;\n  }\n}\n\n/* Smooth scrolling */\nhtml {\n  scroll-behavior: smooth;\n}\n\n/* Prevent layout shift on modal open */\nbody.modal-open {\n  padding-right: 0 !important;\n}"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/lib/utils.ts",
    "content": "import { TFunction } from \"locales/client\";\n\ninterface HeartRateZone {\n  name: string;\n  minHR: number;\n  maxHR: number;\n  emoji: string;\n  color: string;\n  bgColor: string;\n  description: string;\n}\n\ninterface HeartRateResults {\n  maxHeartRate: number;\n  zones: HeartRateZone[];\n}\n\nexport function calculateHeartRateZones(age: number, t: TFunction): HeartRateResults {\n  // Calculate MHR\n  const maxHeartRate = 220 - age;\n\n  // Simple zones with emojis and colors\n  const zones: HeartRateZone[] = [\n    {\n      name: t(\"tools.heart-rate-zones.zones.warm_up.name\"),\n      minHR: Math.round(maxHeartRate * 0.5),\n      maxHR: Math.round(maxHeartRate * 0.6),\n      emoji: \"🚶\",\n      color: \"text-blue-600\",\n      bgColor: \"bg-blue-100\",\n      description: t(\"tools.heart-rate-zones.zones.warm_up.description\"),\n    },\n    {\n      name: t(\"tools.heart-rate-zones.zones.fat_burn.name\"),\n      minHR: Math.round(maxHeartRate * 0.6),\n      maxHR: Math.round(maxHeartRate * 0.7),\n      emoji: \"🔥\",\n      color: \"text-green-600\",\n      bgColor: \"bg-green-100\",\n      description: t(\"tools.heart-rate-zones.zones.fat_burn.description\"),\n    },\n    {\n      name: t(\"tools.heart-rate-zones.zones.aerobic.name\"),\n      minHR: Math.round(maxHeartRate * 0.7),\n      maxHR: Math.round(maxHeartRate * 0.8),\n      emoji: \"🏃\",\n      color: \"text-yellow-600\",\n      bgColor: \"bg-yellow-100\",\n      description: t(\"tools.heart-rate-zones.zones.aerobic.description\"),\n    },\n    {\n      name: t(\"tools.heart-rate-zones.zones.anaerobic.name\"),\n      minHR: Math.round(maxHeartRate * 0.8),\n      maxHR: Math.round(maxHeartRate * 0.9),\n      emoji: \"💪\",\n      color: \"text-orange-600\",\n      bgColor: \"bg-orange-100\",\n      description: t(\"tools.heart-rate-zones.zones.anaerobic.description\"),\n    },\n    {\n      name: t(\"tools.heart-rate-zones.zones.vo2_max.name\"),\n      minHR: Math.round(maxHeartRate * 0.9),\n      maxHR: maxHeartRate,\n      emoji: \"🚀\",\n      color: \"text-red-600\",\n      bgColor: \"bg-red-100\",\n      description: t(\"tools.heart-rate-zones.zones.vo2_max.description\"),\n    },\n  ];\n\n  return {\n    maxHeartRate,\n    zones,\n  };\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/page.tsx",
    "content": "import React from \"react\";\nimport { Metadata } from \"next\";\n\nimport { Locale } from \"locales/types\";\nimport { getI18n } from \"locales/server\";\nimport { HeartRateZonesCalculatorClient } from \"app/[locale]/(app)/tools/heart-rate-zones/ui/HeartRateZonesCalculatorClient\";\nimport { SEOOptimizedContentServer } from \"app/[locale]/(app)/tools/heart-rate-zones/ui/components/SEOOptimizedContentServer\";\nimport { EducationalContentServer } from \"app/[locale]/(app)/tools/heart-rate-zones/ui/components/EducationalContentServer\";\nimport { HEART_RATE_ZONES_CONTENT } from \"app/[locale]/(app)/tools/heart-rate-zones/seo/page-content\";\nimport { HEART_RATE_ZONES_SEO } from \"app/[locale]/(app)/tools/heart-rate-zones/seo/config\";\nimport { calculateHeartRateZones } from \"app/[locale]/(app)/tools/heart-rate-zones/lib/utils\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { env } from \"@/env\";\nimport { generateSEOMetadata, SEOScripts } from \"@/components/seo/SEOHead\";\nimport { HorizontalBottomBanner, HorizontalTopBanner } from \"@/components/ads\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: Locale }> }): Promise<Metadata> {\n  const { locale } = await params;\n\n  // Use centralized SEO config\n  const metadata = HEART_RATE_ZONES_SEO[locale] || HEART_RATE_ZONES_SEO.en;\n\n  return generateSEOMetadata({\n    title: metadata.title,\n    description: metadata.description,\n    keywords: metadata.keywords,\n    locale,\n    canonical: `${getServerUrl()}/${locale}/tools/heart-rate-zones`,\n    structuredData: {\n      type: \"Calculator\",\n      calculatorData: {\n        calculatorType: \"heart-rate-zones\",\n        inputFields: [\"age\", \"resting heart rate\", \"maximum heart rate\", \"calculation method\"],\n        outputFields: [\n          \"Maximum Heart Rate (MHR)\",\n          \"Target Heart Rate (THR)\",\n          \"VO2 Max Zone (90-100%)\",\n          \"Anaerobic Zone (80-90%)\",\n          \"Aerobic Zone (70-80%)\",\n          \"Fat Burn Zone (60-70%)\",\n          \"Warm Up Zone (50-60%)\",\n          \"Heart Rate Reserve (HRR)\",\n        ],\n        formula: \"Basic: THR = MHR × %Intensity | Karvonen: THR = [(MHR - RHR) × %Intensity] + RHR\",\n        accuracy: \"Scientifically validated formulas with personalized calculations\",\n        targetAudience: [\"athletes\", \"runners\", \"cyclists\", \"fitness enthusiasts\", \"personal trainers\"],\n        relatedCalculators: [\"bmi-calculator\", \"calorie-calculator\", \"macro-calculator\"],\n      },\n    },\n  });\n}\n\nconst DEFAULT_AGE = 30;\nexport default async function HeartRateZonesPage({ params }: { params: Promise<{ locale: Locale }> }) {\n  const { locale } = await params;\n  const t = await getI18n();\n\n  const defaultResults = calculateHeartRateZones(DEFAULT_AGE, t);\n\n  // Use centralized configs\n  const seoContent = HEART_RATE_ZONES_SEO[locale] || HEART_RATE_ZONES_SEO.en;\n  const pageContent = HEART_RATE_ZONES_CONTENT[locale] || HEART_RATE_ZONES_CONTENT.en;\n\n  return (\n    <>\n      <SEOScripts\n        canonical={`${getServerUrl()}/${locale}/tools/heart-rate-zones`}\n        description={seoContent.description}\n        hreflangPath=\"/tools/heart-rate-zones\"\n        locale={locale}\n        ogImage={`${getServerUrl()}/images/screenshots/heart-rate-zones/og.jpg`}\n        structuredData={{\n          type: \"Calculator\",\n          calculatorData: {\n            calculatorType: \"heart-rate-zones\",\n            inputFields: [\"age\", \"resting heart rate\", \"maximum heart rate\", \"calculation method\"],\n            outputFields: [\n              \"Maximum Heart Rate (MHR)\",\n              \"Zone 1: Warm Up (50-60%)\",\n              \"Zone 2: Fat Burn (60-70%)\",\n              \"Zone 3: Aerobic (70-80%)\",\n              \"Zone 4: Anaerobic (80-90%)\",\n              \"Zone 5: VO2 Max (90-100%)\",\n            ],\n            formula: \"Basic: THR = MHR × %Intensity | Karvonen: THR = [(MHR - RHR) × %Intensity] + RHR\",\n            accuracy: \"Scientifically validated formulas with personalized calculations\",\n            targetAudience: [\"athletes\", \"runners\", \"cyclists\", \"fitness enthusiasts\", \"personal trainers\"],\n            relatedCalculators: [\"bmi-calculator\", \"calorie-calculator\", \"macro-calculator\"],\n          },\n        }}\n        title={seoContent.title}\n      />\n\n      <script\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify({\n            \"@context\": \"https://schema.org\",\n            \"@type\": \"WebApplication\",\n            name: seoContent.title,\n            applicationCategory: \"HealthApplication\",\n            operatingSystem: \"Any\",\n            offers: {\n              \"@type\": \"Offer\",\n              price: \"0\",\n              priceCurrency: locale === \"ru\" ? \"RUB\" : locale === \"zh-CN\" ? \"CNY\" : locale === \"en\" ? \"USD\" : \"EUR\",\n            },\n            aggregateRating: {\n              \"@type\": \"AggregateRating\",\n              ratingValue: \"4.8\",\n              ratingCount: \"13\",\n              bestRating: \"5\",\n              worstRating: \"3\",\n            },\n            author: {\n              \"@type\": \"Organization\",\n              name: \"WorkoutCool\",\n            },\n            datePublished: \"2024-01-01\",\n            dateModified: new Date().toISOString().split(\"T\")[0],\n            description: seoContent.description,\n            screenshot: `${getServerUrl()}/images/screenshots/heart-rate-zones/${locale}.jpg`,\n            featureList: [\n              \"Heart rate zones calculation\",\n              \"5 personalized training zones\",\n              \"Basic & Karvonen formulas\",\n              \"Age-based reference chart\",\n              \"Complete training guide\",\n              \"User-friendly interface\",\n            ],\n          }),\n        }}\n        type=\"application/ld+json\"\n      />\n      <script\n        dangerouslySetInnerHTML={{\n          __html: JSON.stringify({\n            \"@context\": \"https://schema.org\",\n            \"@type\": \"Article\",\n            headline: seoContent.title,\n            description: seoContent.description,\n            image: {\n              \"@type\": \"ImageObject\",\n              url: `${getServerUrl()}/images/screenshots/heart-rate-zones/og.jpg`,\n              width: 1200,\n              height: 630,\n            },\n            datePublished: \"2024-01-01\",\n            dateModified: new Date().toISOString(),\n            author: {\n              \"@type\": \"Organization\",\n              name: \"WorkoutCool\",\n              url: getServerUrl(),\n            },\n            publisher: {\n              \"@type\": \"Organization\",\n              name: \"WorkoutCool\",\n              logo: {\n                \"@type\": \"ImageObject\",\n                url: `${getServerUrl()}/logo.png`,\n              },\n            },\n            mainEntityOfPage: {\n              \"@type\": \"WebPage\",\n              \"@id\": `${getServerUrl()}/${locale}/tools/heart-rate-zones`,\n            },\n            articleSection: \"Health & Fitness\",\n            keywords: seoContent.keywords,\n            about: {\n              \"@type\": \"Thing\",\n              name: \"Heart Rate Training Zones\",\n              description: \"Scientific method for optimizing cardiovascular training through personalized heart rate zones\",\n            },\n            educationalLevel: \"Beginner to Advanced\",\n            learningResourceType: \"Calculator and Guide\",\n            isAccessibleForFree: true,\n            inLanguage: locale,\n          }),\n        }}\n        type=\"application/ld+json\"\n      />\n      <div className=\"min-h-screen bg-gradient-to-b from-blue-50 to-white dark:from-gray-900 dark:to-gray-800\">\n        {env.NEXT_PUBLIC_TOP_HEART_ZONES_BANNER_AD_SLOT && <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_HEART_ZONES_BANNER_AD_SLOT} />}\n        <div className=\"container mx-auto px-2 sm:px-4 py-6 max-w-4xl relative z-10\">\n          {/* SEO-optimized header */}\n          <div className=\"text-center mb-8\">\n            <div className=\"text-6xl mb-4\">❤️</div>\n            <h1 className=\"text-3xl sm:text-5xl font-bold mb-4 text-gray-900 dark:text-white\">{seoContent.title}</h1>\n            <p className=\"text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto\">{pageContent.heroSubtitle}</p>\n          </div>\n\n          {/* Calculator */}\n          <HeartRateZonesCalculatorClient defaultAge={DEFAULT_AGE} defaultResults={defaultResults} />\n          {/* Educational Content */}\n          <div className=\"mt-16\">\n            <EducationalContentServer />\n            <SEOOptimizedContentServer />\n          </div>\n        </div>\n        {env.NEXT_PUBLIC_BOTTOM_HEART_ZONES_BANNER_AD_SLOT && (\n          <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_BOTTOM_HEART_ZONES_BANNER_AD_SLOT} />\n        )}\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/seo/config.ts",
    "content": "import { Locale } from \"locales/types\";\n\n// All SEO metadata in one place for easy maintenance\nexport const HEART_RATE_ZONES_SEO: Record<\n  Locale,\n  {\n    title: string;\n    description: string;\n    keywords: string[];\n  }\n> = {\n  en: {\n    title: \"Discover your ideal heart rate training zones for optimal workouts\",\n    description:\n      \"Calculate your personalized heart rate training zones with our free calculator. Basic & Karvonen formulas, age-based chart, complete guide to optimize your cardio workouts.\",\n    keywords: [\n      \"heart rate zones calculator\",\n      \"target heart rate calculator\",\n      \"maximum heart rate\",\n      \"training zones\",\n      \"VO2 max zone\",\n      \"anaerobic zone\",\n      \"aerobic zone\",\n      \"fat burn zone\",\n      \"Karvonen formula\",\n      \"heart rate training\",\n      \"THR calculator\",\n      \"MHR calculator\",\n      \"cardio zones\",\n      \"fitness calculator\",\n      \"heart rate by age\",\n    ],\n  },\n  es: {\n    title: \"Descubre tus zonas de frecuencia cardíaca ideales para un entrenamiento óptimo\",\n    description:\n      \"Calcula tus zonas de frecuencia cardíaca personalizadas con nuestra calculadora gratuita. Fórmulas Basic y Karvonen, tabla por edad, guía completa para optimizar tu cardio.\",\n    keywords: [\n      \"calculadora zonas frecuencia cardiaca\",\n      \"frecuencia cardiaca objetivo\",\n      \"frecuencia cardiaca máxima\",\n      \"zonas entrenamiento\",\n      \"zona VO2 max\",\n      \"zona anaeróbica\",\n      \"zona aeróbica\",\n      \"zona quema grasa\",\n      \"fórmula Karvonen\",\n      \"entrenamiento frecuencia cardiaca\",\n      \"calculadora FCM\",\n      \"zonas cardio\",\n      \"calculadora fitness\",\n    ],\n  },\n  fr: {\n    title: \"Découvrez vos zones de fréquence cardiaque idéales pour un entraînement optimal\",\n    description:\n      \"Calculez vos zones de fréquence cardiaque personnalisées avec notre calculateur gratuit. Formules Basic & Karvonen, tableau par âge, guide complet pour optimiser vos entraînements cardio.\",\n    keywords: [\n      \"calculateur zones fréquence cardiaque\",\n      \"calcul FCM\",\n      \"zones cardiaques entraînement\",\n      \"fréquence cardiaque maximale\",\n      \"formule Karvonen\",\n      \"zone combustion graisses\",\n      \"zone aérobie\",\n      \"zone anaérobie\",\n      \"VO2 max zone\",\n      \"calculateur THR\",\n      \"zones cardio training\",\n      \"fréquence cardiaque repos\",\n      \"calculateur fitness gratuit\",\n    ],\n  },\n  pt: {\n    title: \"Descubra suas zonas de frequência cardíaca ideais para um treino ótimo\",\n    description:\n      \"Calcule suas zonas de frequência cardíaca personalizadas com nossa calculadora gratuita. Fórmulas Basic e Karvonen, tabela por idade, guia completo para otimizar seu treino cardio.\",\n    keywords: [\n      \"calculadora zonas frequência cardíaca\",\n      \"frequência cardíaca alvo\",\n      \"frequência cardíaca máxima\",\n      \"zonas treino\",\n      \"zona VO2 max\",\n      \"zona anaeróbica\",\n      \"zona aeróbica\",\n      \"zona queima gordura\",\n      \"fórmula Karvonen\",\n      \"treino frequência cardíaca\",\n      \"calculadora FCM\",\n      \"zonas cardio\",\n      \"calculadora fitness\",\n    ],\n  },\n  ru: {\n    title: \"Рассчитайте персональные зоны пульса с помощью нашего бесплатного калькулятора\",\n    description:\n      \"Рассчитайте персональные зоны пульса с помощью нашего бесплатного калькулятора. Формулы Basic и Карвонена, таблица по возрасту, полное руководство для оптимизации кардио тренировок.\",\n    keywords: [\n      \"калькулятор зон пульса\",\n      \"целевой пульс\",\n      \"максимальный пульс\",\n      \"тренировочные зоны\",\n      \"зона VO2 max\",\n      \"анаэробная зона\",\n      \"аэробная зона\",\n      \"зона жиросжигания\",\n      \"формула Карвонена\",\n      \"тренировка по пульсу\",\n      \"калькулятор ЧСС\",\n      \"кардио зоны\",\n      \"фитнес калькулятор\",\n    ],\n  },\n  \"zh-CN\": {\n    title: \"使用我们的免费计算器计算您的个性化心率训练区间\",\n    description: \"使用我们的免费计算器计算您的个性化心率训练区间。Basic和Karvonen公式，按年龄分类表，优化有氧运动的完整指南。\",\n    keywords: [\n      \"心率区间计算器\",\n      \"目标心率\",\n      \"最大心率\",\n      \"训练区间\",\n      \"VO2最大值区间\",\n      \"无氧区间\",\n      \"有氧区间\",\n      \"燃脂区间\",\n      \"Karvonen公式\",\n      \"心率训练\",\n      \"心率计算器\",\n      \"有氧区间\",\n      \"健身计算器\",\n    ],\n  },\n};\n"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/seo/page-content.ts",
    "content": "import { Locale } from \"locales/types\";\n\ninterface PageContent {\n  heroSubtitle: string;\n}\n\n// Page content separate from SEO metadata\nexport const HEART_RATE_ZONES_CONTENT: Record<Locale, PageContent> = {\n  en: {\n    heroSubtitle: \"Discover your personalized training zones to optimize performance, burn more fat, and improve cardiovascular fitness\",\n  },\n  es: {\n    heroSubtitle:\n      \"Descubre tus zonas de entrenamiento personalizadas para optimizar el rendimiento, quemar más grasa y mejorar tu condición cardiovascular\",\n  },\n  fr: {\n    heroSubtitle:\n      \"Découvrez vos zones d'entraînement personnalisées pour optimiser vos performances, brûler plus de graisses et améliorer votre condition cardiovasculaire\",\n  },\n  pt: {\n    heroSubtitle:\n      \"Descubra suas zonas de treino personalizadas para otimizar o desempenho, queimar mais gordura e melhorar sua condição cardiovascular\",\n  },\n  ru: {\n    heroSubtitle:\n      \"Откройте персональные тренировочные зоны для оптимизации результатов, сжигания жира и улучшения сердечно-сосудистой системы\",\n  },\n  \"zh-CN\": {\n    heroSubtitle: \"发现您的个性化训练区间，优化运动表现，燃烧更多脂肪，改善心血管健康\",\n  },\n};\n"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/ui/HeartRateZonesCalculatorClient.tsx",
    "content": "\"use client\";\n\nimport { z } from \"zod\";\nimport { useForm } from \"react-hook-form\";\nimport React, { useState, useEffect } from \"react\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\nimport { useI18n } from \"locales/client\";\nimport \"./styles.css\";\nimport { env } from \"@/env\";\nimport { InArticle } from \"@/components/ads\";\n\nconst simpleHeartRateSchema = z.object({\n  age: z.coerce.number().min(1).max(120),\n});\n\ntype SimpleHeartRateFormData = z.infer<typeof simpleHeartRateSchema>;\n\ninterface HeartRateZone {\n  name: string;\n  minHR: number;\n  maxHR: number;\n  emoji: string;\n  color: string;\n  bgColor: string;\n  description: string;\n}\n\ninterface HeartRateResults {\n  maxHeartRate: number;\n  zones: HeartRateZone[];\n}\n\ninterface SEOFriendlyHeartRateCalculatorProps {\n  defaultAge?: number;\n  defaultResults?: HeartRateResults;\n}\n\nexport function HeartRateZonesCalculatorClient({ defaultAge = 30, defaultResults }: SEOFriendlyHeartRateCalculatorProps) {\n  const t = useI18n();\n  const [results, setResults] = useState<HeartRateResults | null>(defaultResults || null);\n\n  const { watch, setValue } = useForm<SimpleHeartRateFormData>({\n    resolver: zodResolver(simpleHeartRateSchema),\n    defaultValues: {\n      age: defaultAge,\n    },\n  });\n\n  const age = watch(\"age\");\n\n  const calculateZones = (currentAge: number) => {\n    // Calculate MHR\n    const maxHeartRate = 220 - currentAge;\n\n    // Simple zones with emojis and colors\n    const zones: HeartRateZone[] = [\n      {\n        name: t(\"tools.heart-rate-zones.zones.warm_up.name\"),\n        minHR: Math.round(maxHeartRate * 0.5),\n        maxHR: Math.round(maxHeartRate * 0.6),\n        emoji: \"🚶\",\n        color: \"text-blue-600\",\n        bgColor: \"bg-blue-100\",\n        description: t(\"tools.heart-rate-zones.zones.warm_up.description\"),\n      },\n      {\n        name: t(\"tools.heart-rate-zones.zones.fat_burn.name\"),\n        minHR: Math.round(maxHeartRate * 0.6),\n        maxHR: Math.round(maxHeartRate * 0.7),\n        emoji: \"🔥\",\n        color: \"text-green-600\",\n        bgColor: \"bg-green-100\",\n        description: t(\"tools.heart-rate-zones.zones.fat_burn.description\"),\n      },\n      {\n        name: t(\"tools.heart-rate-zones.zones.aerobic.name\"),\n        minHR: Math.round(maxHeartRate * 0.7),\n        maxHR: Math.round(maxHeartRate * 0.8),\n        emoji: \"🏃\",\n        color: \"text-yellow-600\",\n        bgColor: \"bg-yellow-100\",\n        description: t(\"tools.heart-rate-zones.zones.aerobic.description\"),\n      },\n      {\n        name: t(\"tools.heart-rate-zones.zones.anaerobic.name\"),\n        minHR: Math.round(maxHeartRate * 0.8),\n        maxHR: Math.round(maxHeartRate * 0.9),\n        emoji: \"💪\",\n        color: \"text-orange-500\",\n        bgColor: \"bg-orange-100\",\n        description: t(\"tools.heart-rate-zones.zones.anaerobic.description\"),\n      },\n      {\n        name: t(\"tools.heart-rate-zones.zones.vo2_max.name\"),\n        minHR: Math.round(maxHeartRate * 0.9),\n        maxHR: maxHeartRate,\n        emoji: \"🚀\",\n        color: \"text-red-600\",\n        bgColor: \"bg-red-100\",\n        description: t(\"tools.heart-rate-zones.zones.vo2_max.description\"),\n      },\n    ];\n\n    setResults({\n      maxHeartRate,\n      zones,\n    });\n  };\n\n  // Calculate on age change\n  useEffect(() => {\n    if (age && age >= 1 && age <= 120) {\n      calculateZones(age);\n    }\n  }, [age]);\n\n  return (\n    <div className=\"max-w-4xl mx-auto space-y-8\">\n      {/* Age input section - always visible */}\n      <div className=\"bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-3 sm:p-8\">\n        <div className=\"text-center mb-6\">\n          <div className=\"text-6xl mb-4 animate-heartbeat\">🎂</div>\n          <h2 className=\"text-2xl font-bold mb-2 text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.age\")}</h2>\n          <p className=\"text-gray-600 dark:text-gray-300\">{t(\"tools.heart-rate-zones.age_placeholder\")}</p>\n        </div>\n\n        {/* Age slider */}\n        <div className=\"mb-4\">\n          <div className=\"text-6xl font-bold text-blue-600 mb-4 text-center\">{age}</div>\n          <input\n            className=\"w-full h-4 bg-gray-200 rounded-lg appearance-none cursor-pointer slider\"\n            max=\"120\"\n            min=\"1\"\n            onChange={(e) => setValue(\"age\", parseInt(e.target.value))}\n            style={{\n              background: `linear-gradient(to right, #3B82F6 0%, #3B82F6 ${(age / 120) * 100}%, #E5E7EB ${(age / 120) * 100}%, #E5E7EB 100%)`,\n            }}\n            type=\"range\"\n            value={age}\n          />\n          <div className=\"flex justify-between text-sm text-gray-500 mt-2\">\n            <span>1</span>\n            <span>120</span>\n          </div>\n        </div>\n      </div>\n\n      {/* Results section - always visible */}\n      {results && (\n        <div className=\"space-y-6 animate-fade-in-up\">\n          {/* Max heart rate display */}\n          <div className=\"bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-3 sm:p-8 text-center\">\n            <div className=\"text-5xl mb-4 animate-heartbeat\">💓</div>\n            <h3 className=\"text-xl font-semibold text-gray-600 dark:text-gray-300 mb-2\">\n              {t(\"tools.heart-rate-zones.results.max_heart_rate\")}\n            </h3>\n            <div className=\"text-6xl font-bold text-red-500\">{results.maxHeartRate}</div>\n            <div className=\"text-2xl text-gray-500\">bpm</div>\n          </div>\n\n          {/* Heart rate zones */}\n          <div className=\"bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-3 sm:p-8\">\n            <h3 className=\"text-2xl font-bold text-center mb-8 text-gray-900 dark:text-white\">\n              {t(\"tools.heart-rate-zones.results.target_zones\")} 🎯\n            </h3>\n\n            {/* Global zones visualization */}\n            <div className=\"mb-8 p-4 bg-gray-50 dark:bg-gray-700 rounded-2xl\">\n              <div className=\"mb-2 text-sm font-semibold text-gray-700 dark:text-gray-300\">\n                {t(\"tools.heart-rate-zones.results.overview\")}\n              </div>\n              <div className=\"relative h-8 bg-white dark:bg-gray-600 rounded-full overflow-hidden\">\n                {results.zones.map((zone) => (\n                  <div\n                    className={`absolute h-full ${zone.color.replace(\"text-\", \"bg-\")} opacity-80`}\n                    key={zone.name}\n                    style={{\n                      left: `${(zone.minHR / results.maxHeartRate) * 100}%`,\n                      width: `${((zone.maxHR - zone.minHR) / results.maxHeartRate) * 100}%`,\n                    }}\n                    title={`${zone.name}: ${zone.minHR}-${zone.maxHR} bpm`}\n                  />\n                ))}\n              </div>\n              <div className=\"flex justify-between text-xs text-gray-500 mt-1\">\n                <span>0</span>\n                <span>{Math.round(results.maxHeartRate * 0.5)}</span>\n                <span>{Math.round(results.maxHeartRate * 0.75)}</span>\n                <span>{results.maxHeartRate} bpm</span>\n              </div>\n            </div>\n\n            <div className=\"space-y-4\">\n              {results.zones.map((zone) => (\n                <div\n                  className={`${zone.bgColor} rounded-2xl p-6 transform transition-all hover:scale-105 cursor-pointer card-hover`}\n                  key={zone.name}\n                >\n                  <div className=\"flex items-center justify-between\">\n                    <div className=\"flex items-center gap-4\">\n                      <div className=\"text-5xl\">{zone.emoji}</div>\n                      <div>\n                        <h4 className={`text-xl font-bold ${zone.color}`}>{zone.name}</h4>\n                        <p className=\"text-gray-600 text-sm mt-1\">{zone.description}</p>\n                      </div>\n                    </div>\n                    <div className=\"text-right\">\n                      <div className={`text-3xl font-bold ${zone.color}`}>\n                        {zone.minHR}-{zone.maxHR}\n                      </div>\n                      <div className=\"text-gray-500 text-sm\">bpm</div>\n                    </div>\n                  </div>\n\n                  {/* Visual progress bar */}\n                  <div className=\"mt-4\">\n                    <div className=\"bg-gray-200 dark:bg-gray-600 rounded-full h-3 overflow-hidden\">\n                      <div\n                        className={`h-full ${zone.color.replace(\"text-\", \"bg-\")} transition-all`}\n                        style={{\n                          width: `${(zone.maxHR / results.maxHeartRate) * 100}%`,\n                        }}\n                      />\n                    </div>\n                    <div className=\"text-xs text-gray-500 mt-1 text-right\">\n                      {Math.round((zone.maxHR / results.maxHeartRate) * 100)}% de FCM\n                    </div>\n                  </div>\n                </div>\n              ))}\n            </div>\n          </div>\n          {env.NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_3 && <InArticle adSlot={env.NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_3} />}\n          {/* Simple tips */}\n          <div className=\"bg-blue-50 dark:bg-blue-900/20 rounded-3xl p-3 sm:p-8\">\n            <div className=\"text-center mb-6\">\n              <div className=\"text-5xl mb-2\">💡</div>\n              <h3 className=\"text-2xl font-bold text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.tips.title\")}</h3>\n            </div>\n\n            <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n              <div className=\"bg-white dark:bg-gray-800 rounded-2xl p-4 flex items-start gap-3\">\n                <span className=\"text-2xl\">✅</span>\n                <p className=\"text-gray-700 dark:text-gray-300\">{t(\"tools.heart-rate-zones.tips.tip1\")}</p>\n              </div>\n              <div className=\"bg-white dark:bg-gray-800 rounded-2xl p-4 flex items-start gap-3\">\n                <span className=\"text-2xl\">⏱️</span>\n                <p className=\"text-gray-700 dark:text-gray-300\">{t(\"tools.heart-rate-zones.tips.tip2\")}</p>\n              </div>\n              <div className=\"bg-white dark:bg-gray-800 rounded-2xl p-4 flex items-start gap-3\">\n                <span className=\"text-2xl\">📈</span>\n                <p className=\"text-gray-700 dark:text-gray-300\">{t(\"tools.heart-rate-zones.tips.tip3\")}</p>\n              </div>\n              <div className=\"bg-white dark:bg-gray-800 rounded-2xl p-4 flex items-start gap-3\">\n                <span className=\"text-2xl\">🏥</span>\n                <p className=\"text-gray-700 dark:text-gray-300\">{t(\"tools.heart-rate-zones.tips.tip4\")}</p>\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/ui/components/EducationalContent.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { TFunction, useI18n } from \"locales/client\";\nimport { useScrollToTop } from \"@/shared/hooks/useScrollToTop\";\n\nexport function EducationalContent() {\n  const t = useI18n();\n  const scrollToTop = useScrollToTop();\n\n  const zones = (t: TFunction) => [\n    {\n      emoji: \"🚶\",\n      name: t(\"tools.heart-rate-zones.zones.warm_up.name\"),\n      intensity: \"50-60%\",\n      color: \"bg-blue-100 text-blue-700\",\n      benefits: t(\"tools.heart-rate-zones.zones.warm_up.benefits\"),\n      example: t(\"tools.heart-rate-zones.zones.warm_up.example\"),\n    },\n    {\n      emoji: \"🔥\",\n      name: t(\"tools.heart-rate-zones.zones.fat_burn.name\"),\n      intensity: \"60-70%\",\n      color: \"bg-green-100 text-green-700\",\n      benefits: t(\"tools.heart-rate-zones.zones.fat_burn.benefits\"),\n      example: t(\"tools.heart-rate-zones.zones.fat_burn.example\"),\n    },\n    {\n      emoji: \"🏃\",\n      name: t(\"tools.heart-rate-zones.zones.aerobic.name\"),\n      intensity: \"70-80%\",\n      color: \"bg-yellow-100 text-yellow-700\",\n      benefits: t(\"tools.heart-rate-zones.zones.aerobic.benefits\"),\n      example: t(\"tools.heart-rate-zones.zones.aerobic.example\"),\n    },\n    {\n      emoji: \"💪\",\n      name: t(\"tools.heart-rate-zones.zones.anaerobic.name\"),\n      intensity: \"80-90%\",\n      color: \"bg-orange-100 text-orange-700\",\n      benefits: t(\"tools.heart-rate-zones.zones.anaerobic.benefits\"),\n      example: t(\"tools.heart-rate-zones.zones.anaerobic.example\"),\n    },\n    {\n      emoji: \"🚀\",\n      name: t(\"tools.heart-rate-zones.zones.vo2_max.name\"),\n      intensity: \"90-100%\",\n      color: \"bg-red-100 text-red-700\",\n      benefits: t(\"tools.heart-rate-zones.zones.vo2_max.benefits\"),\n      example: t(\"tools.heart-rate-zones.zones.vo2_max.example\"),\n    },\n  ];\n\n  const tips = (t: TFunction) => [\n    {\n      emoji: \"🎯\",\n      title: t(\"tools.heart-rate-zones.training_tips_2.title\"),\n      description: t(\"tools.heart-rate-zones.training_tips_2.description1\"),\n    },\n    {\n      emoji: \"⏱️\",\n      title: t(\"tools.heart-rate-zones.training_tips_2.title2\"),\n      description: t(\"tools.heart-rate-zones.training_tips_2.description2\"),\n    },\n    {\n      emoji: \"📈\",\n      title: t(\"tools.heart-rate-zones.training_tips_2.title3\"),\n      description: t(\"tools.heart-rate-zones.training_tips_2.description3\"),\n    },\n    {\n      emoji: \"💓\",\n      title: t(\"tools.heart-rate-zones.training_tips_2.title4\"),\n      description: t(\"tools.heart-rate-zones.training_tips_2.description4\"),\n    },\n  ];\n\n  const quickFacts = [\n    {\n      emoji: \"🧮\",\n      fact: t(\"tools.heart-rate-zones.quick_facts.fact1\"),\n    },\n    {\n      emoji: \"🛌\",\n      fact: t(\"tools.heart-rate-zones.quick_facts.fact2\"),\n    },\n    {\n      emoji: \"⌚\",\n      fact: t(\"tools.heart-rate-zones.quick_facts.fact3\"),\n    },\n    {\n      emoji: \"🏃‍♀️\",\n      fact: t(\"tools.heart-rate-zones.quick_facts.fact4\"),\n    },\n  ];\n\n  return (\n    <div className=\"space-y-12 mt-16\">\n      {/* Visual Zone Guide */}\n      <div className=\"bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-3 sm:p-8\">\n        <div className=\"text-center mb-8\">\n          <div className=\"text-5xl mb-4\">🎨</div>\n          <h2 className=\"text-3xl font-bold text-gray-900 dark:text-white mb-2\">{t(\"tools.heart-rate-zones.educational.title\")}</h2>\n          <p className=\"text-lg text-gray-600 dark:text-gray-300\">{t(\"tools.heart-rate-zones.educational.description\")}</p>\n        </div>\n\n        <div className=\"grid gap-4\">\n          {zones(t).map((zone, index) => (\n            <div\n              className={`${zone.color} rounded-2xl p-6 transform transition-all hover:scale-105`}\n              key={zone.name}\n              style={{ animationDelay: `${index * 0.1}s` }}\n            >\n              <div className=\"flex items-center justify-between flex-wrap gap-4\">\n                <div className=\"flex items-center gap-4\">\n                  <div className=\"text-5xl\">{zone.emoji}</div>\n                  <div>\n                    <h3 className=\"text-xl font-bold\">{zone.name}</h3>\n                    <p className=\"text-2xl font-bold mt-1\">{zone.intensity}</p>\n                  </div>\n                </div>\n                <div className=\"text-right\">\n                  <p className=\"text-lg font-semibold\">{zone.benefits}</p>\n                  <p className=\"text-sm opacity-80 mt-1\">Ex: {zone.example}</p>\n                </div>\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {/* Simple Tips Grid */}\n      <div className=\"bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-3xl p-3 sm:p-8\">\n        <div className=\"text-center mb-8\">\n          <div className=\"text-5xl mb-4\">💡</div>\n          <h2 className=\"text-3xl font-bold text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.training_tips_2.title\")}</h2>\n        </div>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n          {tips(t).map((tip) => (\n            <div className=\"bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow\" key={tip.title}>\n              <div className=\"flex items-start gap-4\">\n                <div className=\"text-4xl flex-shrink-0\">{tip.emoji}</div>\n                <div>\n                  <h3 className=\"text-lg font-bold text-gray-900 dark:text-white mb-2\">{tip.title}</h3>\n                  <p className=\"text-gray-600 dark:text-gray-300\">{tip.description}</p>\n                </div>\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {/* Fun Facts */}\n      <div className=\"bg-gradient-to-r from-cyan-50 to-blue-50 dark:from-cyan-900/20 dark:to-blue-900/20 rounded-3xl p-8\">\n        <div className=\"text-center mb-8\">\n          <div className=\"text-5xl mb-4\">🤓</div>\n          <h2 className=\"text-3xl font-bold text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.quick_facts.title\")}</h2>\n        </div>\n\n        <div className=\"space-y-4\">\n          {quickFacts.map((item, index) => (\n            <div\n              className=\"bg-white dark:bg-gray-800 rounded-2xl p-4 flex items-center gap-4 hover:shadow-lg transition-shadow\"\n              key={index}\n            >\n              <div className=\"text-3xl\">{item.emoji}</div>\n              <p className=\"text-gray-700 dark:text-gray-300 flex-1\">{item.fact}</p>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {/* Interactive Weekly Plan */}\n      <div className=\"bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-3xl p-8\">\n        <div className=\"text-center mb-8\">\n          <div className=\"text-5xl mb-4\">📅</div>\n          <h2 className=\"text-3xl font-bold text-gray-900 dark:text-white mb-2\">{t(\"tools.heart-rate-zones.weekly_plan.title\")}</h2>\n          <p className=\"text-lg text-gray-600 dark:text-gray-300\">{t(\"tools.heart-rate-zones.weekly_plan.description\")}</p>\n        </div>\n\n        <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n          <div className=\"bg-blue-100 dark:bg-blue-900/30 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-blue-700 dark:text-blue-300\">{t(\"commons.monday\")}</p>\n            <div className=\"text-3xl my-2\">🚶</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.monday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.monday.description\")}</p>\n          </div>\n          <div className=\"bg-green-100 dark:bg-green-900/30 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-green-700 dark:text-green-300\">{t(\"commons.tuesday\")}</p>\n            <div className=\"text-3xl my-2\">🔥</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.tuesday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.tuesday.description\")}</p>\n          </div>\n          <div className=\"bg-gray-300 dark:bg-gray-800 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-gray-700 dark:text-gray-300\">{t(\"commons.wednesday\")}</p>\n            <div className=\"text-3xl my-2\">😴</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.wednesday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.wednesday.description\")}</p>\n          </div>\n          <div className=\"bg-yellow-100 dark:bg-yellow-900/30 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-yellow-700 dark:text-yellow-300\">{t(\"commons.thursday\")}</p>\n            <div className=\"text-3xl my-2\">🏃</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.thursday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.thursday.description\")}</p>\n          </div>\n          <div className=\"bg-blue-100 dark:bg-blue-900/30 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-blue-700 dark:text-blue-300\">{t(\"commons.friday\")}</p>\n            <div className=\"text-3xl my-2\">🚶</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.friday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.friday.description\")}</p>\n          </div>\n          <div className=\"bg-orange-100 dark:bg-orange-900/30 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-orange-700 dark:text-orange-300\">{t(\"commons.saturday\")}</p>\n            <div className=\"text-3xl my-2\">💪</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.saturday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.saturday.description\")}</p>\n          </div>\n        </div>\n\n        <div className=\"mt-6 text-center\">\n          <p className=\"text-sm text-gray-600 dark:text-gray-400\">{t(\"tools.heart-rate-zones.weekly_plan.tips\")}</p>\n        </div>\n      </div>\n\n      {/* Simple CTA */}\n      <div className=\"text-center\">\n        <button\n          className=\"bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white text-xl font-bold py-6 px-12 rounded-full transform transition-all hover:scale-105 active:scale-95 shadow-xl\"\n          onClick={scrollToTop}\n        >\n          {t(\"tools.heart-rate-zones.weekly_plan.cta\")}\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/ui/components/EducationalContentServer.tsx",
    "content": "import React from \"react\";\n\nimport { getI18n } from \"locales/server\";\nimport { ScrollToTopButton } from \"app/[locale]/(app)/tools/heart-rate-zones/ui/components/ScrollToTopButton\";\n\nexport async function EducationalContentServer() {\n  const t = await getI18n();\n\n  const zones = [\n    {\n      emoji: \"🚶\",\n      name: t(\"tools.heart-rate-zones.zones.warm_up.name\"),\n      intensity: \"50-60%\",\n      color: \"bg-blue-100 text-blue-700\",\n      benefits: t(\"tools.heart-rate-zones.zones.warm_up.benefits\"),\n      example: t(\"tools.heart-rate-zones.zones.warm_up.example\"),\n    },\n    {\n      emoji: \"🔥\",\n      name: t(\"tools.heart-rate-zones.zones.fat_burn.name\"),\n      intensity: \"60-70%\",\n      color: \"bg-green-100 text-green-700\",\n      benefits: t(\"tools.heart-rate-zones.zones.fat_burn.benefits\"),\n      example: t(\"tools.heart-rate-zones.zones.fat_burn.example\"),\n    },\n    {\n      emoji: \"🏃\",\n      name: t(\"tools.heart-rate-zones.zones.aerobic.name\"),\n      intensity: \"70-80%\",\n      color: \"bg-yellow-100 text-yellow-700\",\n      benefits: t(\"tools.heart-rate-zones.zones.aerobic.benefits\"),\n      example: t(\"tools.heart-rate-zones.zones.aerobic.example\"),\n    },\n    {\n      emoji: \"💪\",\n      name: t(\"tools.heart-rate-zones.zones.anaerobic.name\"),\n      intensity: \"80-90%\",\n      color: \"bg-orange-100 text-orange-700\",\n      benefits: t(\"tools.heart-rate-zones.zones.anaerobic.benefits\"),\n      example: t(\"tools.heart-rate-zones.zones.anaerobic.example\"),\n    },\n    {\n      emoji: \"🚀\",\n      name: t(\"tools.heart-rate-zones.zones.vo2_max.name\"),\n      intensity: \"90-100%\",\n      color: \"bg-red-100 text-red-700\",\n      benefits: t(\"tools.heart-rate-zones.zones.vo2_max.benefits\"),\n      example: t(\"tools.heart-rate-zones.zones.vo2_max.example\"),\n    },\n  ];\n\n  const tips = [\n    {\n      emoji: \"🎯\",\n      title: t(\"tools.heart-rate-zones.training_tips_2.title\"),\n      description: t(\"tools.heart-rate-zones.training_tips_2.description1\"),\n    },\n    {\n      emoji: \"⏱️\",\n      title: t(\"tools.heart-rate-zones.training_tips_2.title2\"),\n      description: t(\"tools.heart-rate-zones.training_tips_2.description2\"),\n    },\n    {\n      emoji: \"📈\",\n      title: t(\"tools.heart-rate-zones.training_tips_2.title3\"),\n      description: t(\"tools.heart-rate-zones.training_tips_2.description3\"),\n    },\n    {\n      emoji: \"💓\",\n      title: t(\"tools.heart-rate-zones.training_tips_2.title4\"),\n      description: t(\"tools.heart-rate-zones.training_tips_2.description4\"),\n    },\n  ];\n\n  const quickFacts = [\n    {\n      emoji: \"🧮\",\n      fact: t(\"tools.heart-rate-zones.quick_facts.fact1\"),\n    },\n    {\n      emoji: \"🛌\",\n      fact: t(\"tools.heart-rate-zones.quick_facts.fact2\"),\n    },\n    {\n      emoji: \"⌚\",\n      fact: t(\"tools.heart-rate-zones.quick_facts.fact3\"),\n    },\n    {\n      emoji: \"🏃‍♀️\",\n      fact: t(\"tools.heart-rate-zones.quick_facts.fact4\"),\n    },\n  ];\n\n  return (\n    <div className=\"space-y-12 mt-16\">\n      {/* Visual Zone Guide */}\n      <div className=\"bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-3 sm:p-8\">\n        <div className=\"text-center mb-8\">\n          <div className=\"text-5xl mb-4\">🎨</div>\n          <h2 className=\"text-3xl font-bold text-gray-900 dark:text-white mb-2\">{t(\"tools.heart-rate-zones.educational.title\")}</h2>\n          <p className=\"text-lg text-gray-600 dark:text-gray-300\">{t(\"tools.heart-rate-zones.educational.description\")}</p>\n        </div>\n\n        <div className=\"grid gap-4\">\n          {zones.map((zone, index) => (\n            <div\n              className={`${zone.color} rounded-2xl p-6 transform transition-all hover:scale-105`}\n              key={zone.name}\n              style={{ animationDelay: `${index * 0.1}s` }}\n            >\n              <div className=\"flex items-center justify-between flex-wrap gap-4\">\n                <div className=\"flex items-center gap-4\">\n                  <div className=\"text-5xl\">{zone.emoji}</div>\n                  <div>\n                    <h3 className=\"text-xl font-bold\">{zone.name}</h3>\n                    <p className=\"text-2xl font-bold mt-1\">{zone.intensity}</p>\n                  </div>\n                </div>\n                <div className=\"text-right\">\n                  <p className=\"text-lg font-semibold\">{zone.benefits}</p>\n                  <p className=\"text-sm opacity-80 mt-1\">Ex: {zone.example}</p>\n                </div>\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {/* Simple Tips Grid */}\n      <div className=\"bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-3xl p-3 sm:p-8\">\n        <div className=\"text-center mb-8\">\n          <div className=\"text-5xl mb-4\">💡</div>\n          <h2 className=\"text-3xl font-bold text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.training_tips_2.title\")}</h2>\n        </div>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n          {tips.map((tip) => (\n            <div className=\"bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow\" key={tip.title}>\n              <div className=\"flex items-start gap-4\">\n                <div className=\"text-4xl flex-shrink-0\">{tip.emoji}</div>\n                <div>\n                  <h3 className=\"text-lg font-bold text-gray-900 dark:text-white mb-2\">{tip.title}</h3>\n                  <p className=\"text-gray-600 dark:text-gray-300\">{tip.description}</p>\n                </div>\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {/* Fun Facts */}\n      <div className=\"bg-gradient-to-r from-cyan-50 to-blue-50 dark:from-cyan-900/20 dark:to-blue-900/20 rounded-3xl p-8\">\n        <div className=\"text-center mb-8\">\n          <div className=\"text-5xl mb-4\">🤓</div>\n          <h2 className=\"text-3xl font-bold text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.quick_facts.title\")}</h2>\n        </div>\n\n        <div className=\"space-y-4\">\n          {quickFacts.map((item, index) => (\n            <div\n              className=\"bg-white dark:bg-gray-800 rounded-2xl p-4 flex items-center gap-4 hover:shadow-lg transition-shadow\"\n              key={index}\n            >\n              <div className=\"text-3xl\">{item.emoji}</div>\n              <p className=\"text-gray-700 dark:text-gray-300 flex-1\">{item.fact}</p>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {/* Interactive Weekly Plan */}\n      <div className=\"bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-3xl p-8\">\n        <div className=\"text-center mb-8\">\n          <div className=\"text-5xl mb-4\">📅</div>\n          <h2 className=\"text-3xl font-bold text-gray-900 dark:text-white mb-2\">{t(\"tools.heart-rate-zones.weekly_plan.title\")}</h2>\n          <p className=\"text-lg text-gray-600 dark:text-gray-300\">{t(\"tools.heart-rate-zones.weekly_plan.description\")}</p>\n        </div>\n\n        <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n          <div className=\"bg-blue-100 dark:bg-blue-900/30 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-blue-700 dark:text-blue-300\">{t(\"commons.monday\")}</p>\n            <div className=\"text-3xl my-2\">🚶</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.monday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.monday.description\")}</p>\n          </div>\n          <div className=\"bg-green-100 dark:bg-green-900/30 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-green-700 dark:text-green-300\">{t(\"commons.tuesday\")}</p>\n            <div className=\"text-3xl my-2\">🔥</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.tuesday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.tuesday.description\")}</p>\n          </div>\n          <div className=\"bg-gray-300 dark:bg-gray-800 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-gray-700 dark:text-gray-300\">{t(\"commons.wednesday\")}</p>\n            <div className=\"text-3xl my-2\">😴</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.wednesday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.wednesday.description\")}</p>\n          </div>\n          <div className=\"bg-yellow-100 dark:bg-yellow-900/30 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-yellow-700 dark:text-yellow-300\">{t(\"commons.thursday\")}</p>\n            <div className=\"text-3xl my-2\">🏃</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.thursday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.thursday.description\")}</p>\n          </div>\n          <div className=\"bg-blue-100 dark:bg-blue-900/30 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-blue-700 dark:text-blue-300\">{t(\"commons.friday\")}</p>\n            <div className=\"text-3xl my-2\">🚶</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.friday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.friday.description\")}</p>\n          </div>\n          <div className=\"bg-orange-100 dark:bg-orange-900/30 rounded-2xl p-4 text-center\">\n            <p className=\"font-bold text-orange-700 dark:text-orange-300\">{t(\"commons.saturday\")}</p>\n            <div className=\"text-3xl my-2\">💪</div>\n            <p className=\"text-sm\">{t(\"tools.heart-rate-zones.weekly_plan.saturday.title\")}</p>\n            <p className=\"text-xs opacity-75\">{t(\"tools.heart-rate-zones.weekly_plan.saturday.description\")}</p>\n          </div>\n        </div>\n\n        <div className=\"mt-6 text-center\">\n          <p className=\"text-sm text-gray-600 dark:text-gray-400\">{t(\"tools.heart-rate-zones.weekly_plan.tips\")}</p>\n        </div>\n      </div>\n\n      {/* Simple CTA */}\n      <div className=\"text-center\">\n        <ScrollToTopButton text={t(\"tools.heart-rate-zones.weekly_plan.cta\")} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/ui/components/FAQAccordion.tsx",
    "content": "\"use client\";\n\nimport React, { useState } from \"react\";\n\ninterface FAQItem {\n  question: string;\n  answer: string;\n}\n\ninterface FAQAccordionProps {\n  items: FAQItem[];\n}\n\nexport function FAQAccordion({ items }: FAQAccordionProps) {\n  const [activeTab, setActiveTab] = useState(0);\n\n  return (\n    <div className=\"space-y-4\">\n      {items.map((item, index) => (\n        <div\n          className=\"border border-gray-200 dark:border-gray-700 rounded-2xl overflow-hidden\"\n          itemProp=\"mainEntity\"\n          itemScope\n          itemType=\"https://schema.org/Question\"\n          key={index}\n        >\n          <button\n            className=\"w-full px-6 py-4 text-left flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors\"\n            onClick={() => setActiveTab(activeTab === index ? -1 : index)}\n          >\n            <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white pr-4\" itemProp=\"name\">\n              {item.question}\n            </h3>\n            <span className=\"text-2xl text-gray-500\">{activeTab === index ? \"−\" : \"+\"}</span>\n          </button>\n          {activeTab === index && (\n            <div\n              className=\"px-6 py-4 bg-gray-50 dark:bg-gray-700/50\"\n              itemProp=\"acceptedAnswer\"\n              itemScope\n              itemType=\"https://schema.org/Answer\"\n            >\n              <p className=\"text-gray-700 dark:text-gray-300\" itemProp=\"text\">\n                {item.answer}\n              </p>\n            </div>\n          )}\n        </div>\n      ))}\n    </div>\n  );\n}"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/ui/components/SEOOptimizedContentServer.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\n\nimport { getI18n } from \"locales/server\";\nimport { ScrollToTopButton } from \"app/[locale]/(app)/tools/heart-rate-zones/ui/components/ScrollToTopButton\";\nimport { FAQAccordion } from \"app/[locale]/(app)/tools/heart-rate-zones/ui/components/FAQAccordion\";\nimport { env } from \"@/env\";\nimport { InArticle } from \"@/components/ads\";\n\nexport async function SEOOptimizedContentServer() {\n  const t = await getI18n();\n  // Age-based heart rate chart data\n  const ageChartData = [\n    { age: \"20-29\", maxHR: \"190-200\", target50: \"95-100\", target85: \"162-170\" },\n    { age: \"30-39\", maxHR: \"180-190\", target50: \"90-95\", target85: \"153-162\" },\n    { age: \"40-49\", maxHR: \"170-180\", target50: \"85-90\", target85: \"145-153\" },\n    { age: \"50-59\", maxHR: \"160-170\", target50: \"80-85\", target85: \"136-145\" },\n    { age: \"60-69\", maxHR: \"150-160\", target50: \"75-80\", target85: \"128-136\" },\n    { age: \"70+\", maxHR: \"140-150\", target50: \"70-75\", target85: \"119-128\" },\n  ];\n\n  const faqItems = [\n    {\n      question: t(\"tools.heart-rate-zones.seo_faq_q1_question\"),\n      answer: t(\"tools.heart-rate-zones.seo_faq_q1_answer\"),\n    },\n    {\n      question: t(\"tools.heart-rate-zones.seo_faq_q2_question\"),\n      answer: t(\"tools.heart-rate-zones.seo_faq_q2_answer\"),\n    },\n    {\n      question: t(\"tools.heart-rate-zones.seo_faq_q3_question\"),\n      answer: t(\"tools.heart-rate-zones.seo_faq_q3_answer\"),\n    },\n    {\n      question: t(\"tools.heart-rate-zones.seo_faq_q4_question\"),\n      answer: t(\"tools.heart-rate-zones.seo_faq_q4_answer\"),\n    },\n    {\n      question: t(\"tools.heart-rate-zones.seo_faq_q5_question\"),\n      answer: t(\"tools.heart-rate-zones.seo_faq_q5_answer\"),\n    },\n    {\n      question: t(\"tools.heart-rate-zones.seo_faq_q6_question\"),\n      answer: t(\"tools.heart-rate-zones.seo_faq_q6_answer\"),\n    },\n    {\n      question: t(\"tools.heart-rate-zones.seo_faq_q7_question\"),\n      answer: t(\"tools.heart-rate-zones.seo_faq_q7_answer\"),\n    },\n    {\n      question: t(\"tools.heart-rate-zones.seo_faq_q8_question\"),\n      answer: t(\"tools.heart-rate-zones.seo_faq_q8_answer\"),\n    },\n  ];\n\n  return (\n    <div className=\"space-y-12 mt-16\">\n      {/* Introduction détaillée */}\n      <section className=\"bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-3 sm:p-8\">\n        <h2 className=\"text-3xl font-bold mb-6 text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.guide.title\")}</h2>\n        <div className=\"prose prose-lg max-w-none dark:prose-invert\">\n          <p className=\"text-gray-700 dark:text-gray-300 leading-relaxed\">{t(\"tools.heart-rate-zones.guide.text1\")}</p>\n          <p className=\"text-gray-700 dark:text-gray-300 leading-relaxed mt-4\">{t(\"tools.heart-rate-zones.guide.text2\")}</p>\n        </div>\n      </section>\n      {env.NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_1 && <InArticle adSlot={env.NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_1} />}\n\n      {/* Tableau de référence par âge */}\n      <section className=\"bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-3xl p-8\">\n        <h2 className=\"text-3xl font-bold mb-6 text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.table.title\")}</h2>\n        <div className=\"overflow-x-auto\">\n          <table className=\"w-full bg-white dark:bg-gray-800 rounded-2xl overflow-hidden shadow-lg\">\n            <thead className=\"bg-blue-600 text-white\">\n              <tr>\n                <th className=\"px-6 py-4 text-left\">{t(\"tools.heart-rate-zones.table.col1\")}</th>\n                <th className=\"px-6 py-4 text-left\">{t(\"tools.heart-rate-zones.table.col2\")}</th>\n                <th className=\"px-6 py-4 text-left\">{t(\"tools.heart-rate-zones.table.col3\")}</th>\n                <th className=\"px-6 py-4 text-left\">{t(\"tools.heart-rate-zones.table.col4\")}</th>\n              </tr>\n            </thead>\n            <tbody>\n              {ageChartData.map((row, index) => (\n                <tr className={index % 2 === 0 ? \"bg-gray-50 dark:bg-gray-700\" : \"bg-white dark:bg-gray-800\"} key={row.age}>\n                  <td className=\"px-6 py-4 font-semibold\">{row.age}</td>\n                  <td className=\"px-6 py-4\">{row.maxHR}</td>\n                  <td className=\"px-6 py-4\">{row.target50}</td>\n                  <td className=\"px-6 py-4\">{row.target85}</td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n        <p className=\"text-sm text-gray-600 dark:text-gray-400 mt-4\">{t(\"tools.heart-rate-zones.table.avertiser\")}</p>\n      </section>\n\n      {/* Explication détaillée des zones */}\n      <section className=\"bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-3 sm:p-8\">\n        <h2 className=\"text-3xl font-bold mb-8 text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.details.title\")}</h2>\n\n        <div className=\"space-y-6\">\n          <div className=\"bg-blue-50 dark:bg-blue-900/20 rounded-2xl p-6\">\n            <div className=\"flex items-start gap-4\">\n              <div className=\"text-4xl\">🚶</div>\n              <div className=\"flex-1\">\n                <h3 className=\"text-2xl font-bold text-blue-700 dark:text-blue-300 mb-2\">\n                  {t(\"tools.heart-rate-zones.details.zone1_title\")}\n                </h3>\n                <p className=\"text-gray-700 dark:text-gray-300 mb-3\">{t(\"tools.heart-rate-zones.details.zone1_content\")}</p>\n                <div className=\"grid md:grid-cols-2 gap-4\">\n                  <div>\n                    <h4 className=\"font-semibold text-gray-800 dark:text-gray-200 mb-2\">\n                      {t(\"tools.heart-rate-zones.details.benefits\")} :\n                    </h4>\n                    <ul className=\"list-disc list-inside text-sm text-gray-600 dark:text-gray-400 space-y-1\">\n                      <li>{t(\"tools.heart-rate-zones.details.zone1_details_1\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone1_details_2\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone1_details_3\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone1_details_4\")}</li>\n                    </ul>\n                  </div>\n                  <div>\n                    <h4 className=\"font-semibold text-gray-800 dark:text-gray-200 mb-2\">\n                      {t(\"tools.heart-rate-zones.details.zone1_duration\")} :\n                    </h4>\n                    <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n                      {t(\"tools.heart-rate-zones.details.zone1_duration_value\")}\n                      <br />\n                      {t(\"tools.heart-rate-zones.details.zone1_duration_value_2\")}\n                    </p>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"bg-green-50 dark:bg-green-900/20 rounded-2xl p-6\">\n            <div className=\"flex items-start gap-4\">\n              <div className=\"text-4xl\">🔥</div>\n              <div className=\"flex-1\">\n                <h3 className=\"text-2xl font-bold text-green-700 dark:text-green-300 mb-2\">\n                  {t(\"tools.heart-rate-zones.details.zone2_title\")}\n                </h3>\n                <p className=\"text-gray-700 dark:text-gray-300 mb-3\">{t(\"tools.heart-rate-zones.details.zone2_content\")}</p>\n                <div className=\"grid md:grid-cols-2 gap-4\">\n                  <div>\n                    <h4 className=\"font-semibold text-gray-800 dark:text-gray-200 mb-2\">\n                      {t(\"tools.heart-rate-zones.details.benefits\")} :\n                    </h4>\n                    <ul className=\"list-disc list-inside text-sm text-gray-600 dark:text-gray-400 space-y-1\">\n                      <li>{t(\"tools.heart-rate-zones.details.zone2_details_1\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone2_details_2\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone2_details_3\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone2_details_4\")}</li>\n                    </ul>\n                  </div>\n                  <div>\n                    <h4 className=\"font-semibold text-gray-800 dark:text-gray-200 mb-2\">\n                      {t(\"tools.heart-rate-zones.details.zone2_duration\")} :\n                    </h4>\n                    <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n                      {t(\"tools.heart-rate-zones.details.zone2_duration_value\")}\n                      <br />\n                      {t(\"tools.heart-rate-zones.details.zone2_duration_value_2\")}\n                    </p>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"bg-yellow-50 dark:bg-yellow-900/20 rounded-2xl p-6\">\n            <div className=\"flex items-start gap-4\">\n              <div className=\"text-4xl\">🏃</div>\n              <div className=\"flex-1\">\n                <h3 className=\"text-2xl font-bold text-yellow-700 dark:text-yellow-300 mb-2\">\n                  {t(\"tools.heart-rate-zones.details.zone3_title\")}\n                </h3>\n                <p className=\"text-gray-700 dark:text-gray-300 mb-3\">{t(\"tools.heart-rate-zones.details.zone3_content\")}</p>\n                <div className=\"grid md:grid-cols-2 gap-4\">\n                  <div>\n                    <h4 className=\"font-semibold text-gray-800 dark:text-gray-200 mb-2\">\n                      {t(\"tools.heart-rate-zones.details.benefits\")} :\n                    </h4>\n                    <ul className=\"list-disc list-inside text-sm text-gray-600 dark:text-gray-400 space-y-1\">\n                      <li>{t(\"tools.heart-rate-zones.details.zone3_details_1\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone3_details_2\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone3_details_3\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone3_details_4\")}</li>\n                    </ul>\n                  </div>\n                  <div>\n                    <h4 className=\"font-semibold text-gray-800 dark:text-gray-200 mb-2\">\n                      {t(\"tools.heart-rate-zones.details.zone3_duration\")} :\n                    </h4>\n                    <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n                      {t(\"tools.heart-rate-zones.details.zone3_duration_value\")}\n                      <br />\n                      {t(\"tools.heart-rate-zones.details.zone3_duration_value_2\")}\n                    </p>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"bg-orange-50 dark:bg-orange-900/20 rounded-2xl p-6\">\n            <div className=\"flex items-start gap-4\">\n              <div className=\"text-4xl\">💪</div>\n              <div className=\"flex-1\">\n                <h3 className=\"text-2xl font-bold text-orange-700 dark:text-orange-300 mb-2\">\n                  {t(\"tools.heart-rate-zones.details.zone4_title\")}\n                </h3>\n                <p className=\"text-gray-700 dark:text-gray-300 mb-3\">{t(\"tools.heart-rate-zones.details.zone4_content\")}</p>\n                <div className=\"grid md:grid-cols-2 gap-4\">\n                  <div>\n                    <h4 className=\"font-semibold text-gray-800 dark:text-gray-200 mb-2\">\n                      {t(\"tools.heart-rate-zones.details.benefits\")} :\n                    </h4>\n                    <ul className=\"list-disc list-inside text-sm text-gray-600 dark:text-gray-400 space-y-1\">\n                      <li>{t(\"tools.heart-rate-zones.details.zone4_details_1\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone4_details_2\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone4_details_3\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone4_details_4\")}</li>\n                    </ul>\n                  </div>\n                  <div>\n                    <h4 className=\"font-semibold text-gray-800 dark:text-gray-200 mb-2\">\n                      {t(\"tools.heart-rate-zones.details.zone4_duration\")} :\n                    </h4>\n                    <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n                      {t(\"tools.heart-rate-zones.details.zone4_duration_value\")}\n                      <br />\n                      {t(\"tools.heart-rate-zones.details.zone4_duration_value_2\")}\n                    </p>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"bg-red-50 dark:bg-red-900/20 rounded-2xl p-6\">\n            <div className=\"flex items-start gap-4\">\n              <div className=\"text-4xl\">🚀</div>\n              <div className=\"flex-1\">\n                <h3 className=\"text-2xl font-bold text-red-700 dark:text-red-300 mb-2\">\n                  {t(\"tools.heart-rate-zones.details.zone5_title\")}\n                </h3>\n                <p className=\"text-gray-700 dark:text-gray-300 mb-3\">{t(\"tools.heart-rate-zones.details.zone5_content\")}</p>\n                <div className=\"grid md:grid-cols-2 gap-4\">\n                  <div>\n                    <h4 className=\"font-semibold text-gray-800 dark:text-gray-200 mb-2\">\n                      {t(\"tools.heart-rate-zones.details.benefits\")} :\n                    </h4>\n                    <ul className=\"list-disc list-inside text-sm text-gray-600 dark:text-gray-400 space-y-1\">\n                      <li>{t(\"tools.heart-rate-zones.details.zone5_details_1\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone5_details_2\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone5_details_3\")}</li>\n                      <li>{t(\"tools.heart-rate-zones.details.zone5_details_4\")}</li>\n                    </ul>\n                  </div>\n                  <div>\n                    <h4 className=\"font-semibold text-gray-800 dark:text-gray-200 mb-2\">\n                      {t(\"tools.heart-rate-zones.details.zone5_duration\")} :\n                    </h4>\n                    <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n                      {t(\"tools.heart-rate-zones.details.zone5_duration_value\")}\n                      <br />\n                      {t(\"tools.heart-rate-zones.details.zone5_duration_value_2\")}\n                    </p>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </section>\n      {env.NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_2 && <InArticle adSlot={env.NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_2} />}\n      {/* Conseils d'entraînement */}\n      <section className=\"bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-3xl p-3 sm:p-8\">\n        <h2 className=\"text-3xl font-bold mb-8 text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.training_tips.title\")}</h2>\n\n        <div className=\"grid md:grid-cols-2 gap-6\">\n          {[\n            { icon: \"🔥\", tip: \"tip1\" },\n            { icon: \"📊\", tip: \"tip2\" },\n            { icon: \"🔄\", tip: \"tip3\" },\n            { icon: \"💧\", tip: \"tip4\" },\n            { icon: \"😴\", tip: \"tip5\" },\n            { icon: \"📈\", tip: \"tip6\" },\n          ].map((item) => (\n            <div className=\"bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow\" key={item.tip}>\n              <div className=\"text-4xl mb-4\">{item.icon}</div>\n              <h3 className=\"text-xl font-bold mb-2 text-gray-900 dark:text-white\">\n                {t(`tools.heart-rate-zones.training_tips.${item.tip}.title` as keyof typeof t)}\n              </h3>\n              <p className=\"text-gray-600 dark:text-gray-300\">\n                {t(`tools.heart-rate-zones.training_tips.${item.tip}.description` as keyof typeof t)}\n              </p>\n            </div>\n          ))}\n        </div>\n      </section>\n\n      {/* FAQ complète avec schema */}\n      <section className=\"bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-3 sm:p-8\" itemScope itemType=\"https://schema.org/FAQPage\">\n        <h2 className=\"text-3xl font-bold mb-8 text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.seo_faq_title\")}</h2>\n\n        {/* JSON-LD FAQ Schema for better Google indexing */}\n        <script\n          dangerouslySetInnerHTML={{\n            __html: JSON.stringify({\n              \"@context\": \"https://schema.org\",\n              \"@type\": \"FAQPage\",\n              mainEntity: faqItems.map((item) => ({\n                \"@type\": \"Question\",\n                name: item.question,\n                acceptedAnswer: {\n                  \"@type\": \"Answer\",\n                  text: item.answer,\n                },\n              })),\n            }),\n          }}\n          type=\"application/ld+json\"\n        />\n\n        <FAQAccordion items={faqItems} />\n      </section>\n\n      {/* Liens internes et CTA */}\n      <section className=\"bg-gradient-to-r from-blue-600 to-purple-600 rounded-3xl p-8 text-white\">\n        <div className=\"text-center\">\n          <h2 className=\"text-3xl font-bold mb-4\">{t(\"tools.heart-rate-zones.intern_links_title\")}</h2>\n          <p className=\"text-xl mb-8 opacity-90\">{t(\"tools.heart-rate-zones.intern_links_subtitle\")}</p>\n          <ScrollToTopButton text={t(\"tools.heart-rate-zones.intern_links_button\")} />\n\n          <div className=\"mt-12 grid md:grid-cols-2 gap-6 text-left\">\n            <Link\n              className=\"block bg-white/10 backdrop-blur rounded-2xl p-6 hover:bg-white/20 transition-colors\"\n              href=\"/tools/bmi-calculator\"\n            >\n              <h3 className=\"font-bold text-lg mb-2\">{t(\"tools.heart-rate-zones.intern_links_bmi_title\")}</h3>\n              <p className=\"opacity-90\">{t(\"tools.heart-rate-zones.intern_links_bmi_description\")}</p>\n            </Link>\n            <Link\n              className=\"block bg-white/10 backdrop-blur rounded-2xl p-6 hover:bg-white/20 transition-colors\"\n              href=\"/tools/calorie-calculator\"\n            >\n              <h3 className=\"font-bold text-lg mb-2\">{t(\"tools.heart-rate-zones.intern_links_calorie_title\")}</h3>\n              <p className=\"opacity-90\">{t(\"tools.heart-rate-zones.intern_links_calorie_description\")}</p>\n            </Link>\n          </div>\n        </div>\n      </section>\n\n      {/* Avertissement médical */}\n      <section className=\"bg-yellow-50 dark:bg-yellow-900/20 rounded-3xl p-6 border-2 border-yellow-200 dark:border-yellow-800\">\n        <div className=\"flex items-start gap-4\">\n          <span className=\"text-3xl\">⚠️</span>\n          <div>\n            <h3 className=\"font-bold text-lg mb-2 text-gray-900 dark:text-white\">{t(\"tools.heart-rate-zones.medical_warning_title\")}</h3>\n            <p className=\"text-gray-700 dark:text-gray-300 text-sm\">{t(\"tools.heart-rate-zones.medical_warning_content\")}</p>\n          </div>\n        </div>\n      </section>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/ui/components/ScrollToTopButton.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { useScrollToTop } from \"@/shared/hooks/useScrollToTop\";\n\ninterface ScrollToTopButtonProps {\n  text: string;\n}\n\nexport function ScrollToTopButton({ text }: ScrollToTopButtonProps) {\n  const scrollToTop = useScrollToTop();\n\n  return (\n    <button\n      className=\"bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white text-xl font-bold py-6 px-12 rounded-full transform transition-all hover:scale-105 active:scale-95 shadow-xl\"\n      onClick={scrollToTop}\n    >\n      {text}\n    </button>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/(app)/tools/heart-rate-zones/ui/styles.css",
    "content": "/* Simple animations for heart rate calculator */\n\n@keyframes heartbeat {\n  0% { transform: scale(1); }\n  50% { transform: scale(1.1); }\n  100% { transform: scale(1); }\n}\n\n@keyframes fadeInUp {\n  from {\n    opacity: 0;\n    transform: translateY(20px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slideIn {\n  from {\n    opacity: 0;\n    transform: translateX(-20px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n/* Custom slider styles */\ninput[type=\"range\"] {\n  -webkit-appearance: none;\n  appearance: none;\n  background: transparent;\n  cursor: pointer;\n}\n\ninput[type=\"range\"]::-webkit-slider-track {\n  background: #E5E7EB;\n  height: 16px;\n  border-radius: 9999px;\n}\n\ninput[type=\"range\"]::-moz-range-track {\n  background: #E5E7EB;\n  height: 16px;\n  border-radius: 9999px;\n}\n\ninput[type=\"range\"]::-webkit-slider-thumb {\n  -webkit-appearance: none;\n  appearance: none;\n  background: #3B82F6;\n  height: 32px;\n  width: 32px;\n  border-radius: 50%;\n  margin-top: -8px;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n  transition: all 0.2s;\n}\n\ninput[type=\"range\"]::-moz-range-thumb {\n  border: none;\n  background: #3B82F6;\n  height: 32px;\n  width: 32px;\n  border-radius: 50%;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n  transition: all 0.2s;\n}\n\ninput[type=\"range\"]:hover::-webkit-slider-thumb {\n  transform: scale(1.1);\n  background: #2563EB;\n}\n\ninput[type=\"range\"]:hover::-moz-range-thumb {\n  transform: scale(1.1);\n  background: #2563EB;\n}\n\ninput[type=\"range\"]:active::-webkit-slider-thumb {\n  transform: scale(0.95);\n}\n\ninput[type=\"range\"]:active::-moz-range-thumb {\n  transform: scale(0.95);\n}\n\n/* Animation classes */\n.animate-heartbeat {\n  animation: heartbeat 1.5s ease-in-out infinite;\n}\n\n.animate-fade-in-up {\n  animation: fadeInUp 0.6s ease-out;\n}\n\n.animate-slide-in {\n  animation: slideIn 0.4s ease-out;\n}\n\n/* Button press effect */\n.button-press {\n  transition: all 0.1s ease;\n}\n\n.button-press:active {\n  transform: scale(0.95);\n}\n\n/* Card hover effect */\n.card-hover {\n  transition: all 0.3s ease;\n}\n\n.card-hover:hover {\n  transform: translateY(-4px);\n  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);\n}"
  },
  {
    "path": "app/[locale]/(app)/tools/page.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { Metadata } from \"next\";\nimport { CalculatorIcon, ScaleIcon, HeartIcon, DumbbellIcon, RepeatIcon } from \"lucide-react\";\n\nimport { getI18n } from \"locales/server\";\nimport { env } from \"@/env\";\nimport { HorizontalBottomBanner, HorizontalTopBanner } from \"@/components/ads\";\n\ninterface FitnessTool {\n  id: string;\n  icon: React.ReactNode;\n  emoji: string;\n  gradientFrom: string;\n  gradientTo: string;\n  href: string;\n  coming_soon?: boolean;\n}\n\nconst fitnessTools: FitnessTool[] = [\n  {\n    id: \"calorie-calculator\",\n    icon: <CalculatorIcon className=\"w-8 h-8\" />,\n    emoji: \"WorkoutCoolChief.png\",\n    gradientFrom: \"from-[#4F8EF7]\",\n    gradientTo: \"to-[#238BE6]\",\n    href: \"/tools/calorie-calculator\",\n  },\n  {\n    id: \"bmi-calculator\",\n    icon: <HeartIcon className=\"w-8 h-8\" />,\n    emoji: \"WorkoutCoolLove.png\",\n    gradientFrom: \"from-[#FF5722]\",\n    gradientTo: \"to-[#EF4444]\",\n    href: \"/tools/bmi-calculator\",\n  },\n  {\n    id: \"macro-calculator\",\n    icon: <ScaleIcon className=\"w-8 h-8\" />,\n    emoji: \"WorkoutCoolBiceps.png\",\n    gradientFrom: \"from-[#25CB78]\",\n    gradientTo: \"to-[#22C55E]\",\n    href: \"/tools/macro-calculator\",\n    coming_soon: true,\n  },\n  {\n    id: \"heart-rate-calculator\",\n    icon: <HeartIcon className=\"w-8 h-8\" />,\n    emoji: \"WorkoutCoolMedical.png\",\n    gradientFrom: \"from-[#8B5CF6]\",\n    gradientTo: \"to-[#7C3AED]\",\n    href: \"/tools/heart-rate-zones\",\n  },\n  {\n    id: \"one-rep-max\",\n    icon: <DumbbellIcon className=\"w-8 h-8\" />,\n    emoji: \"WorkoutCoolRich.png\",\n    gradientFrom: \"from-[#F59E0B]\",\n    gradientTo: \"to-[#EF4444]\",\n    href: \"/tools/one-rep-max\",\n    coming_soon: true,\n  },\n];\n\nexport async function generateMetadata(): Promise<Metadata> {\n  const t = await getI18n();\n\n  return {\n    title: t(\"tools.meta.title\"),\n    description: t(\"tools.meta.description\"),\n    keywords: t(\"tools.meta.keywords\"),\n  };\n}\n\nexport default async function ToolsPage() {\n  const t = await getI18n();\n\n  return (\n    <div className=\"light:bg-white dark:bg-base-200\">\n      {env.NEXT_PUBLIC_TOP_TOOLS_BANNER_AD_SLOT && <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_TOOLS_BANNER_AD_SLOT} />}\n      <div className=\"container mx-auto px-4 py-8 sm:py-12\">\n        <div className=\"mb-8 sm:mb-12 text-center\">\n          <h1 className=\"text-4xl sm:text-5xl font-bold mb-4 bg-gradient-to-r from-[#4F8EF7] to-[#25CB78] bg-clip-text text-transparent\">\n            {t(\"tools.title\")}\n          </h1>\n          <p className=\"text-lg sm:text-xl text-base-content/70 max-w-2xl mx-auto\">{t(\"tools.subtitle\")}</p>\n        </div>\n\n        <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-6\">\n          {fitnessTools.map((tool) => {\n            const sharedClassName = `group relative overflow-hidden rounded-2xl border backdrop-blur-sm transition-all duration-300 ${\n              tool.coming_soon\n                ? \"border-base-content/20 bg-base-200/30 cursor-not-allowed opacity-60 dark:border-base-content/10 dark:bg-base-200/20\"\n                : \"border-base-content bg-base-200/50 hover:scale-[1.02] hover:border-primary/50 dark:border-base-content/10 dark:bg-base-200/50\"\n            }`;\n\n            if (tool.coming_soon) {\n              return (\n                <div className={sharedClassName} key={tool.id}>\n                  <div\n                    className={`absolute inset-0 bg-gradient-to-br ${tool.gradientFrom} ${tool.gradientTo} ${\n                      tool.coming_soon ? \"opacity-5\" : \"opacity-0 transition-opacity duration-300 group-hover:opacity-10\"\n                    }`}\n                  />\n\n                  <div className=\"relative p-6 sm:p-8\">\n                    <div className=\"flex items-start justify-between mb-4\">\n                      <div\n                        className={`p-3 rounded-xl bg-gradient-to-br ${tool.gradientFrom} ${tool.gradientTo} text-white ${\n                          tool.coming_soon ? \"opacity-50\" : \"\"\n                        }`}\n                      >\n                        {tool.icon}\n                      </div>\n                      <div\n                        className={`relative w-12 h-12 transition-opacity ${\n                          tool.coming_soon ? \"opacity-30\" : \"opacity-80 group-hover:opacity-100\"\n                        }`}\n                      >\n                        <Image\n                          alt=\"Workout Cool Emoji\"\n                          className=\"object-contain\"\n                          height={48}\n                          src={`/images/emojis/${tool.emoji}`}\n                          width={48}\n                        />\n                      </div>\n                    </div>\n\n                    <h3 className={`text-xl sm:text-2xl font-bold mb-2 ${tool.coming_soon ? \"text-base-content/50\" : \"text-base-content\"}`}>\n                      {t(`tools.${tool.id}.title` as keyof typeof t)}\n                    </h3>\n                    <p className={`text-sm sm:text-base ${tool.coming_soon ? \"text-base-content/40\" : \"text-base-content/70\"}`}>\n                      {t(`tools.${tool.id}.description` as keyof typeof t)}\n                    </p>\n\n                    {tool.coming_soon && (\n                      <div className=\"mt-4 inline-flex items-center px-3 py-1 rounded-full bg-base-content/10 text-xs font-medium text-base-content/60\">\n                        {t(\"commons.coming_soon\")}\n                      </div>\n                    )}\n\n                    {!tool.coming_soon && (\n                      <div className=\"mt-4 flex items-center gap-2 text-primary\">\n                        <span className=\"text-sm font-medium\">{t(\"tools.try_now\")}</span>\n                        <svg\n                          className=\"w-4 h-4 transition-transform duration-300 group-hover:translate-x-1\"\n                          fill=\"none\"\n                          stroke=\"currentColor\"\n                          viewBox=\"0 0 24 24\"\n                        >\n                          <path d=\"M9 5l7 7-7 7\" strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} />\n                        </svg>\n                      </div>\n                    )}\n                  </div>\n                </div>\n              );\n            }\n\n            return (\n              <Link className={sharedClassName} href={tool.href} key={tool.id}>\n                <div\n                  className={`absolute inset-0 bg-gradient-to-br ${tool.gradientFrom} ${tool.gradientTo} ${\n                    tool.coming_soon ? \"opacity-5\" : \"opacity-0 transition-opacity duration-300 group-hover:opacity-10\"\n                  }`}\n                />\n\n                <div className=\"relative p-6 sm:p-8\">\n                  <div className=\"flex items-start justify-between mb-4\">\n                    <div\n                      className={`p-3 rounded-xl bg-gradient-to-br ${tool.gradientFrom} ${tool.gradientTo} text-white ${\n                        tool.coming_soon ? \"opacity-50\" : \"\"\n                      }`}\n                    >\n                      {tool.icon}\n                    </div>\n                    <div\n                      className={`relative w-12 h-12 transition-opacity ${\n                        tool.coming_soon ? \"opacity-30\" : \"opacity-80 group-hover:opacity-100\"\n                      }`}\n                    >\n                      <Image\n                        alt=\"Workout Cool Emoji\"\n                        className=\"object-contain\"\n                        height={48}\n                        src={`/images/emojis/${tool.emoji}`}\n                        width={48}\n                      />\n                    </div>\n                  </div>\n\n                  <h3 className={`text-xl sm:text-2xl font-bold mb-2 ${tool.coming_soon ? \"text-base-content/50\" : \"text-base-content\"}`}>\n                    {t(`tools.${tool.id}.title` as keyof typeof t)}\n                  </h3>\n                  <p className={`text-sm sm:text-base ${tool.coming_soon ? \"text-base-content/40\" : \"text-base-content/70\"}`}>\n                    {t(`tools.${tool.id}.description` as keyof typeof t)}\n                  </p>\n\n                  {tool.coming_soon && (\n                    <div className=\"mt-4 inline-flex items-center px-3 py-1 rounded-full bg-base-content/10 text-xs font-medium text-base-content/60\">\n                      {t(\"commons.coming_soon\")}\n                    </div>\n                  )}\n\n                  {!tool.coming_soon && (\n                    <div className=\"mt-4 flex items-center gap-2 text-primary\">\n                      <span className=\"text-sm font-medium\">{t(\"tools.try_now\")}</span>\n                      <svg\n                        className=\"w-4 h-4 transition-transform duration-300 group-hover:translate-x-1\"\n                        fill=\"none\"\n                        stroke=\"currentColor\"\n                        viewBox=\"0 0 24 24\"\n                      >\n                        <path d=\"M9 5l7 7-7 7\" strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} />\n                      </svg>\n                    </div>\n                  )}\n                </div>\n              </Link>\n            );\n          })}\n        </div>\n\n        {env.NEXT_PUBLIC_BOTTOM_TOOLS_BANNER_AD_SLOT && (\n          <div className=\"mt-12\">\n            <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_BOTTOM_TOOLS_BANNER_AD_SLOT} />\n          </div>\n        )}\n        <div className=\"mt-6 text-center\">\n          <div className=\"inline-flex items-center gap-2 px-4 py-2 rounded-full bg-base-200/50 backdrop-blur-sm border border-base-content/10\">\n            <RepeatIcon className=\"w-5 h-5 text-primary\" />\n            <span className=\"text-sm text-base-content/70\">{t(\"tools.moreComingSoon\")}</span>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/@modal/(.)auth/login/page.tsx",
    "content": "import { CredentialsLoginForm } from \"@/features/auth/signin/ui/CredentialsLoginForm\";\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\n\nexport default function LoginModal() {\n  return (\n    <Dialog defaultOpen>\n      <DialogContent className=\"sm:max-w-md\">\n        <CredentialsLoginForm />\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/layout.tsx",
    "content": "import { Inter, Permanent_Marker } from \"next/font/google\";\nimport { GeistSans } from \"geist/font/sans\";\nimport { GeistMono } from \"geist/font/mono\";\n\nimport { Providers } from \"app/[locale]/providers\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { generateStructuredData, StructuredDataScript } from \"@/shared/lib/structured-data\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { SiteConfig } from \"@/shared/config/site-config\";\nimport { getLocalizedMetadata } from \"@/shared/config/localized-metadata\";\nimport { WorkoutSessionsSynchronizer } from \"@/features/workout-session/ui/workout-sessions-synchronizer\";\nimport { FavoriteExercisesSynchronizer } from \"@/features/workout-builder/model/favorite-exercises-synchronizer\";\nimport { ThemeSynchronizer } from \"@/features/theme/ui/ThemeSynchronizer\";\nimport { env } from \"@/env\";\nimport { Version } from \"@/components/version\";\nimport { TailwindIndicator } from \"@/components/utils/TailwindIndicator\";\nimport { NextTopLoader } from \"@/components/ui/next-top-loader\";\nimport { ServiceWorkerRegistration } from \"@/components/pwa/ServiceWorkerRegistration\";\nimport { VerticalLeftBanner, VerticalRightBanner, AdBlockerForPremium } from \"@/components/ads\";\n\nimport type { ReactElement } from \"react\";\nimport type { Metadata } from \"next\";\n\nimport \"@/shared/styles/globals.css\";\n\nexport async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {\n  const { locale } = await params;\n  const localizedData = getLocalizedMetadata(locale);\n\n  return {\n    title: {\n      default: localizedData.title,\n      template: `%s | ${localizedData.title}`,\n    },\n    description: localizedData.description,\n    keywords: localizedData.keywords as unknown as string[],\n    applicationName: localizedData.applicationName,\n    category: localizedData.category,\n    classification: localizedData.classification,\n    metadataBase: new URL(getServerUrl()),\n    manifest: \"/manifest.json\",\n    robots: {\n      index: true,\n      follow: true,\n      googleBot: {\n        index: true,\n        follow: true,\n        \"max-snippet\": -1,\n        \"max-image-preview\": \"large\",\n        \"max-video-preview\": -1,\n      },\n    },\n    verification: {\n      google: process.env.GOOGLE_SITE_VERIFICATION,\n    },\n    openGraph: {\n      title: localizedData.title,\n      description: localizedData.description,\n      url: getServerUrl(),\n      siteName: SiteConfig.title,\n      locale:\n        locale === \"en\"\n          ? \"en_US\"\n          : locale === \"es\"\n            ? \"es_ES\"\n            : locale === \"pt\"\n              ? \"pt_PT\"\n              : locale === \"ru\"\n                ? \"ru_RU\"\n                : locale === \"zh-CN\"\n                  ? \"zh_CN\"\n                  : \"fr_FR\",\n      alternateLocale: [\n        \"fr_FR\",\n        \"fr_CA\",\n        \"fr_CH\",\n        \"fr_BE\",\n        \"en_US\",\n        \"en_GB\",\n        \"en_CA\",\n        \"en_AU\",\n        \"es_ES\",\n        \"es_MX\",\n        \"es_AR\",\n        \"es_CL\",\n        \"pt_PT\",\n        \"pt_BR\",\n        \"ru_RU\",\n        \"ru_BY\",\n        \"ru_KZ\",\n        \"zh_CN\",\n        \"zh_TW\",\n        \"zh_HK\",\n      ].filter(\n        (alt) =>\n          alt !==\n          (locale === \"en\"\n            ? \"en_US\"\n            : locale === \"es\"\n              ? \"es_ES\"\n              : locale === \"pt\"\n                ? \"pt_PT\"\n                : locale === \"ru\"\n                  ? \"ru_RU\"\n                  : locale === \"zh-CN\"\n                    ? \"zh_CN\"\n                    : \"fr_FR\"),\n      ),\n      images: [\n        {\n          url: `${getServerUrl()}/images/default-og-image_fr.jpg`,\n          width: SiteConfig.seo.ogImage.width,\n          height: SiteConfig.seo.ogImage.height,\n          alt: `${SiteConfig.title} - Plateforme de fitness moderne`,\n        },\n        {\n          url: `${getServerUrl()}/images/default-og-image_en.jpg`,\n          width: SiteConfig.seo.ogImage.width,\n          height: SiteConfig.seo.ogImage.height,\n          alt: `${SiteConfig.title} - Modern fitness platform`,\n        },\n        {\n          url: `${getServerUrl()}/images/default-og-image_es.jpg`,\n          width: SiteConfig.seo.ogImage.width,\n          height: SiteConfig.seo.ogImage.height,\n          alt: `${SiteConfig.title} - Plataforma de fitness moderna`,\n        },\n        {\n          url: `${getServerUrl()}/images/default-og-image_pt.jpg`,\n          width: SiteConfig.seo.ogImage.width,\n          height: SiteConfig.seo.ogImage.height,\n          alt: `${SiteConfig.title} - Plataforma de fitness moderna`,\n        },\n        {\n          url: `${getServerUrl()}/images/default-og-image_ru.jpg`,\n          width: SiteConfig.seo.ogImage.width,\n          height: SiteConfig.seo.ogImage.height,\n          alt: `${SiteConfig.title} - Современная фитнес платформа`,\n        },\n        {\n          url: `${getServerUrl()}/images/default-og-image_zh.jpg`,\n          width: SiteConfig.seo.ogImage.width,\n          height: SiteConfig.seo.ogImage.height,\n          alt: `${SiteConfig.title} - 现代健身平台`,\n        },\n      ],\n      type: \"website\",\n    },\n    twitter: {\n      card: \"summary_large_image\",\n      site: SiteConfig.seo.twitterHandle,\n      creator: SiteConfig.seo.twitterHandle,\n      title: localizedData.title,\n      description: localizedData.description,\n      images: [\n        {\n          url: `${getServerUrl()}/images/default-og-image_${locale === \"zh-CN\" ? \"zh\" : locale}.jpg`,\n          width: SiteConfig.seo.ogImage.width,\n          height: SiteConfig.seo.ogImage.height,\n          alt: localizedData.ogAlt,\n        },\n      ],\n    },\n    alternates: {\n      canonical: \"https://www.workout.cool\",\n      languages: {\n        \"fr-FR\": \"https://www.workout.cool/fr\",\n        \"en-US\": \"https://www.workout.cool/en\",\n        \"es-ES\": \"https://www.workout.cool/es\",\n        \"pt-PT\": \"https://www.workout.cool/pt\",\n        \"ru-RU\": \"https://www.workout.cool/ru\",\n        \"zh-CN\": \"https://www.workout.cool/zh-CN\",\n        \"x-default\": \"https://www.workout.cool\",\n      },\n    },\n    authors: [{ name: SiteConfig.company.name, url: getServerUrl() }],\n    creator: SiteConfig.company.name,\n    publisher: SiteConfig.company.name,\n    formatDetection: {\n      email: false,\n      address: false,\n      telephone: false,\n    },\n    appleWebApp: {\n      capable: true,\n      statusBarStyle: \"default\",\n      title: SiteConfig.title,\n    },\n    icons: {\n      icon: [\n        { url: \"/images/favicon-32x32.png\", sizes: \"32x32\", type: \"image/png\" },\n        { url: \"/images/favicon-16x16.png\", sizes: \"16x16\", type: \"image/png\" },\n        { url: \"/images/favicon.ico\", type: \"image/x-icon\" },\n      ],\n      apple: [{ url: \"/apple-touch-icon.png\", sizes: \"180x180\", type: \"image/png\" }],\n      shortcut: \"/images/favicon.ico\",\n    },\n    other: {\n      \"msapplication-TileColor\": \"#FF5722\",\n      \"msapplication-TileImage\": \"/android-chrome-192x192.png\",\n    },\n  };\n}\n\nconst inter = Inter({\n  subsets: [\"latin\"],\n  variable: \"--font-inter\",\n  display: \"swap\",\n});\n\nconst permanentMarker = Permanent_Marker({\n  weight: \"400\",\n  subsets: [\"latin\"],\n  variable: \"--font-permanent-marker\",\n  display: \"swap\",\n});\n\nexport const preferredRegion = [\"fra1\", \"sfo1\", \"iad1\"];\n\ninterface RootLayoutProps {\n  params: Promise<{ locale: string }>;\n  children: ReactElement;\n}\n\nexport default async function RootLayout({ params, children }: RootLayoutProps) {\n  const { locale } = await params;\n  // Generate structured data\n  const websiteStructuredData = generateStructuredData({\n    type: \"WebSite\",\n    locale,\n  });\n\n  const organizationStructuredData = generateStructuredData({\n    type: \"Organization\",\n    locale,\n  });\n\n  const webAppStructuredData = generateStructuredData({\n    type: \"WebApplication\",\n    locale,\n  });\n\n  return (\n    <>\n      <html className=\"h-full\" dir=\"ltr\" lang={locale} suppressHydrationWarning>\n        <head>\n          <meta charSet=\"UTF-8\" />\n          <meta content=\"width=device-width, initial-scale=1, maximum-scale=1 viewport-fit=cover\" name=\"viewport\" />\n          <meta content={env.NEXT_PUBLIC_AD_CLIENT} name=\"google-adsense-account\" />\n\n          <script\n            async\n            crossOrigin=\"anonymous\"\n            src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${env.NEXT_PUBLIC_AD_CLIENT}`}\n          />\n\n          {/* Ezoic Privacy Scripts */}\n          {/* eslint-disable-next-line @next/next/no-sync-scripts */}\n          <script data-cfasync=\"false\" src=\"https://cmp.gatekeeperconsent.com/min.js\" />\n          {/* eslint-disable-next-line @next/next/no-sync-scripts */}\n          <script data-cfasync=\"false\" src=\"https://the.gatekeeperconsent.com/cmp.min.js\" />\n\n          {/* Ezoic Header Script */}\n          <script async src=\"//www.ezojs.com/ezoic/sa.min.js\" />\n          <script\n            dangerouslySetInnerHTML={{\n              __html: `\n                window.ezstandalone = window.ezstandalone || {};\n                ezstandalone.cmd = ezstandalone.cmd || [];\n              `,\n            }}\n          />\n\n          {/* PWA Meta Tags */}\n          <meta content=\"yes\" name=\"apple-mobile-web-app-capable\" />\n          <meta content=\"default\" name=\"apple-mobile-web-app-status-bar-style\" />\n          <meta content=\"Workout Cool\" name=\"apple-mobile-web-app-title\" />\n          <meta content=\"yes\" name=\"mobile-web-app-capable\" />\n          <meta content=\"#FF5722\" name=\"msapplication-TileColor\" />\n          <meta content=\"/android-chrome-192x192.png\" name=\"msapplication-TileImage\" />\n\n          {/* PWA Manifest */}\n          <link href={`/${locale}/manifest.json`} rel=\"manifest\" />\n\n          {/* eslint-disable-next-line @next/next/no-page-custom-font */}\n          <link as=\"style\" href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap\" rel=\"preload\" />\n\n          {/* Alternate hreflang for i18n */}\n          <link href=\"https://www.workout.cool/fr\" hrefLang=\"fr\" rel=\"alternate\" />\n          <link href=\"https://www.workout.cool/en\" hrefLang=\"en\" rel=\"alternate\" />\n          <link href=\"https://www.workout.cool/es\" hrefLang=\"es\" rel=\"alternate\" />\n          <link href=\"https://www.workout.cool/pt\" hrefLang=\"pt\" rel=\"alternate\" />\n          <link href=\"https://www.workout.cool/ru\" hrefLang=\"ru\" rel=\"alternate\" />\n          <link href=\"https://www.workout.cool/zh-CN\" hrefLang=\"zh-CN\" rel=\"alternate\" />\n          <link href=\"https://www.workout.cool\" hrefLang=\"x-default\" rel=\"alternate\" />\n\n          {/* Theme color for PWA */}\n          <meta content=\"#FF5722\" name=\"theme-color\" />\n\n          {/* Impact site verification */}\n          {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}\n          {/* @ts-ignore */}\n          <meta name=\"impact-site-verification\" value=\"e6afc3fc-0dcd-4625-a8cd-282991d40164\" />\n\n          {/* Google Analytics 4 */}\n          {env.NEXT_PUBLIC_GA4_MEASUREMENT_ID && (\n            <>\n              <script async src={`https://www.googletagmanager.com/gtag/js?id=${env.NEXT_PUBLIC_GA4_MEASUREMENT_ID}`} />\n              <script\n                dangerouslySetInnerHTML={{\n                  __html: `\n                    window.dataLayer = window.dataLayer || [];\n                    function gtag(){dataLayer.push(arguments);}\n                    gtag('js', new Date());\n                    gtag('config', '${env.NEXT_PUBLIC_GA4_MEASUREMENT_ID}');\n                  `,\n                }}\n              />\n            </>\n          )}\n\n          {/* Structured Data */}\n          <StructuredDataScript data={websiteStructuredData} />\n          <StructuredDataScript data={organizationStructuredData} />\n          <StructuredDataScript data={webAppStructuredData} />\n        </head>\n\n        <body\n          className={cn(\n            \"flex items-center justify-center min-h-screen w-full max-sm:p-0 max-sm:min-h-full bg-base-200 dark:bg-[#18181b] dark:text-gray-200 antialiased\",\n            \"bg-hero-light dark:bg-hero-dark\",\n            GeistMono.variable,\n            GeistSans.variable,\n            inter.variable,\n            permanentMarker.variable,\n          )}\n          suppressHydrationWarning\n        >\n          <Providers locale={locale}>\n            <ServiceWorkerRegistration />\n            <FavoriteExercisesSynchronizer />\n            <WorkoutSessionsSynchronizer />\n            <ThemeSynchronizer />\n            {/* <AdSenseAutoAds /> */}\n            <AdBlockerForPremium />\n            <NextTopLoader color=\"#FF5722\" delay={100} showSpinner={false} />\n\n            <div className=\"flex flex-col w-full\">\n              <div className=\"flex justify-center items-start gap-4 w-full\">\n                <VerticalLeftBanner />\n                {children}\n                <VerticalRightBanner />\n              </div>\n            </div>\n            <Version />\n\n            <TailwindIndicator />\n          </Providers>\n        </body>\n      </html>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/[locale]/manifest.json/route.ts",
    "content": "import { NextRequest } from \"next/server\";\n\nimport { getLocalizedMetadata } from \"@/shared/config/localized-metadata\";\n\nexport async function GET(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = await params;\n  const localizedData = getLocalizedMetadata(locale);\n\n  const manifest = {\n    background_color: \"#f3f4f6\",\n    categories: [\"health\", \"fitness\", \"sports\"],\n    description: localizedData.description,\n    display: \"standalone\",\n    icons: [\n      {\n        src: \"/images/favicon-16x16.png\",\n        sizes: \"16x16\",\n        type: \"image/png\",\n      },\n      {\n        src: \"/images/favicon-32x32.png\",\n        sizes: \"32x32\",\n        type: \"image/png\",\n      },\n      {\n        src: \"/apple-touch-icon.png\",\n        sizes: \"180x180\",\n        type: \"image/png\",\n        purpose: \"any maskable\",\n      },\n      {\n        src: \"/android-chrome-192x192.png\",\n        sizes: \"192x192\",\n        type: \"image/png\",\n        purpose: \"any maskable\",\n      },\n      {\n        src: \"/android-chrome-512x512.png\",\n        sizes: \"512x512\",\n        type: \"image/png\",\n        purpose: \"any maskable\",\n      },\n    ],\n    lang: locale,\n    name: localizedData.applicationName,\n    orientation: \"portrait\",\n    scope: `/${locale}/`,\n    short_name: localizedData.applicationName,\n    start_url: `/${locale}/`,\n    theme_color: \"#FF5722\",\n  };\n\n  return new Response(JSON.stringify(manifest, null, 2), {\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n  });\n}\n"
  },
  {
    "path": "app/[locale]/not-found.tsx",
    "content": "import { Page404 } from \"@/widgets/404\";\n\nexport default function NotFoundPage() {\n  return <Page404 />;\n}\n"
  },
  {
    "path": "app/[locale]/providers.tsx",
    "content": "\"use client\";\n\nimport { NuqsAdapter } from \"nuqs/adapters/next/app\";\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\n\nimport { I18nProviderClient } from \"locales/client\";\nimport { AnalyticsProvider } from \"@/shared/lib/analytics/client\";\nimport { DialogRenderer } from \"@/features/dialogs-provider/DialogProvider\";\nimport { useAutoLocale } from \"@/entities/user/model/use-auto-locale\";\nimport { ToastSonner } from \"@/components/ui/ToastSonner\";\nimport { Toaster } from \"@/components/ui/toaster\";\nimport { ThemeProvider } from \"@/components/ui/theme-provider\";\n\nimport type { PropsWithChildren } from \"react\";\n\nconst queryClient = new QueryClient();\n\nfunction LocaleDetector() {\n  useAutoLocale();\n  return null;\n}\n\nexport const Providers = ({ children, locale }: PropsWithChildren<{ locale: string }>) => {\n  return (\n    <>\n      <AnalyticsProvider />\n      <NuqsAdapter>\n        <QueryClientProvider client={queryClient}>\n          <I18nProviderClient locale={locale}>\n            <ThemeProvider attribute=\"class\" defaultTheme=\"system\" disableTransitionOnChange enableSystem>\n              <LocaleDetector />\n              <Toaster />\n              <ToastSonner />\n              <DialogRenderer />\n              <ReactQueryDevtools initialIsOpen={false} />\n              {children}\n            </ThemeProvider>\n          </I18nProviderClient>\n        </QueryClientProvider>\n      </NuqsAdapter>\n    </>\n  );\n};\n"
  },
  {
    "path": "app/ads.txt/route.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nexport async function GET() {\n  // Redirect to Ezoic's ads.txt manager\n  // Replace 19390 with your actual Ezoic Account ID if different\n  const ezoicAdsUrl = \"https://srv.adstxtmanager.com/19390/workout.cool\";\n\n  try {\n    // Fetch the ads.txt content from Ezoic\n    const response = await fetch(ezoicAdsUrl);\n\n    if (response.ok) {\n      const adsContent = await response.text();\n\n      // Return the content with proper headers\n      return new NextResponse(adsContent, {\n        headers: {\n          \"Content-Type\": \"text/plain\",\n          \"Cache-Control\": \"public, max-age=3600\", // Cache for 1 hour\n        },\n      });\n    }\n\n    // If Ezoic's endpoint fails, fallback to existing Google AdSense entry\n    const fallbackContent = \"google.com, pub-3437447245301146, DIRECT, f08c47fec0942fa0\";\n\n    return new NextResponse(fallbackContent, {\n      headers: {\n        \"Content-Type\": \"text/plain\",\n        \"Cache-Control\": \"public, max-age=3600\",\n      },\n    });\n  } catch (error) {\n    // On error, return the existing Google AdSense entry\n    const fallbackContent = \"google.com, pub-3437447245301146, DIRECT, f08c47fec0942fa0\";\n\n    return new NextResponse(fallbackContent, {\n      headers: {\n        \"Content-Type\": \"text/plain\",\n        \"Cache-Control\": \"public, max-age=3600\",\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "app/api/analytics/premium/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { LogEvents } from \"@/shared/lib/analytics/events\";\n\n/**\n * POST /api/analytics/premium\n *\n * Track premium-related analytics events from mobile app\n * Mobile-compatible endpoint for tracking user interactions with premium features\n */\nexport async function POST(request: NextRequest) {\n  try {\n    const { event } = await request.json();\n\n    // Validate event type\n    const validEvents = [\"discovery\", \"paywall_viewed\", \"paywall_purchased\", \"paywall_cancelled\", \"paywall_restored\"];\n\n    if (!validEvents.includes(event)) {\n      return NextResponse.json({ error: \"Invalid event type\" }, { status: 400 });\n    }\n\n    // Map event types to analytics events\n    let analyticsEvent;\n    switch (event) {\n      case \"discovery\":\n        analyticsEvent = LogEvents.PremiumDiscovery;\n        break;\n      case \"paywall_viewed\":\n        analyticsEvent = LogEvents.PaywallViewed;\n        break;\n      case \"paywall_purchased\":\n        analyticsEvent = LogEvents.PaywallPurchased;\n        break;\n      case \"paywall_cancelled\":\n        analyticsEvent = LogEvents.PaywallCancelled;\n        break;\n      case \"paywall_restored\":\n        analyticsEvent = LogEvents.PaywallRestored;\n        break;\n      default:\n        return NextResponse.json({ error: \"Unknown event type\" }, { status: 400 });\n    }\n\n    // todo: add analytics\n    // Track event with user context if available\n    // if (user) {\n    //   await serverAnalytics.event({\n    //     eventName: analyticsEvent.name,\n    //     channel: analyticsEvent.channel,\n    //     user: {\n    //       id: user.id,\n    //       email: user.email || undefined,\n    //     },\n    //     metadata: {\n    //       ...metadata,\n    //       source: \"mobile\",\n    //       timestamp: new Date().toISOString(),\n    //     },\n    //   });\n    // } else {\n    //   // Track anonymous event\n    //   await serverAnalytics.event({\n    //     eventName: analyticsEvent.name,\n    //     channel: analyticsEvent.channel,\n    //     metadata: {\n    //       ...metadata,\n    //       source: \"mobile\",\n    //       anonymous: true,\n    //       timestamp: new Date().toISOString(),\n    //     },\n    //   });\n    // }\n\n    return NextResponse.json({ event, analyticsEvent, success: true });\n  } catch (error) {\n    console.error(\"Error tracking premium analytics:\", error);\n    return NextResponse.json({ error: \"Failed to track event\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/auth/[...all]/route.ts",
    "content": "import { toNextJsHandler } from \"better-auth/next-js\";\n\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nexport const { POST, GET } = toNextJsHandler(auth);\n"
  },
  {
    "path": "app/api/auth/signup/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { UserRole } from \"@prisma/client\";\n\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nconst signUpSchema = z.object({\n  email: z.string().email(),\n  password: z.string().min(8),\n  firstName: z.string().min(1),\n  lastName: z.string().min(1),\n  name: z.string().optional(),\n});\n\nexport async function POST(req: NextRequest) {\n  try {\n    const body = await req.json();\n    \n    const parsed = signUpSchema.safeParse(body);\n    if (!parsed.success) {\n      return NextResponse.json(\n        { error: \"INVALID_INPUT\", details: parsed.error.format() },\n        { status: 400 }\n      );\n    }\n\n    const { email, password, firstName, lastName } = parsed.data;\n    const name = parsed.data.name || `${firstName} ${lastName}`;\n\n    // Utiliser l'API serveur de Better Auth pour créer l'utilisateur\n    const result = await auth.api.signUpEmail({\n      body: {\n        email,\n        password,\n        name,\n        firstName,\n        lastName,\n        role: UserRole.user,\n      },\n    });\n\n    // Retourner l'utilisateur et le token\n    return NextResponse.json({\n      user: result.user,\n      token: result.token,\n    });\n  } catch (error: any) {\n    console.error(\"Signup error:\", error);\n    \n    // Gérer les erreurs spécifiques\n    if (error.message?.includes(\"already exists\")) {\n      return NextResponse.json(\n        { error: \"EMAIL_ALREADY_EXISTS\" },\n        { status: 409 }\n      );\n    }\n\n    return NextResponse.json(\n      { error: \"INTERNAL_SERVER_ERROR\" },\n      { status: 500 }\n    );\n  }\n}"
  },
  {
    "path": "app/api/billing/status/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { PremiumService } from \"@/shared/lib/premium/premium.service\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nexport async function GET(request: NextRequest) {\n  try {\n    const session = await auth.api.getSession({\n      headers: request.headers,\n    });\n\n    if (!session) {\n      return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n    }\n\n    const userId = session.user?.id;\n\n    if (!userId) {\n      return NextResponse.json({ error: \"User not found\" }, { status: 404 });\n    }\n\n    // Use new premium system\n    const premiumStatus = await PremiumService.checkUserPremiumStatus(userId);\n\n    // Get active subscription if present\n    const subscription = await prisma.subscription.findFirst({\n      where: {\n        userId,\n        status: \"ACTIVE\",\n      },\n      include: {\n        plan: true,\n      },\n    });\n\n    // Get license if present (for self-hosted)\n    const license = await prisma.license.findFirst({\n      where: {\n        userId,\n        OR: [{ validUntil: null }, { validUntil: { gte: new Date() } }],\n      },\n    });\n\n    const response = {\n      isPremium: premiumStatus.isPremium,\n      expiresAt: premiumStatus.expiresAt,\n      subscription: subscription || undefined,\n      license: license || undefined,\n      canUpgrade: !premiumStatus.isPremium,\n    };\n\n    return NextResponse.json(response);\n  } catch (error) {\n    console.error(\"Billing status error:\", error);\n    return NextResponse.json({ error: \"Internal server error\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/exercises/[exerciseId]/statistics/one-rep-max/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { OneRepMaxResponse, StatisticsErrorResponse } from \"@/shared/types/statistics.types\";\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { PremiumService } from \"@/shared/lib/premium/premium.service\";\nimport { STATISTICS_TIMEFRAMES, DEFAULT_TIMEFRAME, TIMEFRAME_DAYS, LOMBARDI_DIVISOR } from \"@/shared/constants/statistics\";\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\n\nconst timeframeSchema = z.enum([\n  STATISTICS_TIMEFRAMES.FOUR_WEEKS,\n  STATISTICS_TIMEFRAMES.EIGHT_WEEKS,\n  STATISTICS_TIMEFRAMES.TWELVE_WEEKS,\n  STATISTICS_TIMEFRAMES.ONE_YEAR,\n]);\n\n// Lombardi formula: 1RM = Weight × (1 + (Reps ÷ 30))\nfunction calculateOneRepMax(weight: number, reps: number): number {\n  return weight * (1 + reps / LOMBARDI_DIVISOR);\n}\n\nexport async function GET(request: NextRequest, { params }: { params: Promise<{ exerciseId: string }> }) {\n  try {\n    // Get user session\n    const session = await getMobileCompatibleSession(request);\n    const user = session?.user;\n\n    if (!user) {\n      const errorResponse: StatisticsErrorResponse = {\n        error: \"UNAUTHORIZED\",\n        message: \"Authentication required\",\n      };\n      return NextResponse.json(errorResponse, { status: 401 });\n    }\n\n    // Check premium status\n    const premiumStatus = await PremiumService.checkUserPremiumStatus(user.id);\n\n    if (!premiumStatus.isPremium) {\n      const errorResponse: StatisticsErrorResponse = {\n        error: \"PREMIUM_REQUIRED\",\n        message: \"Exercise statistics is a premium feature\",\n        isPremium: false,\n      };\n      return NextResponse.json(errorResponse, { status: 403 });\n    }\n\n    // Parse timeframe\n    const { searchParams } = new URL(request.url);\n    const timeframeRaw = searchParams.get(\"timeframe\") || DEFAULT_TIMEFRAME;\n\n    const timeframeParsed = timeframeSchema.safeParse(timeframeRaw);\n    if (!timeframeParsed.success) {\n      const errorResponse: StatisticsErrorResponse = {\n        error: \"INVALID_PARAMETERS\",\n        message: \"Invalid timeframe parameter\",\n      };\n      return NextResponse.json(errorResponse, { status: 400 });\n    }\n\n    const timeframe = timeframeParsed.data;\n\n    // Calculate date range (ensuring we're working with UTC dates to avoid timezone issues)\n    const endDate = new Date();\n    const startDate = new Date();\n\n    const daysToSubtract = TIMEFRAME_DAYS[timeframe];\n    if (timeframe === STATISTICS_TIMEFRAMES.ONE_YEAR) {\n      startDate.setFullYear(startDate.getFullYear() - 1);\n    } else {\n      startDate.setDate(startDate.getDate() - daysToSubtract);\n    }\n\n    // Set time to start and end of day in UTC\n    startDate.setUTCHours(0, 0, 0, 0);\n    endDate.setUTCHours(23, 59, 59, 999);\n\n    const { exerciseId } = await params;\n\n    // Fetch sets with weight and reps\n    const workoutSessionExercises = await prisma.workoutSessionExercise.findMany({\n      where: {\n        exerciseId,\n        workoutSession: {\n          userId: user.id,\n          startedAt: {\n            gte: startDate,\n            lte: endDate,\n          },\n        },\n      },\n      include: {\n        workoutSession: {\n          select: {\n            startedAt: true,\n          },\n        },\n        sets: {\n          where: {\n            completed: true,\n            types: {\n              hasEvery: [\"WEIGHT\", \"REPS\"],\n            },\n          },\n          orderBy: {\n            setIndex: \"asc\",\n          },\n        },\n      },\n      orderBy: {\n        workoutSession: {\n          startedAt: \"asc\",\n        },\n      },\n    });\n\n    // Group by session and calculate best 1RM per session\n    const sessionBest1RM = new Map<string, { date: Date; oneRepMax: number }>();\n\n    workoutSessionExercises.forEach((sessionExercise) => {\n      const sessionDate = sessionExercise.workoutSession.startedAt.toISOString().split(\"T\")[0];\n      let bestOneRepMax = 0;\n\n      sessionExercise.sets.forEach((set) => {\n        // Find weight and reps values from arrays\n        const weightIndex = set.types.indexOf(\"WEIGHT\");\n        const repsIndex = set.types.indexOf(\"REPS\");\n\n        if (weightIndex !== -1 && repsIndex !== -1 && set.valuesInt && set.valuesInt[weightIndex] && set.valuesInt[repsIndex]) {\n          const weight = set.valuesInt[weightIndex];\n          const reps = set.valuesInt[repsIndex];\n          const oneRepMax = calculateOneRepMax(weight, reps);\n\n          if (oneRepMax > bestOneRepMax) {\n            bestOneRepMax = oneRepMax;\n          }\n        }\n      });\n\n      if (bestOneRepMax > 0) {\n        const currentBest = sessionBest1RM.get(sessionDate);\n        if (!currentBest || bestOneRepMax > currentBest.oneRepMax) {\n          sessionBest1RM.set(sessionDate, {\n            date: sessionExercise.workoutSession.startedAt,\n            oneRepMax: Math.round(bestOneRepMax * 10) / 10, // Round to 1 decimal\n          });\n        }\n      }\n    });\n\n    // Convert to array format\n    const oneRepMaxProgression = Array.from(sessionBest1RM.values())\n      .sort((a, b) => a.date.getTime() - b.date.getTime())\n      .map(({ date, oneRepMax }) => ({\n        date: date.toISOString(),\n        estimatedOneRepMax: oneRepMax,\n      }));\n\n    const response: OneRepMaxResponse = {\n      exerciseId,\n      timeframe,\n      formula: \"Lombardi\",\n      formulaDescription: \"1RM = Weight × (1 + (Reps ÷ 30))\",\n      data: oneRepMaxProgression,\n      count: oneRepMaxProgression.length,\n    };\n\n    // Add cache headers - 1 hour cache (disabled for debugging)\n    const headers = new Headers();\n    // Temporarily disable cache for debugging\n    headers.set(\"Cache-Control\", \"no-store, no-cache, must-revalidate\");\n    headers.set(\"Pragma\", \"no-cache\");\n    headers.set(\"Expires\", \"0\");\n    // Original: headers.set(\"Cache-Control\", \"private, max-age=3600, stale-while-revalidate=86400\");\n\n    return NextResponse.json(response, { headers });\n  } catch (error) {\n    console.error(\"Error fetching one-rep max progression:\", error);\n    const errorResponse: StatisticsErrorResponse = {\n      error: \"INTERNAL_SERVER_ERROR\",\n      message: \"Failed to fetch one-rep max data\",\n    };\n    return NextResponse.json(errorResponse, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/exercises/[exerciseId]/statistics/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { ExerciseStatisticsResponse, StatisticsErrorResponse } from \"@/shared/types/statistics.types\";\nimport { PremiumService } from \"@/shared/lib/premium/premium.service\";\nimport { STATISTICS_TIMEFRAMES, DEFAULT_TIMEFRAME } from \"@/shared/constants/statistics\";\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\n\nconst timeframeSchema = z.enum([\n  STATISTICS_TIMEFRAMES.FOUR_WEEKS,\n  STATISTICS_TIMEFRAMES.EIGHT_WEEKS,\n  STATISTICS_TIMEFRAMES.TWELVE_WEEKS,\n  STATISTICS_TIMEFRAMES.ONE_YEAR,\n]);\n\nconst statisticsParamsSchema = z.object({\n  exerciseId: z.string(),\n  timeframe: timeframeSchema.optional().default(DEFAULT_TIMEFRAME),\n});\n\nexport async function GET(\n  request: NextRequest,\n  { params }: { params: Promise<{ exerciseId: string }> }\n) {\n  try {\n    // Get user session\n    const session = await getMobileCompatibleSession(request);\n    const user = session?.user;\n\n    if (!user) {\n      const errorResponse: StatisticsErrorResponse = { \n        error: \"UNAUTHORIZED\", \n        message: \"Authentication required\" \n      };\n      return NextResponse.json(errorResponse, { status: 401 });\n    }\n\n    // Check premium status\n    const premiumStatus = await PremiumService.checkUserPremiumStatus(user.id);\n    \n    if (!premiumStatus.isPremium) {\n      const errorResponse: StatisticsErrorResponse = { \n        error: \"PREMIUM_REQUIRED\", \n        message: \"Exercise statistics is a premium feature\",\n        isPremium: false \n      };\n      return NextResponse.json(errorResponse, { status: 403 });\n    }\n\n    const { exerciseId } = await params;\n\n    // Parse and validate parameters\n    const { searchParams } = new URL(request.url);\n    const timeframe = searchParams.get(\"timeframe\") || DEFAULT_TIMEFRAME;\n\n    const parsed = statisticsParamsSchema.safeParse({\n      exerciseId,\n      timeframe,\n    });\n\n    if (!parsed.success) {\n      const errorResponse: StatisticsErrorResponse = { \n        error: \"INVALID_PARAMETERS\", \n        message: \"Invalid request parameters\",\n        details: parsed.error.format() \n      };\n      return NextResponse.json(errorResponse, { status: 400 });\n    }\n\n    // TODO: Implement actual statistics fetching\n    // This is a placeholder response structure\n    const response: ExerciseStatisticsResponse = {\n      exerciseId: parsed.data.exerciseId,\n      timeframe: parsed.data.timeframe,\n      statistics: {\n        weightProgression: [],\n        estimatedOneRepMax: [],\n        volume: [],\n      },\n    };\n    \n    return NextResponse.json(response);\n\n  } catch (error) {\n    console.error(\"Error fetching exercise statistics:\", error);\n    const errorResponse: StatisticsErrorResponse = { \n      error: \"INTERNAL_SERVER_ERROR\", \n      message: \"Failed to fetch statistics\" \n    };\n    return NextResponse.json(errorResponse, { status: 500 });\n  }\n}"
  },
  {
    "path": "app/api/exercises/[exerciseId]/statistics/volume/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { VolumeResponse, StatisticsErrorResponse } from \"@/shared/types/statistics.types\";\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { PremiumService } from \"@/shared/lib/premium/premium.service\";\nimport { STATISTICS_TIMEFRAMES, DEFAULT_TIMEFRAME, TIMEFRAME_DAYS } from \"@/shared/constants/statistics\";\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\n\nconst timeframeSchema = z.enum([\n  STATISTICS_TIMEFRAMES.FOUR_WEEKS,\n  STATISTICS_TIMEFRAMES.EIGHT_WEEKS,\n  STATISTICS_TIMEFRAMES.TWELVE_WEEKS,\n  STATISTICS_TIMEFRAMES.ONE_YEAR,\n]);\n\n// Get week number from date\nfunction getWeekNumber(date: Date): string {\n  const tempDate = new Date(date.getTime());\n  tempDate.setHours(0, 0, 0, 0);\n  tempDate.setDate(tempDate.getDate() + 3 - ((tempDate.getDay() + 6) % 7));\n  const week1 = new Date(tempDate.getFullYear(), 0, 4);\n  const weekNumber = 1 + Math.round(((tempDate.getTime() - week1.getTime()) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7);\n  return `${tempDate.getFullYear()}-W${weekNumber.toString().padStart(2, \"0\")}`;\n}\n\n// Get week start date (Monday)\nfunction getWeekStartDate(date: Date): Date {\n  const tempDate = new Date(date);\n  const day = tempDate.getDay();\n  const diff = tempDate.getDate() - day + (day === 0 ? -6 : 1);\n  return new Date(tempDate.setDate(diff));\n}\n\nexport async function GET(request: NextRequest, { params }: { params: Promise<{ exerciseId: string }> }) {\n  try {\n    // Get user session\n    const session = await getMobileCompatibleSession(request);\n    const user = session?.user;\n\n    if (!user) {\n      const errorResponse: StatisticsErrorResponse = {\n        error: \"UNAUTHORIZED\",\n        message: \"Authentication required\",\n      };\n      return NextResponse.json(errorResponse, { status: 401 });\n    }\n\n    // Check premium status\n    const premiumStatus = await PremiumService.checkUserPremiumStatus(user.id);\n\n    if (!premiumStatus.isPremium) {\n      const errorResponse: StatisticsErrorResponse = {\n        error: \"PREMIUM_REQUIRED\",\n        message: \"Exercise statistics is a premium feature\",\n        isPremium: false,\n      };\n      return NextResponse.json(errorResponse, { status: 403 });\n    }\n\n    // Parse timeframe\n    const { searchParams } = new URL(request.url);\n    const timeframeRaw = searchParams.get(\"timeframe\") || DEFAULT_TIMEFRAME;\n\n    const timeframeParsed = timeframeSchema.safeParse(timeframeRaw);\n    if (!timeframeParsed.success) {\n      const errorResponse: StatisticsErrorResponse = {\n        error: \"INVALID_PARAMETERS\",\n        message: \"Invalid timeframe parameter\",\n      };\n      return NextResponse.json(errorResponse, { status: 400 });\n    }\n\n    const timeframe = timeframeParsed.data;\n\n    // Calculate date range (ensuring we're working with UTC dates to avoid timezone issues)\n    const endDate = new Date();\n    const startDate = new Date();\n\n    const daysToSubtract = TIMEFRAME_DAYS[timeframe];\n    if (timeframe === STATISTICS_TIMEFRAMES.ONE_YEAR) {\n      startDate.setFullYear(startDate.getFullYear() - 1);\n    } else {\n      startDate.setDate(startDate.getDate() - daysToSubtract);\n    }\n\n    // Set time to start and end of day in UTC\n    startDate.setUTCHours(0, 0, 0, 0);\n    endDate.setUTCHours(23, 59, 59, 999);\n\n    const { exerciseId } = await params;\n\n    // Fetch all sets for volume calculation\n    const workoutSessionExercises = await prisma.workoutSessionExercise.findMany({\n      where: {\n        exerciseId,\n        workoutSession: {\n          userId: user.id,\n          startedAt: {\n            gte: startDate,\n            lte: endDate,\n          },\n        },\n      },\n      include: {\n        workoutSession: {\n          select: {\n            startedAt: true,\n          },\n        },\n        sets: {\n          where: {\n            completed: true,\n          },\n          orderBy: {\n            setIndex: \"asc\",\n          },\n        },\n      },\n      orderBy: {\n        workoutSession: {\n          startedAt: \"asc\",\n        },\n      },\n    });\n\n    // Calculate weekly volume\n    const weeklyVolume = new Map<string, { weekStart: Date; totalVolume: number; setCount: number }>();\n\n    workoutSessionExercises.forEach((sessionExercise) => {\n      const weekKey = getWeekNumber(sessionExercise.workoutSession.startedAt);\n      const weekStart = getWeekStartDate(sessionExercise.workoutSession.startedAt);\n\n      sessionExercise.sets.forEach((set) => {\n        let volume = 0;\n\n        // Calculate volume based on set type\n        const weightIndex = set.types.indexOf(\"WEIGHT\");\n        const repsIndex = set.types.indexOf(\"REPS\");\n        const timeIndex = set.types.indexOf(\"TIME\");\n\n        if (weightIndex !== -1 && repsIndex !== -1 && set.valuesInt && set.valuesInt[weightIndex] && set.valuesInt[repsIndex]) {\n          // Weight-based exercise: reps × weight\n          volume = set.valuesInt[repsIndex] * set.valuesInt[weightIndex];\n        } else if (repsIndex !== -1 && set.valuesInt && set.valuesInt[repsIndex]) {\n          // Bodyweight exercise: count reps as volume\n          volume = set.valuesInt[repsIndex];\n        } else if (timeIndex !== -1 && set.valuesSec && set.valuesSec[0]) {\n          // Time-based exercise: use seconds as volume\n          volume = set.valuesSec[0];\n        }\n\n        if (volume > 0) {\n          const currentWeek = weeklyVolume.get(weekKey) || { weekStart, totalVolume: 0, setCount: 0 };\n          weeklyVolume.set(weekKey, {\n            weekStart,\n            totalVolume: currentWeek.totalVolume + volume,\n            setCount: currentWeek.setCount + 1,\n          });\n        }\n      });\n    });\n\n    // Convert to array format\n    const volumeProgression = Array.from(weeklyVolume.entries())\n      .sort(([, a], [, b]) => a.weekStart.getTime() - b.weekStart.getTime())\n      .map(([week, data]) => ({\n        week,\n        weekStart: data.weekStart.toISOString(),\n        totalVolume: Math.round(data.totalVolume),\n        setCount: data.setCount,\n      }));\n\n    const response: VolumeResponse = {\n      exerciseId,\n      timeframe,\n      data: volumeProgression,\n      count: volumeProgression.length,\n      calculationNote: \"Volume = sets × reps × weight (or reps for bodyweight, or seconds for time-based)\",\n    };\n\n    // Add cache headers - 1 hour cache (disabled for debugging)\n    const headers = new Headers();\n    // Temporarily disable cache for debugging\n    headers.set(\"Cache-Control\", \"no-store, no-cache, must-revalidate\");\n    headers.set(\"Pragma\", \"no-cache\");\n    headers.set(\"Expires\", \"0\");\n    // Original: headers.set(\"Cache-Control\", \"private, max-age=3600, stale-while-revalidate=86400\");\n\n    return NextResponse.json(response, { headers });\n  } catch (error) {\n    console.error(\"Error fetching volume data:\", error);\n    const errorResponse: StatisticsErrorResponse = {\n      error: \"INTERNAL_SERVER_ERROR\",\n      message: \"Failed to fetch volume data\",\n    };\n    return NextResponse.json(errorResponse, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/exercises/[exerciseId]/statistics/weight-progression/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { WeightProgressionResponse, StatisticsErrorResponse } from \"@/shared/types/statistics.types\";\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { PremiumService } from \"@/shared/lib/premium/premium.service\";\nimport { STATISTICS_TIMEFRAMES, DEFAULT_TIMEFRAME, TIMEFRAME_DAYS } from \"@/shared/constants/statistics\";\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\n\nconst timeframeSchema = z.enum([\n  STATISTICS_TIMEFRAMES.FOUR_WEEKS,\n  STATISTICS_TIMEFRAMES.EIGHT_WEEKS,\n  STATISTICS_TIMEFRAMES.TWELVE_WEEKS,\n  STATISTICS_TIMEFRAMES.ONE_YEAR,\n]);\n\nexport async function GET(request: NextRequest, { params }: { params: Promise<{ exerciseId: string }> }) {\n  try {\n    // Get user session\n    const session = await getMobileCompatibleSession(request);\n    const user = session?.user;\n\n    if (!user) {\n      const errorResponse: StatisticsErrorResponse = {\n        error: \"UNAUTHORIZED\",\n        message: \"Authentication required\",\n      };\n      return NextResponse.json(errorResponse, { status: 401 });\n    }\n\n    // Check premium status\n    const premiumStatus = await PremiumService.checkUserPremiumStatus(user.id);\n\n    if (!premiumStatus.isPremium) {\n      const errorResponse: StatisticsErrorResponse = {\n        error: \"PREMIUM_REQUIRED\",\n        message: \"Exercise statistics is a premium feature\",\n        isPremium: false,\n      };\n      return NextResponse.json(errorResponse, { status: 403 });\n    }\n\n    // Parse timeframe\n    const { searchParams } = new URL(request.url);\n    const timeframeRaw = searchParams.get(\"timeframe\") || DEFAULT_TIMEFRAME;\n\n    const timeframeParsed = timeframeSchema.safeParse(timeframeRaw);\n    if (!timeframeParsed.success) {\n      const errorResponse: StatisticsErrorResponse = {\n        error: \"INVALID_PARAMETERS\",\n        message: \"Invalid timeframe parameter\",\n      };\n      return NextResponse.json(errorResponse, { status: 400 });\n    }\n\n    const timeframe = timeframeParsed.data;\n\n    // Calculate date range (ensuring we're working with UTC dates to avoid timezone issues)\n    const endDate = new Date();\n    const startDate = new Date();\n\n    const daysToSubtract = TIMEFRAME_DAYS[timeframe];\n    if (timeframe === STATISTICS_TIMEFRAMES.ONE_YEAR) {\n      startDate.setFullYear(startDate.getFullYear() - 1);\n    } else {\n      startDate.setDate(startDate.getDate() - daysToSubtract);\n    }\n\n    // Set time to start and end of day in UTC\n    startDate.setUTCHours(0, 0, 0, 0);\n    endDate.setUTCHours(23, 59, 59, 999);\n\n    const { exerciseId } = await params;\n\n    // Fetch weight progression data\n    const workoutSessionExercises = await prisma.workoutSessionExercise.findMany({\n      where: {\n        exerciseId,\n        workoutSession: {\n          userId: user.id,\n          startedAt: {\n            gte: startDate,\n            lte: endDate,\n          },\n        },\n      },\n      include: {\n        workoutSession: {\n          select: {\n            startedAt: true,\n          },\n        },\n        sets: {\n          where: {\n            completed: true,\n            types: {\n              has: \"WEIGHT\",\n            },\n          },\n          orderBy: {\n            setIndex: \"asc\",\n          },\n        },\n      },\n      orderBy: {\n        workoutSession: {\n          startedAt: \"asc\",\n        },\n      },\n    });\n\n    // Group by session and find max weight per session\n    const sessionMaxWeights = new Map<string, { date: Date; maxWeight: number }>();\n    console.log(\"sessionMaxWeights:\", sessionMaxWeights);\n\n    workoutSessionExercises.forEach((sessionExercise) => {\n      const sessionDate = sessionExercise.workoutSession.startedAt.toISOString().split(\"T\")[0];\n      let maxWeight = 0;\n\n      sessionExercise.sets.forEach((set) => {\n        // Find weight value from arrays\n        const weightIndex = set.types.indexOf(\"WEIGHT\");\n        if (weightIndex !== -1 && set.valuesInt && set.valuesInt[weightIndex]) {\n          const weight = set.valuesInt[weightIndex];\n          if (weight > maxWeight) {\n            maxWeight = weight;\n          }\n        }\n      });\n\n      if (maxWeight > 0) {\n        const currentMax = sessionMaxWeights.get(sessionDate);\n        if (!currentMax || maxWeight > currentMax.maxWeight) {\n          sessionMaxWeights.set(sessionDate, {\n            date: sessionExercise.workoutSession.startedAt,\n            maxWeight: maxWeight,\n          });\n        }\n      }\n    });\n\n    // Convert to array format\n    const weightProgression = Array.from(sessionMaxWeights.values())\n      .sort((a, b) => a.date.getTime() - b.date.getTime())\n      .map(({ date, maxWeight }) => ({\n        date: date.toISOString(),\n        weight: maxWeight,\n      }));\n\n    const response: WeightProgressionResponse = {\n      exerciseId,\n      timeframe,\n      data: weightProgression,\n      count: weightProgression.length,\n    };\n\n    // Add cache headers - 1 hour cache (disabled for debugging)\n    const headers = new Headers();\n    // Temporarily disable cache for debugging\n    headers.set(\"Cache-Control\", \"no-store, no-cache, must-revalidate\");\n    headers.set(\"Pragma\", \"no-cache\");\n    headers.set(\"Expires\", \"0\");\n    // Original: headers.set(\"Cache-Control\", \"private, max-age=3600, stale-while-revalidate=86400\");\n\n    return NextResponse.json(response, { headers });\n  } catch (error) {\n    console.error(\"Error fetching weight progression:\", error);\n    const errorResponse: StatisticsErrorResponse = {\n      error: \"INTERNAL_SERVER_ERROR\",\n      message: \"Failed to fetch weight progression\",\n    };\n    return NextResponse.json(errorResponse, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/exercises/all/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\n\nconst paginationSchema = z.object({\n  page: z.coerce.number().min(1).default(1),\n  limit: z.coerce.number().min(1).max(100).default(20),\n  search: z.string().optional(),\n  muscle: z.string().optional(),\n  equipment: z.string().optional(),\n});\n\nexport async function GET(request: NextRequest) {\n  try {\n    // Get user session for authentication\n    // const session = await getMobileCompatibleSession(request);\n    // const user = session?.user;\n\n    // if (!user) {\n    //   return NextResponse.json({ error: \"UNAUTHORIZED\", message: \"Authentication required\" }, { status: 401 });\n    // }\n\n    // Parse query parameters\n    const { searchParams } = new URL(request.url);\n    const params = {\n      page: searchParams.get(\"page\") || \"1\",\n      limit: searchParams.get(\"limit\") || \"20\",\n      search: searchParams.get(\"search\") || undefined,\n      muscle: searchParams.get(\"muscle\") || undefined,\n      equipment: searchParams.get(\"equipment\") || undefined,\n    };\n\n    const parsed = paginationSchema.safeParse(params);\n    if (!parsed.success) {\n      return NextResponse.json(\n        {\n          error: \"INVALID_PARAMETERS\",\n          message: \"Invalid query parameters\",\n          details: parsed.error.format(),\n        },\n        { status: 400 },\n      );\n    }\n\n    const { page, limit, search, muscle, equipment } = parsed.data;\n    const skip = (page - 1) * limit;\n\n    // Build where clause for filtering\n    const whereClause: any = {};\n    const conditions = [];\n\n    // Search by exercise name\n    if (search) {\n      conditions.push({\n        OR: [{ name: { contains: search, mode: \"insensitive\" } }, { nameEn: { contains: search, mode: \"insensitive\" } }],\n      });\n    }\n\n    // Build attribute filters array\n    const attributeFilters = [];\n\n    // Filter by muscle group\n    if (muscle) {\n      attributeFilters.push({\n        attributeName: { name: \"PRIMARY_MUSCLE\" },\n        attributeValue: { value: muscle },\n      });\n    }\n\n    // Filter by equipment\n    if (equipment) {\n      attributeFilters.push({\n        attributeName: { name: \"EQUIPMENT\" },\n        attributeValue: { value: equipment },\n      });\n    }\n\n    // Apply attribute filters (AND logic - exercise must have ALL specified attributes)\n    if (attributeFilters.length > 0) {\n      conditions.push(\n        ...attributeFilters.map((filter) => ({\n          attributes: {\n            some: filter,\n          },\n        })),\n      );\n    }\n\n    // Combine all conditions with AND logic\n    if (conditions.length > 0) {\n      whereClause.AND = conditions;\n    }\n\n    // Get total count for pagination\n    const totalCount = await prisma.exercise.count({ where: whereClause });\n\n    // Fetch exercises with pagination\n    const exercises = await prisma.exercise.findMany({\n      where: whereClause,\n      select: {\n        id: true,\n        name: true,\n        nameEn: true,\n        fullVideoUrl: true,\n        fullVideoImageUrl: true,\n        attributes: {\n          select: {\n            id: true,\n            attributeName: {\n              select: { name: true },\n            },\n            attributeValue: {\n              select: { value: true },\n            },\n          },\n        },\n      },\n      orderBy: { name: \"asc\" },\n      skip,\n      take: limit,\n    });\n\n    // Calculate pagination metadata\n    const totalPages = Math.ceil(totalCount / limit);\n    const hasNextPage = page < totalPages;\n    const hasPreviousPage = page > 1;\n\n    const response = {\n      data: exercises,\n      pagination: {\n        page,\n        limit,\n        totalCount,\n        totalPages,\n        hasNextPage,\n        hasPreviousPage,\n      },\n    };\n\n    // Add cache headers - 5 minutes cache\n    const headers = new Headers();\n    headers.set(\"Cache-Control\", \"public, max-age=300, stale-while-revalidate=600\");\n\n    return NextResponse.json(response, { headers });\n  } catch (error) {\n    console.error(\"Error fetching exercises:\", error);\n    return NextResponse.json(\n      {\n        error: \"INTERNAL_SERVER_ERROR\",\n        message: \"Failed to fetch exercises\",\n      },\n      { status: 500 },\n    );\n  }\n}\n"
  },
  {
    "path": "app/api/exercises/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { getExercisesSchema } from \"@/features/workout-builder/schema/get-exercises.schema\";\nimport { getExercisesAction } from \"@/features/workout-builder/actions/get-exercises.action\";\n\nexport async function GET(req: NextRequest) {\n  try {\n    const { searchParams } = new URL(req.url);\n\n    const params = {\n      equipment: searchParams.get(\"equipment\")?.split(\",\").filter(Boolean) || [],\n      muscles: searchParams.get(\"muscles\")?.split(\",\").filter(Boolean) || [],\n      limit: searchParams.get(\"limit\") ? parseInt(searchParams.get(\"limit\")!) : 3,\n    };\n\n    const parsed = getExercisesSchema.safeParse(params);\n    if (!parsed.success) {\n      return NextResponse.json({ error: \"INVALID_INPUT\", details: parsed.error.format() }, { status: 400 });\n    }\n\n    const result = await getExercisesAction(parsed.data);\n\n    if (result?.data && result.data.length === 0) {\n      return NextResponse.json({ error: \"NO_EXERCISES_FOUND\" }, { status: 404 });\n    }\n\n    if (!result?.data) {\n      return NextResponse.json({ error: \"NO_EXERCISES_FOUND\" }, { status: 404 });\n    }\n\n    return NextResponse.json(result.data);\n  } catch (error) {\n    console.error(error);\n    return NextResponse.json({ error: \"INTERNAL_SERVER_ERROR\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/exercises/shuffle/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { shuffleExerciseAction } from \"@/features/workout-builder/actions/shuffle-exercise.action\";\n\nconst shuffleExerciseSchema = z.object({\n  muscle: z.nativeEnum(ExerciseAttributeValueEnum),\n  equipment: z.array(z.nativeEnum(ExerciseAttributeValueEnum)),\n  excludeExerciseIds: z.array(z.string()),\n});\n\nexport async function POST(req: NextRequest) {\n  try {\n    const body = await req.json();\n\n    const parsed = shuffleExerciseSchema.safeParse(body);\n    if (!parsed.success) {\n      return NextResponse.json({ error: \"INVALID_INPUT\", details: parsed.error.format() }, { status: 400 });\n    }\n\n    const result = await shuffleExerciseAction(parsed.data);\n\n    if (result?.serverError) {\n      if (result.serverError === \"No alternative exercises found\") {\n        return NextResponse.json({ error: \"NO_EXERCISES_FOUND\" }, { status: 404 });\n      }\n      return NextResponse.json({ error: \"SHUFFLE_FAILED\", message: result.serverError }, { status: 500 });\n    }\n\n    if (!result?.data?.exercise) {\n      return NextResponse.json({ error: \"NO_EXERCISE_RETURNED\" }, { status: 500 });\n    }\n\n    return NextResponse.json({ exercise: result.data.exercise });\n  } catch (error) {\n    console.error(\"Shuffle exercise error:\", error);\n    return NextResponse.json({ error: \"INTERNAL_SERVER_ERROR\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/premium/billing-portal/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { PremiumManager } from \"@/shared/lib/premium/premium.manager\";\nimport { serverRequiredUser } from \"@/entities/user/model/get-server-session-user\";\n\n/**\n * POST /api/premium/billing-portal\n *\n * Create billing portal session for subscription management\n * Body: { returnUrl?: string, provider?: string }\n */\nexport async function POST(request: NextRequest) {\n  try {\n    const user = await serverRequiredUser();\n    const { returnUrl, provider = \"stripe\" } = await request.json();\n\n    const result = await PremiumManager.createBillingPortal(user.id, provider, returnUrl);\n\n    if (result.success) {\n      return NextResponse.json({ success: true, url: result.checkoutUrl });\n    } else {\n      return NextResponse.json({ success: false, error: result.error }, { status: 400 });\n    }\n  } catch (error) {\n    console.error(\"Billing portal creation error:\", error);\n\n    return NextResponse.json(\n      {\n        success: false,\n        error: \"Failed to create billing portal session\",\n      },\n      { status: 500 },\n    );\n  }\n}\n"
  },
  {
    "path": "app/api/premium/checkout/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { PremiumManager } from \"@/shared/lib/premium/premium.manager\";\nimport { serverRequiredUser } from \"@/entities/user/model/get-server-session-user\";\n\n/**\n * POST /api/premium/checkout\n *\n * Create checkout session for premium subscription\n * Body: { planId: string, provider?: string }\n */\nexport async function POST(request: NextRequest) {\n  try {\n    const user = await serverRequiredUser();\n\n    const { planId, provider = \"stripe\" } = await request.json();\n\n    if (!planId) {\n      return NextResponse.json({ success: false, error: \"Plan ID is required\" }, { status: 400 });\n    }\n\n    const result = await PremiumManager.createCheckout(user.id, planId, provider);\n\n    return NextResponse.json(result);\n  } catch (error) {\n    console.error(\"Checkout creation error:\", error);\n\n    return NextResponse.json(\n      {\n        success: false,\n        error: \"Failed to create checkout session\",\n        provider: \"stripe\",\n      },\n      { status: 500 },\n    );\n  }\n}\n"
  },
  {
    "path": "app/api/premium/plans/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { PremiumManager } from \"@/shared/lib/premium/premium.manager\";\n\n/**\n * Detect user region - Platform agnostic approach\n * Works with Vercel, Cloudflare, or any other hosting\n */\nfunction detectUserRegion(request: NextRequest): string {\n  console.log(\"🔍 === DETECTION DE REGION (Platform Agnostic) ===\");\n\n  // 1. Try multiple possible geo headers (works with various services)\n  const geoHeaders = {\n    country:\n      request.headers.get(\"x-vercel-ip-country\") || // Vercel\n      request.headers.get(\"cf-ipcountry\") || // Cloudflare\n      request.headers.get(\"x-country-code\") || // Generic\n      request.headers.get(\"x-real-ip-country\"), // Nginx\n    region:\n      request.headers.get(\"x-vercel-ip-country-region\") || // Vercel\n      request.headers.get(\"cf-region-code\"), // Cloudflare\n    city:\n      request.headers.get(\"x-vercel-ip-city\") || // Vercel\n      request.headers.get(\"cf-city\"), // Cloudflare\n  };\n\n  console.log(\"📍 Headers géo détectés:\");\n  console.log(\"  - Country:\", geoHeaders.country || \"non détecté\");\n  console.log(\"  - Region:\", geoHeaders.region || \"non détecté\");\n  console.log(\"  - City:\", geoHeaders.city || \"non détecté\");\n\n  // 2. Country-based detection (if available)\n  if (geoHeaders.country) {\n    const country = geoHeaders.country.toUpperCase();\n\n    // US and Canada -> US region\n    if ([\"US\", \"CA\"].includes(country)) {\n      console.log(\"🏷️ => Région: US (Amériques)\");\n      return \"US\";\n    }\n\n    // UK\n    if (country === \"GB\") {\n      console.log(\"🏷️ => Région: UK\");\n      return \"UK\";\n    }\n\n    // Brazil special case\n    if (country === \"BR\") {\n      console.log(\"🏷️ => Région: BR (Brésil)\");\n      return \"BR\";\n    }\n\n    // Russia\n    if (country === \"RU\") {\n      console.log(\"🏷️ => Région: RU (Russie)\");\n      return \"RU\";\n    }\n\n    // China\n    if ([\"CN\", \"HK\", \"TW\"].includes(country)) {\n      console.log(\"🏷️ => Région: CN (Chine)\");\n      return \"CN\";\n    }\n\n    // Latin America (Spanish/Portuguese speaking, except Brazil)\n    const latamCountries = [\"MX\", \"AR\", \"CL\", \"CO\", \"PE\", \"VE\", \"EC\", \"GT\", \"CU\", \"BO\", \"DO\", \"HN\", \"PY\", \"SV\", \"NI\", \"CR\", \"PA\", \"UY\"];\n    if (latamCountries.includes(country)) {\n      console.log(\"🏷️ => Région: LATAM (Amérique Latine)\");\n      return \"LATAM\";\n    }\n\n    // EU countries\n    const euCountries = [\"FR\", \"DE\", \"IT\", \"ES\", \"PT\", \"NL\", \"BE\", \"PL\", \"SE\", \"DK\", \"FI\", \"NO\", \"AT\", \"CH\", \"IE\"];\n    if (euCountries.includes(country)) {\n      console.log(\"🏷️ => Région: EU (Europe)\");\n      return \"EU\";\n    }\n  }\n\n  // 3. Fallback: Accept-Language header (always available)\n  const acceptLanguage = request.headers.get(\"accept-language\");\n  console.log(\"🗣️ Accept-Language:\", acceptLanguage);\n\n  if (acceptLanguage) {\n    const primaryLang = acceptLanguage.split(\",\")[0].toLowerCase();\n\n    // US English speakers\n    if (primaryLang.includes(\"en-us\")) {\n      console.log(\"🏷️ => Région via langue: US\");\n      return \"US\";\n    }\n\n    // UK English speakers\n    if (primaryLang.includes(\"en-gb\")) {\n      console.log(\"🏷️ => Région via langue: UK\");\n      return \"UK\";\n    }\n\n    // Portuguese (Brazilian)\n    if (primaryLang.includes(\"pt-br\")) {\n      console.log(\"🏷️ => Région via langue: BR\");\n      return \"BR\";\n    }\n\n    // Spanish speakers (Latin America)\n    if (primaryLang.includes(\"es\") && !primaryLang.includes(\"es-es\")) {\n      console.log(\"🏷️ => Région via langue: LATAM\");\n      return \"LATAM\";\n    }\n\n    // Russian\n    if (primaryLang.includes(\"ru\")) {\n      console.log(\"🏷️ => Région via langue: RU\");\n      return \"RU\";\n    }\n\n    // Chinese\n    if (primaryLang.includes(\"zh\")) {\n      console.log(\"🏷️ => Région via langue: CN\");\n      return \"CN\";\n    }\n\n    // European languages\n    if (primaryLang.match(/^(fr|de|it|es-es|pt-pt|nl|pl|sv|da|fi|no)/)) {\n      console.log(\"🏷️ => Région via langue: EU\");\n      return \"EU\";\n    }\n  }\n\n  // 4. Time zone detection fallback (client-side would be needed)\n  // Could be passed as a query param from frontend\n  const timezone = request.nextUrl.searchParams.get(\"tz\");\n  if (timezone) {\n    console.log(\"🕐 Timezone fourni:\", timezone);\n\n    // Brazil\n    if (timezone.includes(\"America/Sao_Paulo\") || timezone.includes(\"America/Brasilia\")) {\n      console.log(\"🏷️ => Région via timezone: BR\");\n      return \"BR\";\n    }\n\n    // Latin America\n    if (\n      timezone.includes(\"America/Mexico_City\") ||\n      timezone.includes(\"America/Buenos_Aires\") ||\n      timezone.includes(\"America/Santiago\") ||\n      timezone.includes(\"America/Bogota\") ||\n      timezone.includes(\"America/Lima\")\n    ) {\n      console.log(\"🏷️ => Région via timezone: LATAM\");\n      return \"LATAM\";\n    }\n\n    // US/Canada\n    if (timezone.includes(\"America/\") && !timezone.includes(\"America/Sao_Paulo\") && !timezone.includes(\"America/Mexico_City\")) {\n      console.log(\"🏷️ => Région via timezone: US\");\n      return \"US\";\n    }\n\n    // UK\n    if (timezone === \"Europe/London\") {\n      console.log(\"🏷️ => Région via timezone: UK\");\n      return \"UK\";\n    }\n\n    // Russia (multiple timezones!)\n    if (\n      timezone.includes(\"Europe/Moscow\") ||\n      timezone.includes(\"Europe/Samara\") ||\n      timezone.includes(\"Asia/Yekaterinburg\") ||\n      timezone.includes(\"Asia/Novosibirsk\") ||\n      timezone.includes(\"Asia/Vladivostok\") ||\n      timezone.includes(\"Europe/Kaliningrad\")\n    ) {\n      console.log(\"🏷️ => Région via timezone: RU\");\n      return \"RU\";\n    }\n\n    // China (single timezone)\n    if (timezone.includes(\"Asia/Shanghai\") || timezone.includes(\"Asia/Hong_Kong\") || timezone.includes(\"Asia/Taipei\")) {\n      console.log(\"🏷️ => Région via timezone: CN\");\n      return \"CN\";\n    }\n\n    // Europe\n    if (timezone.includes(\"Europe/\")) {\n      console.log(\"🏷️ => Région via timezone: EU\");\n      return \"EU\";\n    }\n\n    // Additional Asia-Pacific that might use USD\n    if (timezone.includes(\"Asia/Tokyo\") || timezone.includes(\"Australia/\") || timezone.includes(\"Pacific/\")) {\n      console.log(\"🏷️ => Région via timezone: US (Asia-Pacific default)\");\n      return \"US\";\n    }\n  }\n\n  // Default\n  console.log(\"🏷️ => Région par défaut: EU\");\n  console.log(\"=========================\\n\");\n  return \"EU\";\n}\n\n/**\n * GET /api/premium/plans\n *\n * Get available premium plans with optional provider/region filtering\n * Query params: provider, region, tz (timezone)\n * Public endpoint - no auth required\n */\nexport async function GET(request: NextRequest) {\n  try {\n    const { searchParams } = new URL(request.url);\n    const provider = searchParams.get(\"provider\") || undefined;\n\n    // Use provided region or auto-detect\n    let region = searchParams.get(\"region\") || undefined;\n    if (!region) {\n      region = detectUserRegion(request);\n    } else {\n      console.log(\"📍 Région fournie explicitement:\", region);\n    }\n\n    const plans = await PremiumManager.getAvailablePlans(provider, region);\n    console.log(`\\n✅ Retour de ${plans.length} plan(s) pour la région: ${region}\\n`);\n\n    // Include debug info only in development\n    const response: any = {\n      plans,\n      detectedRegion: region,\n    };\n\n    if (process.env.NODE_ENV === \"development\") {\n      response.debug = {\n        headers: {\n          country: request.headers.get(\"x-vercel-ip-country\") || request.headers.get(\"cf-ipcountry\"),\n          acceptLanguage: request.headers.get(\"accept-language\"),\n          timezone: searchParams.get(\"tz\"),\n        },\n      };\n    }\n\n    return NextResponse.json(response);\n  } catch (error) {\n    console.error(\"Error fetching plans:\", error);\n\n    return NextResponse.json({ error: \"Failed to fetch plans\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/premium/status/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\n\n/**\n * Get Premium Status (Unified)\n *\n * GET /api/premium/status\n *\n * Returns the premium status for the current user, checking both:\n * - RevenueCat subscriptions (mobile)\n * - Stripe subscriptions (legacy web)\n */\nexport async function GET(request: NextRequest) {\n  try {\n    // Get authenticated user\n    const session = await getMobileCompatibleSession(request);\n    const user = session?.user;\n\n    if (!user) {\n      return NextResponse.json({\n        isPremium: false,\n        source: null,\n        message: \"User not authenticated\",\n      });\n    }\n\n    // First, check the user's isPremium flag (updated by RevenueCat sync or Stripe)\n    const dbUser = await prisma.user.findUnique({\n      where: { id: user.id },\n      select: { isPremium: true },\n    });\n\n    // Then check for active subscriptions\n    const activeSubscriptions = await prisma.subscription.findMany({\n      where: {\n        userId: user.id,\n        status: \"ACTIVE\",\n      },\n      select: {\n        id: true,\n        platform: true,\n        revenueCatUserId: true,\n        currentPeriodEnd: true,\n        plan: {\n          select: {\n            interval: true,\n            priceMonthly: true,\n            priceYearly: true,\n            currency: true,\n          },\n        },\n      },\n    });\n\n    // Determine premium status and source\n    const hasRevenueCatSub = activeSubscriptions.some((sub) => sub.revenueCatUserId);\n    const hasStripeSub = activeSubscriptions.some((sub) => !sub.revenueCatUserId && sub.platform === \"WEB\");\n    const isPremium = dbUser?.isPremium || activeSubscriptions.length > 0;\n\n    // Find the most relevant subscription to display\n    const primarySubscription = activeSubscriptions.find((sub) => sub.revenueCatUserId) || activeSubscriptions[0];\n\n    return NextResponse.json({\n      isPremium,\n      source: hasRevenueCatSub ? \"revenuecat\" : hasStripeSub ? \"stripe\" : null,\n      subscriptions: {\n        hasRevenueCat: hasRevenueCatSub,\n        hasStripe: hasStripeSub,\n        count: activeSubscriptions.length,\n      },\n      currentSubscription: primarySubscription\n        ? {\n            platform: primarySubscription.platform,\n            expiresAt: primarySubscription.currentPeriodEnd,\n            plan: primarySubscription.plan,\n          }\n        : null,\n      // Legacy support message for Stripe users\n      legacyMessage:\n        hasStripeSub && !hasRevenueCatSub ? \"Vous avez un abonnement Stripe actif. Il restera valide jusqu'à son expiration.\" : null,\n    });\n  } catch (error) {\n    console.error(\"Error getting premium status:\", error);\n    return NextResponse.json({ error: \"Failed to get premium status\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/programs/[slug]/enroll/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\nimport { headers } from \"next/headers\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { enrollInProgram } from \"@/features/programs/actions/enroll-program.action\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nexport async function POST(req: NextRequest, { params }: { params: Promise<{ slug: string }> }) {\n  try {\n    const { slug } = await params;\n\n    const session = await auth.api.getSession({\n      headers: await headers(),\n    });\n\n    if (!session) {\n      return NextResponse.json({ error: \"Unauthorized\", code: \"UNAUTHORIZED\" }, { status: 401 });\n    }\n\n    const program = await prisma.program.findUnique({\n      where: { slug },\n      select: { id: true, participantCount: true },\n    });\n\n    if (!program) {\n      return NextResponse.json({ error: \"Program not found\", code: \"NOT_FOUND\" }, { status: 404 });\n    }\n\n    const result = await enrollInProgram(program.id);\n\n    const updatedProgram = await prisma.program.findUnique({\n      where: { id: program.id },\n      select: { participantCount: true },\n    });\n\n    return NextResponse.json({\n      enrollment: result.enrollment,\n      isNew: result.isNew,\n      totalEnrollments: updatedProgram?.participantCount || program.participantCount,\n    });\n  } catch (error) {\n    console.error(\"Error enrolling in program:\", error);\n\n    if (error instanceof Error && error.message === \"User not found\") {\n      return NextResponse.json({ error: \"User not found\", code: \"USER_NOT_FOUND\" }, { status: 404 });\n    }\n\n    return NextResponse.json({ error: \"Failed to enroll in program\", code: \"INTERNAL_ERROR\" }, { status: 500 });\n  }\n}\n\nexport async function GET(req: NextRequest, { params }: { params: Promise<{ slug: string }> }) {\n  try {\n    const session = await auth.api.getSession({\n      headers: await headers(),\n    });\n\n    const { slug } = await params;\n\n    if (!session) {\n      return NextResponse.json({ error: \"Unauthorized\", code: \"UNAUTHORIZED\" }, { status: 401 });\n    }\n\n    const userId = session.user?.id;\n    if (!userId) {\n      return NextResponse.json({ error: \"User not found\", code: \"USER_NOT_FOUND\" }, { status: 404 });\n    }\n\n    const program = await prisma.program.findUnique({\n      where: { slug },\n      select: { id: true },\n    });\n\n    if (!program) {\n      return NextResponse.json({ error: \"Program not found\", code: \"NOT_FOUND\" }, { status: 404 });\n    }\n\n    const enrollment = await prisma.userProgramEnrollment.findUnique({\n      where: {\n        userId_programId: {\n          userId,\n          programId: program.id,\n        },\n      },\n    });\n\n    return NextResponse.json({\n      isEnrolled: !!enrollment,\n      enrollment,\n    });\n  } catch (error) {\n    console.error(\"Error checking enrollment status:\", error);\n    return NextResponse.json({ error: \"Failed to check enrollment status\", code: \"INTERNAL_ERROR\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/programs/[slug]/progress/route.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nimport { getProgramProgressBySlug } from \"@/features/programs/actions/get-program-progress-by-slug.action\";\n\nexport async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) {\n  try {\n    const { slug } = await params;\n\n    const progress = await getProgramProgressBySlug(slug);\n\n    if (!progress) {\n      return NextResponse.json({ error: \"Program progress not found\" }, { status: 404 });\n    }\n\n    return NextResponse.json(progress);\n  } catch (error) {\n    console.error(\"Error fetching program progress:\", error);\n    return NextResponse.json({ error: \"Failed to fetch program progress\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/programs/[slug]/route.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nimport { getProgramBySlug } from \"@/features/programs/actions/get-program-by-slug.action\";\n\nexport async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) {\n  try {\n    const { slug } = await params;\n    const program = await getProgramBySlug(slug);\n\n    if (!program) {\n      return NextResponse.json({ error: \"Program not found\" }, { status: 404 });\n    }\n\n    return NextResponse.json(program);\n  } catch (error) {\n    console.error(\"Error fetching program:\", error);\n    return NextResponse.json({ error: \"Failed to fetch program\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/programs/[slug]/sessions/[sessionSlug]/route.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nimport { getSessionBySlug } from \"@/features/programs/actions/get-session-by-slug.action\";\n\nexport async function GET(request: Request, { params }: { params: Promise<{ slug: string; sessionSlug: string }> }) {\n  try {\n    const { searchParams } = new URL(request.url);\n    const locale = searchParams.get(\"locale\") || \"fr\";\n\n    const { slug, sessionSlug } = await params;\n    const sessionDetail = await getSessionBySlug(slug, sessionSlug, locale as any);\n\n    if (!sessionDetail) {\n      return NextResponse.json({ error: \"Session not found\" }, { status: 404 });\n    }\n\n    return NextResponse.json(sessionDetail);\n  } catch (error) {\n    console.error(\"Error fetching session:\", error);\n    return NextResponse.json({ error: \"Failed to fetch session\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/programs/route.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nimport { getPublicPrograms } from \"@/features/programs/actions/get-public-programs.action\";\n\nexport async function GET() {\n  try {\n    const programs = await getPublicPrograms();\n    return NextResponse.json(programs);\n  } catch (error) {\n    console.error(\"Error fetching programs:\", error);\n    return NextResponse.json({ error: \"Failed to fetch programs\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/programs/session-progress/[progressId]/complete/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { completeProgramSession } from \"@/features/programs/actions/complete-program-session.action\";\n\nexport async function POST(request: NextRequest, { params }: { params: Promise<{ progressId: string }> }) {\n  try {\n    const { progressId } = await params;\n    const body = await request.json();\n    const { workoutSessionId } = body;\n\n    if (!workoutSessionId) {\n      return NextResponse.json({ error: \"Missing workout session ID\" }, { status: 400 });\n    }\n\n    // Use the existing server action\n    const result = await completeProgramSession(progressId, workoutSessionId);\n\n    return NextResponse.json(result);\n  } catch (error: any) {\n    console.error(\"Error completing session progress:\", error);\n\n    if (error.message === \"Unauthorized\") {\n      return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n    }\n\n    if (error.message === \"User not found\") {\n      return NextResponse.json({ error: \"User not found\" }, { status: 401 });\n    }\n\n    if (error.message === \"Session progress not found\") {\n      return NextResponse.json({ error: \"Session progress not found\" }, { status: 404 });\n    }\n\n    return NextResponse.json({ error: \"Internal server error\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/programs/session-progress/start/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { startProgramSession } from \"@/features/programs/actions/start-program-session.action\";\n\nexport async function POST(request: NextRequest) {\n  try {\n    const body = await request.json();\n    const { enrollmentId, sessionId } = body;\n\n    if (!enrollmentId || !sessionId) {\n      return NextResponse.json({ error: \"Missing required fields\" }, { status: 400 });\n    }\n\n    // Use the existing server action\n    const result = await startProgramSession(enrollmentId, sessionId);\n\n    return NextResponse.json(result);\n  } catch (error: any) {\n    console.error(\"Error starting session progress:\", error);\n    \n    if (error.message === \"Unauthorized\") {\n      return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n    }\n    \n    if (error.message === \"User not found\") {\n      return NextResponse.json({ error: \"User not found\" }, { status: 401 });\n    }\n    \n    if (error.message === \"Enrollment not found\") {\n      return NextResponse.json({ error: \"Enrollment not found\" }, { status: 404 });\n    }\n    \n    if (error.message === \"Session not found\") {\n      return NextResponse.json({ error: \"Session not found\" }, { status: 404 });\n    }\n    \n    return NextResponse.json({ error: \"Internal server error\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/revenuecat/link-user/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\n/**\n * Get Premium Status\n *\n * GET /api/revenuecat/link-user\n *\n * Gets the current user's premium status\n * This endpoint is kept for backward compatibility\n */\nexport async function GET(request: NextRequest) {\n  try {\n    const { getMobileCompatibleSession } = await import(\"@/shared/api/mobile-auth\");\n    const { PremiumService } = await import(\"@/shared/lib/premium/premium.service\");\n\n    // Get authenticated user\n    const session = await getMobileCompatibleSession(request);\n    const user = session?.user;\n\n    if (!user) {\n      console.log(\"No user found\");\n      return NextResponse.json({\n        premiumStatus: {\n          isPremium: false,\n        },\n      });\n    }\n\n    // Get premium status\n    const premiumStatus = await PremiumService.checkUserPremiumStatus(user.id);\n\n    return NextResponse.json({\n      premiumStatus,\n    });\n  } catch (error) {\n    console.error(\"Error getting premium status:\", error);\n\n    return NextResponse.json({ error: \"Failed to get premium status\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/revenuecat/sync-status/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { Platform } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { serverRequiredUser } from \"@/entities/user/model/get-server-session-user\";\n\n/**\n * Sync RevenueCat Status with Backend (Simplified)\n *\n * POST /api/revenuecat/sync-status\n *\n * Synchronizes the current RevenueCat subscription status with the backend.\n * RevenueCat is the source of truth - we only store the status for web access.\n */\n\nconst syncStatusSchema = z.object({\n  userId: z.string().min(1, \"User ID is required\"),\n  isPremium: z.boolean(),\n  revenueCatUserId: z.string().optional(),\n  entitlements: z.array(z.string()).optional(),\n  platform: z.string(),\n  expirationDate: z.number(),\n});\n\nexport async function POST(request: NextRequest) {\n  try {\n    // Get authenticated user\n    const user = await serverRequiredUser();\n\n    // Parse and validate request body\n    const body = await request.json();\n    const { userId, isPremium: isPremiumRevenueCat, revenueCatUserId, expirationDate } = syncStatusSchema.parse(body);\n    console.log(\"expirationDate:\", expirationDate);\n\n    // Verify the userId matches the authenticated user\n    if (userId !== user.id) {\n      return NextResponse.json({ error: \"User ID mismatch\" }, { status: 403 });\n    }\n\n    const isPremium = isPremiumRevenueCat || user.isPremium;\n\n    console.log(`[RevenueCat Sync] Syncing status for user ${userId}: isPremium=${isPremium}`);\n\n    // Update user's premium status in the database\n    await prisma.user.update({\n      where: { id: userId },\n      data: { isPremium },\n    });\n\n    // If user has premium, ensure we have a subscription record\n    if (isPremium && revenueCatUserId) {\n      // Check if user already has a mobile subscription\n      const existingSubscription = await prisma.subscription.findUnique({\n        where: {\n          userId_platform: {\n            userId,\n            platform: Platform.ANDROID || Platform.IOS,\n          },\n        },\n      });\n\n      if (existingSubscription) {\n        // Update existing subscription\n        await prisma.subscription.update({\n          where: { id: existingSubscription.id },\n          data: {\n            revenueCatUserId,\n            status: \"ACTIVE\",\n            currentPeriodEnd: new Date(expirationDate),\n            updatedAt: new Date(),\n          },\n        });\n      } else {\n        // Create new subscription record\n        // Find or create a default mobile plan\n        let mobilePlan = await prisma.subscriptionPlan.findFirst({\n          where: { interval: \"month\", isActive: true },\n        });\n\n        if (!mobilePlan) {\n          // Create a default mobile plan if none exists\n          mobilePlan = await prisma.subscriptionPlan.create({\n            data: {\n              priceMonthly: 9.99,\n              currency: \"USD\",\n              interval: \"month\",\n              isActive: true,\n            },\n          });\n        }\n\n        await prisma.subscription.create({\n          data: {\n            userId,\n            planId: mobilePlan.id,\n            revenueCatUserId,\n            status: \"ACTIVE\",\n            platform: Platform.ANDROID || Platform.IOS,\n            startedAt: new Date(),\n            currentPeriodEnd: new Date(expirationDate),\n          },\n        });\n      }\n    } else if (!isPremium) {\n      // User is not premium - update any active mobile subscriptions\n      await prisma.subscription.updateMany({\n        where: {\n          userId,\n          platform: Platform.ANDROID || Platform.IOS,\n          status: \"ACTIVE\",\n        },\n        data: {\n          status: \"EXPIRED\",\n          cancelledAt: new Date(),\n        },\n      });\n    }\n\n    // Check if user has any Stripe subscriptions (legacy)\n    const hasStripeSubscription = await prisma.subscription.findFirst({\n      where: {\n        userId,\n        platform: { in: [Platform.WEB, Platform.IOS, Platform.ANDROID] },\n        status: \"ACTIVE\",\n        revenueCatUserId: null, // No RevenueCat ID means it's from Stripe\n      },\n    });\n\n    return NextResponse.json({\n      success: true,\n      premiumStatus: {\n        isPremium: isPremium || !!hasStripeSubscription,\n        hasRevenueCatSubscription: isPremium,\n        hasStripeSubscription: !!hasStripeSubscription,\n        revenueCatUserId,\n      },\n    });\n  } catch (error) {\n    console.error(\"Error syncing RevenueCat status:\", error);\n\n    // Handle validation errors\n    if (error instanceof z.ZodError) {\n      return NextResponse.json({ error: \"Invalid request data\", details: error.errors }, { status: 400 });\n    }\n\n    // Handle known errors\n    if (error instanceof Error) {\n      return NextResponse.json({ error: error.message }, { status: 400 });\n    }\n\n    return NextResponse.json({ error: \"Failed to sync RevenueCat status\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/revenuecat/webhook/route.ts",
    "content": "import crypto from \"crypto\";\n\nimport { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { Platform } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\n\n/**\n * RevenueCat Webhook Handler\n *\n * POST /api/revenuecat/webhook\n *\n * Handles RevenueCat webhook events for subscription status changes.\n * Events: https://www.revenuecat.com/docs/webhooks\n */\n\n// RevenueCat webhook event schema\nconst webhookEventSchema = z.object({\n  api_version: z.string(),\n  event: z.object({\n    app_user_id: z.string(),\n    aliases: z.array(z.string()).optional(),\n    country_code: z.string().optional(),\n    currency: z.string().optional(),\n    entitlement_id: z.string().optional(),\n    entitlement_ids: z.array(z.string()).optional(),\n    environment: z.enum([\"SANDBOX\", \"PRODUCTION\"]),\n    event_timestamp_ms: z.number(),\n    expiration_at_ms: z.number().optional(),\n    id: z.string(),\n    is_family_share: z.boolean().optional(),\n    offer_code: z.string().optional().nullable(),\n    original_app_user_id: z.string(),\n    original_transaction_id: z.string().optional(),\n    period_type: z.string().optional(),\n    presented_offering_id: z.string().optional().nullable(),\n    price: z.number().optional(),\n    price_in_purchased_currency: z.number().optional(),\n    product_id: z.string(),\n    purchased_at_ms: z.number().optional(),\n    store: z.enum([\"APP_STORE\", \"MAC_APP_STORE\", \"PLAY_STORE\", \"STRIPE\", \"PROMOTIONAL\", \"UNKNOWN\"]),\n    subscriber_attributes: z.record(z.any()).optional(),\n    takehome_percentage: z.number().optional(),\n    tax_percentage: z.number().optional(),\n    transaction_id: z.string().optional(),\n    type: z.string(),\n  }),\n});\n\n// Verify webhook signature\nfunction verifyWebhookSignature(request: Request, body: string, secret: string): boolean {\n  const signature = request.headers.get(\"X-RevenueCat-Signature\");\n  if (!signature) {\n    return false;\n  }\n\n  const hmac = crypto.createHmac(\"sha256\", secret);\n  hmac.update(body);\n  const expectedSignature = hmac.digest(\"hex\");\n\n  return crypto.timingSafeEqual(Buffer.from(signature, \"hex\"), Buffer.from(expectedSignature, \"hex\"));\n}\n\nexport async function POST(request: NextRequest) {\n  try {\n    // Get webhook secret from environment\n    const webhookSecret = process.env.REVENUECAT_WEBHOOK_SECRET;\n    if (!webhookSecret) {\n      console.error(\"[RevenueCat Webhook] No webhook secret configured\");\n      return NextResponse.json({ error: \"Webhook not configured\" }, { status: 500 });\n    }\n\n    // Get raw body for signature verification\n    const rawBody = await request.text();\n\n    // Verify webhook signature\n    if (!verifyWebhookSignature(request, rawBody, webhookSecret)) {\n      console.error(\"[RevenueCat Webhook] Invalid signature\");\n      return NextResponse.json({ error: \"Invalid signature\" }, { status: 401 });\n    }\n\n    // Parse webhook event\n    const body = JSON.parse(rawBody);\n    const webhookEvent = webhookEventSchema.parse(body);\n    const { event } = webhookEvent;\n\n    console.log(`[RevenueCat Webhook] Received event: ${event.type} for user: ${event.app_user_id}`);\n\n    // Handle different event types\n    switch (event.type) {\n      case \"INITIAL_PURCHASE\":\n      case \"RENEWAL\":\n      case \"UNCANCELLATION\":\n        await handleSubscriptionActive(event);\n        break;\n\n      case \"CANCELLATION\":\n      case \"EXPIRATION\":\n        await handleSubscriptionInactive(event);\n        break;\n\n      case \"BILLING_ISSUE\":\n        await handleBillingIssue(event);\n        break;\n\n      case \"PRODUCT_CHANGE\":\n        await handleProductChange(event);\n        break;\n\n      default:\n        console.log(`[RevenueCat Webhook] Unhandled event type: ${event.type}`);\n    }\n\n    return NextResponse.json({ success: true });\n  } catch (error) {\n    console.error(\"[RevenueCat Webhook] Error processing webhook:\", error);\n\n    if (error instanceof z.ZodError) {\n      return NextResponse.json({ error: \"Invalid webhook data\", details: error.errors }, { status: 400 });\n    }\n\n    return NextResponse.json({ error: \"Failed to process webhook\" }, { status: 500 });\n  }\n}\n\n// Handle subscription becoming active\nasync function handleSubscriptionActive(event: z.infer<typeof webhookEventSchema>[\"event\"]) {\n  const userId = event.app_user_id;\n  const expirationDate = event.expiration_at_ms ? new Date(event.expiration_at_ms) : null;\n\n  // Update user premium status\n  await prisma.user.update({\n    where: { id: userId },\n    data: { isPremium: true },\n  });\n\n  // Update or create subscription record\n  const subscription = await prisma.subscription.findUnique({\n    where: {\n      userId_platform: {\n        userId,\n        platform: Platform.ANDROID || Platform.IOS,\n      },\n    },\n  });\n\n  if (subscription) {\n    await prisma.subscription.update({\n      where: { id: subscription.id },\n      data: {\n        status: \"ACTIVE\",\n        currentPeriodEnd: expirationDate,\n        revenueCatUserId: event.original_app_user_id,\n        updatedAt: new Date(),\n      },\n    });\n  } else {\n    // Find a default plan\n    const plan = await prisma.subscriptionPlan.findFirst({\n      where: { isActive: true },\n    });\n\n    if (plan) {\n      await prisma.subscription.create({\n        data: {\n          userId,\n          planId: plan.id,\n          revenueCatUserId: event.original_app_user_id,\n          status: \"ACTIVE\",\n          platform: Platform.ANDROID || Platform.IOS,\n          startedAt: new Date(event.purchased_at_ms || Date.now()),\n          currentPeriodEnd: expirationDate,\n        },\n      });\n    }\n  }\n}\n\n// Handle subscription becoming inactive\nasync function handleSubscriptionInactive(event: z.infer<typeof webhookEventSchema>[\"event\"]) {\n  const userId = event.app_user_id;\n\n  // Update user premium status\n  await prisma.user.update({\n    where: { id: userId },\n    data: { isPremium: false },\n  });\n\n  // Update subscription status\n  await prisma.subscription.updateMany({\n    where: {\n      userId,\n      platform: Platform.ANDROID || Platform.IOS,\n      status: \"ACTIVE\",\n    },\n    data: {\n      status: event.type === \"CANCELLATION\" ? \"CANCELLED\" : \"EXPIRED\",\n      cancelledAt: new Date(),\n      updatedAt: new Date(),\n    },\n  });\n}\n\n// Handle billing issues\nasync function handleBillingIssue(event: z.infer<typeof webhookEventSchema>[\"event\"]) {\n  const userId = event.app_user_id;\n\n  // Update subscription status to indicate billing issue\n  await prisma.subscription.updateMany({\n    where: {\n      userId,\n      platform: Platform.ANDROID || Platform.IOS,\n      status: \"ACTIVE\",\n    },\n    data: {\n      status: \"EXPIRED\",\n      updatedAt: new Date(),\n    },\n  });\n}\n\n// Handle product changes\nasync function handleProductChange(event: z.infer<typeof webhookEventSchema>[\"event\"]) {\n  const userId = event.app_user_id;\n\n  // For now, just update the expiration date\n  // In a more complex system, you might update the plan as well\n  const expirationDate = event.expiration_at_ms ? new Date(event.expiration_at_ms) : null;\n\n  await prisma.subscription.updateMany({\n    where: {\n      userId,\n      platform: Platform.ANDROID || Platform.IOS,\n      status: \"ACTIVE\",\n    },\n    data: {\n      currentPeriodEnd: expirationDate,\n      updatedAt: new Date(),\n    },\n  });\n}\n"
  },
  {
    "path": "app/api/user/password/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { ActionError } from \"@/shared/api/safe-actions\";\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\nimport { updateUserPassword } from \"@/features/update-password/model/update-password.action\";\n\nconst changePasswordSchema = z.object({\n  currentPassword: z.string().min(1),\n  newPassword: z.string().min(8),\n  confirmPassword: z.string().min(8),\n});\n\nexport async function PUT(req: NextRequest) {\n  try {\n    const session = await getMobileCompatibleSession(req);\n\n    if (!session?.user) {\n      return NextResponse.json({ error: \"UNAUTHORIZED\" }, { status: 401 });\n    }\n\n    const body = await req.json();\n    const parsed = changePasswordSchema.safeParse(body);\n\n    if (!parsed.success) {\n      return NextResponse.json({ error: \"INVALID_INPUT\", details: parsed.error.format() }, { status: 400 });\n    }\n\n    // Use the core password update function directly\n    await updateUserPassword(session.user.id, parsed.data.currentPassword, parsed.data.newPassword, parsed.data.confirmPassword);\n\n    return NextResponse.json({\n      success: true,\n      message: \"Password updated successfully\",\n    });\n  } catch (error: any) {\n    console.error(\"Update password error:\", error);\n\n    // Handle ActionError instances\n    if (error instanceof ActionError) {\n      return NextResponse.json({ error: error.message }, { status: 400 });\n    }\n\n    return NextResponse.json({ error: \"INTERNAL_SERVER_ERROR\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/user/profile/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\n\nconst updateProfileSchema = z.object({\n  firstName: z.string().min(1).optional(),\n  lastName: z.string().min(1).optional(),\n  image: z.string().url().optional(),\n});\n\n// GET current user profile\nexport async function GET(req: NextRequest) {\n  try {\n    const session = await getMobileCompatibleSession(req);\n\n    if (!session?.user) {\n      return NextResponse.json({ error: \"UNAUTHORIZED\" }, { status: 401 });\n    }\n\n    return NextResponse.json({\n      user: {\n        id: session.user.id,\n        email: session.user.email,\n        firstName: session.user.firstName,\n        lastName: session.user.lastName,\n        name: session.user.name,\n        image: session.user.image,\n      },\n    });\n  } catch (error) {\n    console.error(\"Get profile error:\", error);\n    return NextResponse.json({ error: \"INTERNAL_SERVER_ERROR\" }, { status: 500 });\n  }\n}\n\n// PUT update user profile\nexport async function PUT(req: NextRequest) {\n  try {\n    const session = await getMobileCompatibleSession(req);\n\n    if (!session?.user) {\n      return NextResponse.json({ error: \"UNAUTHORIZED\" }, { status: 401 });\n    }\n\n    const body = await req.json();\n    const parsed = updateProfileSchema.safeParse(body);\n\n    if (!parsed.success) {\n      return NextResponse.json({ error: \"INVALID_INPUT\", details: parsed.error.format() }, { status: 400 });\n    }\n\n    // Update user directly\n    const { firstName, lastName, image } = parsed.data;\n\n    // Build update object with only provided fields\n    const updateData: Record<string, any> = {};\n    if (firstName !== undefined) updateData.firstName = firstName;\n    if (lastName !== undefined) updateData.lastName = lastName;\n    if (image !== undefined) updateData.image = image;\n\n    // Only perform update if there are fields to update\n    if (Object.keys(updateData).length > 0) {\n      await prisma.user.update({\n        where: { id: session.user.id },\n        data: updateData,\n      });\n    }\n\n    // Get updated user data\n    const updatedSession = await getMobileCompatibleSession(req);\n\n    return NextResponse.json({\n      success: true,\n      user: updatedSession?.user,\n    });\n  } catch (error) {\n    console.error(\"Update profile error:\", error);\n    return NextResponse.json({ error: \"INTERNAL_SERVER_ERROR\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/webhooks/revenuecat/route.ts",
    "content": "/* eslint-disable @typescript-eslint/naming-convention */\n\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { RevenueCatMapping } from \"@/shared/lib/revenuecat/revenuecat.mapping\";\nimport { RevenueCatConfig } from \"@/shared/lib/revenuecat/revenuecat.config\";\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { PremiumService } from \"@/shared/lib/premium/premium.service\";\n\n/**\n * RevenueCat Webhook Handler\n *\n * Processes RevenueCat webhook events for subscription lifecycle management\n * Follows security best practices with signature validation\n */\n\ninterface RevenueCatWebhookEvent {\n  event: {\n    type: string;\n    event_timestamp_ms: number;\n    app_user_id: string;\n    original_app_user_id?: string;\n    aliases?: string[];\n    product_id?: string;\n    period_type?: \"NORMAL\" | \"TRIAL\" | \"INTRO\";\n    purchased_at?: number;\n    expiration_at_ms?: number;\n    environment: \"SANDBOX\" | \"PRODUCTION\";\n    entitlement_id?: string;\n    purchased_at_ms?: number;\n    entitlement_ids?: string[];\n    price?: number;\n    currency?: string;\n    is_family_share?: boolean;\n    country_code?: string;\n    app_id: string;\n    offer_code?: string;\n    takehome_percentage?: number;\n    transaction_id?: string;\n    original_transaction_id?: string;\n    is_renewal?: boolean;\n    cancel_reason?: string;\n    grace_period_expiration_at?: number;\n    auto_resume_at?: number;\n    store?: \"APP_STORE\" | \"MAC_APP_STORE\" | \"PLAY_STORE\" | \"STRIPE\" | \"PROMOTIONAL\";\n  };\n}\n\n/**\n * Verify RevenueCat webhook authorization header\n * RevenueCat uses a simple authorization header verification, not HMAC signatures\n */\nfunction verifyWebhookAuthorization(authHeader: string, expectedSecret: string): boolean {\n  try {\n    // RevenueCat sends the authorization header as \"Bearer <your_secret>\"\n    const receivedSecret = authHeader.replace(\"Bearer \", \"\");\n    return receivedSecret === expectedSecret;\n  } catch (error) {\n    console.error(\"Error verifying webhook authorization:\", error);\n    return false;\n  }\n}\n\n/**\n * Log webhook event for debugging and monitoring\n */\nasync function logWebhookEvent(event: RevenueCatWebhookEvent, success: boolean, processingError?: string) {\n  console.log(\"event:\", event);\n  try {\n    await prisma.revenueCatWebhookEvent.create({\n      data: {\n        eventType: event.event.type,\n        eventTimestamp: new Date(event.event.event_timestamp_ms),\n        appUserId: event.event.app_user_id,\n        environment: event.event.environment,\n        productId: event.event.product_id,\n        transactionId: event.event.transaction_id,\n        originalTransactionId: event.event.original_transaction_id,\n        entitlementIds: event.event.entitlement_ids ? JSON.stringify(event.event.entitlement_ids) : null,\n        processed: success,\n        processingError: processingError,\n        rawEventData: JSON.stringify(event),\n      },\n    });\n\n    // Also log to console in development\n    if (RevenueCatConfig.isDevelopment()) {\n      console.log(\"RevenueCat webhook event:\", {\n        type: event.event.type,\n        appUserId: event.event.app_user_id,\n        environment: event.event.environment,\n        success,\n        timestamp: new Date(event.event.event_timestamp_ms * 1000).toISOString(),\n      });\n    }\n  } catch (error) {\n    console.error(\"Failed to log webhook event:\", error);\n  }\n}\n\n/**\n * Process subscription events\n */\nasync function processSubscriptionEvent(webhook: RevenueCatWebhookEvent) {\n  const { type, app_user_id, expiration_at_ms, product_id } = webhook.event;\n\n  // Find user by RevenueCat user ID\n  const user = await prisma.user.findFirst({\n    where: {\n      subscriptions: {\n        some: {\n          OR: [{ userId: app_user_id }, { revenueCatUserId: app_user_id }],\n        },\n      },\n    },\n    include: {\n      subscriptions: true,\n    },\n  });\n\n  if (!user) {\n    console.warn(`User not found for RevenueCat user ID: ${app_user_id}`);\n    return;\n  }\n\n  // Validate product ID if provided\n  if (product_id) {\n    const isValidProduct = await RevenueCatMapping.validateProductId(product_id);\n    if (!isValidProduct) {\n      console.warn(`Unknown RevenueCat product ID: ${product_id}`);\n      // Continue processing but log the warning\n    }\n  }\n\n  const expirationDate = expiration_at_ms ? new Date(expiration_at_ms) : null;\n\n  switch (type) {\n    case \"INITIAL_PURCHASE\":\n    case \"RENEWAL\":\n    case \"PRODUCT_CHANGE\":\n      await handlePurchaseEvent(webhook);\n      break;\n\n    case \"CANCELLATION\":\n    case \"EXPIRATION\":\n    case \"BILLING_ISSUE\":\n      await handleCancellationEvent(webhook);\n      break;\n\n    case \"UNCANCELLATION\":\n      // Restore premium access\n      await prisma.user.update({\n        where: { id: user.id },\n        data: { isPremium: true },\n      });\n\n      await prisma.subscription.updateMany({\n        where: {\n          userId: user.id,\n          revenueCatUserId: app_user_id,\n        },\n        data: {\n          status: \"ACTIVE\",\n          cancelledAt: null,\n          updatedAt: new Date(),\n        },\n      });\n      break;\n\n    default:\n      console.log(`Unhandled RevenueCat event type: ${type}`);\n  }\n}\n\nasync function handleCancellationEvent(webhook: RevenueCatWebhookEvent) {\n  const { app_user_id, type } = webhook.event;\n\n  console.log(`[RevenueCat Webhook] Processing ${type} for user: ${app_user_id}`);\n\n  // Check if this is an anonymous user\n  const isAnonymous = app_user_id.startsWith(\"$RCAnonymousID:\");\n\n  if (isAnonymous) {\n    console.log(`[RevenueCat Webhook] Anonymous user ${type} - storing event`);\n\n    // Store the cancellation/expiration event\n    await prisma.revenueCatWebhookEvent.create({\n      data: {\n        eventType: webhook.event.type,\n        eventTimestamp: new Date(),\n        appUserId: app_user_id,\n        environment: webhook.event.environment,\n        rawEventData: JSON.stringify(webhook.event),\n        processed: false,\n      },\n    });\n\n    return;\n  }\n\n  // Handle authenticated user cancellation/expiration\n  const user = await prisma.user.findUnique({\n    where: { id: app_user_id },\n  });\n\n  if (user) {\n    console.log(`[RevenueCat Webhook] Processing ${type} for authenticated user: ${user.id}`);\n\n    // Sync with RevenueCat to get current status\n    // This will update the user's premium status based on current entitlements\n    await PremiumService.syncRevenueCatStatus(user.id, app_user_id);\n\n    console.log(`[RevenueCat Webhook] ${type} processed for user: ${user.id}`);\n  } else {\n    console.log(`[RevenueCat Webhook] User not found for ${type} event: ${app_user_id}`);\n  }\n}\n\n/**\n * POST /api/webhooks/revenuecat\n *\n * Handle RevenueCat webhook events\n */\nexport async function POST(request: NextRequest) {\n  try {\n    // Check if RevenueCat is configured\n    if (!RevenueCatConfig.isConfigured()) {\n      console.warn(\"RevenueCat webhook received but not configured\");\n      console.log(RevenueCatConfig.getWebhookConfig());\n      return NextResponse.json({ error: \"RevenueCat not configured\" }, { status: 503 });\n    }\n\n    // Get raw body and authorization header\n    const body = await request.text();\n    const authHeader = request.headers.get(\"Authorization\") || \"\";\n\n    // Verify webhook authorization\n    const webhookConfig = RevenueCatConfig.getWebhookConfig();\n    const isValidAuth = verifyWebhookAuthorization(authHeader, webhookConfig.secret);\n\n    if (!isValidAuth) {\n      console.error(\"Invalid RevenueCat webhook authorization\");\n      return NextResponse.json({ error: \"Invalid authorization\" }, { status: 401 });\n    }\n\n    // Parse webhook event\n    const event: RevenueCatWebhookEvent = JSON.parse(body);\n\n    // Log event for monitoring\n    await logWebhookEvent(event, true);\n\n    // Process subscription event\n    await processSubscriptionEvent(event);\n\n    return NextResponse.json({ received: true });\n  } catch (error) {\n    console.error(\"RevenueCat webhook error:\", error);\n\n    // Log failed event\n    try {\n      const body = await request.text();\n      const event: RevenueCatWebhookEvent = JSON.parse(body);\n      await logWebhookEvent(event, false, error instanceof Error ? error.message : \"Unknown error\");\n    } catch (logError) {\n      console.error(\"Failed to log failed webhook event:\", logError);\n    }\n\n    return NextResponse.json({ error: \"Webhook processing failed\" }, { status: 500 });\n  }\n}\n\n/**\n * GET /api/webhooks/revenuecat\n *\n * Health check endpoint for webhook configuration\n */\nexport async function GET() {\n  return NextResponse.json({\n    status: \"RevenueCat webhook endpoint active\",\n    configured: RevenueCatConfig.isConfigured(),\n    environment: RevenueCatConfig.isDevelopment() ? \"development\" : \"production\",\n  });\n}\n\nasync function handlePurchaseEvent(webhook: RevenueCatWebhookEvent) {\n  const { app_user_id, product_id, entitlement_id, purchased_at_ms } = webhook.event;\n\n  console.log(`[RevenueCat Webhook] Processing purchase for user: ${app_user_id}`);\n\n  // Check if this is an anonymous user\n  const isAnonymous = app_user_id.startsWith(\"$RCAnonymousID:\");\n\n  if (isAnonymous) {\n    console.log(\"[RevenueCat Webhook] Anonymous user purchase detected\");\n\n    // Store the webhook event for later processing when user authenticates\n    await prisma.revenueCatWebhookEvent.create({\n      data: {\n        eventType: webhook.event.type,\n        eventTimestamp: new Date(purchased_at_ms || Date.now()),\n        appUserId: app_user_id,\n        environment: webhook.event.environment,\n        productId: product_id,\n        entitlementIds: entitlement_id ? JSON.stringify([entitlement_id]) : null,\n        rawEventData: JSON.stringify(webhook.event),\n        processed: false, // Will be processed when user authenticates\n      },\n    });\n\n    console.log(\"[RevenueCat Webhook] Stored anonymous purchase event for later processing\");\n    return;\n  }\n\n  // Handle authenticated user purchases\n  console.log(`[RevenueCat Webhook] Processing purchase for authenticated user: ${app_user_id}`);\n\n  // Find user by their authenticated ID (not RevenueCat ID)\n  const user = await prisma.user.findUnique({\n    where: { id: app_user_id },\n  });\n\n  if (!user) {\n    console.log(`[RevenueCat Webhook] User not found for ID: ${app_user_id}`);\n    return;\n  }\n\n  // Sync the purchase status\n  await PremiumService.syncRevenueCatStatus(user.id, app_user_id);\n\n  // Mark any pending anonymous events as processed\n  await prisma.revenueCatWebhookEvent.updateMany({\n    where: {\n      appUserId: app_user_id,\n      processed: false,\n    },\n    data: {\n      processed: true,\n      updatedAt: new Date(),\n    },\n  });\n}\n"
  },
  {
    "path": "app/api/webhooks/stripe/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { PremiumManager } from \"@/shared/lib/premium/premium.manager\";\n\n/**\n * POST /api/webhooks/stripe\n * \n * Handle Stripe webhooks - New Premium System Only\n * Simple, clean implementation without legacy fallbacks\n */\nexport async function POST(request: NextRequest) {\n  try {\n    const body = await request.text();\n    const signature = request.headers.get(\"stripe-signature\");\n\n    if (!signature) {\n      return NextResponse.json({ error: \"Missing signature\" }, { status: 401 });\n    }\n\n    const result = await PremiumManager.processWebhook(\"stripe\", body, signature);\n    \n    if (result.success) {\n      return NextResponse.json({ received: true });\n    } else {\n      console.error(\"Webhook processing failed:\", result.error);\n      return NextResponse.json({ error: result.error }, { status: 400 });\n    }\n  } catch (error) {\n    console.error(\"Stripe webhook error:\", error);\n    return NextResponse.json({ error: \"Webhook handler failed\" }, { status: 400 });\n  }\n}"
  },
  {
    "path": "app/api/workout-sessions/[sessionId]/feedback/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\n\n// Validation schema for feedback\nconst feedbackSchema = z.object({\n  feedbackEmoji: z.enum([\"😃\", \"🙂\", \"😐\", \"🙁\", \"😢\"]).optional(),\n  feedbackText: z.string().max(200).optional(),\n});\n\nexport async function POST(request: NextRequest, { params }: { params: Promise<{ sessionId: string }> }) {\n  try {\n    // Check authentication\n    const session = await getMobileCompatibleSession(request);\n    if (!session?.user) {\n      return NextResponse.json({ error: \"Not authenticated\" }, { status: 401 });\n    }\n\n    // Parse and validate request body\n    const body = await request.json();\n    const parsed = feedbackSchema.safeParse(body);\n\n    if (!parsed.success) {\n      return NextResponse.json({ error: \"INVALID_INPUT\", details: parsed.error.format() }, { status: 400 });\n    }\n\n    const { feedbackEmoji, feedbackText } = parsed.data;\n\n    // Await params\n    const { sessionId } = await params;\n\n    // Verify the workout session exists and belongs to the user\n    const workoutSession = await prisma.workoutSession.findFirst({\n      where: {\n        id: sessionId,\n        userId: session.user.id,\n      },\n    });\n\n    if (!workoutSession) {\n      return NextResponse.json({ error: \"Workout session not found or unauthorized\" }, { status: 404 });\n    }\n\n    // Update the workout session with feedback (map to existing fields)\n    const updatedSession = await prisma.workoutSession.update({\n      where: { id: sessionId },\n      data: {\n        ratingComment: feedbackText || null,\n      },\n    });\n\n    return NextResponse.json({\n      success: true,\n      data: {\n        feedbackEmoji: feedbackEmoji || null,\n        feedbackText: updatedSession.ratingComment,\n      },\n    });\n  } catch (error) {\n    console.error(\"Error updating workout session feedback:\", error);\n    return NextResponse.json({ error: \"Failed to update feedback\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/workout-sessions/[sessionId]/rating/route.ts",
    "content": "import { z } from \"zod\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\n\n// Validation schema for rating\nconst ratingSchema = z.object({\n  rating: z.number().int().min(1).max(5),\n  ratingComment: z.string().optional(),\n});\n\nexport async function POST(request: NextRequest, { params }: { params: Promise<{ sessionId: string }> }) {\n  try {\n    // Check authentication\n    const session = await getMobileCompatibleSession(request);\n    if (!session?.user) {\n      return NextResponse.json({ error: \"Not authenticated\" }, { status: 401 });\n    }\n\n    // Parse and validate request body\n    const body = await request.json();\n    const parsed = ratingSchema.safeParse(body);\n\n    if (!parsed.success) {\n      return NextResponse.json({ error: \"INVALID_INPUT\", details: parsed.error.format() }, { status: 400 });\n    }\n\n    const { rating, ratingComment } = parsed.data;\n\n    // Await params\n    const { sessionId } = await params;\n\n    // Verify the workout session exists and belongs to the user\n    const workoutSession = await prisma.workoutSession.findFirst({\n      where: {\n        id: sessionId,\n        userId: session.user.id,\n      },\n    });\n\n    if (!workoutSession) {\n      return NextResponse.json({ error: \"Workout session not found or unauthorized\" }, { status: 404 });\n    }\n\n    // Update the workout session with rating\n    const updatedSession = await prisma.workoutSession.update({\n      where: { id: sessionId },\n      data: {\n        rating,\n        ratingComment,\n      },\n    });\n\n    return NextResponse.json({\n      success: true,\n      data: {\n        rating: updatedSession.rating,\n        ratingComment: updatedSession.ratingComment,\n      },\n    });\n  } catch (error) {\n    console.error(\"Error updating workout session rating:\", error);\n    return NextResponse.json({ error: \"Failed to update rating\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/workout-sessions/[sessionId]/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\nimport { deleteWorkoutSessionAction } from \"@/features/workout-session/actions/delete-workout-session.action\";\n\nexport async function DELETE(request: NextRequest, { params }: { params: Promise<{ sessionId: string }> }) {\n  try {\n    const { sessionId } = await params;\n    // Check authentication\n    const session = await getMobileCompatibleSession(request);\n    if (!session?.user) {\n      return NextResponse.json({ error: \"Not authenticated\" }, { status: 401 });\n    }\n\n    // Use the existing server action for deletion\n    const result = await deleteWorkoutSessionAction({\n      id: sessionId,\n    });\n\n    if (result?.serverError) {\n      if (result.serverError === \"NOT_AUTHENTICATED\") {\n        return NextResponse.json({ error: \"Not authenticated\" }, { status: 401 });\n      }\n      if (result.serverError === \"Unauthorized\") {\n        return NextResponse.json({ error: \"Unauthorized\" }, { status: 403 });\n      }\n      if (result.serverError === \"Session not found\") {\n        return NextResponse.json({ error: \"Session not found\" }, { status: 404 });\n      }\n      return NextResponse.json({ error: result.serverError }, { status: 500 });\n    }\n\n    return NextResponse.json({ success: true });\n  } catch (error) {\n    console.error(\"Error deleting workout session:\", error);\n    return NextResponse.json({ error: \"Internal server error\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/workout-sessions/[sessionId]/summary/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\n\nexport async function GET(request: NextRequest, { params }: { params: Promise<{ sessionId: string }> }) {\n  try {\n    // Check authentication\n    const session = await getMobileCompatibleSession(request);\n    if (!session?.user) {\n      return NextResponse.json({ error: \"Not authenticated\" }, { status: 401 });\n    }\n\n    // Await params\n    const { sessionId } = await params;\n\n    // Get the workout session with all related data\n    const workoutSession = await prisma.workoutSession.findFirst({\n      where: {\n        id: sessionId,\n        userId: session.user.id,\n      },\n      include: {\n        exercises: {\n          include: {\n            exercise: {\n              select: {\n                name: true,\n                nameEn: true,\n                attributes: {\n                  include: {\n                    attributeName: true,\n                    attributeValue: true,\n                  },\n                },\n              },\n            },\n            sets: true,\n          },\n          orderBy: {\n            order: \"asc\",\n          },\n        },\n      },\n    });\n\n    if (!workoutSession) {\n      return NextResponse.json({ error: \"Workout session not found or unauthorized\" }, { status: 404 });\n    }\n\n    // Calculate summary metrics\n    let totalSets = 0;\n    let totalReps = 0;\n    let totalVolume = 0; // weight × reps\n    let totalWeightLifted = 0;\n    const exerciseCount = workoutSession.exercises.length;\n\n    workoutSession.exercises.forEach((sessionExercise) => {\n      sessionExercise.sets.forEach((set) => {\n        if (set.completed) {\n          totalSets++;\n\n          // Calculate reps\n          if (set.types.includes(\"REPS\") && set.valuesInt.length > 0) {\n            const reps = set.valuesInt[0];\n            totalReps += reps;\n\n            // Calculate volume if weight is present\n            if (set.types.includes(\"WEIGHT\") && set.valuesInt.length > 1) {\n              const weight = set.valuesInt[1];\n              totalVolume += weight * reps;\n              totalWeightLifted += weight;\n            }\n          }\n        }\n      });\n    });\n\n    // Calculate calories burned (simplified estimation)\n    // Using MET (Metabolic Equivalent of Task) values\n    // Strength training: ~6 METs\n    // Formula: METs × weight in kg × hours\n    const durationHours = (workoutSession.duration || 0) / 3600;\n    const assumedWeightKg = 70; // Default weight, could be user-specific\n    const metValue = 6; // Strength training MET\n    const caloriesBurned = Math.round(metValue * assumedWeightKg * durationHours);\n\n    return NextResponse.json({\n      success: true,\n      data: {\n        id: workoutSession.id,\n        startedAt: workoutSession.startedAt,\n        endedAt: workoutSession.endedAt,\n        duration: workoutSession.duration,\n\n        // Summary metrics\n        totalSets,\n        totalReps,\n        totalVolume,\n        totalWeightLifted,\n        exerciseCount,\n        caloriesBurned,\n\n        // Target muscles\n        muscles: workoutSession.muscles,\n\n        rating: workoutSession.rating,\n        ratingComment: workoutSession.ratingComment,\n\n        // Exercise details\n        exercises: workoutSession.exercises.map((sessionExercise) => ({\n          name: sessionExercise.exercise.name,\n          nameEn: sessionExercise.exercise.nameEn,\n          completedSets: sessionExercise.sets.filter((s) => s.completed).length,\n          totalSets: sessionExercise.sets.length,\n        })),\n      },\n    });\n  } catch (error) {\n    console.error(\"Error fetching workout session summary:\", error);\n    return NextResponse.json({ error: \"Failed to fetch session summary\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/workout-sessions/sync/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\nimport { syncWorkoutSessionAction } from \"@/features/workout-session/actions/sync-workout-sessions.action\";\n\nexport async function POST(request: NextRequest) {\n  try {\n    const session = await getMobileCompatibleSession(request);\n    console.log(\"session:\", session);\n\n    if (!session?.user) {\n      return NextResponse.json({ error: \"Not authenticated\" }, { status: 401 });\n    }\n\n    const body = await request.json();\n\n    if (!body.session) {\n      return NextResponse.json({ error: \"Session data is required\" }, { status: 400 });\n    }\n\n    // Use the existing server action\n    const result = await syncWorkoutSessionAction({ session: body.session });\n\n    if (result?.serverError) {\n      return NextResponse.json({ error: result.serverError }, { status: 500 });\n    }\n\n    if (result?.data) {\n      return NextResponse.json({\n        success: true,\n        data: result.data,\n      });\n    }\n\n    console.error(\"Failed to sync session\", JSON.stringify(result, null, 2));\n    return NextResponse.json({ error: \"Failed to sync session\" }, { status: 500 });\n  } catch (error) {\n    console.error(\"Error in workout session sync:\", error);\n    return NextResponse.json({ error: \"Internal server error\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/workout-sessions/user/[userId]/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\n\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\nimport { getWorkoutSessionsAction } from \"@/features/workout-session/actions/get-workout-sessions.action\";\n\nexport async function GET(request: NextRequest) {\n  try {\n    // Check authentication\n    const session = await getMobileCompatibleSession(request);\n\n    if (!session?.user) {\n      return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n    }\n\n    // Fetch workout sessions\n    const result = await getWorkoutSessionsAction({ userId: session.user.id });\n\n    // Transform to match the expected response format\n    const formattedSessions = result?.data?.sessions?.map((session) => ({\n      id: session.id,\n      userId: session.userId,\n      startedAt: session.startedAt.toISOString(),\n      endedAt: session.endedAt?.toISOString() || null,\n      duration: session.duration || null,\n      muscles: session.muscles || [],\n      rating: session.rating || null,\n      ratingComment: session.ratingComment || null,\n      exercises: session.exercises.map((sessionExercise) => ({\n        id: sessionExercise.exerciseId,\n        order: sessionExercise.order,\n        exercise: {\n          ...sessionExercise.exercise,\n          createdAt: sessionExercise.exercise.createdAt.toISOString(),\n          updatedAt: sessionExercise.exercise.updatedAt.toISOString(),\n          attributes: sessionExercise.exercise.attributes.map((attr) => ({\n            id: attr.id,\n            exerciseId: attr.exerciseId,\n            attributeNameId: attr.attributeNameId,\n            attributeValueId: attr.attributeValueId,\n            createdAt: attr.createdAt.toISOString(),\n            updatedAt: attr.updatedAt.toISOString(),\n            attributeName: {\n              id: attr.attributeName.id,\n              name: attr.attributeName.name,\n              createdAt: attr.attributeName.createdAt.toISOString(),\n              updatedAt: attr.attributeName.updatedAt.toISOString(),\n            },\n            attributeValue: {\n              id: attr.attributeValue.id,\n              attributeNameId: attr.attributeValue.attributeNameId,\n              value: attr.attributeValue.value,\n              createdAt: attr.attributeValue.createdAt.toISOString(),\n              updatedAt: attr.attributeValue.updatedAt.toISOString(),\n            },\n          })),\n        },\n        sets: sessionExercise.sets.map((set) => ({\n          id: set.id,\n          workoutSessionExerciseId: set.workoutSessionExerciseId,\n          setIndex: set.setIndex,\n          type: set.type,\n          types: set.types || [],\n          valuesInt: set.valuesInt || [],\n          valuesSec: set.valuesSec || [],\n          units: set.units || [],\n          completed: set.completed,\n        })),\n      })),\n    }));\n\n    return NextResponse.json(formattedSessions);\n  } catch (error) {\n    console.error(\"Error fetching workout sessions:\", error);\n    return NextResponse.json({ error: \"Internal server error\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/robots.txt",
    "content": "User-agent: *\nAllow: /\nDisallow: /admin/\nDisallow: /api/\nDisallow: /dashboard/\nDisallow: /preview/\nDisallow: /auth/\nDisallow: /onboarding/\nDisallow: /profile/\nDisallow: /_next/\nDisallow: /.*\\?\n\n# Crawl delay\nCrawl-delay: 1\n\n# Specific bot configurations\nUser-agent: Googlebot\nAllow: /\nDisallow: /admin/\nDisallow: /api/\nDisallow: /auth/\nDisallow: /onboarding/\nDisallow: /profile/\n\nUser-agent: Bingbot\nAllow: /\nDisallow: /admin/\nDisallow: /api/\nDisallow: /auth/\nDisallow: /onboarding/\nDisallow: /profile/\n\n# Sitemap\nSitemap: https://www.workout.cool/sitemap.xml\n"
  },
  {
    "path": "app/sitemap.ts",
    "content": "import { MetadataRoute } from \"next/types\";\n\nimport { getSitemapData } from \"@/features/programs/actions/get-sitemap-data.action\";\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n  const baseUrl = \"https://www.workout.cool\";\n  const currentDate = new Date().toISOString();\n\n  // Static routes with locale support\n  const locales = [\"fr\", \"en\", \"es\", \"pt\", \"ru\", \"zh-CN\"];\n\n  const staticRoutes = [\n    // Home page (root)\n    {\n      url: baseUrl,\n      lastModified: currentDate,\n      changeFrequency: \"daily\" as const,\n      priority: 1.0,\n    },\n    // Home pages for all locales\n    ...locales.map((locale) => ({\n      url: `${baseUrl}/${locale}`,\n      lastModified: currentDate,\n      changeFrequency: \"daily\" as const,\n      priority: 1.0,\n    })),\n    // Tools pages for all locales\n    ...locales.map((locale) => ({\n      url: `${baseUrl}/${locale}/tools`,\n      lastModified: currentDate,\n      changeFrequency: \"weekly\" as const,\n      priority: 0.8,\n    })),\n    // Calorie calculator hub pages for all locales\n    ...locales.map((locale) => ({\n      url: `${baseUrl}/${locale}/tools/calorie-calculator`,\n      lastModified: currentDate,\n      changeFrequency: \"weekly\" as const,\n      priority: 0.9,\n    })),\n    // Calorie calculator formula pages for all locales\n    ...locales.flatMap((locale) =>\n      [\n        `${baseUrl}/${locale}/tools/calorie-calculator`,\n        `${baseUrl}/${locale}/tools/calorie-calculator/mifflin-st-jeor-calculator`,\n        `${baseUrl}/${locale}/tools/calorie-calculator/harris-benedict-calculator`,\n        `${baseUrl}/${locale}/tools/calorie-calculator/katch-mcardle-calculator`,\n        `${baseUrl}/${locale}/tools/calorie-calculator/cunningham-calculator`,\n        `${baseUrl}/${locale}/tools/calorie-calculator/oxford-calculator`,\n        `${baseUrl}/${locale}/tools/calorie-calculator/calorie-calculator-comparison`,\n      ].map((url) => ({\n        url,\n        lastModified: currentDate,\n        changeFrequency: \"monthly\" as const,\n        priority: 0.85,\n      })),\n    ),\n\n    // Heart rate calculator pages for all locales\n    ...locales.flatMap((locale) =>\n      [`${baseUrl}/${locale}/tools/heart-rate-zones`].map((url) => ({\n        url,\n        lastModified: currentDate,\n        changeFrequency: \"monthly\" as const,\n        priority: 0.85,\n      })),\n    ),\n\n    // BMI calculator\n    ...locales.flatMap((locale) =>\n      [`${baseUrl}/${locale}/tools/bmi-calculator`].map((url) => ({\n        url,\n        lastModified: currentDate,\n        changeFrequency: \"monthly\" as const,\n        priority: 0.85,\n      })),\n    ),\n    // Auth pages (lower priority as they're functional pages)\n    {\n      url: `${baseUrl}/auth/signin`,\n      lastModified: currentDate,\n      changeFrequency: \"monthly\" as const,\n      priority: 0.3,\n    },\n    {\n      url: `${baseUrl}/auth/signup`,\n      lastModified: currentDate,\n      changeFrequency: \"monthly\" as const,\n      priority: 0.3,\n    },\n    // About pages for all locales\n    {\n      url: `${baseUrl}/about`,\n      lastModified: currentDate,\n      changeFrequency: \"monthly\" as const,\n      priority: 0.7,\n    },\n    ...locales.map((locale) => ({\n      url: `${baseUrl}/${locale}/about`,\n      lastModified: currentDate,\n      changeFrequency: \"monthly\" as const,\n      priority: 0.7,\n    })),\n    // Tools pages for all locales\n    {\n      url: `${baseUrl}/tools`,\n      lastModified: currentDate,\n      changeFrequency: \"monthly\" as const,\n      priority: 0.7,\n    },\n\n    // Legal pages\n    {\n      url: `${baseUrl}/legal/privacy`,\n      lastModified: currentDate,\n      changeFrequency: \"yearly\" as const,\n      priority: 0.2,\n    },\n    {\n      url: `${baseUrl}/legal/terms`,\n      lastModified: currentDate,\n      changeFrequency: \"yearly\" as const,\n      priority: 0.2,\n    },\n    {\n      url: `${baseUrl}/legal/sales-terms`,\n      lastModified: currentDate,\n      changeFrequency: \"yearly\" as const,\n      priority: 0.2,\n    },\n    // Legal pages for all locales\n    ...locales.flatMap((locale) => [\n      {\n        url: `${baseUrl}/${locale}/legal/privacy`,\n        lastModified: currentDate,\n        changeFrequency: \"yearly\" as const,\n        priority: 0.2,\n      },\n      {\n        url: `${baseUrl}/${locale}/legal/terms`,\n        lastModified: currentDate,\n        changeFrequency: \"yearly\" as const,\n        priority: 0.2,\n      },\n      {\n        url: `${baseUrl}/${locale}/legal/sales-terms`,\n        lastModified: currentDate,\n        changeFrequency: \"yearly\" as const,\n        priority: 0.2,\n      },\n    ]),\n  ];\n\n  // Get dynamic program data for sitemap\n  const programsData = await getSitemapData();\n\n  // Generate dynamic routes\n  const dynamicRoutes: MetadataRoute.Sitemap = [];\n\n  // Add programs index pages for each locale\n  locales.forEach((locale) => {\n    dynamicRoutes.push({\n      url: `${baseUrl}/${locale}/programs`,\n      lastModified: new Date().toISOString(),\n      changeFrequency: \"weekly\" as const,\n      priority: 0.8,\n    });\n  });\n\n  // Add program detail pages and their sessions\n  programsData.forEach((program) => {\n    // Program detail pages for each locale\n    const programSlugs = {\n      fr: program.slug,\n      en: program.slugEn,\n      es: program.slugEs,\n      pt: program.slugPt,\n      ru: program.slugRu,\n      \"zh-CN\": program.slugZhCn,\n    };\n\n    Object.entries(programSlugs).forEach(([locale, slug]) => {\n      if (slug) {\n        // Program detail page\n        dynamicRoutes.push({\n          url: `${baseUrl}/${locale}/programs/${slug}`,\n          lastModified: program.updatedAt.toISOString(),\n          changeFrequency: \"weekly\" as const,\n          priority: 0.9,\n        });\n\n        // Program weeks and sessions\n        program.weeks.forEach((week) => {\n          week.sessions.forEach((session) => {\n            const sessionSlugs = {\n              fr: session.slug,\n              en: session.slugEn,\n              es: session.slugEs,\n              pt: session.slugPt,\n              ru: session.slugRu,\n              \"zh-CN\": session.slugZhCn,\n            };\n\n            const sessionSlug = sessionSlugs[locale as keyof typeof sessionSlugs];\n            if (sessionSlug) {\n              dynamicRoutes.push({\n                url: `${baseUrl}/${locale}/programs/${slug}/session/${sessionSlug}`,\n                // lastModified: session.updatedAt.toISOString(),\n                lastModified: new Date().toISOString(), // TODO: add this back in when we have a way to update the sitemap\n                changeFrequency: \"monthly\" as const,\n                priority: 0.7,\n              });\n            }\n          });\n        });\n      }\n    });\n  });\n\n  // Combine static and dynamic routes\n  return [...staticRoutes, ...dynamicRoutes];\n}\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/shared/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/shared/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"rsc\": true,\n  \"style\": \"new-york\",\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"app/css/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"tsx\": true\n}\n"
  },
  {
    "path": "content/about/en.mdx",
    "content": "import Link from \"next/link\";\n\n# About Workout.cool\n\n## Why Workout.cool?\n\nWorkout.cool was born out of the desire to offer a reliable, modern, and actively maintained workout platform, after the original project <WorkoutLol variant=\"muted\" /> was abandoned.\n\n## The Story\n\nWorkout.cool is the result of a community-driven adventure.\n\nI was the **first open source contributor** to the <WorkoutLol variant=\"muted\" /> project.\n\nThis means I saw the project *come to life*, *grow*, then get **sold** and ultimately **abandoned** by its new owner.\n\nLike many users, I felt a **deep frustration** and a *sense of abandonment* watching a tool I had contributed so much to disappear, with feature requests left unanswered and growing old.\n\n---\n\n*For months*, I tried to contact the new owner—**never receiving a single reply** despite many attempts (*about 15*).\n\nFaced with this **silence** and the **distress of the community**, I decided to **take matters into my own hands**:\n\n> Rather than let all this work vanish, **I relaunched an even more ambitious, modern, and open project for everyone.**\n\nThis project is not driven by profit, but by **passion** and a desire to serve the open source fitness community.\n\n**Someone had to save the community—_I decided to be that someone!_**\n\n## Open Source & Community\n\nWorkout.cool is open source, ensuring transparency, modularity, and scalability.  \nEveryone is welcome to contribute—code, documentation, or ideas!\n\n- [See the project on GitHub](https://github.com/Snouzy/workout-cool)\n- [Buy a coffee to support](https://ko-fi.com/workoutcool)\n\n## Our Partner\n\nWe're proud to partner with [Fit'Distance](https://fitdistance.io), thanks to whom all our exercise videos are free and comprehensive. Their support enables us to provide high-quality fitness content to everyone in our community.\n\n## Join the Mission!\n\nWant to contribute, suggest a feature, or simply support the project?  \nContact us or open an issue on GitHub!\n\n**[hello@workout.cool](mailto:hello@workout.cool)**\n"
  },
  {
    "path": "content/about/es.mdx",
    "content": "import Link from \"next/link\";\n\n# Acerca de Workout.cool\n\n## ¿Por qué Workout.cool?\n\nWorkout.cool nació del deseo de ofrecer una plataforma de entrenamiento confiable, moderna y activamente mantenida, después de que el proyecto original <WorkoutLol variant=\"muted\" /> fuera abandonado.\n\n## La historia\n\nWorkout.cool es el resultado de una aventura impulsada por la comunidad.\n\nFui el **primer colaborador de open source** del proyecto <WorkoutLol variant=\"muted\" />.\n\nEsto significa que vi el proyecto *cobrar vida*, *crecer*, luego ser **vendido** y finalmente **abandonado** por su nuevo propietario.\n\nComo muchos usuarios, sentí una **profunda frustración** y una *sensación de abandono* al ver desaparecer una herramienta a la que había contribuido tanto, con solicitudes de funciones sin respuesta y envejeciendo.\n\n---\n\n*Durante meses*, intenté contactar al nuevo propietario **sin recibir nunca una sola respuesta** a pesar de muchos intentos (*alrededor de 15*).\n\nAnte este **silencio** y la **angustia de la comunidad**, decidí **tomar el asunto en mis propias manos**:\n\n> En lugar de dejar que todo este trabajo desaparezca, **relancé un proyecto aún más ambicioso, moderno y abierto para todos.**\n\nEste proyecto no está impulsado por el beneficio, sino por la **pasión** y el deseo de servir a la comunidad fitness de open source.\n\n**Alguien tenía que salvar a la comunidad—_¡decidí ser ese alguien!_**\n\n## Open Source y comunidad\n\nWorkout.cool es de open source, garantizando transparencia, modularidad y escalabilidad.\n¡Todos son bienvenidos a contribuir—código, documentación o ideas!\n\n- [Ver el proyecto en GitHub](https://github.com/Snouzy/workout-cool)\n- [Comprar un café para apoyar](https://ko-fi.com/workoutcool)\n\n## Nuestro socio\n\nEstamos orgullosos de asociarnos con [Fit'Distance](https://fitdistance.io), gracias a quien todos nuestros videos de ejercicios son gratuitos y completos. Su apoyo nos permite proporcionar contenido de fitness de alta calidad a todos en nuestra comunidad.\n\n## ¡Únete a la misión!\n\n¿Quieres contribuir, sugerir una función o simplemente apoyar el proyecto?\n¡Contáctanos o abre un issue en GitHub!\n\n**[hello@workout.cool](mailto:hello@workout.cool)**"
  },
  {
    "path": "content/about/fr.mdx",
    "content": "import Link from \"next/link\";\n\n# À propos de Workout.cool\n\n## Pourquoi Workout.cool ?\n\nWorkout.cool est né de la volonté de proposer une plateforme d'entraînement fiable, moderne et maintenue, après l'abandon du projet <WorkoutLol variant=\"muted\" />.\n\n## L'histoire\n\nWorkout.cool est le fruit d'une aventure communautaire.\n\nJ'ai été le **premier contributeur open source** du projet <WorkoutLol variant=\"muted\" />.\n\nDe ce fait, j'ai vu ce projet *naître*, *grandir*, puis être **vendu** et finalement **abandonné** par son nouveau propriétaire.\n\nComme beaucoup d'utilisateurs, j'ai ressenti une **grande frustration** et un *sentiment d'abandon* en voyant disparaître un outil auquel j'avais tant contribué, et en voyant les demandes d'évolution se perdre et prendre de l'âge.\n\n---\n\n*Pendant des mois*, j'ai tenté de contacter le nouveau propriétaire, **sans jamais obtenir de réponse** malgré de nombreux essais (*environ 15*).\n\nFace à ce **silence** et à la **détresse de la communauté**, j'ai décidé de **prendre les choses en main** :\n\n> Plutôt que de laisser ce travail disparaître, **j'ai relancé un projet encore plus ambitieux, moderne et ouvert à tous.**\n\nCe projet n'est pas motivé par le profit, mais par la **passion** et l'envie de servir la communauté fitness open source.\n\n**Quelqu'un devait sauver la communauté, _j'ai décidé d'être ce quelqu'un_ !**\n\n## Open source & communauté\n\nWorkout.cool est open source, garantir transparence, modularité et évolutivité.  \nToute contribution est la bienvenue, que ce soit pour le code, la documentation ou les idées !\n\n- [Voir le projet sur GitHub](https://github.com/Snouzy/workout-cool)\n- [Payer un café en guise de soutien](https://ko-fi.com/workoutcool)\n\n## Notre partenaire\n\nNous sommes fiers de collaborer avec [Fit'Distance](https://fitdistance.io), grâce à qui toutes nos vidéos d'exercices sont gratuites et complètes. Leur soutien nous permet de fournir un contenu fitness de haute qualité à toute notre communauté.\n\n## Rejoignez la mission !\n\nVous souhaitez contribuer, proposer une fonctionnalité ou simplement soutenir le projet ?  \nContactez-nous ou ouvrez une issue sur GitHub !\n\n**[hello@workout.cool](mailto:hello@workout.cool)**"
  },
  {
    "path": "content/about/pt.mdx",
    "content": "import Link from \"next/link\";\n\n# Sobre o Workout.cool\n\n## Por que Workout.cool?\n\nWorkout.cool nasceu do desejo de oferecer uma plataforma de treino confiável, moderna e ativamente mantida, após o projeto original <WorkoutLol variant=\"muted\" /> ter sido abandonado.\n\n## A história\n\nWorkout.cool é o resultado de uma aventura impulsionada pela comunidade.\n\nFui o **primeiro colaborador open source** do projeto <WorkoutLol variant=\"muted\" />.\n\nIsso significa que vi o projeto *ganhar vida*, *crescer*, depois ser **vendido** e finalmente **abandonado** por seu novo proprietário.\n\nComo muitos usuários, senti uma **profunda frustração** e uma *sensação de abandono* ao ver desaparecer uma ferramenta para a qual havia contribuído tanto, com solicitações de recursos sem resposta e envelhecendo.\n\n---\n\n*Durante meses*, tentei contatar o novo proprietário—**sem nunca receber uma única resposta** apesar de muitas tentativas (*cerca de 15*).\n\nDiante deste **silêncio** e da **angústia da comunidade**, decidi **tomar o assunto em minhas próprias mãos**:\n\n> Em vez de deixar todo esse trabalho desaparecer, **relancei um projeto ainda mais ambicioso, moderno e aberto para todos.**\n\nEste projeto não é movido pelo lucro, mas pela **paixão** e pelo desejo de servir a comunidade fitness open source.\n\n**Alguém tinha que salvar a comunidade—_decidi ser essa pessoa!_**\n\n## Open Source e comunidade\n\nWorkout.cool é open source, garantindo transparência, modularidade e escalabilidade.\nTodos são bem-vindos para contribuir com código, documentação ou ideias!\n\n- [Ver o projeto no GitHub](https://github.com/Snouzy/workout-cool)\n- [Comprar um café para apoiar](https://ko-fi.com/workoutcool)\n\n## Nosso parceiro\n\nTemos orgulho de fazer parceria com [Fit'Distance](https://fitdistance.io), graças a quem todos os nossos vídeos de exercícios são gratuitos e completos. Seu apoio nos permite fornecer conteúdo fitness de alta qualidade para toda a nossa comunidade.\n\n## Junte-se à missão!\n\nQuer contribuir, sugerir um recurso ou simplesmente apoiar o projeto?\nEntre em contato conosco ou abra uma issue no GitHub!\n\n**[hello@workout.cool](mailto:hello@workout.cool)**"
  },
  {
    "path": "content/about/ru.mdx",
    "content": "import Link from \"next/link\";\n\n# О Workout.cool\n\n## Почему Workout.cool?\n\nWorkout.cool родился из желания предложить надежную, современную и активно поддерживаемую платформу для тренировок после того, как оригинальный проект <WorkoutLol variant=\"muted\" /> был заброшен.\n\n## История\n\nWorkout.cool — это результат приключения, движимого сообществом.\n\nЯ был **первым участником с открытым исходным кодом** проекта <WorkoutLol variant=\"muted\" />.\n\nЭто означает, что я видел, как проект *оживал*, *рос*, затем был **продан** и в конечном итоге **заброшен** его новым владельцем.\n\nКак и многие пользователи, я испытал **глубокое разочарование** и *чувство брошенности*, наблюдая, как исчезает инструмент, в который я внес такой большой вклад, с запросами функций, оставшимися без ответа и устаревающими.\n\n---\n\n*В течение месяцев* я пытался связаться с новым владельцем — **ни разу не получив ответа**, несмотря на многочисленные попытки (*около 15*).\n\nСтолкнувшись с этим **молчанием** и **отчаянием сообщества**, я решил **взять дело в свои руки**:\n\n> Вместо того чтобы позволить всей этой работе исчезнуть, **я перезапустил еще более амбициозный, современный и открытый проект для всех.**\n\nЭтот проект движим не прибылью, а **страстью** и желанием служить сообществу фитнеса с открытым исходным кодом.\n\n**Кто-то должен был спасти сообщество — _я решил быть этим человеком!_**\n\n## Открытый исходный код и сообщество\n\nWorkout.cool имеет открытый исходный код, обеспечивая прозрачность, модульность и масштабируемость.  \nКаждый может внести свой вклад — код, документацию или идеи!\n\n- [Посмотреть проект на GitHub](https://github.com/Snouzy/workout-cool)\n- [Купить кофе в поддержку](https://ko-fi.com/workoutcool)\n\n## Наш партнер\n\nМы гордимся партнерством с [Fit'Distance](https://fitdistance.io), благодаря которому все наши видео упражнений бесплатны и полноценны. Их поддержка позволяет нам предоставлять высококачественный фитнес-контент всему нашему сообществу.\n\n## Присоединяйтесь к миссии!\n\nХотите внести свой вклад, предложить функцию или просто поддержать проект?  \nСвяжитесь с нами или откройте issue на GitHub!\n\n**[hello@workout.cool](mailto:hello@workout.cool)**"
  },
  {
    "path": "content/about/zh-CN.mdx",
    "content": "import Link from \"next/link\";\n\n# 关于 Workout.cool\n\n## 为什么选择 Workout.cool？\n\nWorkout.cool 诞生于在原项目 <WorkoutLol variant=\"muted\" /> 被放弃后，为用户提供一个可靠、现代化且持续维护的健身平台的愿望。\n\n## 我们的故事\n\nWorkout.cool 是社区驱动的成果。\n\n我是 <WorkoutLol variant=\"muted\" /> 项目的**第一位开源贡献者**。\n\n这意味着我见证了这个项目的*诞生*、*成长*，然后被**出售**，最终被新所有者**抛弃**。\n\n和许多用户一样，我感到**深深的沮丧**和*被抛弃的感觉*，看着一个我贡献了这么多的工具消失，功能请求得不到回应并逐渐过时。\n\n---\n\n*几个月来*，我试图联系新所有者——尽管尝试了很多次（*大约15次*），但**从未收到过任何回复**。\n\n面对这种**沉默**和**社区的困境**，我决定**自己动手**：\n\n> 与其让所有这些工作消失，**我重新启动了一个更加雄心勃勃、现代化、对所有人开放的项目。**\n\n这个项目不是由利润驱动，而是由**激情**和为开源健身社区服务的愿望驱动。\n\n**必须有人来拯救这个社区——_我决定成为那个人！_**\n\n## 开源与社区\n\nWorkout.cool 是开源的，确保透明度、模块化和可扩展性。  \n欢迎所有人贡献——代码、文档或想法！\n\n- [在 GitHub 上查看项目](https://github.com/Snouzy/workout-cool)\n- [买杯咖啡支持我们](https://ko-fi.com/workoutcool)\n\n## 我们的合作伙伴\n\n我们很自豪能与 [Fit'Distance](https://fitdistance.io) 合作，感谢他们让我们所有的运动视频都免费且完整。他们的支持使我们能够为整个社区提供高质量的健身内容。\n\n## 加入我们的使命！\n\n想要贡献、建议功能或仅仅支持项目？  \n联系我们或在 GitHub 上创建 issue！\n\n**[hello@workout.cool](mailto:hello@workout.cool)**"
  },
  {
    "path": "content/privacy-policy/en.mdx",
    "content": "# WorkoutCool Privacy Policy\n\n## 1. Introduction\n\nAt **WorkoutCool**, we take our users' privacy very seriously.  \nThis Privacy Policy explains how we collect, use, and protect your personal data.  \nBy using our services, you agree to the practices described below.\n\n## 2. Information We Collect\n\n**Personal Information**: When you sign up, we collect your name, email address, and other details related to your activity on WorkoutCool.\n\n**Usage Data**: We track interactions with WorkoutCool pages, including clicks, views, and link performance.\n\n**Cookies**: We use cookies to enhance your user experience and monitor usage.\n\n## 3. How We Use Your Information\n\n- **Service delivery**: To provide, maintain, and improve our services.\n- **Communication**: To send you updates, notifications, or marketing content (if you have consented).\n- **Analytics**: To understand how our services are used and improve them continuously.\n\n## 4. Sharing Data with Third Parties\n\nWe may share aggregated or anonymized data with trusted partners for marketing, analytics, or product improvement purposes.  \nNo personal data is sold or shared without your explicit consent.\n\n## 5. Data Retention\n\nYour data is retained for as long as your account is active or necessary to provide our services.  \nSome data may be archived for legal, administrative, or security purposes.\n\n## 6. Protecting Your Data\n\nWe implement industry-standard security measures to prevent unauthorized access, alteration, or deletion of your data.\n\n## 7. Your Rights\n\nYou have the following rights:\n\n- **Access**: Request a copy of your personal data.\n- **Correction**: Request the correction of inaccurate or incomplete data.\n- **Deletion**: Request the deletion of your data, unless retention is required by law.\n\n## 8. Cookies\n\nWorkoutCool uses cookies to:\n\n- enhance navigation,\n- track performance,\n- personalize displayed content.\n\nYou can disable cookies in your browser settings.\n\n## 9. Advertising & Compliance\n\nWe comply with advertising policies from platforms like Facebook and Google.\nWe do not use your data in a way that violates their guidelines or applicable regulations.\n\n<div class=\"privacy-content\">\n<p>\n    Ezoic Services<br />\nThis website uses the services of Ezoic Inc. (\"Ezoic\"), including to manage third-party interest-based advertising. Ezoic may employ a variety of technologies on this website, including tools to serve content, display advertisements and enable advertising to visitors of this website, which may utilize first and third-party cookies.\n\n<br />A cookie is a small text file sent to your device by a web server that enables the website to remember information about your browsing activity. First-party cookies are created by the site you are visiting, while third-party cookies are set by domains other than the one you're visiting. Ezoic and our partners may place third-party cookies, tags, beacons, pixels, and similar technologies to monitor interactions with advertisements and optimize ad targeting.  Please note that disabling cookies may limit access to certain content and features on the website, and rejecting cookies does not eliminate advertisements but will result in non-personalized advertising. You can find more information about cookies and how to manage them <a href=\"https://allaboutcookies.org/\" target=\"_blank\">here</a>.\n<br />The following information may be collected, used, and stored in a cookie when serving personalized ads:\n</p><ul>\n<li>IP address</li>\n<li>Operating system type and version</li>\n<li>Device type</li>\n<li>Language preferences</li>\n<li>Web browser type</li>\n<li>Email (in a hashed or encrypted form)</li>\n</ul>\nEzoic and its partners may use this data in combination with information that has been independently collected to deliver targeted advertisements across various platforms and websites. Ezoic's partners may also gather additional data, such as unique IDs, advertising IDs, geolocation data, usage data, device information, traffic data, referral sources, and interactions between users and websites or advertisements, to create audience segments for targeted advertising across different devices, browsers, and apps. You can find more information about interest-based advertising and how to manage them <a href=\"https://youradchoices.com/\" target=\"_blank\">here</a>.\n<br />\nYou can view Ezoic's privacy policy <a href=\"https://ezoic.com/privacy/\" target=\"_blank\">here</a>, or for additional information about Ezoic's advertising and other partners, you can view Ezoic's advertising partners <a href=\"https://www.ezoic.com/privacy-policy/advertising-partners/\" target=\"_blank\">here</a>.\n<p></p>\n</div>\n\n## 10. Changes to This Policy\n\nThis Privacy Policy may be updated from time to time.  \nWe encourage you to review it regularly.  \nContinued use of our services implies acceptance of any changes.\n\n## 11. Contact\n\nIf you have any questions or concerns regarding this policy, feel free to contact us at:  \n**[hello@WorkoutCool.io](mailto:hello@WorkoutCool.io)**\n"
  },
  {
    "path": "content/privacy-policy/es.mdx",
    "content": "# Política de Privacidad de WorkoutCool\n\n## 1. Introducción\n\nEn **WorkoutCool**, tomamos muy en serio la privacidad de nuestros usuarios.  \nEsta Política de Privacidad explica cómo recopilamos, usamos y protegemos sus datos personales.  \nAl utilizar nuestros servicios, usted acepta las prácticas descritas a continuación.\n\n## 2. Información que recopilamos\n\n**Información personal**: Cuando se registra, recopilamos su nombre, dirección de correo electrónico y otros detalles relacionados con su actividad en WorkoutCool.\n\n**Datos de uso**: Rastreamos las interacciones con las páginas de WorkoutCool, incluyendo clics, vistas y rendimiento de enlaces.\n\n**Cookies**: Utilizamos cookies para mejorar su experiencia de usuario y monitorear el uso.\n\n## 3. Cómo usamos su información\n\n- **Prestación de servicios**: Para proporcionar, mantener y mejorar nuestros servicios.\n- **Comunicación**: Para enviarle actualizaciones, notificaciones o contenido de marketing (si ha dado su consentimiento).\n- **Análisis**: Para comprender cómo se utilizan nuestros servicios y mejorarlos continuamente.\n\n## 4. Compartir datos con terceros\n\nPodemos compartir datos agregados o anonimizados con socios de confianza para fines de marketing, análisis o mejora del producto.  \nNo se venden ni comparten datos personales sin su consentimiento explícito.\n\n## 5. Retención de datos\n\nSus datos se conservan mientras su cuenta esté activa o sea necesario para proporcionar nuestros servicios.  \nAlgunos datos pueden archivarse por razones legales, administrativas o de seguridad.\n\n## 6. Protección de sus datos\n\nImplementamos medidas de seguridad estándar de la industria para prevenir el acceso, alteración o eliminación no autorizada de sus datos.\n\n## 7. Sus derechos\n\nUsted tiene los siguientes derechos:\n\n- **Acceso**: Solicitar una copia de sus datos personales.\n- **Corrección**: Solicitar la corrección de datos inexactos o incompletos.\n- **Eliminación**: Solicitar la eliminación de sus datos, a menos que la retención sea requerida por ley.\n\n## 8. Cookies\n\nWorkoutCool utiliza cookies para:\n\n- mejorar la navegación,\n- rastrear el rendimiento,\n- personalizar el contenido mostrado.\n\nPuede desactivar las cookies en la configuración de su navegador.\n\n## 9. Publicidad y cumplimiento\n\nCumplimos con las políticas publicitarias de plataformas como Facebook y Google.\nNo utilizamos sus datos de manera que viole sus directrices o las regulaciones aplicables.\n\n<div class=\"privacy-content\">\n<p>\n    Servicios Ezoic<br />\nEste sitio web utiliza los servicios de Ezoic Inc. (\"Ezoic\"), incluyendo la gestión de publicidad basada en intereses de terceros. Ezoic puede emplear diversas tecnologías en este sitio web, incluidas herramientas para servir contenido, mostrar anuncios y habilitar publicidad a los visitantes de este sitio web, que pueden utilizar cookies propias y de terceros.\n\n<br />Una cookie es un pequeño archivo de texto enviado a su dispositivo por un servidor web que permite al sitio web recordar información sobre su actividad de navegación. Las cookies propias son creadas por el sitio que está visitando, mientras que las cookies de terceros son establecidas por dominios distintos al que está visitando. Ezoic y nuestros socios pueden colocar cookies de terceros, etiquetas, balizas, píxeles y tecnologías similares para monitorear las interacciones con los anuncios y optimizar la segmentación publicitaria. Tenga en cuenta que desactivar las cookies puede limitar el acceso a cierto contenido y funciones del sitio web, y rechazar las cookies no elimina los anuncios, sino que resultará en publicidad no personalizada. Puede encontrar más información sobre las cookies y cómo gestionarlas <a href=\"https://allaboutcookies.org/\" target=\"_blank\">aquí</a>.\n<br />La siguiente información puede ser recopilada, utilizada y almacenada en una cookie al servir anuncios personalizados:\n</p><ul>\n<li>Dirección IP</li>\n<li>Tipo y versión del sistema operativo</li>\n<li>Tipo de dispositivo</li>\n<li>Preferencias de idioma</li>\n<li>Tipo de navegador web</li>\n<li>Correo electrónico (en forma cifrada o encriptada)</li>\n</ul>\nEzoic y sus socios pueden utilizar estos datos en combinación con información recopilada de forma independiente para entregar anuncios dirigidos en varias plataformas y sitios web. Los socios de Ezoic también pueden recopilar datos adicionales, como identificadores únicos, identificadores publicitarios, datos de geolocalización, datos de uso, información del dispositivo, datos de tráfico, fuentes de referencia e interacciones entre usuarios y sitios web o anuncios, para crear segmentos de audiencia para publicidad dirigida en diferentes dispositivos, navegadores y aplicaciones. Puede encontrar más información sobre la publicidad basada en intereses y cómo gestionarla <a href=\"https://youradchoices.com/\" target=\"_blank\">aquí</a>.\n<br />\nPuede ver la política de privacidad de Ezoic <a href=\"https://ezoic.com/privacy/\" target=\"_blank\">aquí</a>, o para obtener información adicional sobre los socios publicitarios y otros de Ezoic, puede ver los socios publicitarios de Ezoic <a href=\"https://www.ezoic.com/privacy-policy/advertising-partners/\" target=\"_blank\">aquí</a>.\n<p></p>\n</div>\n\n## 10. Cambios en esta política\n\nEsta Política de Privacidad puede actualizarse de vez en cuando.  \nLe recomendamos revisarla regularmente.  \nEl uso continuado de nuestros servicios implica la aceptación de cualquier cambio.\n\n## 11. Contacto\n\nSi tiene alguna pregunta o inquietud sobre esta política, no dude en contactarnos en:  \n**[hello@WorkoutCool.io](mailto:hello@WorkoutCool.io)**"
  },
  {
    "path": "content/privacy-policy/fr.mdx",
    "content": "# Politique de Confidentialité WorkoutCool\n\n## 1. Introduction\n\nChez **WorkoutCool**, nous accordons une grande importance à la confidentialité de nos utilisateurs.  \nCette Politique de Confidentialité décrit la manière dont nous collectons, utilisons et protégeons vos informations personnelles.  \nEn utilisant nos services, vous acceptez les pratiques décrites ci-dessous.\n\n## 2. Informations que nous collectons\n\n**Informations personnelles** : lorsque vous vous inscrivez, nous collectons votre nom, votre adresse e-mail, et d’autres informations liées à votre activité sur WorkoutCool.\n\n**Données d’utilisation** : nous suivons les interactions avec les pages WorkoutCool, notamment les clics, les vues et les performances des liens.\n\n**Cookies** : nous utilisons des cookies pour améliorer votre expérience utilisateur et suivre les interactions.\n\n## 3. Utilisation de vos informations\n\n- **Prestation de service** : fournir, maintenir et améliorer nos services.\n- **Communication** : vous envoyer des mises à jour, notifications ou contenus marketing (si vous y avez consenti).\n- **Analyse** : comprendre l’utilisation de nos services et les améliorer en continu.\n\n## 4. Partage des données avec des tiers\n\nNous pouvons partager certaines données agrégées ou anonymisées avec des partenaires de confiance à des fins de marketing, d’analyse ou d’amélioration produit.  \nAucune donnée personnelle n’est vendue ou partagée sans votre consentement explicite.\n\n## 5. Conservation des données\n\nVos données sont conservées tant que votre compte est actif ou nécessaires à la fourniture de nos services.  \nCertaines données peuvent être archivées à des fins légales, administratives ou de sécurité.\n\n## 6. Protection de vos données\n\nNous mettons en œuvre des mesures de sécurité conformes aux standards de l’industrie pour prévenir l'accès, l'altération ou la suppression non autorisée de vos données.\n\n## 7. Vos droits\n\nVous disposez des droits suivants :\n\n- **Accès** : demander une copie de vos données personnelles.\n- **Rectification** : corriger des données inexactes ou incomplètes.\n- **Suppression** : demander la suppression de vos données, sauf obligation légale de conservation.\n\n## 8. Cookies\n\nWorkoutCool utilise des cookies pour :\n\n- améliorer la navigation,\n- suivre les performances,\n- personnaliser le contenu affiché.\n\nVous pouvez les désactiver dans les paramètres de votre navigateur.\n\n## 9. Publicité & conformité\n\nNous respectons les politiques publicitaires des plateformes comme Facebook ou Google.\nNous n'utilisons pas vos données d'une manière contraire à leurs directives ou à la réglementation en vigueur.\n\n<div class=\"privacy-content\">\n<p>\n    Services Ezoic<br />\nCe site utilise les services d'Ezoic Inc. (« Ezoic »), notamment pour gérer la publicité ciblée par centres d'intérêt de tiers. Ezoic peut employer diverses technologies sur ce site, y compris des outils pour diffuser du contenu, afficher des publicités et permettre la publicité aux visiteurs de ce site, qui peuvent utiliser des cookies propriétaires et tiers.\n\n<br />Un cookie est un petit fichier texte envoyé à votre appareil par un serveur web qui permet au site de se souvenir d'informations sur votre activité de navigation. Les cookies propriétaires sont créés par le site que vous visitez, tandis que les cookies tiers sont définis par des domaines autres que celui que vous visitez. Ezoic et nos partenaires peuvent placer des cookies tiers, des balises, des pixels et des technologies similaires pour surveiller les interactions avec les publicités et optimiser le ciblage publicitaire. Veuillez noter que la désactivation des cookies peut limiter l'accès à certains contenus et fonctionnalités du site, et que le rejet des cookies n'élimine pas les publicités mais entraînera une publicité non personnalisée. Vous pouvez trouver plus d'informations sur les cookies et comment les gérer <a href=\"https://allaboutcookies.org/\" target=\"_blank\">ici</a>.\n<br />Les informations suivantes peuvent être collectées, utilisées et stockées dans un cookie lors de la diffusion de publicités personnalisées :\n</p><ul>\n<li>Adresse IP</li>\n<li>Type et version du système d'exploitation</li>\n<li>Type d'appareil</li>\n<li>Préférences linguistiques</li>\n<li>Type de navigateur web</li>\n<li>Email (sous forme hachée ou cryptée)</li>\n</ul>\nEzoic et ses partenaires peuvent utiliser ces données en combinaison avec des informations collectées de manière indépendante pour diffuser des publicités ciblées sur diverses plateformes et sites web. Les partenaires d'Ezoic peuvent également recueillir des données supplémentaires, telles que des identifiants uniques, des identifiants publicitaires, des données de géolocalisation, des données d'utilisation, des informations sur les appareils, des données de trafic, des sources de référence et des interactions entre les utilisateurs et les sites web ou les publicités, afin de créer des segments d'audience pour la publicité ciblée sur différents appareils, navigateurs et applications. Vous pouvez trouver plus d'informations sur la publicité basée sur les centres d'intérêt et comment les gérer <a href=\"https://youradchoices.com/\" target=\"_blank\">ici</a>.\n<br />\nVous pouvez consulter la politique de confidentialité d'Ezoic <a href=\"https://ezoic.com/privacy/\" target=\"_blank\">ici</a>, ou pour plus d'informations sur les partenaires publicitaires et autres d'Ezoic, vous pouvez consulter les partenaires publicitaires d'Ezoic <a href=\"https://www.ezoic.com/privacy-policy/advertising-partners/\" target=\"_blank\">ici</a>.\n<p></p>\n</div>\n\n## 10. Modifications de cette politique\n\nCette Politique de Confidentialité peut être modifiée.  \nNous vous invitons à la consulter régulièrement.  \nL’utilisation continue de nos services vaut acceptation des modifications.\n\n## 11. Contact\n\nPour toute question ou demande concernant cette politique, vous pouvez nous écrire à :  \n**[hello@WorkoutCool.io](mailto:hello@WorkoutCool.io)**\n"
  },
  {
    "path": "content/privacy-policy/pt.mdx",
    "content": "# Política de Privacidade WorkoutCool\n\n## 1. Introdução\n\nNa **WorkoutCool**, levamos a privacidade dos nossos usuários muito a sério.  \nEsta Política de Privacidade explica como coletamos, usamos e protegemos seus dados pessoais.  \nAo usar nossos serviços, você concorda com as práticas descritas abaixo.\n\n## 2. Informações que coletamos\n\n**Informações pessoais**: Quando você se cadastra, coletamos seu nome, endereço de e-mail e outros detalhes relacionados à sua atividade no WorkoutCool.\n\n**Dados de uso**: Rastreamos interações com as páginas do WorkoutCool, incluindo cliques, visualizações e desempenho de links.\n\n**Cookies**: Usamos cookies para melhorar sua experiência de usuário e monitorar o uso.\n\n## 3. Como usamos suas informações\n\n- **Prestação de serviços**: Para fornecer, manter e melhorar nossos serviços.\n- **Comunicação**: Para enviar atualizações, notificações ou conteúdo de marketing (se você consentiu).\n- **Análise**: Para entender como nossos serviços são usados e melhorá-los continuamente.\n\n## 4. Compartilhamento de dados com terceiros\n\nPodemos compartilhar dados agregados ou anonimizados com parceiros confiáveis para fins de marketing, análise ou melhoria de produtos.  \nNenhum dado pessoal é vendido ou compartilhado sem seu consentimento explícito.\n\n## 5. Retenção de dados\n\nSeus dados são mantidos enquanto sua conta estiver ativa ou for necessário para fornecer nossos serviços.  \nAlguns dados podem ser arquivados por razões legais, administrativas ou de segurança.\n\n## 6. Proteção de seus dados\n\nImplementamos medidas de segurança padrão da indústria para prevenir acesso, alteração ou exclusão não autorizada de seus dados.\n\n## 7. Seus direitos\n\nVocê tem os seguintes direitos:\n\n- **Acesso**: Solicitar uma cópia de seus dados pessoais.\n- **Correção**: Solicitar a correção de dados imprecisos ou incompletos.\n- **Exclusão**: Solicitar a exclusão de seus dados, salvo obrigação legal de retenção.\n\n## 8. Cookies\n\nWorkoutCool usa cookies para:\n\n- melhorar a navegação,\n- rastrear o desempenho,\n- personalizar o conteúdo exibido.\n\nVocê pode desativá-los nas configurações do seu navegador.\n\n## 9. Publicidade e conformidade\n\nCumprimos as políticas publicitárias de plataformas como Facebook e Google.\nNão usamos seus dados de forma que viole suas diretrizes ou regulamentos aplicáveis.\n\n<div class=\"privacy-content\">\n<p>\n    Serviços Ezoic<br />\nEste site utiliza os serviços da Ezoic Inc. (\"Ezoic\"), incluindo para gerenciar publicidade baseada em interesses de terceiros. A Ezoic pode empregar diversas tecnologias neste site, incluindo ferramentas para servir conteúdo, exibir anúncios e habilitar publicidade aos visitantes deste site, que podem utilizar cookies próprios e de terceiros.\n\n<br />Um cookie é um pequeno arquivo de texto enviado ao seu dispositivo por um servidor web que permite ao site lembrar informações sobre sua atividade de navegação. Cookies próprios são criados pelo site que você está visitando, enquanto cookies de terceiros são definidos por domínios diferentes daquele que você está visitando. A Ezoic e nossos parceiros podem colocar cookies de terceiros, tags, beacons, pixels e tecnologias similares para monitorar interações com anúncios e otimizar a segmentação publicitária. Observe que desativar cookies pode limitar o acesso a certos conteúdos e recursos do site, e rejeitar cookies não elimina anúncios, mas resultará em publicidade não personalizada. Você pode encontrar mais informações sobre cookies e como gerenciá-los <a href=\"https://allaboutcookies.org/\" target=\"_blank\">aqui</a>.\n<br />As seguintes informações podem ser coletadas, usadas e armazenadas em um cookie ao servir anúncios personalizados:\n</p><ul>\n<li>Endereço IP</li>\n<li>Tipo e versão do sistema operacional</li>\n<li>Tipo de dispositivo</li>\n<li>Preferências de idioma</li>\n<li>Tipo de navegador web</li>\n<li>E-mail (em forma hash ou criptografada)</li>\n</ul>\nA Ezoic e seus parceiros podem usar esses dados em combinação com informações coletadas de forma independente para entregar anúncios direcionados em várias plataformas e sites. Os parceiros da Ezoic também podem coletar dados adicionais, como IDs únicos, IDs de publicidade, dados de geolocalização, dados de uso, informações do dispositivo, dados de tráfego, fontes de referência e interações entre usuários e sites ou anúncios, para criar segmentos de público para publicidade direcionada em diferentes dispositivos, navegadores e aplicativos. Você pode encontrar mais informações sobre publicidade baseada em interesses e como gerenciá-la <a href=\"https://youradchoices.com/\" target=\"_blank\">aqui</a>.\n<br />\nVocê pode ver a política de privacidade da Ezoic <a href=\"https://ezoic.com/privacy/\" target=\"_blank\">aqui</a>, ou para informações adicionais sobre os parceiros publicitários e outros da Ezoic, você pode ver os parceiros publicitários da Ezoic <a href=\"https://www.ezoic.com/privacy-policy/advertising-partners/\" target=\"_blank\">aqui</a>.\n<p></p>\n</div>\n\n## 10. Alterações nesta política\n\nEsta Política de Privacidade pode ser atualizada periodicamente.  \nRecomendamos que você a revise regularmente.  \nO uso contínuo de nossos serviços implica a aceitação de quaisquer alterações.\n\n## 11. Contato\n\nSe você tiver dúvidas ou preocupações sobre esta política, entre em contato conosco em:  \n**[hello@WorkoutCool.io](mailto:hello@WorkoutCool.io)**"
  },
  {
    "path": "content/privacy-policy/ru.mdx",
    "content": "# Политика конфиденциальности WorkoutCool\n\n## 1. Введение\n\nВ **WorkoutCool** мы очень серьезно относимся к конфиденциальности наших пользователей.  \nЭта Политика конфиденциальности объясняет, как мы собираем, используем и защищаем ваши персональные данные.  \nИспользуя наши услуги, вы соглашаетесь с практиками, описанными ниже.\n\n## 2. Информация, которую мы собираем\n\n**Персональная информация**: При регистрации мы собираем ваше имя, адрес электронной почты и другие данные, связанные с вашей активностью на WorkoutCool.\n\n**Данные об использовании**: Мы отслеживаем взаимодействия со страницами WorkoutCool, включая клики, просмотры и производительность ссылок.\n\n**Файлы cookie**: Мы используем файлы cookie для улучшения пользовательского опыта и мониторинга использования.\n\n## 3. Как мы используем вашу информацию\n\n- **Предоставление услуг**: Для предоставления, поддержки и улучшения наших услуг.\n- **Коммуникация**: Для отправки вам обновлений, уведомлений или маркетингового контента (если вы дали согласие).\n- **Аналитика**: Для понимания того, как используются наши услуги, и их постоянного улучшения.\n\n## 4. Обмен данными с третьими лицами\n\nМы можем делиться агрегированными или анонимизированными данными с доверенными партнерами в целях маркетинга, аналитики или улучшения продукта.  \nНикакие персональные данные не продаются и не передаются без вашего явного согласия.\n\n## 5. Хранение данных\n\nВаши данные хранятся, пока ваша учетная запись активна или необходима для предоставления наших услуг.  \nНекоторые данные могут архивироваться в юридических, административных целях или целях безопасности.\n\n## 6. Защита ваших данных\n\nМы применяем стандартные для отрасли меры безопасности для предотвращения несанкционированного доступа, изменения или удаления ваших данных.\n\n## 7. Ваши права\n\nУ вас есть следующие права:\n\n- **Доступ**: Запросить копию ваших персональных данных.\n- **Исправление**: Запросить исправление неточных или неполных данных.\n- **Удаление**: Запросить удаление ваших данных, если только закон не требует их сохранения.\n\n## 8. Файлы cookie\n\nWorkoutCool использует файлы cookie для:\n\n- улучшения навигации,\n- отслеживания производительности,\n- персонализации отображаемого контента.\n\nВы можете отключить их в настройках браузера.\n\n## 9. Реклама и соответствие требованиям\n\nМы соблюдаем рекламные политики таких платформ, как Facebook и Google.\nМы не используем ваши данные способом, нарушающим их руководящие принципы или применимые правила.\n\n<div class=\"privacy-content\">\n<p>\n    Услуги Ezoic<br />\nЭтот сайт использует услуги Ezoic Inc. («Ezoic»), в том числе для управления сторонней рекламой на основе интересов. Ezoic может использовать различные технологии на этом сайте, включая инструменты для предоставления контента, показа рекламы и включения рекламы для посетителей этого сайта, которые могут использовать собственные и сторонние файлы cookie.\n\n<br />Файл cookie — это небольшой текстовый файл, отправляемый на ваше устройство веб-сервером, который позволяет сайту запоминать информацию о вашей активности в браузере. Собственные файлы cookie создаются сайтом, который вы посещаете, в то время как сторонние файлы cookie устанавливаются доменами, отличными от того, который вы посещаете. Ezoic и наши партнеры могут размещать сторонние файлы cookie, теги, маяки, пиксели и аналогичные технологии для мониторинга взаимодействия с рекламой и оптимизации таргетинга рекламы. Обратите внимание, что отключение файлов cookie может ограничить доступ к определенному контенту и функциям веб-сайта, а отклонение файлов cookie не устраняет рекламу, но приведет к неперсонализированной рекламе. Вы можете найти более подробную информацию о файлах cookie и способах управления ими <a href=\"https://allaboutcookies.org/\" target=\"_blank\">здесь</a>.\n<br />При показе персонализированной рекламы могут собираться, использоваться и храниться в файле cookie следующие данные:\n</p><ul>\n<li>IP-адрес</li>\n<li>Тип и версия операционной системы</li>\n<li>Тип устройства</li>\n<li>Языковые предпочтения</li>\n<li>Тип веб-браузера</li>\n<li>Электронная почта (в хешированной или зашифрованной форме)</li>\n</ul>\nEzoic и его партнеры могут использовать эти данные в сочетании с информацией, которая была собрана независимо, для показа целевой рекламы на различных платформах и веб-сайтах. Партнеры Ezoic также могут собирать дополнительные данные, такие как уникальные идентификаторы, рекламные идентификаторы, данные геолокации, данные об использовании, информацию об устройстве, данные о трафике, источники переходов и взаимодействие между пользователями и веб-сайтами или рекламой, для создания аудиторных сегментов для целевой рекламы на различных устройствах, в браузерах и приложениях. Вы можете найти более подробную информацию о рекламе на основе интересов и способах управления ею <a href=\"https://youradchoices.com/\" target=\"_blank\">здесь</a>.\n<br />\nВы можете ознакомиться с политикой конфиденциальности Ezoic <a href=\"https://ezoic.com/privacy/\" target=\"_blank\">здесь</a>, или для получения дополнительной информации о рекламных и других партнерах Ezoic вы можете просмотреть список рекламных партнеров Ezoic <a href=\"https://www.ezoic.com/privacy-policy/advertising-partners/\" target=\"_blank\">здесь</a>.\n<p></p>\n</div>\n\n## 10. Изменения в этой политике\n\nЭта Политика конфиденциальности может периодически обновляться.  \nМы рекомендуем вам регулярно просматривать ее.  \nПродолжение использования наших услуг означает принятие любых изменений.\n\n## 11. Контакты\n\nЕсли у вас есть вопросы или опасения относительно этой политики, свяжитесь с нами по адресу:  \n**[hello@WorkoutCool.io](mailto:hello@WorkoutCool.io)**"
  },
  {
    "path": "content/privacy-policy/zh-CN.mdx",
    "content": "# WorkoutCool 隐私政策\n\n## 1. 简介\n\n在 **WorkoutCool**，我们非常重视用户的隐私。  \n本隐私政策解释了我们如何收集、使用和保护您的个人数据。  \n使用我们的服务即表示您同意以下所述的做法。\n\n## 2. 我们收集的信息\n\n**个人信息**：当您注册时，我们会收集您的姓名、电子邮件地址以及与您在 WorkoutCool 上的活动相关的其他详细信息。\n\n**使用数据**：我们跟踪与 WorkoutCool 页面的交互，包括点击、查看和链接性能。\n\n**Cookie**：我们使用 Cookie 来增强您的用户体验并监控使用情况。\n\n## 3. 我们如何使用您的信息\n\n- **服务提供**：提供、维护和改进我们的服务。\n- **沟通**：向您发送更新、通知或营销内容（如果您已同意）。\n- **分析**：了解我们的服务如何被使用并持续改进。\n\n## 4. 与第三方共享数据\n\n我们可能会与值得信赖的合作伙伴共享聚合或匿名化的数据，用于营销、分析或产品改进目的。  \n未经您的明确同意，不会出售或共享个人数据。\n\n## 5. 数据保留\n\n只要您的账户处于活动状态或需要提供我们的服务，您的数据就会被保留。  \n某些数据可能因法律、行政或安全目的而被存档。\n\n## 6. 保护您的数据\n\n我们实施行业标准的安全措施，以防止未经授权的访问、更改或删除您的数据。\n\n## 7. 您的权利\n\n您拥有以下权利：\n\n- **访问**：请求获取您的个人数据副本。\n- **更正**：请求更正不准确或不完整的数据。\n- **删除**：请求删除您的数据，除非法律要求保留。\n\n## 8. Cookie\n\nWorkoutCool 使用 Cookie 来：\n\n- 改善导航，\n- 跟踪性能，\n- 个性化显示的内容。\n\n您可以在浏览器设置中禁用 Cookie。\n\n## 9. 广告与合规\n\n我们遵守 Facebook 和 Google 等平台的广告政策。\n我们不会以违反其指导原则或适用法规的方式使用您的数据。\n\n<div class=\"privacy-content\">\n<p>\n    Ezoic 服务<br />\n本网站使用 Ezoic Inc.（\"Ezoic\"）的服务，包括管理第三方基于兴趣的广告。Ezoic 可能在本网站上使用各种技术，包括用于提供内容、展示广告和为本网站访问者启用广告的工具，这些工具可能使用第一方和第三方 Cookie。\n\n<br />Cookie 是由 Web 服务器发送到您设备的小型文本文件，使网站能够记住有关您浏览活动的信息。第一方 Cookie 由您访问的网站创建，而第三方 Cookie 由您访问的域以外的域设置。Ezoic 及其合作伙伴可能会放置第三方 Cookie、标签、信标、像素和类似技术，以监控与广告的交互并优化广告定位。请注意，禁用 Cookie 可能会限制对网站某些内容和功能的访问，拒绝 Cookie 不会消除广告，但会导致非个性化广告。您可以在<a href=\"https://allaboutcookies.org/\" target=\"_blank\">此处</a>找到有关 Cookie 及其管理方法的更多信息。\n<br />在提供个性化广告时，可能会在 Cookie 中收集、使用和存储以下信息：\n</p><ul>\n<li>IP 地址</li>\n<li>操作系统类型和版本</li>\n<li>设备类型</li>\n<li>语言偏好</li>\n<li>Web 浏览器类型</li>\n<li>电子邮件（哈希或加密形式）</li>\n</ul>\nEzoic 及其合作伙伴可能会将这些数据与独立收集的信息结合使用，以在各种平台和网站上投放定向广告。Ezoic 的合作伙伴还可能收集其他数据，例如唯一 ID、广告 ID、地理位置数据、使用数据、设备信息、流量数据、引荐来源以及用户与网站或广告之间的交互，以创建受众细分，用于跨不同设备、浏览器和应用程序进行定向广告投放。您可以在<a href=\"https://youradchoices.com/\" target=\"_blank\">此处</a>找到有关基于兴趣的广告及其管理方法的更多信息。\n<br />\n您可以在<a href=\"https://ezoic.com/privacy/\" target=\"_blank\">此处</a>查看 Ezoic 的隐私政策，或者要了解有关 Ezoic 的广告和其他合作伙伴的更多信息，您可以在<a href=\"https://www.ezoic.com/privacy-policy/advertising-partners/\" target=\"_blank\">此处</a>查看 Ezoic 的广告合作伙伴。\n<p></p>\n</div>\n\n## 10. 本政策的变更\n\n本隐私政策可能会不时更新。  \n我们建议您定期查看。  \n继续使用我们的服务即表示接受任何更改。\n\n## 11. 联系方式\n\n如果您对本政策有任何疑问或顾虑，请通过以下方式联系我们：  \n**[hello@WorkoutCool.io](mailto:hello@WorkoutCool.io)**"
  },
  {
    "path": "content/sales-terms/en.mdx",
    "content": "# General Terms of Sale – WorkoutCool\n\n## ARTICLE 1: Purpose\n\nThese terms govern the provision of services offered via the WorkoutCool platform (SaaS model).  \nBy subscribing to a plan or using the WorkoutCool application, the Client fully and unconditionally agrees to these Terms of Sale, along with the Terms of Use and Privacy Policy.\n\n## ARTICLE 2: Definitions\n\n- **Subscriber**: any individual or entity who has subscribed to a paid plan.  \n- **Subscription**: a paid service granting access to specific features of the application.  \n- **Application**: the WorkoutCool web application (and mobile app when available).  \n- **Client**: the user of the platform, whether for personal or professional use.  \n- **Company**: refers to WorkoutCool, operated by Mathias BRADICEANU.\n\n## ARTICLE 3: Ordering and Activation\n\nSubscriptions are purchased online via WorkoutCool.io.  \nAccess is activated after successful payment validation.  \nThe Company reserves the right to refuse or cancel any order, especially in the case of suspected fraud, abuse, or violation of these terms.\n\n## ARTICLE 4: Pricing and Payment\n\nSubscriptions are billed in advance for the selected period (monthly, yearly, etc.).  \nFailure to pay will result in immediate suspension of access without notice.  \nPrices are shown in euros, inclusive of all applicable taxes.  \nPricing may change at any time, but only future renewals will be affected.\n\nNo refund will be issued in case of suspension due to breach of these terms.\n\n## ARTICLE 5: Duration, Renewal, Trial Period and Withdrawal\n\nSubscriptions are for a fixed term and renew automatically unless cancelled beforehand.  \nThe Client may cancel at any time before the renewal date via their personal account.  \nA free 14-day trial is offered once per user.  \nAttempts to bypass this restriction (multiple accounts, false identities) may lead to immediate suspension and legal action.\n\nThe right of withdrawal applies in accordance with Article L221-28 of the French Consumer Code, **unless the service is used during the trial period**.  \nAny withdrawal request must be sent to [support@WorkoutCool.io](mailto:support@WorkoutCool.io) or by registered mail to the Company's registered address.\n\n## ARTICLE 6: Client Obligations\n\nThe Client agrees to:\n\n- Not share, transfer or sell access to their account  \n- Not exploit the trial period fraudulently  \n- Respect WorkoutCool’ intellectual property rights  \n- Avoid any action that harms the platform’s integrity or security  \n- Provide accurate and up-to-date billing and contact information  \n- Pay their subscription on time\n\nAny violation may result in suspension or deletion of the account without prior notice or compensation.\n\n## ARTICLE 7: Liability\n\nWorkoutCool shall not be liable for:\n\n- Internet-related issues or client-side technical problems  \n- Temporary unavailability due to maintenance  \n- Illegal or improper use of the service by third parties  \n- Data loss due to the Client's failure to back up their content\n\nNo guarantee is made as to the suitability of the service for the Client’s specific needs.\n\nThe Company may suspend, modify, or remove any service feature without obligation to compensate.\n\n## ARTICLE 8: Indemnification\n\nThe Client agrees to indemnify and hold harmless WorkoutCool from any claim, loss, or liability arising from misuse, unlawful use, or breach of these terms, including legal and administrative fees.\n\n## ARTICLE 9: Changes to the Terms\n\nWorkoutCool reserves the right to update these Terms of Sale at any time.  \nThe applicable version is the one available on the website at the time of the Client's order or renewal.  \nClients are encouraged to consult the most recent version regularly.\n\n## ARTICLE 10: Force Majeure\n\nWorkoutCool shall not be held liable for any failure or delay caused by events beyond its reasonable control, such as:  \nnatural disasters, pandemics, cyberattacks, outages, fire, war, strike, or any unforeseeable event.\n\n## ARTICLE 11: Proof and Archiving\n\nDigital records stored by WorkoutCool’ systems constitute valid proof of transactions and communications.  \nInvoices are available at any time in the user’s account.\n\n## ARTICLE 12: Contact – Complaints\n\nFor questions, complaints, or issues, Clients can contact WorkoutCool:\n\n- By email: [support@WorkoutCool.io](mailto:support@WorkoutCool.io)  \n- By mail: Mathias BRADICEANU, Strada Fagului 40F, 077010 Afumați, Romania  \n- Through the in-app messaging system\n"
  },
  {
    "path": "content/sales-terms/es.mdx",
    "content": "# Términos Generales de Venta – WorkoutCool\n\n## ARTÍCULO 1: Objeto\n\nEstos términos rigen la prestación de servicios ofrecidos a través de la plataforma WorkoutCool (modelo SaaS).  \nAl suscribirse a un plan o usar la aplicación WorkoutCool, el Cliente acepta completa e incondicionalmente estos Términos de Venta, junto con los Términos de Uso y la Política de Privacidad.\n\n## ARTÍCULO 2: Definiciones\n\n- **Suscriptor**: cualquier individuo o entidad que se haya suscrito a un plan de pago.  \n- **Suscripción**: un servicio de pago que otorga acceso a características específicas de la aplicación.  \n- **Aplicación**: la aplicación web de WorkoutCool (y aplicación móvil cuando esté disponible).  \n- **Cliente**: el usuario de la plataforma, ya sea para uso personal o profesional.  \n- **Empresa**: se refiere a WorkoutCool, operada por Mathias BRADICEANU.\n\n## ARTÍCULO 3: Pedido y activación\n\nLas suscripciones se compran en línea a través de WorkoutCool.io.  \nEl acceso se activa después de la validación exitosa del pago.  \nLa Empresa se reserva el derecho de rechazar o cancelar cualquier pedido, especialmente en caso de sospecha de fraude, abuso o violación de estos términos.\n\n## ARTÍCULO 4: Precios y pago\n\nLas suscripciones se facturan por adelantado para el período seleccionado (mensual, anual, etc.).  \nEl incumplimiento del pago resultará en la suspensión inmediata del acceso sin aviso.  \nLos precios se muestran en euros, con todos los impuestos aplicables incluidos.  \nLos precios pueden cambiar en cualquier momento, pero solo las renovaciones futuras se verán afectadas.\n\nNo se emitirá reembolso en caso de suspensión debido al incumplimiento de estos términos.\n\n## ARTÍCULO 5: Duración, renovación, período de prueba y desistimiento\n\nLas suscripciones son por un término fijo y se renuevan automáticamente a menos que se cancelen de antemano.  \nEl Cliente puede cancelar en cualquier momento antes de la fecha de renovación a través de su cuenta personal.  \nSe ofrece una prueba gratuita de 14 días una vez por usuario.  \nLos intentos de eludir esta restricción (múltiples cuentas, identidades falsas) pueden llevar a suspensión inmediata y acción legal.\n\nEl derecho de desistimiento se aplica de acuerdo con el Artículo L221-28 del Código del Consumidor francés, **a menos que el servicio se use durante el período de prueba**.  \nCualquier solicitud de desistimiento debe enviarse a [support@WorkoutCool.io](mailto:support@WorkoutCool.io) o por correo certificado a la dirección registrada de la Empresa.\n\n## ARTÍCULO 6: Obligaciones del Cliente\n\nEl Cliente acepta:\n\n- No compartir, transferir o vender el acceso a su cuenta  \n- No explotar el período de prueba de manera fraudulenta  \n- Respetar los derechos de propiedad intelectual de WorkoutCool  \n- Evitar cualquier acción que dañe la integridad o seguridad de la plataforma  \n- Proporcionar información de facturación y contacto precisa y actualizada  \n- Pagar su suscripción a tiempo\n\nCualquier violación puede resultar en suspensión o eliminación de la cuenta sin aviso previo o compensación.\n\n## ARTÍCULO 7: Responsabilidad\n\nWorkoutCool no será responsable de:\n\n- Problemas relacionados con Internet o problemas técnicos del lado del cliente  \n- Indisponibilidad temporal debido a mantenimiento  \n- Uso ilegal o inapropiado del servicio por terceros  \n- Pérdida de datos debido a la falta del Cliente de respaldar su contenido\n\nNo se garantiza la idoneidad del servicio para las necesidades específicas del Cliente.\n\nLa Empresa puede suspender, modificar o eliminar cualquier característica del servicio sin obligación de compensar.\n\n## ARTÍCULO 8: Indemnización\n\nEl Cliente acepta indemnizar y eximir de responsabilidad a WorkoutCool de cualquier reclamo, pérdida o responsabilidad que surja del mal uso, uso ilegal o incumplimiento de estos términos, incluyendo honorarios legales y administrativos.\n\n## ARTÍCULO 9: Cambios en los términos\n\nWorkoutCool se reserva el derecho de actualizar estos Términos de Venta en cualquier momento.  \nLa versión aplicable es la que está disponible en el sitio web al momento del pedido o renovación del Cliente.  \nSe alienta a los Clientes a consultar la versión más reciente regularmente.\n\n## ARTÍCULO 10: Fuerza mayor\n\nWorkoutCool no será responsable de cualquier falla o retraso causado por eventos más allá de su control razonable, tales como:  \ndesastres naturales, pandemias, ciberataques, interrupciones, incendio, guerra, huelga o cualquier evento imprevisto.\n\n## ARTÍCULO 11: Prueba y archivo\n\nLos registros digitales almacenados por los sistemas de WorkoutCool constituyen prueba válida de transacciones y comunicaciones.  \nLas facturas están disponibles en cualquier momento en la cuenta del usuario.\n\n## ARTÍCULO 12: Contacto – Quejas\n\nPara preguntas, quejas o problemas, los Clientes pueden contactar a WorkoutCool:\n\n- Por email: [support@WorkoutCool.io](mailto:support@WorkoutCool.io)  \n- Por correo: Mathias BRADICEANU, Strada Fagului 40F, 077010 Afumați, Rumania  \n- A través del sistema de mensajería en la aplicación"
  },
  {
    "path": "content/sales-terms/fr.mdx",
    "content": "# Conditions Générales de Vente – WorkoutCool\n\n## ARTICLE 1 : Objet\n\nLes présentes conditions régissent la fourniture des services proposés via la plateforme WorkoutCool (en mode SaaS).  \nEn souscrivant à un abonnement ou en utilisant l'application WorkoutCool, le Client accepte pleinement et sans réserve les présentes conditions générales de vente, ainsi que les Conditions Générales d’Utilisation et la Politique de Confidentialité associées.\n\n## ARTICLE 2 : Définitions\n\n- **Abonné** : toute personne physique ou morale ayant souscrit à un abonnement payant.  \n- **Abonnement** : service donnant droit à l’accès à des fonctionnalités spécifiques de l'application, contre paiement.  \n- **Application** : l'application web (et mobile le cas échéant) mise à disposition par WorkoutCool.  \n- **Client** : utilisateur de la plateforme, qu’il soit à titre personnel ou professionnel.  \n- **Société** : désigne WorkoutCool, éditée par Mathias BRADICEANU.\n\n## ARTICLE 3 : Commande et activation\n\nLa souscription se fait en ligne sur WorkoutCool.io.  \nL'accès est activé après validation du paiement.  \nLa Société se réserve le droit de refuser ou d’annuler toute commande, notamment en cas de suspicion de fraude, d’utilisation abusive ou de non-respect des présentes conditions.\n\n## ARTICLE 4 : Conditions financières\n\nLes abonnements sont payants et facturés à l’avance pour la période choisie (mensuelle, annuelle…).  \nTout défaut de paiement entraîne la suspension immédiate et sans préavis de l'accès au service.  \nLes prix sont indiqués en euros TTC, et peuvent être révisés à tout moment.  \nToute modification tarifaire ne s’appliquera qu’aux renouvellements futurs.\n\nAucun remboursement ne sera effectué en cas de suspension pour violation des présentes CGV.\n\n## ARTICLE 5 : Durée, reconduction, période d’essai et rétractation\n\nLes abonnements sont souscrits pour une durée déterminée, renouvelable tacitement.  \nLe Client peut résilier son abonnement à tout moment via son espace personnel, avant la date de reconduction.  \nUne seule période d’essai gratuite de 14 jours est autorisée par utilisateur.  \nToute tentative de contournement (multi-comptes, fausse identité) pourra faire l’objet de poursuites.\n\nLe droit de rétractation s’applique selon l’article L221-28 du Code de la consommation, **sauf si le service a été pleinement utilisé pendant l’essai**.  \nToute demande doit être adressée à [support@WorkoutCool.io](mailto:support@WorkoutCool.io), ou par courrier recommandé.\n\n## ARTICLE 6 : Obligations du Client\n\nLe Client s’engage à :\n\n- Ne pas céder ou partager son compte à des tiers  \n- Ne pas créer plusieurs comptes pour bénéficier à nouveau d’un essai gratuit  \n- Ne pas perturber le bon fonctionnement de la plateforme  \n- Respecter les droits de propriété intellectuelle de WorkoutCool  \n- Ne pas détourner l’utilisation du service à des fins illicites  \n- Renseigner des informations exactes et à jour  \n- Régler son abonnement dans les délais prévus\n\nToute infraction autorise WorkoutCool à suspendre ou supprimer le compte, sans préavis ni indemnité.\n\n## ARTICLE 7 : Responsabilités\n\nWorkoutCool ne pourra être tenu responsable des interruptions de service liées à :\n\n- des maintenances techniques,  \n- des problèmes liés à Internet ou à l’environnement du Client,  \n- des intrusions ou failles de sécurité imputables à des tiers,  \n- des pertes de données non sauvegardées par le Client.\n\nAucune garantie n’est donnée quant à l’adéquation du service aux besoins spécifiques du Client.\n\nLa Société se réserve le droit de modifier, suspendre ou retirer tout ou partie du service, sans obligation d’indemnisation.\n\n## ARTICLE 8 : Indemnisation\n\nLe Client s'engage à indemniser WorkoutCool contre toute réclamation ou dommage résultant de l'utilisation fautive, illégale ou abusive du service, y compris les frais de défense et d’expertise.\n\n## ARTICLE 9 : Modifications contractuelles\n\nWorkoutCool peut modifier les présentes CGV à tout moment.  \nLa version applicable est celle publiée au moment de la commande ou du renouvellement.  \nLe Client est invité à consulter régulièrement la dernière version disponible sur WorkoutCool.io.\n\n## ARTICLE 10 : Force majeure\n\nWorkoutCool ne pourra être tenu responsable d’un manquement à ses obligations en cas de force majeure :  \ncatastrophe naturelle, épidémie, cyberattaque, panne réseau, incendie, guerre, grève, ou tout événement imprévisible échappant à son contrôle raisonnable.\n\n## ARTICLE 11 : Preuve et archivage\n\nLes données enregistrées dans les systèmes informatiques de WorkoutCool constituent la preuve des commandes et des paiements.  \nLes factures sont disponibles dans l’espace client et peuvent être téléchargées à tout moment.\n\n## ARTICLE 12 : Contact – Réclamation\n\nPour toute question ou réclamation :\n\n- Par email : [support@WorkoutCool.io](mailto:support@WorkoutCool.io)  \n- Par courrier : Mathias BRADICEANU, Strada Fagului 40F, 077010 Afumați, Roumanie  \n- Via la messagerie interne de l’Application\n"
  },
  {
    "path": "content/sales-terms/pt.mdx",
    "content": "# Termos Gerais de Venda – WorkoutCool\n\n## ARTIGO 1: Objeto\n\nEstes termos regem a prestação de serviços oferecidos através da plataforma WorkoutCool (modelo SaaS).  \nAo se inscrever em um plano ou usar a aplicação WorkoutCool, o Cliente concorda total e incondicionalmente com estes Termos de Venda, juntamente com os Termos de Uso e Política de Privacidade.\n\n## ARTIGO 2: Definições\n\n- **Assinante**: qualquer indivíduo ou entidade que tenha se inscrito em um plano pago.  \n- **Assinatura**: um serviço pago que concede acesso a recursos específicos da aplicação.  \n- **Aplicação**: a aplicação web WorkoutCool (e aplicação móvel quando disponível).  \n- **Cliente**: o usuário da plataforma, seja para uso pessoal ou profissional.  \n- **Empresa**: refere-se à WorkoutCool, operada por Mathias BRADICEANU.\n\n## ARTIGO 3: Pedido e ativação\n\nAs assinaturas são adquiridas online através de WorkoutCool.io.  \nO acesso é ativado após a validação bem-sucedida do pagamento.  \nA Empresa reserva-se o direito de recusar ou cancelar qualquer pedido, especialmente em caso de suspeita de fraude, abuso ou violação destes termos.\n\n## ARTIGO 4: Preços e pagamento\n\nAs assinaturas são cobradas antecipadamente para o período selecionado (mensal, anual, etc.).  \nO não pagamento resultará em suspensão imediata do acesso sem aviso prévio.  \nOs preços são mostrados em euros, com todos os impostos aplicáveis incluídos.  \nOs preços podem mudar a qualquer momento, mas apenas as renovações futuras serão afetadas.\n\nNenhum reembolso será emitido em caso de suspensão devido à violação destes termos.\n\n## ARTIGO 5: Duração, renovação, período de teste e desistência\n\nAs assinaturas são por prazo fixo e renovam automaticamente, a menos que sejam canceladas antecipadamente.  \nO Cliente pode cancelar a qualquer momento antes da data de renovação através de sua conta pessoal.  \nUm teste gratuito de 14 dias é oferecido uma vez por usuário.  \nTentativas de contornar esta restricção (múltiplas contas, identidades falsas) podem levar à suspensão imediata e ação legal.\n\nO direito de desistência aplica-se de acordo com o Artigo L221-28 do Código do Consumidor francês, **a menos que o serviço seja usado durante o período de teste**.  \nQualquer solicitação de desistência deve ser enviada para [support@WorkoutCool.io](mailto:support@WorkoutCool.io) ou por correio registrado para o endereço registrado da Empresa.\n\n## ARTIGO 6: Obrigações do Cliente\n\nO Cliente concorda em:\n\n- Não compartilhar, transferir ou vender o acesso à sua conta  \n- Não explorar o período de teste de forma fraudulenta  \n- Respeitar os direitos de propriedade intelectual da WorkoutCool  \n- Evitar qualquer ação que prejudique a integridade ou segurança da plataforma  \n- Fornecer informações de cobrança e contato precisas e atualizadas  \n- Pagar sua assinatura em dia\n\nQualquer violação pode resultar em suspensão ou exclusão da conta sem aviso prévio ou compensação.\n\n## ARTIGO 7: Responsabilidade\n\nWorkoutCool não será responsável por:\n\n- Problemas relacionados à Internet ou problemas técnicos do lado do cliente  \n- Indisponibilidade temporária devido à manutenção  \n- Uso ilegal ou inadequado do serviço por terceiros  \n- Perda de dados devido à falha do Cliente em fazer backup de seu conteúdo\n\nNenhuma garantia é dada quanto à adequação do serviço às necessidades específicas do Cliente.\n\nA Empresa pode suspender, modificar ou remover qualquer recurso do serviço sem obrigação de compensar.\n\n## ARTIGO 8: Indenização\n\nO Cliente concorda em indenizar e isentar a WorkoutCool de qualquer reivindicação, perda ou responsabilidade decorrente de uso indevido, uso ilegal ou violação destes termos, incluindo taxas legais e administrativas.\n\n## ARTIGO 9: Alterações nos termos\n\nWorkoutCool reserva-se o direito de atualizar estes Termos de Venda a qualquer momento.  \nA versão aplicável é aquela disponível no site no momento do pedido ou renovação do Cliente.  \nOs Clientes são encorajados a consultar a versão mais recente regularmente.\n\n## ARTIGO 10: Força maior\n\nWorkoutCool não será responsabilizada por qualquer falha ou atraso causado por eventos além de seu controle razoável, tais como:  \ndesastres naturais, pandemias, ciberataques, interrupções, incêndio, guerra, greve ou qualquer evento imprevisto.\n\n## ARTIGO 11: Prova e arquivamento\n\nOs registros digitais armazenados pelos sistemas da WorkoutCool constituem prova válida de transações e comunicações.  \nAs faturas estão disponíveis a qualquer momento na conta do usuário.\n\n## ARTIGO 12: Contato – Reclamações\n\nPara perguntas, reclamações ou problemas, os Clientes podem contatar a WorkoutCool:\n\n- Por email: [support@WorkoutCool.io](mailto:support@WorkoutCool.io)  \n- Por correio: Mathias BRADICEANU, Strada Fagului 40F, 077010 Afumați, Romênia  \n- Através do sistema de mensagens no aplicativo"
  },
  {
    "path": "content/sales-terms/ru.mdx",
    "content": "# Общие условия продажи – WorkoutCool\n\n## СТАТЬЯ 1: Предмет\n\nЭти условия регулируют предоставление услуг, предлагаемых через платформу WorkoutCool (модель SaaS).  \nПодписываясь на план или используя приложение WorkoutCool, Клиент полностью и безоговорочно соглашается с этими Условиями продажи, а также с Условиями использования и Политикой конфиденциальности.\n\n## СТАТЬЯ 2: Определения\n\n- **Подписчик**: любое физическое или юридическое лицо, которое подписалось на платный план.  \n- **Подписка**: платный сервис, предоставляющий доступ к определенным функциям приложения.  \n- **Приложение**: веб-приложение WorkoutCool (и мобильное приложение, когда доступно).  \n- **Клиент**: пользователь платформы, будь то для личного или профессионального использования.  \n- **Компания**: относится к WorkoutCool, управляемой Mathias BRADICEANU.\n\n## СТАТЬЯ 3: Заказ и активация\n\nПодписки приобретаются онлайн через WorkoutCool.io.  \nДоступ активируется после успешной проверки платежа.  \nКомпания оставляет за собой право отказать или отменить любой заказ, особенно в случае подозрения в мошенничестве, злоупотреблении или нарушении этих условий.\n\n## СТАТЬЯ 4: Цены и оплата\n\nПодписки оплачиваются заранее за выбранный период (месячный, годовой и т.д.).  \nНеоплата приведет к немедленной приостановке доступа без уведомления.  \nЦены указаны в евро, включая все применимые налоги.  \nЦены могут изменяться в любое время, но это повлияет только на будущие продления.\n\nВ случае приостановки из-за нарушения этих условий возврат средств не производится.\n\n## СТАТЬЯ 5: Продолжительность, продление, пробный период и отказ\n\nПодписки заключаются на фиксированный срок и продлеваются автоматически, если не отменены заранее.  \nКлиент может отменить в любое время до даты продления через свою личную учетную запись.  \nБесплатный 14-дневный пробный период предлагается один раз на пользователя.  \nПопытки обойти это ограничение (множественные учетные записи, ложные личности) могут привести к немедленной приостановке и судебным действиям.\n\nПраво на отказ применяется в соответствии со статьей L221-28 Французского потребительского кодекса, **если только услуга не используется в течение пробного периода**.  \nЛюбой запрос на отказ должен быть отправлен на [support@WorkoutCool.io](mailto:support@WorkoutCool.io) или заказным письмом на зарегистрированный адрес Компании.\n\n## СТАТЬЯ 6: Обязательства Клиента\n\nКлиент соглашается:\n\n- Не делиться, не передавать и не продавать доступ к своей учетной записи  \n- Не использовать пробный период мошенническим образом  \n- Уважать права интеллектуальной собственности WorkoutCool  \n- Избегать любых действий, которые наносят ущерб целостности или безопасности платформы  \n- Предоставлять точную и актуальную информацию для выставления счетов и контактов  \n- Своевременно оплачивать подписку\n\nЛюбое нарушение может привести к приостановке или удалению учетной записи без предварительного уведомления или компенсации.\n\n## СТАТЬЯ 7: Ответственность\n\nWorkoutCool не несет ответственности за:\n\n- Проблемы, связанные с Интернетом, или технические проблемы на стороне клиента  \n- Временную недоступность из-за обслуживания  \n- Незаконное или неправильное использование сервиса третьими лицами  \n- Потерю данных из-за невозможности Клиента создать резервную копию своего контента\n\nНе дается никаких гарантий относительно соответствия сервиса конкретным потребностям Клиента.\n\nКомпания может приостановить, изменить или удалить любую функцию сервиса без обязательства компенсировать.\n\n## СТАТЬЯ 8: Возмещение ущерба\n\nКлиент соглашается возместить ущерб и освободить от ответственности WorkoutCool от любых претензий, убытков или ответственности, возникающих из-за неправильного использования, незаконного использования или нарушения этих условий, включая юридические и административные расходы.\n\n## СТАТЬЯ 9: Изменения в условиях\n\nWorkoutCool оставляет за собой право обновлять эти Условия продажи в любое время.  \nПрименимая версия — это та, которая доступна на веб-сайте во время заказа или продления Клиента.  \nКлиентам рекомендуется регулярно консультироваться с самой последней версией.\n\n## СТАТЬЯ 10: Форс-мажор\n\nWorkoutCool не несет ответственности за любую неудачу или задержку, вызванную событиями вне его разумного контроля, такими как:  \nстихийные бедствия, пандемии, кибератаки, отключения, пожар, война, забастовка или любое непредвиденное событие.\n\n## СТАТЬЯ 11: Доказательство и архивирование\n\nЦифровые записи, хранящиеся в системах WorkoutCool, являются действительным доказательством транзакций и коммуникаций.  \nСчета доступны в любое время в учетной записи пользователя.\n\n## СТАТЬЯ 12: Контакты – Жалобы\n\nПо вопросам, жалобам или проблемам Клиенты могут связаться с WorkoutCool:\n\n- По электронной почте: [support@WorkoutCool.io](mailto:support@WorkoutCool.io)  \n- По почте: Mathias BRADICEANU, Strada Fagului 40F, 077010 Afumați, Румыния  \n- Через систему сообщений в приложении"
  },
  {
    "path": "content/sales-terms/zh-CN.mdx",
    "content": "# 销售通用条款 – WorkoutCool\n\n## 第1条：目的\n\n这些条款规范通过 WorkoutCool 平台提供的服务（SaaS 模式）。  \n通过订阅计划或使用 WorkoutCool 应用程序，客户完全无条件地同意这些销售条款，以及使用条款和隐私政策。\n\n## 第2条：定义\n\n- **订阅者**：任何已订阅付费计划的个人或实体。  \n- **订阅**：付费服务，授权访问应用程序的特定功能。  \n- **应用程序**：WorkoutCool 网络应用程序（和移动应用程序，如果可用）。  \n- **客户**：平台的用户，无论是个人使用还是专业使用。  \n- **公司**：指由 Mathias BRADICEANU 运营的 WorkoutCool。\n\n## 第3条：订购和激活\n\n订阅通过 WorkoutCool.io 在线购买。  \n成功验证付款后激活访问权限。  \n公司保留拒绝或取消任何订单的权利，特别是在怀疑欺诈、滥用或违反这些条款的情况下。\n\n## 第4条：价格和付款\n\n订阅按所选期间（月度、年度等）预付费。  \n不付款将导致立即暂停访问而不另行通知。  \n价格以欧元显示，包含所有适用税费。  \n价格可能随时更改，但只影响未来的续订。\n\n如因违反这些条款而被暂停，不予退款。\n\n## 第5条：期限、续订、试用期和撤回\n\n订阅为固定期限，除非提前取消，否则自动续订。  \n客户可以在续订日期之前随时通过其个人账户取消。  \n每位用户提供一次 14 天免费试用。  \n尝试绕过此限制（多个账户、虚假身份）可能导致立即暂停和法律行动。\n\n撤回权根据法国消费者法典第 L221-28 条适用，**除非在试用期间使用了服务**。  \n任何撤回请求必须发送至 [support@WorkoutCool.io](mailto:support@WorkoutCool.io) 或通过挂号邮件发送至公司注册地址。\n\n## 第6条：客户义务\n\n客户同意：\n\n- 不共享、转让或出售其账户访问权限  \n- 不欺诈性地利用试用期  \n- 尊重 WorkoutCool 的知识产权  \n- 避免任何损害平台完整性或安全性的行为  \n- 提供准确和最新的账单和联系信息  \n- 按时支付订阅费用\n\n任何违规行为可能导致账户被暂停或删除，而不另行通知或补偿。\n\n## 第7条：责任\n\nWorkoutCool 不对以下情况负责：\n\n- 与互联网相关的问题或客户端技术问题  \n- 因维护导致的临时不可用  \n- 第三方对服务的非法或不当使用  \n- 因客户未能备份其内容而导致的数据丢失\n\n不保证服务适合客户的特定需求。\n\n公司可以暂停、修改或删除任何服务功能，无义务进行补偿。\n\n## 第8条：赔偿\n\n客户同意就因误用、非法使用或违反这些条款而产生的任何索赔、损失或责任向 WorkoutCool 进行赔偿并使其免受损害，包括法律和行政费用。\n\n## 第9条：条款变更\n\nWorkoutCool 保留随时更新这些销售条款的权利。  \n适用版本是客户订购或续订时在网站上可用的版本。  \n鼓励客户定期查阅最新版本。\n\n## 第10条：不可抗力\n\nWorkoutCool 不对因超出其合理控制范围的事件而造成的任何故障或延误承担责任，例如：  \n自然灾害、流行病、网络攻击、停电、火灾、战争、罢工或任何不可预见的事件。\n\n## 第11条：证明和存档\n\nWorkoutCool 系统存储的数字记录构成交易和通信的有效证明。  \n发票在用户账户中随时可用。\n\n## 第12条：联系方式 – 投诉\n\n如有问题、投诉或疑问，客户可以联系 WorkoutCool：\n\n- 通过电子邮件：[support@WorkoutCool.io](mailto:support@WorkoutCool.io)  \n- 通过邮件：Mathias BRADICEANU, Strada Fagului 40F, 077010 Afumați, 罗马尼亚  \n- 通过应用内消息系统"
  },
  {
    "path": "content/terms/en.mdx",
    "content": "# Terms of Use – WorkoutCool\n\nThese Terms of Use (\"Terms\") define the conditions for accessing and using the services provided by the WorkoutCool platform and govern the rights and obligations between WorkoutCool and its users.\n\n_Last updated: May 3, 2025_\n\n## ARTICLE 1: Legal Notice\n\nThe website WorkoutCool.io is published by **Mathias BRADICEANU**.\n\nThe website is hosted by **Vercel Inc.**, 440 N Barranca Ave #4133, Covina, CA 91723, USA.\n\n## ARTICLE 2: Access to the Platform\n\nWorkoutCool allows users to:\n\n- Create and manage a personalized bio link page  \n- Add links, media, and modules to a public profile  \n- Monitor engagement statistics (clicks, views, etc.)  \n- Customize appearance and content layout\n\nSome features are available only through a paid subscription or during a free trial.\n\nAccess to the platform is provided “as is” and does not constitute any obligation of result.\n\n## ARTICLE 3: Data Collection\n\nWorkoutCool collects and stores personal data entered by users or automatically generated while using the service.  \nThis includes names, email addresses, links, page content, and usage statistics.  \nUsers may request access, correction, or deletion of their personal data, subject to legal obligations.\n\n## ARTICLE 4: Intellectual Property\n\nAll elements of the WorkoutCool platform (text, images, code, logo, interface, web components, etc.) are protected by intellectual property law and remain the exclusive property of WorkoutCool or its partners.  \nAny reproduction, distribution, or commercial use without prior written consent is strictly prohibited.\n\nUser accounts and hosted content are non-transferable and subject to WorkoutCool’ approval.\n\n## ARTICLE 5: User Responsibilities\n\nUsers agree to:\n\n- Provide accurate and lawful information  \n- Not share illegal, offensive, defamatory, or misleading content  \n- Secure their account and credentials  \n- Comply with applicable laws and community standards  \n- Not use the platform for fraudulent, unauthorized commercial, or competitive purposes\n\nIn case of violation, WorkoutCool may suspend or delete the offending account without notice or refund.\n\n## ARTICLE 6: Availability and Disclaimer of Warranty\n\nWorkoutCool makes every effort to provide a stable and secure service.  \nHowever, the platform is provided **without any express or implied warranty**, including but not limited to availability, performance, compatibility, or error-free operation.\n\nTechnical support is not contractually guaranteed unless stated in a specific offer.\n\nUsers are solely responsible for backing up their content and data.\n\n## ARTICLE 7: Limitation of Liability\n\nWorkoutCool shall not be held liable for any direct or indirect damages, including material or immaterial losses, arising from:\n\n- Service interruption  \n- Data loss or corruption  \n- Errors, delays, or technical failures  \n- Improper or illegal use of the platform by users or third parties\n\nIf WorkoutCool is found liable, its responsibility is expressly limited to the amount of the last subscription payment made by the user.\n\n## ARTICLE 8: Suspension or Termination of Account\n\nWorkoutCool reserves the right to suspend or terminate any account:\n\n- In case of violation of these Terms  \n- In case of fraudulent or suspicious behavior  \n- In case of illegal or inappropriate content  \n- In case of excessive or abusive use of the service\n\nNo refund will be issued in case of suspension or termination for breach of contract.\n\n## ARTICLE 9: Service Modifications\n\nWorkoutCool may modify its services, features, pricing, or access conditions at any time.  \nUsers will be informed of major changes within a reasonable timeframe.  \nContinued use of the platform implies acceptance of such changes.\n\n## ARTICLE 10: External Links\n\nUser-generated pages may contain links to third-party websites.  \nWorkoutCool is not responsible for the content, security, or performance of external websites.\n\n## ARTICLE 11: Reversibility\n\nIn the event of account deletion or platform shutdown, users are responsible for exporting and securing their data beforehand.  \nWorkoutCool does not guarantee automated data portability to third-party services unless explicitly stated in a dedicated offer.\n\n## ARTICLE 12: Indemnification\n\nUsers agree to defend, indemnify, and hold harmless WorkoutCool from any claims, liabilities, losses, damages, or expenses (including legal fees) arising from:\n\n- Violation of these Terms  \n- Content published via the platform  \n- Activities carried out through their account, even if unauthorized\n\n## ARTICLE 13: Contact\n\nFor any questions regarding these Terms, you can contact us at:  \n**[support@WorkoutCool.io](mailto:support@WorkoutCool.io)**  \nor by postal mail to the publisher’s address listed in ARTICLE 1.\n\n## ARTICLE 14: Governing Law and Jurisdiction\n\nThese Terms are governed by French law.  \nIn the event of a dispute, the courts of **Mulhouse, France** shall have exclusive jurisdiction, unless otherwise required by consumer protection regulations.\n\n## ARTICLE 15: Force Majeure\n\nWorkoutCool shall not be held liable for failure to fulfill its obligations in the event of force majeure, including but not limited to: natural disaster, fire, flood, riot, war, pandemic, strike, cyberattack, infrastructure failure, or any other unforeseeable event beyond its control.\n"
  },
  {
    "path": "content/terms/es.mdx",
    "content": "# Términos de Uso – WorkoutCool\n\nEstos Términos de Uso (\"Términos\") definen las condiciones para acceder y usar los servicios proporcionados por la plataforma WorkoutCool y rigen los derechos y obligaciones entre WorkoutCool y sus usuarios.\n\n_Última actualización: 3 de mayo de 2025_\n\n## ARTÍCULO 1: Aviso Legal\n\nEl sitio web WorkoutCool.io es publicado por **Mathias BRADICEANU**.\n\nEl sitio web está alojado por **Vercel Inc.**, 440 N Barranca Ave #4133, Covina, CA 91723, EE.UU.\n\n## ARTÍCULO 2: Acceso a la Plataforma\n\nWorkoutCool permite a los usuarios:\n\n- Crear y gestionar una página de enlace bio personalizada  \n- Agregar enlaces, medios y módulos a un perfil público  \n- Monitorear estadísticas de participación (clics, vistas, etc.)  \n- Personalizar la apariencia y el diseño del contenido\n\nAlgunas características están disponibles solo a través de una suscripción paga o durante una prueba gratuita.\n\nEl acceso a la plataforma se proporciona \"tal como está\" y no constituye ninguna obligación de resultado.\n\n## ARTÍCULO 3: Recopilación de Datos\n\nWorkoutCool recopila y almacena datos personales ingresados por los usuarios o generados automáticamente mientras usan el servicio.  \nEsto incluye nombres, direcciones de correo electrónico, enlaces, contenido de páginas y estadísticas de uso.  \nLos usuarios pueden solicitar acceso, corrección o eliminación de sus datos personales, sujeto a obligaciones legales.\n\n## ARTÍCULO 4: Propiedad Intelectual\n\nTodos los elementos de la plataforma WorkoutCool (texto, imágenes, código, logo, interfaz, componentes web, etc.) están protegidos por la ley de propiedad intelectual y siguen siendo propiedad exclusiva de WorkoutCool o sus socios.  \nCualquier reproducción, distribución o uso comercial sin consentimiento previo por escrito está estrictamente prohibido.\n\nLas cuentas de usuario y el contenido alojado no son transferibles y están sujetos a la aprobación de WorkoutCool.\n\n## ARTÍCULO 5: Responsabilidades del Usuario\n\nLos usuarios acuerdan:\n\n- Proporcionar información precisa y legal  \n- No compartir contenido ilegal, ofensivo, difamatorio o engañoso  \n- Proteger su cuenta y credenciales  \n- Cumplir con las leyes aplicables y estándares comunitarios  \n- No usar la plataforma para propósitos fraudulentos, comerciales no autorizados o competitivos\n\nEn caso de violación, WorkoutCool puede suspender o eliminar la cuenta infractora sin aviso o reembolso.\n\n## ARTÍCULO 6: Disponibilidad y Descargo de Garantía\n\nWorkoutCool hace todo lo posible para proporcionar un servicio estable y seguro.  \nSin embargo, la plataforma se proporciona **sin ninguna garantía expresa o implícita**, incluyendo pero no limitado a disponibilidad, rendimiento, compatibilidad u operación libre de errores.\n\nEl soporte técnico no está garantizado contractualmente a menos que se establezca en una oferta específica.\n\nLos usuarios son los únicos responsables de hacer copias de seguridad de su contenido y datos.\n\n## ARTÍCULO 7: Limitación de Responsabilidad\n\nWorkoutCool no será responsable de ningún daño directo o indirecto, incluyendo pérdidas materiales o inmateriales, que surjan de:\n\n- Interrupción del servicio  \n- Pérdida o corrupción de datos  \n- Errores, retrasos o fallas técnicas  \n- Uso inadecuado o ilegal de la plataforma por usuarios o terceros\n\nSi WorkoutCool es encontrado responsable, su responsabilidad está expresamente limitada al monto del último pago de suscripción realizado por el usuario.\n\n## ARTÍCULO 8: Suspensión o Terminación de Cuenta\n\nWorkoutCool se reserva el derecho de suspender o terminar cualquier cuenta:\n\n- En caso de violación de estos Términos  \n- En caso de comportamiento fraudulento o sospechoso  \n- En caso de contenido ilegal o inapropiado  \n- En caso de uso excesivo o abusivo del servicio\n\nNo se emitirá reembolso en caso de suspensión o terminación por incumplimiento de contrato.\n\n## ARTÍCULO 9: Modificaciones del Servicio\n\nWorkoutCool puede modificar sus servicios, características, precios o condiciones de acceso en cualquier momento.  \nLos usuarios serán informados de cambios importantes dentro de un plazo razonable.  \nEl uso continuado de la plataforma implica la aceptación de dichos cambios.\n\n## ARTÍCULO 10: Enlaces Externos\n\nLas páginas generadas por usuarios pueden contener enlaces a sitios web de terceros.  \nWorkoutCool no es responsable del contenido, seguridad o rendimiento de sitios web externos.\n\n## ARTÍCULO 11: Reversibilidad\n\nEn caso de eliminación de cuenta o cierre de la plataforma, los usuarios son responsables de exportar y asegurar sus datos de antemano.  \nWorkoutCool no garantiza la portabilidad automatizada de datos a servicios de terceros a menos que se establezca explícitamente en una oferta dedicada.\n\n## ARTÍCULO 12: Indemnización\n\nLos usuarios acuerdan defender, indemnizar y eximir de responsabilidad a WorkoutCool de cualquier reclamo, responsabilidad, pérdida, daño o gasto (incluyendo honorarios legales) que surja de:\n\n- Violación de estos Términos  \n- Contenido publicado a través de la plataforma  \n- Actividades realizadas a través de su cuenta, incluso si no están autorizadas\n\n## ARTÍCULO 13: Contacto\n\nPara cualquier pregunta sobre estos Términos, puede contactarnos en:  \n**[support@WorkoutCool.io](mailto:support@WorkoutCool.io)**  \no por correo postal a la dirección del editor listada en el ARTÍCULO 1.\n\n## ARTÍCULO 14: Ley Aplicable y Jurisdicción\n\nEstos Términos se rigen por la ley francesa.  \nEn caso de disputa, los tribunales de **Mulhouse, Francia** tendrán jurisdicción exclusiva, a menos que las regulaciones de protección al consumidor requieran lo contrario.\n\n## ARTÍCULO 15: Fuerza Mayor\n\nWorkoutCool no será responsable del incumplimiento de sus obligaciones en caso de fuerza mayor, incluyendo pero no limitado a: desastre natural, incendio, inundación, disturbio, guerra, pandemia, huelga, ciberataque, falla de infraestructura, o cualquier otro evento imprevisto fuera de su control."
  },
  {
    "path": "content/terms/fr.mdx",
    "content": "# Conditions Générales d’Utilisation – WorkoutCool\n\nLes présentes Conditions Générales d’Utilisation (ci-après « CGU ») définissent les modalités d’accès et d’utilisation des services proposés par la plateforme WorkoutCool, ainsi que les droits et obligations entre WorkoutCool et ses utilisateurs.\n\n## ARTICLE 1 : Mentions légales\n\nLe site WorkoutCool.io est édité par **Mathias BRADICEANU**\n\nL’hébergement du site est assuré par **Vercel Inc.**, 440 N Barranca Ave #4133, Covina, CA 91723, États-Unis.\n\n## ARTICLE 2 : Accès à la plateforme\n\nLa plateforme WorkoutCool permet :\n\n- La création et la gestion d’une page de lien en bio personnalisée  \n- L’ajout de liens, médias et modules à un profil public  \n- Le suivi des statistiques d’engagement (clics, vues, etc.)  \n- La personnalisation de l’apparence et l’organisation des éléments\n\nCertaines fonctionnalités sont accessibles uniquement avec un abonnement payant ou pendant une période d’essai ou une période de promotion.\n\nL’accès à la plateforme est fourni « en l’état » et ne constitue en aucun cas une obligation de résultat.\n\n## ARTICLE 3 : Collecte des données\n\nWorkoutCool collecte et conserve les données saisies par les utilisateurs ou générées automatiquement lors de l’utilisation du service.  \nCela inclut les noms, adresses e-mail, liens, contenus de page, et statistiques d’utilisation.  \nL’utilisateur peut à tout moment demander l’accès, la rectification ou la suppression de ses données, sous réserve des obligations légales.\n\n## ARTICLE 4 : Propriété intellectuelle\n\nL’ensemble des éléments de la plateforme WorkoutCool (textes, images, code, logo, interface, composants web, etc.) est protégé par le droit de la propriété intellectuelle et demeure la propriété exclusive de WorkoutCool ou de ses partenaires.  \nToute reproduction, distribution ou utilisation commerciale sans autorisation écrite préalable est strictement interdite.\n\nLe compte utilisateur et ses contenus hébergés sur WorkoutCool ne sont pas transférables et restent soumis à l’approbation de la plateforme.\n\n## ARTICLE 5 : Responsabilités de l’utilisateur\n\nL’utilisateur s’engage à :\n\n- Fournir des informations exactes et licites  \n- Ne pas diffuser de contenus illégaux, offensants, diffamatoires ou trompeurs  \n- Protéger l’accès à son compte et à ses identifiants  \n- Respecter les lois en vigueur et les règles de bonne conduite  \n- Ne pas utiliser la plateforme à des fins frauduleuses, commerciales non autorisées ou concurrentielles\n\nEn cas de non-respect, WorkoutCool se réserve le droit de suspendre ou de supprimer le compte concerné, sans préavis ni remboursement.\n\n## ARTICLE 6 : Disponibilité et limites de garantie\n\nWorkoutCool met tout en œuvre pour garantir un service stable et sécurisé.  \nCependant, la plateforme est fournie **sans aucune garantie expresse ou implicite**, notamment en termes de disponibilité, de performance, de compatibilité ou d’absence d’erreur.\n\nAucune assistance technique n’est due contractuellement, sauf mention contraire dans une offre spécifique.\n\nL’utilisateur est seul responsable de la sauvegarde de ses contenus et données.\n\n## ARTICLE 7 : Limitation de responsabilité\n\nWorkoutCool ne pourra être tenu responsable des dommages directs ou indirects, matériels ou immatériels, résultant notamment :\n\n- d’une interruption de service  \n- d’une perte de données ou de contenu  \n- d’une erreur, d’un retard ou d’une défaillance technique  \n- d’une utilisation non conforme ou illégale de la plateforme par un utilisateur ou un tiers\n\nLa responsabilité de WorkoutCool, si elle venait à être engagée, serait expressément limitée au montant de la dernière mensualité effectivement payée.\n\n## ARTICLE 8 : Suspension ou suppression de compte\n\nWorkoutCool se réserve le droit de suspendre ou clôturer tout compte :\n\n- En cas de violation des présentes CGU  \n- En cas de comportement frauduleux ou suspect  \n- En cas de diffusion de contenus illicites ou contraires aux valeurs de la plateforme  \n- En cas d’utilisation excessive ou abusive du service\n\nAucun remboursement ne pourra être exigé en cas de suspension ou de suppression liée à un manquement contractuel.\n\n## ARTICLE 9 : Évolutions du service\n\nWorkoutCool peut faire évoluer à tout moment ses services, fonctionnalités, tarifs ou conditions d’accès.  \nCes évolutions peuvent être mises en œuvre sans préavis, sous réserve d’en informer les utilisateurs dans un délai raisonnable.\n\nLa poursuite de l’utilisation de la plateforme vaut acceptation des modifications.\n\n## ARTICLE 10 : Liens externes\n\nLes pages créées sur WorkoutCool peuvent contenir des liens vers des sites tiers.  \nWorkoutCool ne peut être tenu responsable du contenu, de la sécurité ou du bon fonctionnement de ces sites externes.\n\n## ARTICLE 11 : Réversibilité\n\nEn cas de suppression du compte ou d’arrêt du service, l’utilisateur est responsable de la récupération de ses données et contenus.  \nWorkoutCool ne garantit pas la portabilité automatique vers un service tiers, sauf disposition expresse dans une offre dédiée.\n\n## ARTICLE 12 : Indemnisation\n\nL’utilisateur s’engage à garantir, défendre et indemniser WorkoutCool contre toute réclamation, responsabilité, perte, dommage ou frais (y compris les honoraires d’avocat) résultant :\n\n- de sa violation des présentes CGU  \n- de tout contenu diffusé via la plateforme  \n- de toute activité effectuée avec son compte, même à son insu\n\n## ARTICLE 13 : Contact\n\nPour toute question relative aux présentes CGU, vous pouvez nous contacter à :  \n**[support@WorkoutCool.io](mailto:support@WorkoutCool.io)**  \nou par courrier recommandé à l’adresse de l’éditeur mentionnée à l’ARTICLE 1.\n\n## ARTICLE 15 : Force majeure\n\nWorkoutCool ne pourra être tenu responsable d’un manquement à ses obligations en cas de force majeure, tels que : catastrophe naturelle, incendie, inondation, émeute, guerre, pandémie, grève, cyberattaque, panne d’infrastructure, ou toute autre situation imprévisible échappant à son contrôle.\n"
  },
  {
    "path": "content/terms/pt.mdx",
    "content": "# Termos de Uso – WorkoutCool\n\nEstes Termos de Uso (\"Termos\") definem as condições para acessar e usar os serviços fornecidos pela plataforma WorkoutCool e regem os direitos e obrigações entre WorkoutCool e seus usuários.\n\n_Última atualização: 3 de maio de 2025_\n\n## ARTIGO 1: Aviso Legal\n\nO site WorkoutCool.io é publicado por **Mathias BRADICEANU**.\n\nO site é hospedado pela **Vercel Inc.**, 440 N Barranca Ave #4133, Covina, CA 91723, EUA.\n\n## ARTIGO 2: Acesso à Plataforma\n\nWorkoutCool permite aos usuários:\n\n- Criar e gerenciar uma página de link bio personalizada  \n- Adicionar links, mídia e módulos a um perfil público  \n- Monitorar estatísticas de engajamento (cliques, visualizações, etc.)  \n- Personalizar aparência e layout do conteúdo\n\nAlguns recursos estão disponíveis apenas através de uma assinatura paga ou durante um teste gratuito.\n\nO acesso à plataforma é fornecido \"como está\" e não constitui qualquer obrigação de resultado.\n\n## ARTIGO 3: Coleta de Dados\n\nWorkoutCool coleta e armazena dados pessoais inseridos pelos usuários ou gerados automaticamente durante o uso do serviço.  \nIsso inclui nomes, endereços de e-mail, links, conteúdo de páginas e estatísticas de uso.  \nOs usuários podem solicitar acesso, correção ou exclusão de seus dados pessoais, sujeito a obrigações legais.\n\n## ARTIGO 4: Propriedade Intelectual\n\nTodos os elementos da plataforma WorkoutCool (texto, imagens, código, logo, interface, componentes web, etc.) são protegidos pela lei de propriedade intelectual e permanecem propriedade exclusiva da WorkoutCool ou seus parceiros.  \nQualquer reprodução, distribuição ou uso comercial sem consentimento prévio por escrito é estritamente proibido.\n\nAs contas de usuário e conteúdo hospedado não são transferíveis e estão sujeitos à aprovação da WorkoutCool.\n\n## ARTIGO 5: Responsabilidades do Usuário\n\nOs usuários concordam em:\n\n- Fornecer informações precisas e legais  \n- Não compartilhar conteúdo ilegal, ofensivo, difamatório ou enganoso  \n- Proteger sua conta e credenciais  \n- Cumprir as leis aplicáveis e padrões da comunidade  \n- Não usar a plataforma para fins fraudulentos, comerciais não autorizados ou competitivos\n\nEm caso de violação, WorkoutCool pode suspender ou excluir a conta infratora sem aviso ou reembolso.\n\n## ARTIGO 6: Disponibilidade e Isenção de Garantia\n\nWorkoutCool faz todos os esforços para fornecer um serviço estável e seguro.  \nNo entanto, a plataforma é fornecida **sem qualquer garantia expressa ou implícita**, incluindo mas não limitado a disponibilidade, desempenho, compatibilidade ou operação livre de erros.\n\nO suporte técnico não é garantido contratualmente, a menos que declarado em uma oferta específica.\n\nOs usuários são os únicos responsáveis por fazer backup de seu conteúdo e dados.\n\n## ARTIGO 7: Limitação de Responsabilidade\n\nWorkoutCool não será responsabilizada por quaisquer danos diretos ou indiretos, incluindo perdas materiais ou imateriais, decorrentes de:\n\n- Interrupção do serviço  \n- Perda ou corrupção de dados  \n- Erros, atrasos ou falhas técnicas  \n- Uso inadequado ou ilegal da plataforma por usuários ou terceiros\n\nSe WorkoutCool for considerada responsável, sua responsabilidade é expressamente limitada ao valor do último pagamento de assinatura feito pelo usuário.\n\n## ARTIGO 8: Suspensão ou Terminação de Conta\n\nWorkoutCool reserva-se o direito de suspender ou terminar qualquer conta:\n\n- Em caso de violação destes Termos  \n- Em caso de comportamento fraudulento ou suspeito  \n- Em caso de conteúdo ilegal ou inapropriado  \n- Em caso de uso excessivo ou abusivo do serviço\n\nNenhum reembolso será emitido em caso de suspensão ou terminação por violação de contrato.\n\n## ARTIGO 9: Modificações do Serviço\n\nWorkoutCool pode modificar seus serviços, recursos, preços ou condições de acesso a qualquer momento.  \nOs usuários serão informados de mudanças importantes dentro de um prazo razoável.  \nO uso continuado da plataforma implica aceitação de tais mudanças.\n\n## ARTIGO 10: Links Externos\n\nPáginas geradas pelos usuários podem conter links para sites de terceiros.  \nWorkoutCool não é responsável pelo conteúdo, segurança ou desempenho de sites externos.\n\n## ARTIGO 11: Reversibilidade\n\nEm caso de exclusão de conta ou encerramento da plataforma, os usuários são responsáveis por exportar e proteger seus dados antecipadamente.  \nWorkoutCool não garante portabilidade automatizada de dados para serviços de terceiros, a menos que explicitamente declarado em uma oferta dedicada.\n\n## ARTIGO 12: Indenização\n\nOs usuários concordam em defender, indenizar e isentar WorkoutCool de quaisquer reivindicações, responsabilidades, perdas, danos ou despesas (incluindo honorários legais) decorrentes de:\n\n- Violação destes Termos  \n- Conteúdo publicado através da plataforma  \n- Atividades realizadas através de sua conta, mesmo se não autorizadas\n\n## ARTIGO 13: Contato\n\nPara quaisquer perguntas sobre estes Termos, você pode nos contatar em:  \n**[support@WorkoutCool.io](mailto:support@WorkoutCool.io)**  \nou por correio postal para o endereço do editor listado no ARTIGO 1.\n\n## ARTIGO 14: Lei Aplicável e Jurisdição\n\nEstes Termos são regidos pela lei francesa.  \nEm caso de disputa, os tribunais de **Mulhouse, França** terão jurisdição exclusiva, a menos que exigido de outra forma pelas regulamentações de proteção ao consumidor.\n\n## ARTIGO 15: Força Maior\n\nWorkoutCool não será responsabilizada pelo não cumprimento de suas obrigações em caso de força maior, incluindo mas não limitado a: desastre natural, incêndio, inundação, tumulto, guerra, pandemia, greve, ciberataque, falha de infraestrutura, ou qualquer outro evento imprevisto fora de seu controle."
  },
  {
    "path": "content/terms/ru.mdx",
    "content": "# Условия использования – WorkoutCool\n\nЭти Условия использования (\"Условия\") определяют условия доступа и использования услуг, предоставляемых платформой WorkoutCool, и регулируют права и обязанности между WorkoutCool и её пользователями.\n\n_Последнее обновление: 3 мая 2025 года_\n\n## СТАТЬЯ 1: Правовое уведомление\n\nВеб-сайт WorkoutCool.io публикуется **Mathias BRADICEANU**.\n\nВеб-сайт размещается на **Vercel Inc.**, 440 N Barranca Ave #4133, Covina, CA 91723, США.\n\n## СТАТЬЯ 2: Доступ к платформе\n\nWorkoutCool позволяет пользователям:\n\n- Создавать и управлять персонализированной страницей биоссылки  \n- Добавлять ссылки, медиа и модули в публичный профиль  \n- Отслеживать статистику вовлечения (клики, просмотры и т.д.)  \n- Настраивать внешний вид и макет контента\n\nНекоторые функции доступны только через платную подписку или во время бесплатного пробного периода.\n\nДоступ к платформе предоставляется \"как есть\" и не подразумевает никаких обязательств по результату.\n\n## СТАТЬЯ 3: Сбор данных\n\nWorkoutCool собирает и хранит персональные данные, введенные пользователями или автоматически генерируемые при использовании сервиса.  \nЭто включает имена, адреса электронной почты, ссылки, содержимое страниц и статистику использования.  \nПользователи могут запросить доступ, исправление или удаление своих персональных данных, с учетом правовых обязательств.\n\n## СТАТЬЯ 4: Интеллектуальная собственность\n\nВсе элементы платформы WorkoutCool (текст, изображения, код, логотип, интерфейс, веб-компоненты и т.д.) защищены законом об интеллектуальной собственности и остаются исключительной собственностью WorkoutCool или её партнеров.  \nЛюбое воспроизведение, распространение или коммерческое использование без предварительного письменного согласия строго запрещено.\n\nПользовательские аккаунты и размещенный контент не подлежат передаче и требуют одобрения WorkoutCool.\n\n## СТАТЬЯ 5: Обязанности пользователя\n\nПользователи соглашаются:\n\n- Предоставлять точную и законную информацию  \n- Не делиться незаконным, оскорбительным, клеветническим или вводящим в заблуждение контентом  \n- Защищать свой аккаунт и учетные данные  \n- Соблюдать применимые законы и стандарты сообщества  \n- Не использовать платформу в мошеннических, несанкционированных коммерческих или конкурентных целях\n\nВ случае нарушения WorkoutCool может приостановить или удалить нарушающий аккаунт без уведомления или возмещения.\n\n## СТАТЬЯ 6: Доступность и отказ от гарантий\n\nWorkoutCool прилагает все усилия для предоставления стабильного и безопасного сервиса.  \nОднако платформа предоставляется **без каких-либо явных или подразумеваемых гарантий**, включая, но не ограничиваясь доступностью, производительностью, совместимостью или безошибочной работой.\n\nТехническая поддержка не гарантируется контрактно, если не указано в конкретном предложении.\n\nПользователи несут единоличную ответственность за резервное копирование своего контента и данных.\n\n## СТАТЬЯ 7: Ограничение ответственности\n\nWorkoutCool не несет ответственности за любые прямые или косвенные ущербы, включая материальные или нематериальные потери, возникающие из-за:\n\n- Прерывания сервиса  \n- Потери или повреждения данных  \n- Ошибок, задержек или технических сбоев  \n- Неправильного или незаконного использования платформы пользователями или третьими лицами\n\nЕсли WorkoutCool признается ответственной, её ответственность явно ограничивается суммой последнего платежа за подписку, сделанного пользователем.\n\n## СТАТЬЯ 8: Приостановка или прекращение аккаунта\n\nWorkoutCool оставляет за собой право приостановить или прекратить любой аккаунт:\n\n- В случае нарушения этих Условий  \n- В случае мошеннического или подозрительного поведения  \n- В случае незаконного или неподходящего контента  \n- В случае чрезмерного или злоупотребительного использования сервиса\n\nВ случае приостановки или прекращения за нарушение контракта возврат средств не производится.\n\n## СТАТЬЯ 9: Изменения сервиса\n\nWorkoutCool может изменять свои сервисы, функции, цены или условия доступа в любое время.  \nПользователи будут уведомлены о серьезных изменениях в разумные сроки.  \nПродолжение использования платформы подразумевает принятие таких изменений.\n\n## СТАТЬЯ 10: Внешние ссылки\n\nСозданные пользователями страницы могут содержать ссылки на сторонние веб-сайты.  \nWorkoutCool не несет ответственности за содержимое, безопасность или работу внешних веб-сайтов.\n\n## СТАТЬЯ 11: Обратимость\n\nВ случае удаления аккаунта или закрытия платформы пользователи несут ответственность за экспорт и защиту своих данных заранее.  \nWorkoutCool не гарантирует автоматизированную переносимость данных в сторонние сервисы, если это не указано явно в специальном предложении.\n\n## СТАТЬЯ 12: Возмещение ущерба\n\nПользователи соглашаются защищать, возмещать ущерб и освобождать от ответственности WorkoutCool от любых претензий, обязательств, потерь, ущерба или расходов (включая юридические сборы), возникающих из-за:\n\n- Нарушения этих Условий  \n- Контента, опубликованного через платформу  \n- Действий, выполненных через их аккаунт, даже если они не авторизованы\n\n## СТАТЬЯ 13: Контакты\n\nПо любым вопросам относительно этих Условий вы можете связаться с нами по адресу:  \n**[support@WorkoutCool.io](mailto:support@WorkoutCool.io)**  \nили по почте на адрес издателя, указанный в СТАТЬЕ 1.\n\n## СТАТЬЯ 14: Применимое право и юрисдикция\n\nЭти Условия регулируются французским правом.  \nВ случае спора суды **Мюлуза, Франция** имеют исключительную юрисдикцию, если иное не требуется правилами защиты прав потребителей.\n\n## СТАТЬЯ 15: Форс-мажор\n\nWorkoutCool не несет ответственности за невыполнение своих обязательств в случае форс-мажора, включая, но не ограничиваясь: стихийными бедствиями, пожаром, наводнением, беспорядками, войной, пандемией, забастовкой, кибератакой, отказом инфраструктуры или любым другим непредвиденным событием вне её контроля."
  },
  {
    "path": "content/terms/zh-CN.mdx",
    "content": "# 使用条款 – WorkoutCool\n\n这些使用条款（\"条款\"）定义了访问和使用 WorkoutCool 平台提供的服务的条件，并规范 WorkoutCool 与其用户之间的权利和义务。\n\n_最后更新：2025年5月3日_\n\n## 第1条：法律声明\n\n网站 WorkoutCool.io 由 **Mathias BRADICEANU** 发布。\n\n网站由 **Vercel Inc.** 托管，地址：440 N Barranca Ave #4133, Covina, CA 91723, 美国。\n\n## 第2条：平台访问\n\nWorkoutCool 允许用户：\n\n- 创建和管理个性化的生物链接页面  \n- 向公共档案添加链接、媒体和模块  \n- 监控参与统计（点击、浏览等）  \n- 自定义外观和内容布局\n\n某些功能仅通过付费订阅或免费试用期提供。\n\n平台访问按\"现状\"提供，不构成任何结果义务。\n\n## 第3条：数据收集\n\nWorkoutCool 收集并存储用户输入的个人数据或在使用服务时自动生成的数据。  \n这包括姓名、电子邮件地址、链接、页面内容和使用统计。  \n用户可以请求访问、更正或删除他们的个人数据，但需遵守法律义务。\n\n## 第4条：知识产权\n\nWorkoutCool 平台的所有元素（文本、图像、代码、标志、界面、Web 组件等）均受知识产权法保护，仍为 WorkoutCool 或其合作伙伴的专有财产。  \n未经事先书面同意，严格禁止任何复制、分发或商业使用。\n\n用户账户和托管内容不可转让，需经 WorkoutCool 批准。\n\n## 第5条：用户责任\n\n用户同意：\n\n- 提供准确和合法的信息  \n- 不分享非法、冒犯性、诽谤性或误导性内容  \n- 保护其账户和凭据  \n- 遵守适用法律和社区标准  \n- 不将平台用于欺诈、未经授权的商业或竞争目的\n\n如有违反，WorkoutCool 可能会暂停或删除违规账户，而不发出通知或退款。\n\n## 第6条：可用性和免责声明\n\nWorkoutCool 竭尽全力提供稳定和安全的服务。  \n但是，平台按**\"现状\"提供，不提供任何明示或暗示的保证**，包括但不限于可用性、性能、兼容性或无错误操作。\n\n除非在特定报价中说明，否则不保证合同技术支持。\n\n用户独自负责备份其内容和数据。\n\n## 第7条：责任限制\n\nWorkoutCool 不对任何直接或间接损害负责，包括因以下原因造成的物质或非物质损失：\n\n- 服务中断  \n- 数据丢失或损坏  \n- 错误、延迟或技术故障  \n- 用户或第三方对平台的不当或非法使用\n\n如果 WorkoutCool 被认定有责任，其责任明确限制为用户最后一次订阅付款的金额。\n\n## 第8条：账户暂停或终止\n\nWorkoutCool 保留暂停或终止任何账户的权利：\n\n- 如违反这些条款  \n- 如有欺诈或可疑行为  \n- 如有非法或不当内容  \n- 如过度或滥用服务\n\n因违约而暂停或终止的情况下，不予退款。\n\n## 第9条：服务修改\n\nWorkoutCool 可能随时修改其服务、功能、定价或访问条件。  \n用户将在合理时间内被告知重大变更。  \n继续使用平台意味着接受此类变更。\n\n## 第10条：外部链接\n\n用户生成的页面可能包含指向第三方网站的链接。  \nWorkoutCool 不对外部网站的内容、安全或性能负责。\n\n## 第11条：可逆性\n\n如果账户删除或平台关闭，用户有责任事先导出和保护其数据。  \nWorkoutCool 不保证向第三方服务的自动数据可移植性，除非在专门报价中明确说明。\n\n## 第12条：赔偿\n\n用户同意为 WorkoutCool 辩护、赔偿并使其免受因以下原因产生的任何索赔、责任、损失、损害或费用（包括法律费用）的损害：\n\n- 违反这些条款  \n- 通过平台发布的内容  \n- 通过其账户进行的活动，即使未经授权\n\n## 第13条：联系方式\n\n对于有关这些条款的任何问题，您可以通过以下方式联系我们：  \n**[support@WorkoutCool.io](mailto:support@WorkoutCool.io)**  \n或邮寄至第1条中列出的发布者地址。\n\n## 第14条：适用法律和管辖权\n\n这些条款受法国法律管辖。  \n如有争议，**法国米卢斯**的法院具有专属管辖权，除非消费者保护法规另有要求。\n\n## 第15条：不可抗力\n\nWorkoutCool 不对因不可抗力导致的履行义务失败承担责任，包括但不限于：自然灾害、火灾、洪水、暴乱、战争、流行病、罢工、网络攻击、基础设施故障，或任何其他超出其控制范围的不可预见事件。"
  },
  {
    "path": "data/sample-exercises.csv",
    "content": "id,name,name_en,description,description_en,full_video_url,full_video_image_url,introduction,introduction_en,slug,slug_en,attribute_name,attribute_value\n157,Fentes arrières à la barre,Barbell Alternating Reverse Lunges,\"<p>Tenez-vous droit en tenant une barre placée sur l'arrière de vos épaules.</p><p>Faites un pas en arrière de 2 à 3 pieds avec un pied et abaissez votre corps au sol.</p><p>Votre genou arrière doit presque toucher le sol et votre genou avant doit être à un angle de 90 degrés.</p><p>Poussez vers le haut et revenez à la position de départ.</p><p>Répétez avec l'autre jambe.</p><p>Répétez le mouvement pour le nombre recommandé de répétitions, puis effectuez avec l'autre jambe.</p>\",\"<p>Stand upright holding a barbell placed across the back of your shoulders.</p><p>Step back 2-3 feet with one foot and lower your body to the ground.</p><p>Your back knee should almost touch the ground and your front knee should be at a 90-degree angle.</p><p>Push up to return to the starting position.</p><p>Repeat with the other leg.</p><p>Repeat the movement for the recommended number of repetitions, then switch to the other leg.</p>\",https://www.youtube.com/embed/NmfQzqGktgs?autoplay=1,https://img.youtube.com/vi/NmfQzqGktgs/hqdefault.jpg,\"<p>Les <strong>fentes arrières à la barre</strong> sont un exercice efficace pour cibler les <strong>muscles des jambes</strong> et les <strong>fessiers</strong>. Idéal pour les sportifs intermédiaires à avancés, cet exercice aide à améliorer l'<em>équilibre</em> et la <em>stabilité</em> tout en augmentant la <strong>force des jambes</strong>.</p>\",\"<p>The <strong>barbell alternating reverse lunges</strong> are an effective exercise to target the <strong>leg muscles</strong> and <strong>glutes</strong>. Ideal for intermediate to advanced athletes, this exercise helps improve <em>balance</em> and <em>stability</em> while increasing <strong>leg strength</strong>.</p>\",fentes-arrieres-barre,barbell-alternating-reverse-lunges,TYPE,STRENGTH\n157,Fentes arrières à la barre,Barbell Alternating Reverse Lunges,\"<p>Tenez-vous droit en tenant une barre placée sur l'arrière de vos épaules.</p><p>Faites un pas en arrière de 2 à 3 pieds avec un pied et abaissez votre corps au sol.</p><p>Votre genou arrière doit presque toucher le sol et votre genou avant doit être à un angle de 90 degrés.</p><p>Poussez vers le haut et revenez à la position de départ.</p><p>Répétez avec l'autre jambe.</p><p>Répétez le mouvement pour le nombre recommandé de répétitions, puis effectuez avec l'autre jambe.</p>\",\"<p>Stand upright holding a barbell placed across the back of your shoulders.</p><p>Step back 2-3 feet with one foot and lower your body to the ground.</p><p>Your back knee should almost touch the ground and your front knee should be at a 90-degree angle.</p><p>Push up to return to the starting position.</p><p>Repeat with the other leg.</p><p>Repeat the movement for the recommended number of repetitions, then switch to the other leg.</p>\",https://www.youtube.com/embed/NmfQzqGktgs?autoplay=1,https://img.youtube.com/vi/NmfQzqGktgs/hqdefault.jpg,\"<p>Les <strong>fentes arrières à la barre</strong> sont un exercice efficace pour cibler les <strong>muscles des jambes</strong> et les <strong>fessiers</strong>. Idéal pour les sportifs intermédiaires à avancés, cet exercice aide à améliorer l'<em>équilibre</em> et la <em>stabilité</em> tout en augmentant la <strong>force des jambes</strong>.</p>\",\"<p>The <strong>barbell alternating reverse lunges</strong> are an effective exercise to target the <strong>leg muscles</strong> and <strong>glutes</strong>. Ideal for intermediate to advanced athletes, this exercise helps improve <em>balance</em> and <em>stability</em> while increasing <strong>leg strength</strong>.</p>\",fentes-arrieres-barre,barbell-alternating-reverse-lunges,PRIMARY_MUSCLE,QUADRICEPS\n157,Fentes arrières à la barre,Barbell Alternating Reverse Lunges,\"<p>Tenez-vous droit en tenant une barre placée sur l'arrière de vos épaules.</p><p>Faites un pas en arrière de 2 à 3 pieds avec un pied et abaissez votre corps au sol.</p><p>Votre genou arrière doit presque toucher le sol et votre genou avant doit être à un angle de 90 degrés.</p><p>Poussez vers le haut et revenez à la position de départ.</p><p>Répétez avec l'autre jambe.</p><p>Répétez le mouvement pour le nombre recommandé de répétitions, puis effectuez avec l'autre jambe.</p>\",\"<p>Stand upright holding a barbell placed across the back of your shoulders.</p><p>Step back 2-3 feet with one foot and lower your body to the ground.</p><p>Your back knee should almost touch the ground and your front knee should be at a 90-degree angle.</p><p>Push up to return to the starting position.</p><p>Repeat with the other leg.</p><p>Repeat the movement for the recommended number of repetitions, then switch to the other leg.</p>\",https://www.youtube.com/embed/NmfQzqGktgs?autoplay=1,https://img.youtube.com/vi/NmfQzqGktgs/hqdefault.jpg,\"<p>Les <strong>fentes arrières à la barre</strong> sont un exercice efficace pour cibler les <strong>muscles des jambes</strong> et les <strong>fessiers</strong>. Idéal pour les sportifs intermédiaires à avancés, cet exercice aide à améliorer l'<em>équilibre</em> et la <em>stabilité</em> tout en augmentant la <strong>force des jambes</strong>.</p>\",\"<p>The <strong>barbell alternating reverse lunges</strong> are an effective exercise to target the <strong>leg muscles</strong> and <strong>glutes</strong>. Ideal for intermediate to advanced athletes, this exercise helps improve <em>balance</em> and <em>stability</em> while increasing <strong>leg strength</strong>.</p>\",fentes-arrieres-barre,barbell-alternating-reverse-lunges,SECONDARY_MUSCLE,GLUTES\n157,Fentes arrières à la barre,Barbell Alternating Reverse Lunges,\"<p>Tenez-vous droit en tenant une barre placée sur l'arrière de vos épaules.</p><p>Faites un pas en arrière de 2 à 3 pieds avec un pied et abaissez votre corps au sol.</p><p>Votre genou arrière doit presque toucher le sol et votre genou avant doit être à un angle de 90 degrés.</p><p>Poussez vers le haut et revenez à la position de départ.</p><p>Répétez avec l'autre jambe.</p><p>Répétez le mouvement pour le nombre recommandé de répétitions, puis effectuez avec l'autre jambe.</p>\",\"<p>Stand upright holding a barbell placed across the back of your shoulders.</p><p>Step back 2-3 feet with one foot and lower your body to the ground.</p><p>Your back knee should almost touch the ground and your front knee should be at a 90-degree angle.</p><p>Push up to return to the starting position.</p><p>Repeat with the other leg.</p><p>Repeat the movement for the recommended number of repetitions, then switch to the other leg.</p>\",https://www.youtube.com/embed/NmfQzqGktgs?autoplay=1,https://img.youtube.com/vi/NmfQzqGktgs/hqdefault.jpg,\"<p>Les <strong>fentes arrières à la barre</strong> sont un exercice efficace pour cibler les <strong>muscles des jambes</strong> et les <strong>fessiers</strong>. Idéal pour les sportifs intermédiaires à avancés, cet exercice aide à améliorer l'<em>équilibre</em> et la <em>stabilité</em> tout en augmentant la <strong>force des jambes</strong>.</p>\",\"<p>The <strong>barbell alternating reverse lunges</strong> are an effective exercise to target the <strong>leg muscles</strong> and <strong>glutes</strong>. Ideal for intermediate to advanced athletes, this exercise helps improve <em>balance</em> and <em>stability</em> while increasing <strong>leg strength</strong>.</p>\",fentes-arrieres-barre,barbell-alternating-reverse-lunges,SECONDARY_MUSCLE,HAMSTRINGS\n157,Fentes arrières à la barre,Barbell Alternating Reverse Lunges,\"<p>Tenez-vous droit en tenant une barre placée sur l'arrière de vos épaules.</p><p>Faites un pas en arrière de 2 à 3 pieds avec un pied et abaissez votre corps au sol.</p><p>Votre genou arrière doit presque toucher le sol et votre genou avant doit être à un angle de 90 degrés.</p><p>Poussez vers le haut et revenez à la position de départ.</p><p>Répétez avec l'autre jambe.</p><p>Répétez le mouvement pour le nombre recommandé de répétitions, puis effectuez avec l'autre jambe.</p>\",\"<p>Stand upright holding a barbell placed across the back of your shoulders.</p><p>Step back 2-3 feet with one foot and lower your body to the ground.</p><p>Your back knee should almost touch the ground and your front knee should be at a 90-degree angle.</p><p>Push up to return to the starting position.</p><p>Repeat with the other leg.</p><p>Repeat the movement for the recommended number of repetitions, then switch to the other leg.</p>\",https://www.youtube.com/embed/NmfQzqGktgs?autoplay=1,https://img.youtube.com/vi/NmfQzqGktgs/hqdefault.jpg,\"<p>Les <strong>fentes arrières à la barre</strong> sont un exercice efficace pour cibler les <strong>muscles des jambes</strong> et les <strong>fessiers</strong>. Idéal pour les sportifs intermédiaires à avancés, cet exercice aide à améliorer l'<em>équilibre</em> et la <em>stabilité</em> tout en augmentant la <strong>force des jambes</strong>.</p>\",\"<p>The <strong>barbell alternating reverse lunges</strong> are an effective exercise to target the <strong>leg muscles</strong> and <strong>glutes</strong>. Ideal for intermediate to advanced athletes, this exercise helps improve <em>balance</em> and <em>stability</em> while increasing <strong>leg strength</strong>.</p>\",fentes-arrieres-barre,barbell-alternating-reverse-lunges,EQUIPMENT,BARBELL\n157,Fentes arrières à la barre,Barbell Alternating Reverse Lunges,\"<p>Tenez-vous droit en tenant une barre placée sur l'arrière de vos épaules.</p><p>Faites un pas en arrière de 2 à 3 pieds avec un pied et abaissez votre corps au sol.</p><p>Votre genou arrière doit presque toucher le sol et votre genou avant doit être à un angle de 90 degrés.</p><p>Poussez vers le haut et revenez à la position de départ.</p><p>Répétez avec l'autre jambe.</p><p>Répétez le mouvement pour le nombre recommandé de répétitions, puis effectuez avec l'autre jambe.</p>\",\"<p>Stand upright holding a barbell placed across the back of your shoulders.</p><p>Step back 2-3 feet with one foot and lower your body to the ground.</p><p>Your back knee should almost touch the ground and your front knee should be at a 90-degree angle.</p><p>Push up to return to the starting position.</p><p>Repeat with the other leg.</p><p>Repeat the movement for the recommended number of repetitions, then switch to the other leg.</p>\",https://www.youtube.com/embed/NmfQzqGktgs?autoplay=1,https://img.youtube.com/vi/NmfQzqGktgs/hqdefault.jpg,\"<p>Les <strong>fentes arrières à la barre</strong> sont un exercice efficace pour cibler les <strong>muscles des jambes</strong> et les <strong>fessiers</strong>. Idéal pour les sportifs intermédiaires à avancés, cet exercice aide à améliorer l'<em>équilibre</em> et la <em>stabilité</em> tout en augmentant la <strong>force des jambes</strong>.</p>\",\"<p>The <strong>barbell alternating reverse lunges</strong> are an effective exercise to target the <strong>leg muscles</strong> and <strong>glutes</strong>. Ideal for intermediate to advanced athletes, this exercise helps improve <em>balance</em> and <em>stability</em> while increasing <strong>leg strength</strong>.</p>\",fentes-arrieres-barre,barbell-alternating-reverse-lunges,EQUIPMENT,BAR\n157,Fentes arrières à la barre,Barbell Alternating Reverse Lunges,\"<p>Tenez-vous droit en tenant une barre placée sur l'arrière de vos épaules.</p><p>Faites un pas en arrière de 2 à 3 pieds avec un pied et abaissez votre corps au sol.</p><p>Votre genou arrière doit presque toucher le sol et votre genou avant doit être à un angle de 90 degrés.</p><p>Poussez vers le haut et revenez à la position de départ.</p><p>Répétez avec l'autre jambe.</p><p>Répétez le mouvement pour le nombre recommandé de répétitions, puis effectuez avec l'autre jambe.</p>\",\"<p>Stand upright holding a barbell placed across the back of your shoulders.</p><p>Step back 2-3 feet with one foot and lower your body to the ground.</p><p>Your back knee should almost touch the ground and your front knee should be at a 90-degree angle.</p><p>Push up to return to the starting position.</p><p>Repeat with the other leg.</p><p>Repeat the movement for the recommended number of repetitions, then switch to the other leg.</p>\",https://www.youtube.com/embed/NmfQzqGktgs?autoplay=1,https://img.youtube.com/vi/NmfQzqGktgs/hqdefault.jpg,\"<p>Les <strong>fentes arrières à la barre</strong> sont un exercice efficace pour cibler les <strong>muscles des jambes</strong> et les <strong>fessiers</strong>. Idéal pour les sportifs intermédiaires à avancés, cet exercice aide à améliorer l'<em>équilibre</em> et la <em>stabilité</em> tout en augmentant la <strong>force des jambes</strong>.</p>\",\"<p>The <strong>barbell alternating reverse lunges</strong> are an effective exercise to target the <strong>leg muscles</strong> and <strong>glutes</strong>. Ideal for intermediate to advanced athletes, this exercise helps improve <em>balance</em> and <em>stability</em> while increasing <strong>leg strength</strong>.</p>\",fentes-arrieres-barre,barbell-alternating-reverse-lunges,MECHANICS_TYPE,COMPOUND\n163,Tirage horizontal (front) corde à la poulie haute,Facepulls,\"<p>Fixez une corde à la machine à câble à un réglage bas.</p><p>Tenez-vous face à la machine et tenez la corde avec une prise en pronation.</p><p>Reculez pour créer une tension dans le câble, les pieds écartés à la largeur des épaules.</p><p>Gardez le dos droit et penchez-vous légèrement en avant, en fléchissant légèrement les genoux.</p><p>Tirez la corde vers votre poitrine, en contractant vos omoplates ensemble.</p><p>Faites une pause à la fin du mouvement, puis relâchez lentement et étendez vos bras jusqu'à la position de départ.</p><p>Répétez le nombre souhaité de répétitions.</p>\",\"<p>Attach a rope to a low pulley cable machine.</p><p>Stand facing the machine and hold the rope with an overhand grip.</p><p>Step back to create tension in the cable, with feet shoulder-width apart.</p><p>Keep your back straight and lean slightly forward, bending your knees slightly.</p><p>Pull the rope towards your chest, squeezing your shoulder blades together.</p><p>Pause at the end of the movement, then slowly release and extend your arms back to the starting position.</p><p>Repeat for the desired number of repetitions.</p>\",https://www.youtube.com/embed/3ZViIERC1QQ?autoplay=1,https://img.youtube.com/vi/3ZViIERC1QQ/hqdefault.jpg,\"<p>Le <strong>Tirage horizontal (front) corde à la poulie haute</strong>, ou <em>Facepull</em>, est un excellent <em>exercice d'isolement</em> pour renforcer les<strong> muscles de la partie postérieure des épaules</strong> et du <strong>haut du dos</strong>. Particulièrement prisé pour son efficacité à prévenir et combattre les déséquilibres posturaux, il est adapté tant aux débutants qu'aux pratiquants confirmés.</p>\",\"<p>The <strong>Facepull</strong> or <em>Face Pull</em> is an excellent <em>isolation exercise</em> for strengthening the <strong>posterior shoulder muscles</strong> and the <strong>upper back</strong>. Highly valued for its effectiveness in preventing and combating postural imbalances, it is suitable for both beginners and advanced trainees.</p>\",tirage-horizontal-corde-poulie-haute,facepulls,TYPE,STRENGTH\n163,Tirage horizontal (front) corde à la poulie haute,Facepulls,\"<p>Fixez une corde à la machine à câble à un réglage bas.</p><p>Tenez-vous face à la machine et tenez la corde avec une prise en pronation.</p><p>Reculez pour créer une tension dans le câble, les pieds écartés à la largeur des épaules.</p><p>Gardez le dos droit et penchez-vous légèrement en avant, en fléchissant légèrement les genoux.</p><p>Tirez la corde vers votre poitrine, en contractant vos omoplates ensemble.</p><p>Faites une pause à la fin du mouvement, puis relâchez lentement et étendez vos bras jusqu'à la position de départ.</p><p>Répétez le nombre souhaité de répétitions.</p>\",\"<p>Attach a rope to a low pulley cable machine.</p><p>Stand facing the machine and hold the rope with an overhand grip.</p><p>Step back to create tension in the cable, with feet shoulder-width apart.</p><p>Keep your back straight and lean slightly forward, bending your knees slightly.</p><p>Pull the rope towards your chest, squeezing your shoulder blades together.</p><p>Pause at the end of the movement, then slowly release and extend your arms back to the starting position.</p><p>Repeat for the desired number of repetitions.</p>\",https://www.youtube.com/embed/3ZViIERC1QQ?autoplay=1,https://img.youtube.com/vi/3ZViIERC1QQ/hqdefault.jpg,\"<p>Le <strong>Tirage horizontal (front) corde à la poulie haute</strong>, ou <em>Facepull</em>, est un excellent <em>exercice d'isolement</em> pour renforcer les<strong> muscles de la partie postérieure des épaules</strong> et du <strong>haut du dos</strong>. Particulièrement prisé pour son efficacité à prévenir et combattre les déséquilibres posturaux, il est adapté tant aux débutants qu'aux pratiquants confirmés.</p>\",\"<p>The <strong>Facepull</strong> or <em>Face Pull</em> is an excellent <em>isolation exercise</em> for strengthening the <strong>posterior shoulder muscles</strong> and the <strong>upper back</strong>. Highly valued for its effectiveness in preventing and combating postural imbalances, it is suitable for both beginners and advanced trainees.</p>\",tirage-horizontal-corde-poulie-haute,facepulls,PRIMARY_MUSCLE,SHOULDERS\n163,Tirage horizontal (front) corde à la poulie haute,Facepulls,\"<p>Fixez une corde à la machine à câble à un réglage bas.</p><p>Tenez-vous face à la machine et tenez la corde avec une prise en pronation.</p><p>Reculez pour créer une tension dans le câble, les pieds écartés à la largeur des épaules.</p><p>Gardez le dos droit et penchez-vous légèrement en avant, en fléchissant légèrement les genoux.</p><p>Tirez la corde vers votre poitrine, en contractant vos omoplates ensemble.</p><p>Faites une pause à la fin du mouvement, puis relâchez lentement et étendez vos bras jusqu'à la position de départ.</p><p>Répétez le nombre souhaité de répétitions.</p>\",\"<p>Attach a rope to a low pulley cable machine.</p><p>Stand facing the machine and hold the rope with an overhand grip.</p><p>Step back to create tension in the cable, with feet shoulder-width apart.</p><p>Keep your back straight and lean slightly forward, bending your knees slightly.</p><p>Pull the rope towards your chest, squeezing your shoulder blades together.</p><p>Pause at the end of the movement, then slowly release and extend your arms back to the starting position.</p><p>Repeat for the desired number of repetitions.</p>\",https://www.youtube.com/embed/3ZViIERC1QQ?autoplay=1,https://img.youtube.com/vi/3ZViIERC1QQ/hqdefault.jpg,\"<p>Le <strong>Tirage horizontal (front) corde à la poulie haute</strong>, ou <em>Facepull</em>, est un excellent <em>exercice d'isolement</em> pour renforcer les<strong> muscles de la partie postérieure des épaules</strong> et du <strong>haut du dos</strong>. Particulièrement prisé pour son efficacité à prévenir et combattre les déséquilibres posturaux, il est adapté tant aux débutants qu'aux pratiquants confirmés.</p>\",\"<p>The <strong>Facepull</strong> or <em>Face Pull</em> is an excellent <em>isolation exercise</em> for strengthening the <strong>posterior shoulder muscles</strong> and the <strong>upper back</strong>. Highly valued for its effectiveness in preventing and combating postural imbalances, it is suitable for both beginners and advanced trainees.</p>\",tirage-horizontal-corde-poulie-haute,facepulls,SECONDARY_MUSCLE,FOREARMS\n163,Tirage horizontal (front) corde à la poulie haute,Facepulls,\"<p>Fixez une corde à la machine à câble à un réglage bas.</p><p>Tenez-vous face à la machine et tenez la corde avec une prise en pronation.</p><p>Reculez pour créer une tension dans le câble, les pieds écartés à la largeur des épaules.</p><p>Gardez le dos droit et penchez-vous légèrement en avant, en fléchissant légèrement les genoux.</p><p>Tirez la corde vers votre poitrine, en contractant vos omoplates ensemble.</p><p>Faites une pause à la fin du mouvement, puis relâchez lentement et étendez vos bras jusqu'à la position de départ.</p><p>Répétez le nombre souhaité de répétitions.</p>\",\"<p>Attach a rope to a low pulley cable machine.</p><p>Stand facing the machine and hold the rope with an overhand grip.</p><p>Step back to create tension in the cable, with feet shoulder-width apart.</p><p>Keep your back straight and lean slightly forward, bending your knees slightly.</p><p>Pull the rope towards your chest, squeezing your shoulder blades together.</p><p>Pause at the end of the movement, then slowly release and extend your arms back to the starting position.</p><p>Repeat for the desired number of repetitions.</p>\",https://www.youtube.com/embed/3ZViIERC1QQ?autoplay=1,https://img.youtube.com/vi/3ZViIERC1QQ/hqdefault.jpg,\"<p>Le <strong>Tirage horizontal (front) corde à la poulie haute</strong>, ou <em>Facepull</em>, est un excellent <em>exercice d'isolement</em> pour renforcer les<strong> muscles de la partie postérieure des épaules</strong> et du <strong>haut du dos</strong>. Particulièrement prisé pour son efficacité à prévenir et combattre les déséquilibres posturaux, il est adapté tant aux débutants qu'aux pratiquants confirmés.</p>\",\"<p>The <strong>Facepull</strong> or <em>Face Pull</em> is an excellent <em>isolation exercise</em> for strengthening the <strong>posterior shoulder muscles</strong> and the <strong>upper back</strong>. Highly valued for its effectiveness in preventing and combating postural imbalances, it is suitable for both beginners and advanced trainees.</p>\",tirage-horizontal-corde-poulie-haute,facepulls,EQUIPMENT,CABLE\n163,Tirage horizontal (front) corde à la poulie haute,Facepulls,\"<p>Fixez une corde à la machine à câble à un réglage bas.</p><p>Tenez-vous face à la machine et tenez la corde avec une prise en pronation.</p><p>Reculez pour créer une tension dans le câble, les pieds écartés à la largeur des épaules.</p><p>Gardez le dos droit et penchez-vous légèrement en avant, en fléchissant légèrement les genoux.</p><p>Tirez la corde vers votre poitrine, en contractant vos omoplates ensemble.</p><p>Faites une pause à la fin du mouvement, puis relâchez lentement et étendez vos bras jusqu'à la position de départ.</p><p>Répétez le nombre souhaité de répétitions.</p>\",\"<p>Attach a rope to a low pulley cable machine.</p><p>Stand facing the machine and hold the rope with an overhand grip.</p><p>Step back to create tension in the cable, with feet shoulder-width apart.</p><p>Keep your back straight and lean slightly forward, bending your knees slightly.</p><p>Pull the rope towards your chest, squeezing your shoulder blades together.</p><p>Pause at the end of the movement, then slowly release and extend your arms back to the starting position.</p><p>Repeat for the desired number of repetitions.</p>\",https://www.youtube.com/embed/3ZViIERC1QQ?autoplay=1,https://img.youtube.com/vi/3ZViIERC1QQ/hqdefault.jpg,\"<p>Le <strong>Tirage horizontal (front) corde à la poulie haute</strong>, ou <em>Facepull</em>, est un excellent <em>exercice d'isolement</em> pour renforcer les<strong> muscles de la partie postérieure des épaules</strong> et du <strong>haut du dos</strong>. Particulièrement prisé pour son efficacité à prévenir et combattre les déséquilibres posturaux, il est adapté tant aux débutants qu'aux pratiquants confirmés.</p>\",\"<p>The <strong>Facepull</strong> or <em>Face Pull</em> is an excellent <em>isolation exercise</em> for strengthening the <strong>posterior shoulder muscles</strong> and the <strong>upper back</strong>. Highly valued for its effectiveness in preventing and combating postural imbalances, it is suitable for both beginners and advanced trainees.</p>\",tirage-horizontal-corde-poulie-haute,facepulls,EQUIPMENT,ROPE\n163,Tirage horizontal (front) corde à la poulie haute,Facepulls,\"<p>Fixez une corde à la machine à câble à un réglage bas.</p><p>Tenez-vous face à la machine et tenez la corde avec une prise en pronation.</p><p>Reculez pour créer une tension dans le câble, les pieds écartés à la largeur des épaules.</p><p>Gardez le dos droit et penchez-vous légèrement en avant, en fléchissant légèrement les genoux.</p><p>Tirez la corde vers votre poitrine, en contractant vos omoplates ensemble.</p><p>Faites une pause à la fin du mouvement, puis relâchez lentement et étendez vos bras jusqu'à la position de départ.</p><p>Répétez le nombre souhaité de répétitions.</p>\",\"<p>Attach a rope to a low pulley cable machine.</p><p>Stand facing the machine and hold the rope with an overhand grip.</p><p>Step back to create tension in the cable, with feet shoulder-width apart.</p><p>Keep your back straight and lean slightly forward, bending your knees slightly.</p><p>Pull the rope towards your chest, squeezing your shoulder blades together.</p><p>Pause at the end of the movement, then slowly release and extend your arms back to the starting position.</p><p>Repeat for the desired number of repetitions.</p>\",https://www.youtube.com/embed/3ZViIERC1QQ?autoplay=1,https://img.youtube.com/vi/3ZViIERC1QQ/hqdefault.jpg,\"<p>Le <strong>Tirage horizontal (front) corde à la poulie haute</strong>, ou <em>Facepull</em>, est un excellent <em>exercice d'isolement</em> pour renforcer les<strong> muscles de la partie postérieure des épaules</strong> et du <strong>haut du dos</strong>. Particulièrement prisé pour son efficacité à prévenir et combattre les déséquilibres posturaux, il est adapté tant aux débutants qu'aux pratiquants confirmés.</p>\",\"<p>The <strong>Facepull</strong> or <em>Face Pull</em> is an excellent <em>isolation exercise</em> for strengthening the <strong>posterior shoulder muscles</strong> and the <strong>upper back</strong>. Highly valued for its effectiveness in preventing and combating postural imbalances, it is suitable for both beginners and advanced trainees.</p>\",tirage-horizontal-corde-poulie-haute,facepulls,MECHANICS_TYPE,ISOLATION\n164,Sauts altérnés aux côtés du banc,Bench Hops,\"<p>Commencez avec une box ou un banc devant vous. Tenez-vous debout, les pieds écartés de la largeur des épaules. ce sera votre position de départ.</p><p> Effectuez un court squat en préparation du saut</p><p> Sautez par-dessus le banc, atterrissez avec les genoux pliés, en absorbant l'impact à travers les jambes.</p>\",\"<p>Start with a box or bench in front of you. Stand with feet shoulder-width apart. This will be your starting position.</p><p> Perform a short squat in preparation for the jump.</p><p> Jump over the bench, landing with your knees bent, absorbing the impact through your legs.</p>\",https://www.youtube.com/embed/R3TCOHRwCl8?autoplay=1,https://img.youtube.com/vi/R3TCOHRwCl8/hqdefault.jpg,\"<p>Les <strong>sauts altérnés aux côtés du banc</strong> sont un excellent moyen d'<em>améliorer la puissance explosive</em> et l'<em>agilité</em>. En sautant de manière répétitive d'un côté à l'autre du banc, vous ferez travailler vos <strong>quadriceps, ischio-jambiers et mollets</strong>. Ce mouvement intense est particulièrement bénéfique pour les athlètes et ceux cherchant à améliorer leur condition physique générale.</p>\",\"<p><strong>Bench hops</strong> are an excellent way to <em>improve explosive power</em> and <em>agility</em>. By repeatedly hopping from side to side over a bench, you'll work your <strong>quads, hamstrings, and calves</strong>. This intense movement is especially beneficial for athletes and those looking to boost their overall fitness.</p>\",sauts-alternes-cotes-banc,bench-hops,TYPE,PLYOMETRICS\n164,Sauts altérnés aux côtés du banc,Bench Hops,\"<p>Commencez avec une box ou un banc devant vous. Tenez-vous debout, les pieds écartés de la largeur des épaules. ce sera votre position de départ.</p><p> Effectuez un court squat en préparation du saut</p><p> Sautez par-dessus le banc, atterrissez avec les genoux pliés, en absorbant l'impact à travers les jambes.</p>\",\"<p>Start with a box or bench in front of you. Stand with feet shoulder-width apart. This will be your starting position.</p><p> Perform a short squat in preparation for the jump.</p><p> Jump over the bench, landing with your knees bent, absorbing the impact through your legs.</p>\",https://www.youtube.com/embed/R3TCOHRwCl8?autoplay=1,https://img.youtube.com/vi/R3TCOHRwCl8/hqdefault.jpg,\"<p>Les <strong>sauts altérnés aux côtés du banc</strong> sont un excellent moyen d'<em>améliorer la puissance explosive</em> et l'<em>agilité</em>. En sautant de manière répétitive d'un côté à l'autre du banc, vous ferez travailler vos <strong>quadriceps, ischio-jambiers et mollets</strong>. Ce mouvement intense est particulièrement bénéfique pour les athlètes et ceux cherchant à améliorer leur condition physique générale.</p>\",\"<p><strong>Bench hops</strong> are an excellent way to <em>improve explosive power</em> and <em>agility</em>. By repeatedly hopping from side to side over a bench, you'll work your <strong>quads, hamstrings, and calves</strong>. This intense movement is especially beneficial for athletes and those looking to boost their overall fitness.</p>\",sauts-alternes-cotes-banc,bench-hops,TYPE,CROSSFIT\n164,Sauts altérnés aux côtés du banc,Bench Hops,\"<p>Commencez avec une box ou un banc devant vous. Tenez-vous debout, les pieds écartés de la largeur des épaules. ce sera votre position de départ.</p><p> Effectuez un court squat en préparation du saut</p><p> Sautez par-dessus le banc, atterrissez avec les genoux pliés, en absorbant l'impact à travers les jambes.</p>\",\"<p>Start with a box or bench in front of you. Stand with feet shoulder-width apart. This will be your starting position.</p><p> Perform a short squat in preparation for the jump.</p><p> Jump over the bench, landing with your knees bent, absorbing the impact through your legs.</p>\",https://www.youtube.com/embed/R3TCOHRwCl8?autoplay=1,https://img.youtube.com/vi/R3TCOHRwCl8/hqdefault.jpg,\"<p>Les <strong>sauts altérnés aux côtés du banc</strong> sont un excellent moyen d'<em>améliorer la puissance explosive</em> et l'<em>agilité</em>. En sautant de manière répétitive d'un côté à l'autre du banc, vous ferez travailler vos <strong>quadriceps, ischio-jambiers et mollets</strong>. Ce mouvement intense est particulièrement bénéfique pour les athlètes et ceux cherchant à améliorer leur condition physique générale.</p>\",\"<p><strong>Bench hops</strong> are an excellent way to <em>improve explosive power</em> and <em>agility</em>. By repeatedly hopping from side to side over a bench, you'll work your <strong>quads, hamstrings, and calves</strong>. This intense movement is especially beneficial for athletes and those looking to boost their overall fitness.</p>\",sauts-alternes-cotes-banc,bench-hops,TYPE,CARDIO\n164,Sauts altérnés aux côtés du banc,Bench Hops,\"<p>Commencez avec une box ou un banc devant vous. Tenez-vous debout, les pieds écartés de la largeur des épaules. ce sera votre position de départ.</p><p> Effectuez un court squat en préparation du saut</p><p> Sautez par-dessus le banc, atterrissez avec les genoux pliés, en absorbant l'impact à travers les jambes.</p>\",\"<p>Start with a box or bench in front of you. Stand with feet shoulder-width apart. This will be your starting position.</p><p> Perform a short squat in preparation for the jump.</p><p> Jump over the bench, landing with your knees bent, absorbing the impact through your legs.</p>\",https://www.youtube.com/embed/R3TCOHRwCl8?autoplay=1,https://img.youtube.com/vi/R3TCOHRwCl8/hqdefault.jpg,\"<p>Les <strong>sauts altérnés aux côtés du banc</strong> sont un excellent moyen d'<em>améliorer la puissance explosive</em> et l'<em>agilité</em>. En sautant de manière répétitive d'un côté à l'autre du banc, vous ferez travailler vos <strong>quadriceps, ischio-jambiers et mollets</strong>. Ce mouvement intense est particulièrement bénéfique pour les athlètes et ceux cherchant à améliorer leur condition physique générale.</p>\",\"<p><strong>Bench hops</strong> are an excellent way to <em>improve explosive power</em> and <em>agility</em>. By repeatedly hopping from side to side over a bench, you'll work your <strong>quads, hamstrings, and calves</strong>. This intense movement is especially beneficial for athletes and those looking to boost their overall fitness.</p>\",sauts-alternes-cotes-banc,bench-hops,PRIMARY_MUSCLE,FULL_BODY\n164,Sauts altérnés aux côtés du banc,Bench Hops,\"<p>Commencez avec une box ou un banc devant vous. Tenez-vous debout, les pieds écartés de la largeur des épaules. ce sera votre position de départ.</p><p> Effectuez un court squat en préparation du saut</p><p> Sautez par-dessus le banc, atterrissez avec les genoux pliés, en absorbant l'impact à travers les jambes.</p>\",\"<p>Start with a box or bench in front of you. Stand with feet shoulder-width apart. This will be your starting position.</p><p> Perform a short squat in preparation for the jump.</p><p> Jump over the bench, landing with your knees bent, absorbing the impact through your legs.</p>\",https://www.youtube.com/embed/R3TCOHRwCl8?autoplay=1,https://img.youtube.com/vi/R3TCOHRwCl8/hqdefault.jpg,\"<p>Les <strong>sauts altérnés aux côtés du banc</strong> sont un excellent moyen d'<em>améliorer la puissance explosive</em> et l'<em>agilité</em>. En sautant de manière répétitive d'un côté à l'autre du banc, vous ferez travailler vos <strong>quadriceps, ischio-jambiers et mollets</strong>. Ce mouvement intense est particulièrement bénéfique pour les athlètes et ceux cherchant à améliorer leur condition physique générale.</p>\",\"<p><strong>Bench hops</strong> are an excellent way to <em>improve explosive power</em> and <em>agility</em>. By repeatedly hopping from side to side over a bench, you'll work your <strong>quads, hamstrings, and calves</strong>. This intense movement is especially beneficial for athletes and those looking to boost their overall fitness.</p>\",sauts-alternes-cotes-banc,bench-hops,EQUIPMENT,BENCH\n164,Sauts altérnés aux côtés du banc,Bench Hops,\"<p>Commencez avec une box ou un banc devant vous. Tenez-vous debout, les pieds écartés de la largeur des épaules. ce sera votre position de départ.</p><p> Effectuez un court squat en préparation du saut</p><p> Sautez par-dessus le banc, atterrissez avec les genoux pliés, en absorbant l'impact à travers les jambes.</p>\",\"<p>Start with a box or bench in front of you. Stand with feet shoulder-width apart. This will be your starting position.</p><p> Perform a short squat in preparation for the jump.</p><p> Jump over the bench, landing with your knees bent, absorbing the impact through your legs.</p>\",https://www.youtube.com/embed/R3TCOHRwCl8?autoplay=1,https://img.youtube.com/vi/R3TCOHRwCl8/hqdefault.jpg,\"<p>Les <strong>sauts altérnés aux côtés du banc</strong> sont un excellent moyen d'<em>améliorer la puissance explosive</em> et l'<em>agilité</em>. En sautant de manière répétitive d'un côté à l'autre du banc, vous ferez travailler vos <strong>quadriceps, ischio-jambiers et mollets</strong>. Ce mouvement intense est particulièrement bénéfique pour les athlètes et ceux cherchant à améliorer leur condition physique générale.</p>\",\"<p><strong>Bench hops</strong> are an excellent way to <em>improve explosive power</em> and <em>agility</em>. By repeatedly hopping from side to side over a bench, you'll work your <strong>quads, hamstrings, and calves</strong>. This intense movement is especially beneficial for athletes and those looking to boost their overall fitness.</p>\",sauts-alternes-cotes-banc,bench-hops,MECHANICS_TYPE,COMPOUND"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  postgres:\n    image: postgres:15\n    ports:\n      - \"${DB_PORT:-5432}:5432\"\n    volumes:\n      - pgdata:/var/lib/postgresql/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n    env_file: .env\n\n  workout_cool:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - \"${APP_PORT:-3000}:3000\"\n    depends_on:\n      postgres:\n        condition: service_healthy\n    env_file: .env\nvolumes:\n  pgdata:\n"
  },
  {
    "path": "docs/SELF-HOSTING.md",
    "content": "# Self-Hosting Workout-Cool\n\nDeploy **Workout-Cool** on your own server using Docker. This guide provides two deployment options with step-by-step instructions.\n\n---\n\n## 📑 Table of Contents\n\n- [Prerequisites](#-prerequisites)\n- [Quick Start](#-quick-start)\n  - [Common Setup Steps](#common-setup-steps)\n  - [Option 1: Docker Compose (All-in-One)](#option-1-docker-compose-all-in-one)\n  - [Option 2: Docker Only (External Database)](#option-2-docker-only-external-database)\n- [Management Commands](#️-management-commands)\n- [Mapping Your Domain & Enabling HTTPS](#-mapping-your-domain--enabling-https)\n- [Troubleshooting](#-troubleshooting)\n- [Resources & Support](#-resources--support)\n\n---\n\n## 📋 Prerequisites\n\nBefore you begin, ensure your server meets these requirements:\n\n- **Operating System**: Linux server or VPS (Ubuntu 20.04+ recommended)\n- **Docker**: [Install Docker](https://docs.docker.com/get-docker/)\n- **Docker Compose**: [Install Docker Compose](https://docs.docker.com/compose/install/)\n- **Git**: [Install Git](https://git-scm.com/downloads)\n- **Basic Knowledge**: Familiarity with command line operations\n\n---\n\n\n## 🚀 Quick Start\n\n**Prefer watching?** Watch our [3-minute video guide on self-hosting Workout.Cool](https://www.youtube.com/watch?v=HQecjb0CfAo):\n\n<p align=\"center\">\n  <a href=\"https://www.youtube.com/watch?v=HQecjb0CfAo\">\n    <img src=\"https://img.youtube.com/vi/HQecjb0CfAo/maxresdefault.jpg\" alt=\"Self-Host Workout.Cool in 3 Minutes Video Guide\" style=\"max-width: 400px;\">\n  </a>\n</p>\n\n### Common Setup Steps\n\nThese steps are required for both deployment options:\n\n#### 1. Connect to Your Server\n\n```bash\nssh your-user@your-server-ip\n```\n\n#### 2. Clone the Repository\n\n```bash\nmkdir -p ~/apps\ncd ~/apps\ngit clone https://github.com/Snouzy/workout-cool.git\ncd workout-cool\n```\n\n#### 3. Configure Environment Variables\n\n```bash\ncp .env.example .env\nnano .env\n```\n\n**Essential Environment Variables:**\n\n```bash\n# Application Configuration (Required for both options)\nBETTER_AUTH_URL=http://your-server-ip:3000\nBETTER_AUTH_SECRET=your-secret-key-here\n\n# Optional: Seed sample data on first run\nSEED_SAMPLE_DATA=true\n```\n\n#### 4. Customize Sample Data (Optional)\n\n```bash\nnano data/sample-exercises.csv\n```\n\n**📚 See [Exercise Database Import section in the README](../README.md#exercise-database-import).**\n\n---\n\n### Option 1: Docker Compose (All-in-One)\n\nThis option automatically sets up both the application and PostgreSQL database.\n\n**Additional Environment Variables:**\n\n```bash\n# Database Configuration (Docker Compose)\nPOSTGRES_USER=my-user\nPOSTGRES_PASSWORD=my-password\nPOSTGRES_DB=workout-cool\nDB_HOST=postgres\nDB_PORT=5432\n\nDATABASE_URL=postgresql://username:password@postgres:5432/workout-cool\n```\n\n**Deploy:**\n\n```bash\ndocker compose up -d\n```\n\n**Access:**\n\nVisit `http://your-server-ip:3000`\n\n---\n\n### Option 2: Docker Only (External Database)\n\nUse this option if you have an existing PostgreSQL database.\n\n**Additional Environment Variables:**\n\n```bash\n# Database Configuration (External Database)\nDATABASE_URL=postgresql://username:password@your-db-host:5432/workout-cool\n```\n\n**Deploy:**\n\n```bash\ndocker build -t workout-cool .\ndocker run -d --name workout-cool -p 3000:3000 --env-file .env workout-cool\n```\n\n**Access:**\n\nVisit `http://your-server-ip:3000`\n\n---\n\n## 🌐 Mapping Your Domain & Enabling HTTPS\n\nMake your app accessible via your own domain and secure it with HTTPS. Here's how:\n\n### 1. Point Your Domain\n\n1. Log in to your domain registrar.\n2. Create an **A record** for `yourdomain.com` pointing to your server's IP.\n3. (Optional) Create a **CNAME** record for `www` pointing to `yourdomain.com`.\n\n### 2. Set Up HTTPS with a Reverse Proxy\n\n#### Option A: Caddy (Recommended)\n\nCaddy provides automatic HTTPS and a simpler config.\n\n1. **Install Caddy:**\n    ```bash\n    sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https\n    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg\n    echo \"deb [signed-by=/usr/share/keyrings/caddy-stable-archive-keyring.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian all main\" | sudo tee /etc/apt/sources.list.d/caddy-stable.list\n    sudo apt update\n    sudo apt install caddy\n    ```\n2. **Configure Caddy:**\n    ```bash\n    sudo nano /etc/caddy/Caddyfile\n    ```\n    Add:\n    ```\n    yourdomain.com {\n        reverse_proxy localhost:3000\n    }\n    ```\n3. **Reload Caddy:**\n    ```bash\n    sudo systemctl reload caddy\n    ```\n\n✅ Caddy will:\n- Automatically request and install an SSL certificate via Let’s Encrypt\n- Renew it automatically\n- Proxy requests to your app running on port `3000`\n\n#### Option B: Nginx + Let's Encrypt\n\nUse this if you're more familiar with Nginx.\n\n1. **Install Nginx & Certbot:**\n    ```bash\n    sudo apt update && sudo apt install nginx certbot python3-certbot-nginx\n    ```\n2. **Create Nginx config:**\n\n    ```bash\n    sudo nano /etc/nginx/sites-available/workout-cool\n    ```\n\n    Add:\n    ```\n    server {\n        listen 80;\n        server_name yourdomain.com www.yourdomain.com;\n\n        location / {\n            proxy_pass http://localhost:3000;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n        }\n    }\n    ```\n3.  **Enable and reload:**\n    ```bash\n    sudo ln -s /etc/nginx/sites-available/workout-cool /etc/nginx/sites-enabled/\n    sudo nginx -t && sudo systemctl reload nginx\n    ```\n4. **Get an HTTPS certificate:**\n    ```bash\n    sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com\n    ```\n\n### 3. Update Environment\n\nUpdate your `.env` file with the new domain\n\n```env\nBETTER_AUTH_URL=https://yourdomain.com\nNEXT_PUBLIC_APP_URL=https://yourdomain.com\n```\n\n---\n\n## 🛠️ Management Commands\n\n### Docker Compose Commands\n\n```bash\n# Start/Stop services\ndocker compose up -d\ndocker compose down\n\n# View logs\ndocker compose logs -f\n\n# Update and restart\ngit pull\ndocker compose down\ndocker compose up -d --build\n```\n\n### Docker Commands\n\n```bash\n# Start/Stop container\ndocker start workout-cool\ndocker stop workout-cool\n\n# View logs\ndocker logs -f workout-cool\n\n# Update application\ngit pull\ndocker build -t workout-cool .\ndocker stop workout-cool\ndocker rm workout-cool\ndocker run -d --name workout-cool -p 3000:3000 --env-file .env workout-cool\n```\n\n---\n\n## 🐛 Troubleshooting\n\n### Common Issues\n\n#### Application Won't Start\n```bash\n# Check logs\ndocker compose logs workout_cool  # or docker logs workout-cool\n\n# Verify environment variables using docker compose\ndocker compose exec workout_cool env | grep DATABASE_URL  # or docker exec workout-cool env | grep DATABASE_URL\n```\n\n#### Database Connection Issues\n```bash\n# Test database connectivity (Docker Compose)\ndocker compose exec postgres psql -U postgres -d workout_cool -c \"SELECT 1;\"\n\n# Check database status\ndocker compose ps postgres\n```\n\n#### Port Already in Use\n```bash\n# Check what's using port 3000\nsudo lsof -i :3000\n\n# Change port in docker-compose.yml\n# ports:\n#   - \"3001:3000\"  # Use port 3001 instead\n```\n\n### Getting Help\n\nIf you encounter issues:\n\n1. **Check the logs**: Use `docker compose logs` or `docker logs`\n2. **Verify configuration**: Ensure all environment variables are set correctly\n3. **Database connectivity**: Test database connection manually\n4. **Search existing issues** or create a new one on [GitHub](https://github.com/Snouzy/workout-cool/issues)\n5. **Join our [Discord community](https://discord.gg/NtrsUBuHUB)** for support\n\n---\n\n## 📚 Resources & Support\n\n### Documentation\n- [Docker Documentation](https://docs.docker.com/)\n- [Docker Compose Documentation](https://docs.docker.com/compose/)\n- [PostgreSQL Documentation](https://www.postgresql.org/docs/)\n- [Next.js Deployment Guide](https://nextjs.org/docs/deployment)\n- [Caddy Documentation](https://caddyserver.com/docs/install)\n\n### Community & Support\n- **GitHub**: [Repository](https://github.com/Snouzy/workout-cool) | [Issues](https://github.com/Snouzy/workout-cool/issues)\n- **Discord**: [Join our community](https://discord.gg/NtrsUBuHUB)\n\n---\n\n*Last updated: July 2025*\n"
  },
  {
    "path": "emails/ContactSupportEmail.tsx",
    "content": "import * as React from \"react\";\nimport { Body, Container, Head, Heading, Hr, Html, Preview, Section, Text, Tailwind } from \"@react-email/components\";\n\ninterface ContactSupportEmailProps {\n  email: string;\n  subject: string;\n  message: string;\n}\n\nconst ContactSupportEmail = ({ email, subject, message }: ContactSupportEmailProps) => (\n  <Html>\n    <Head />\n    <Preview>New Contact Request - {subject}</Preview>\n    <Tailwind>\n      <Body className=\"mx-auto my-auto bg-white font-sans\">\n        <Container className=\"mx-auto my-[40px] w-[465px] rounded border border-solid border-[#eaeaea] p-[20px]\">\n          <Section className=\"mt-[32px]\">\n            <Heading className=\"mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-black\">New Contact Request</Heading>\n            <Text className=\"text-[14px] leading-[24px] text-black\">\n              You received a new message from: <strong>{email}</strong>\n            </Text>\n            <Text className=\"text-[14px] leading-[24px] text-black\">\n              <strong>Subject:</strong> {subject}\n            </Text>\n            <Hr className=\"mx-0 my-[26px] w-full border border-solid border-[#eaeaea]\" />\n            <Text className=\"text-[14px] leading-[24px] text-black\">\n              <strong>Message:</strong>\n            </Text>\n            <Text className=\"whitespace-pre-wrap text-[14px] leading-[24px] text-black\">{message}</Text>\n          </Section>\n        </Container>\n      </Body>\n    </Tailwind>\n  </Html>\n);\n\nexport default ContactSupportEmail;\n"
  },
  {
    "path": "emails/DeleteAccountEmail.tsx",
    "content": "import { Link, Section, Text } from \"@react-email/components\";\n\nimport { SiteConfig } from \"@/shared/config/site-config\";\n\nimport { BaseEmailLayout } from \"./utils/BaseEmailLayout\";\n\nexport default function DeleteAccountEmail({ email }: { email: string }) {\n  return (\n    <BaseEmailLayout previewText={\"Your account has been deleted\"}>\n      <Section className=\"my-6\">\n        <Text className=\"text-lg leading-6\">Hello,</Text>\n        <Text className=\"text-lg leading-6\">\n          You account with email{\" \"}\n          <Link className=\"text-sky-500 hover:underline\" href={`mailto:${email}`}>\n            {email}\n          </Link>{\" \"}\n          has been deleted.\n        </Text>\n        <Text className=\"text-lg leading-6\">This action is irreversible.</Text>\n        <Text className=\"text-lg leading-6\">If you have any questions, please contact us at {SiteConfig.email.contact}.</Text>\n      </Section>\n      <Text className=\"text-lg leading-6\">\n        Best,\n        <br />- {SiteConfig.maker.name} from {SiteConfig.title}\n      </Text>\n    </BaseEmailLayout>\n  );\n}\n"
  },
  {
    "path": "emails/ResetPasswordEmail.tsx",
    "content": "import * as React from \"react\";\nimport { Button, Heading, Hr, Link, Section, Text } from \"@react-email/components\";\n\nimport { SiteConfig } from \"@/shared/config/site-config\";\n\nimport { BaseEmailLayout } from \"./utils/BaseEmailLayout\"; // Import the layout\n\ninterface ResetPasswordEmailProps {\n  url: string;\n}\n\nconst primaryColor = \"#2563EB\"; // Blue-600\n\nexport const ResetPasswordEmail = ({ url }: ResetPasswordEmailProps) => (\n  <BaseEmailLayout previewText={`Reset your password for ${SiteConfig.title}`}>\n    <Heading className=\"mb-6 text-center text-2xl font-semibold text-gray-900\">🔒 Reset Your Password</Heading>\n\n    <Section>\n      <Text className=\"text-text text-base leading-relaxed\">Hello,</Text>\n      <Text className=\"text-text text-base leading-relaxed\">\n        We received a request to reset the password for your {SiteConfig.title} account. If this was you, click the button below to set a\n        new password:\n      </Text>\n    </Section>\n\n    <Section className=\"my-8 text-center\">\n      <Button\n        className=\"inline-block rounded-md bg-primary px-6 py-3 text-center text-sm font-medium text-white no-underline transition hover:opacity-90\"\n        href={url}\n        style={{ backgroundColor: primaryColor }} // Inline style for better email client compatibility\n      >\n        Set New Password\n      </Button>\n    </Section>\n\n    <Section>\n      <Text className=\"text-text text-base leading-relaxed\">\n        If you didn&apos;t request a password reset, please ignore this email. Your password will remain unchanged.\n      </Text>\n    </Section>\n\n    <Hr className=\"my-6 border-t\" />\n\n    <Section>\n      <Text className=\"text-lightText text-sm leading-normal\">\n        If the button above doesn&apos;t work, you can copy and paste this link into your browser:\n      </Text>\n      <Link className=\"block break-all text-sm text-primary hover:underline\" href={url}>\n        {url}\n      </Link>\n    </Section>\n    {/* Footer is now handled by BaseEmailLayout */}\n  </BaseEmailLayout>\n);\n\nexport default ResetPasswordEmail; // Keep export consistent\n"
  },
  {
    "path": "emails/VerifyEmail.tsx",
    "content": "import * as React from \"react\";\nimport { Button, Heading, Hr, Link, Section, Text } from \"@react-email/components\";\n\nimport { SiteConfig } from \"@/shared/config/site-config\";\n\nimport { BaseEmailLayout } from \"./utils/BaseEmailLayout\"; // Import the layout\n\ninterface VerifyEmailProps {\n  url: string;\n}\n\nconst primaryColor = \"#2563EB\"; // Blue-600\n\nexport const VerifyEmail = ({ url }: VerifyEmailProps) => (\n  <BaseEmailLayout previewText={`Verify your email address for ${SiteConfig.title}`}>\n    <Heading className=\"mb-6 text-center text-2xl font-semibold text-gray-900\">✅ Verify Your Email</Heading>\n\n    <Section>\n      <Text className=\"text-text text-base leading-relaxed\">Welcome to {SiteConfig.title}!</Text>\n      <Text className=\"text-text text-base leading-relaxed\">\n        Please click the button below to verify your email address and complete your signup or login:\n      </Text>\n    </Section>\n\n    <Section className=\"my-8 text-center\">\n      <Button\n        className=\"inline-block rounded-md bg-primary px-6 py-3 text-center text-sm font-medium text-white no-underline transition hover:opacity-90\"\n        href={url}\n        style={{ backgroundColor: primaryColor }} // Inline style for better email client compatibility\n      >\n        Verify Email Address\n      </Button>\n    </Section>\n\n    <Section>\n      <Text className=\"text-text text-base leading-relaxed\">If you didn&apos;t request this email, you can safely ignore it.</Text>\n    </Section>\n\n    <Hr className=\"my-6 border-t\" />\n\n    <Section>\n      <Text className=\"text-lightText text-sm leading-normal\">\n        If the button above doesn&apos;t work, you can copy and paste this link into your browser:\n      </Text>\n      <Link className=\"block break-all text-sm text-primary hover:underline\" href={url}>\n        {url}\n      </Link>\n    </Section>\n    {/* Footer is now handled by BaseEmailLayout */}\n  </BaseEmailLayout>\n);\n"
  },
  {
    "path": "emails/utils/BaseEmailLayout.tsx",
    "content": "import * as React from \"react\";\nimport { Body, Container, Head, Hr, Html, Img, Preview, Section, Text, Tailwind } from \"@react-email/components\";\n\nimport { SiteConfig } from \"@/shared/config/site-config\";\n\ninterface BaseEmailLayoutProps {\n  previewText: string;\n  children: React.ReactNode;\n}\n\n// Consistent styling variables\nconst primaryColor = \"#2563EB\"; // Blue-600\n// eslint-disable-next-line quotes\nconst fontFamily = 'Inter, \"Helvetica Neue\", Helvetica, Arial, sans-serif';\nconst containerPadding = \"32px\"; // p-8\nconst mainBgColor = \"#f9fafb\"; // bg-gray-50\nconst containerBgColor = \"#ffffff\"; // bg-white\nconst textColor = \"#374151\"; // text-gray-700\nconst lightTextColor = \"#6b7280\"; // text-gray-500\nconst borderColor = \"#e5e7eb\"; // border-gray-200\n\nexport const BaseEmailLayout = ({ previewText, children }: BaseEmailLayoutProps) => (\n  <Html>\n    <Head>\n      {/* Font import */}\n      <link href=\"https://fonts.googleapis.com\" rel=\"preconnect\" />\n      <link crossOrigin=\"\" href=\"https://fonts.gstatic.com\" rel=\"preconnect\" />\n      <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap\" rel=\"stylesheet\" />\n    </Head>\n    <Preview>{previewText}</Preview>\n    <Tailwind\n      config={{\n        theme: {\n          extend: {\n            colors: {\n              primary: primaryColor,\n              text: textColor,\n              lightText: lightTextColor,\n            },\n            fontFamily: {\n              sans: [fontFamily],\n            },\n            borderColor: {\n              DEFAULT: borderColor,\n            },\n          },\n        },\n      }}\n    >\n      <Body className=\"mx-auto my-auto bg-gray-50 font-sans\" style={{ backgroundColor: mainBgColor }}>\n        <Container\n          className=\"mx-auto my-10 max-w-md rounded-lg border border-solid bg-white shadow-sm\"\n          style={{\n            padding: containerPadding,\n            backgroundColor: containerBgColor,\n            borderColor: borderColor,\n          }}\n        >\n          {/* Logo Section */}\n          <Section className=\"mb-6 text-center\">\n            <Img alt={`${SiteConfig.title} Logo`} className=\"mx-auto\" height=\"36\" src={SiteConfig.logo} width=\"auto\" />\n          </Section>\n\n          {/* Email specific content */}\n          {children}\n\n          {/* Footer Section */}\n          <Hr className=\"my-6 border-t\" style={{ borderColor: borderColor }} />\n          <Section>\n            <Text className=\"text-lightText text-sm\" style={{ color: lightTextColor }}>\n              Best regards,\n              <br />\n              The {SiteConfig.title} Team\n            </Text>\n            {/* Optional: Add company address or other info here if needed */}\n            {/* <Text className=\"text-xs text-gray-400\">{SiteConfig.company.address}</Text> */}\n          </Section>\n        </Container>\n      </Body>\n    </Tailwind>\n  </Html>\n);\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import { fixupConfigRules, fixupPluginRules } from \"@eslint/compat\";\nimport js from \"@eslint/js\";\nimport { FlatCompat } from \"@eslint/eslintrc\";\nimport { configs as tsConfigs } from \"typescript-eslint\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport globals from \"globals\";\nimport nextPlugin from \"@next/eslint-plugin-next\";\nimport reactHooks from \"eslint-plugin-react-hooks\";\nimport reactPlugin from \"eslint-plugin-react\";\nimport importPlugin from \"eslint-plugin-import\";\nimport unusedImportsPlugin from \"eslint-plugin-unused-imports\";\nimport tsParser from \"@typescript-eslint/parser\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n  recommendedConfig: js.configs.recommended,\n});\n\nconst config = [\n  js.configs.recommended,\n  ...tsConfigs.recommended,\n  ...fixupConfigRules(\n    compat.extends(\n      \"plugin:react/recommended\",\n      \"plugin:react/jsx-runtime\",\n      \"plugin:react-hooks/recommended\",\n      \"plugin:prettier/recommended\",\n      \"plugin:import/recommended\",\n      \"plugin:import/typescript\",\n      \"next/core-web-vitals\",\n    ),\n  ),\n  {\n    files: [\"**/*.{js,jsx,ts,tsx}\"],\n    ignores: [\n      \"**/node_modules/**\",\n      \"**/.next/**\",\n      \"**/out/**\",\n      \"**/coverage/**\",\n      \"**/build/**\",\n      \"**/dist/**\",\n      \"**/package.json\",\n      \"**/package-lock.json\",\n      \"**/eslint.config.mjs\",\n      \"**/next.config.js\",\n      \"src/utils/attempt2.js\",\n      \"src/utils/inapp.js\",\n      \"src/utils/externalLinkOpener.js\",\n      \"src/utils/browserEscape.js\",\n    ],\n    plugins: {\n      \"react-hooks\": fixupPluginRules(reactHooks),\n      react: fixupPluginRules(reactPlugin),\n      import: fixupPluginRules(importPlugin),\n      \"unused-imports\": fixupPluginRules(unusedImportsPlugin),\n      next: nextPlugin,\n    },\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n        ...globals.node,\n      },\n      ecmaVersion: 2018,\n      sourceType: \"module\",\n      parser: tsParser,\n      parserOptions: {\n        project: \"./tsconfig.json\",\n        ecmaFeatures: {\n          jsx: true,\n        },\n      },\n    },\n    settings: {\n      \"import/resolver\": {\n        node: {\n          paths: [\"src\"],\n          extensions: [\".js\", \".jsx\", \".ts\", \".tsx\"],\n        },\n      },\n      react: {\n        version: \"detect\",\n      },\n    },\n    rules: {\n      \"prettier/prettier\": [\"off\", { singleQuote: true }],\n      \"no-use-before-define\": [\"off\", { functions: false, classes: false }],\n      \"@typescript-eslint/naming-convention\": [\n        \"error\",\n        {\n          selector: \"parameter\",\n          format: [\"camelCase\", \"PascalCase\"],\n          leadingUnderscore: \"allow\",\n        },\n        {\n          selector: \"variable\",\n          format: [\"camelCase\", \"UPPER_CASE\", \"PascalCase\"],\n          leadingUnderscore: \"allow\",\n        },\n      ],\n      \"import/no-extraneous-dependencies\": [\n        \"error\",\n        {\n          devDependencies: true,\n          optionalDependencies: false,\n          peerDependencies: false,\n        },\n      ],\n      \"@typescript-eslint/default-param-last\": \"off\",\n      \"@typescript-eslint/no-use-before-define\": \"off\",\n      \"comma-dangle\": \"off\",\n      \"@typescript-eslint/comma-dangle\": \"off\",\n      \"import/prefer-default-export\": \"off\",\n      \"unused-imports/no-unused-imports\": \"warn\",\n      \"max-len\": [\"warn\", { code: 140, ignorePattern: \"^import .*| className=*|background=*\", ignoreStrings: true }],\n      \"import/order\": [\n        \"error\",\n        {\n          groups: [\"builtin\", \"external\", \"internal\", [\"sibling\", \"parent\"], \"index\", \"type\"],\n          alphabetize: { order: \"desc\", caseInsensitive: true },\n          pathGroups: [\n            { pattern: \"components\", group: \"internal\" },\n            { pattern: \"components/**\", group: \"internal\" },\n            { pattern: \"constants/**\", group: \"internal\" },\n            { pattern: \"common\", group: \"internal\" },\n            { pattern: \"error/**\", group: \"internal\" },\n            { pattern: \"hooks/**\", group: \"internal\" },\n            { pattern: \"locale/**\", group: \"internal\" },\n            { pattern: \"routes/**\", group: \"internal\" },\n            { pattern: \"selectors\", group: \"internal\" },\n            { pattern: \"store\", group: \"internal\" },\n          ],\n          \"newlines-between\": \"always\",\n        },\n      ],\n      \"@typescript-eslint/no-explicit-any\": \"off\",\n      \"react/prop-types\": \"off\",\n      \"react/require-default-props\": \"off\",\n      \"import/no-unresolved\": \"off\",\n      \"import/no-cycle\": [\"off\", { maxDepth: \"∞\" }],\n      \"@typescript-eslint/no-shadow\": \"off\",\n      \"no-shadow\": \"off\",\n      \"no-console\": \"off\",\n      \"no-plusplus\": \"off\",\n      \"react-hooks/exhaustive-deps\": \"off\",\n      \"react/jsx-filename-extension\": \"off\",\n      \"react/jsx-props-no-spreading\": \"off\",\n      \"class-methods-use-this\": \"off\",\n      \"@typescript-eslint/explicit-module-boundary-types\": \"off\",\n      \"@typescript-eslint/no-unused-vars\": [\"warn\", { argsIgnorePattern: \"^_\", varsIgnorePattern: \"^_\" }],\n      \"@typescript-eslint/no-empty-object-type\": \"off\",\n      \"react/jsx-sort-props\": [\n        \"error\",\n        {\n          callbacksLast: false,\n          shorthandFirst: false,\n          shorthandLast: false,\n          ignoreCase: true,\n          noSortAlphabetically: false,\n          reservedFirst: false,\n        },\n      ],\n      quotes: [\"error\", \"double\", { avoidEscape: false, allowTemplateLiterals: false }],\n    },\n  },\n];\n\nexport default config;\n"
  },
  {
    "path": "locales/client.ts",
    "content": "\"use client\";\n\nimport { createI18nClient } from \"next-international/client\";\n\n// NOTE: Also update middleware.ts to support locale\nexport const languages = [\"en\", \"fr\", \"es\", \"zh-CN\", \"ru\", \"pt\"];\n\nexport const { useI18n, useScopedI18n, I18nProviderClient, useChangeLocale, defineLocale, useCurrentLocale } = createI18nClient(\n  {\n    en: async () => {\n      await new Promise((resolve) => setTimeout(resolve, 100));\n      return import(\"./en\");\n    },\n    fr: async () => {\n      await new Promise((resolve) => setTimeout(resolve, 100));\n      return import(\"./fr\");\n    },\n    es: async () => {\n      await new Promise((resolve) => setTimeout(resolve, 100));\n      return import(\"./es\");\n    },\n    \"zh-CN\": async () => {\n      await new Promise((resolve) => setTimeout(resolve, 100));\n      return import(\"./zh-CN\");\n    },\n    ru: async () => {\n      await new Promise((resolve) => setTimeout(resolve, 100));\n      return import(\"./ru\");\n    },\n    pt: async () => {\n      await new Promise((resolve) => setTimeout(resolve, 100));\n      return import(\"./pt\");\n    },\n  },\n  {\n    // Uncomment to set base path\n    // basePath: '/base',\n    // Uncomment to use custom segment name\n    // segmentName: 'locale',\n    // Uncomment to set fallback locale\n    // fallbackLocale: en,\n  },\n);\n\nexport type TFunction = Awaited<ReturnType<typeof useI18n>>;"
  },
  {
    "path": "locales/en.ts",
    "content": "export default {\n  leaderboard: {\n    title: \"Leaderboard\",\n    description: \"Top workout champions\",\n    champion_badge: \"🏆 Champion\",\n    runner_up_badge: \"🥈 Runner-up\",\n    third_place_badge: \"🥉 Third Place\",\n    second_place: \"2nd Place\",\n    third_place: \"3rd Place\",\n    workouts: \"workouts\",\n    unable_to_load: \"Unable to load leaderboard\",\n    try_again_later: \"Please try again later\",\n    no_champions_yet: \"No champions yet\",\n    complete_first_workout: \"Complete your first workout to claim the throne!\",\n    member_since: \"Member since\",\n    workouts_per_week: \"workouts/week\",\n    last_workout: \"Last workout\",\n    page_title: \"Champions Leaderboard\",\n    page_subtitle: \"Climb to the top and become a Workout.cool legend\",\n    period_all_time: \"All Time\",\n    period_monthly: \"Month\",\n    period_weekly: \"Week\",\n    no_sessions_this_week: \"No sessions this week\",\n    no_sessions_this_month: \"No sessions this month\",\n    registered_members_only: \"Registered members only\",\n    registered_members_description: \"Create an account to appear in the leaderboard\",\n    reset_timezone: \"Europe/Paris Reset\",\n    reset_timezone_description: \"Weekly and monthly leaderboards reset at midnight Paris time\",\n  },\n  programs: {\n    available_programs: \"Available programs\",\n    exercises_in_session: \"Exercises in session\",\n    start_session: \"Start session\",\n    starting_session: \"Starting...\",\n    more_than: \"more than\",\n    my_progress: \"My progress\",\n    session: \"session\",\n    completed_feminine: \"completed\",\n    completed_sets: \"completed sets\",\n    \"set#zero\": \"set\",\n    \"set#one\": \"set\",\n    \"set#other\": \"sets\",\n    error_starting_session: \"Error starting session\",\n    premium_session: \"Premium session\",\n    premium_session_description: \"This session is part of the premium content. You can see the details but not perform the workout.\",\n    premium_session_exercises: \"Included exercises\",\n    workout_description: \"Workout description\",\n    connect_to_access: \"Connect to access\",\n    become_premium: \"Become Premium\",\n    back_to_program: \"Back to program\",\n    no_equipment: \"No equipment\",\n    workout_programs_title: \"Workout programs (+ in progress)\",\n    workout_programs: \"Workout programs\",\n    workout_programs_description: \"Choose your challenge and become stronger! 💪\",\n    no_programs_available: \"No programs available\",\n    no_programs_available_description: \"Programs will be available soon!\",\n    completed: \"Completed\",\n    about: \"About\",\n    program: \"Program\",\n    not_found: \"Program not found\",\n    characteristics: \"Characteristics\",\n    weeks: \"weeks\",\n    sessions_per_week: \"sessions/week\",\n    session_duration: \"min/session\",\n    \"your_coach#zero\": \"Your cool coach\",\n    \"your_coach#one\": \"Your cool coach\",\n    \"your_coach#other\": \"Your cool coaches\",\n    community: \"Active community\",\n    community_count: \"coolbuilders have joined\",\n    week_short: \"Week\",\n    week: \"Week\",\n    exercises: \"exercises\",\n    min_short: \"min\",\n    premium: \"Premium\",\n    free: \"Free\",\n    join_cta: \"Join the challenge\",\n    continue: \"Continue\",\n    sessions: \"Sessions\",\n    auth_required: \"Authentication Required\",\n    auth_required_description: \"You need to sign in to access this workout session.\",\n    login_to_continue: \"Sign In to Continue\",\n    signup_to_continue: \"Sign Up to Continue\",\n    premium_required: \"Premium Required\",\n    upgrade_to_premium: \"Upgrade to Premium\",\n    program_completed: \"Program completed\",\n    check_out_program: \"Check out this workout program!\",\n    share_success: \"Shared successfully!\",\n    copied_to_clipboard: \"Link copied!\",\n    share_failed: \"Share failed\",\n    premium_required_description: \"This is a premium access. Upgrade to access all premium content.\",\n    important_info: \"Important information\",\n    donation_teaser:\n      \"At first, we were running on donations. But as you can imagine, donations weren't sufficient to cover development and running costs. So we made you a package that will help us keep the lights on and unlock a few superpowers along the way.\",\n    new: \"NEW\",\n    more_programs_coming_title: \"More programs coming soon!\",\n    more_programs_coming_description:\n      \"We're working hard to create new programs. By upgrading to premium now, you'll have them all automatically. Thanks for your support. 🚀\",\n    coming_strength: \"Force & Muscle\",\n    coming_cardio: \"Cardio HIIT\",\n    coming_yoga: \"Yoga & Mobility\",\n    sessions_coming_soon: \"Sessions coming soon!\",\n    sessions_in_creation: \"Our team is working hard to create quality sessions for this week. Come back soon! 🚀\",\n    welcome_modal: {\n      welcome_title: \"Welcome to {programTitle}!\",\n      subtitle: \"Get ready to push your limits! 💪\",\n      level_label: \"Level\",\n      duration_label: \"Duration\",\n      frequency_label: \"Frequency\",\n      later_button: \"Later\",\n      start_button: \"Let's go!\",\n    },\n  },\n  premium: {\n    checkout_error: \"Error during checkout\",\n    premium_required_title: \"Premium Required\",\n    premium_required_subtitle: \"This is a premium access. Upgrade to access all premium content.\",\n    premium_required_button: \"Upgrade to Premium\",\n    already_premium: \"You're enjoying Workout.cool Premium\",\n    no_ads: \"No ads\",\n    upgrade: \"Upgrade\",\n\n    // Checkout\n    checkout: {\n      processing: \"Processing...\",\n    },\n\n    // Pricing\n    pricing: {\n      month: \"month\",\n      year: \"year\",\n      monthly: \"Monthly\",\n      yearly: \"Yearly\",\n      discount: \"-48%\",\n    },\n\n    // Hero Section\n    hero: {\n      badge: \"Open-Source & Self-hosting ALWAYS free\",\n      title: \"Train freely, support the mission\",\n      subtitle: \"For those who believe in the project and want to (re)believe in themselves with power boosters !\",\n      stats: {\n        athletes: {\n          count: \"12.4K+\",\n          label: \"Active athletes\",\n        },\n        series: {\n          count: \"1.2M+\",\n          label: \"Series recorded\",\n        },\n        rating: {\n          count: \"4.9/5\",\n          label: \"Community rating\",\n        },\n        progression: {\n          count: \"+23%\",\n          label: \"Average progression\",\n        },\n      },\n    },\n\n    // Mission Banner\n    mission: {\n      supporters_count: \"234\",\n      supporters_text: \"supporters helping the mission\",\n      limited: \"Limited\",\n      early_access: \"early access spots\",\n    },\n\n    // Plans\n    plans: {\n      monthly: \"Monthly\",\n      yearly: \"Yearly\",\n      yearly_discount: \"-48%\",\n      per_month: \"/month\",\n      per_year: \"/year\",\n\n      free: {\n        name: \"FREE\",\n        price: \"€0\",\n        period: \"/forever\",\n        price_label: \"€0/forever\",\n        badge: \"Open-Source • Always Free\",\n        description: \"All essential functions for training\",\n        features: [\n          \"Exercise generator\",\n          \"Exercises with instructions and videos\",\n          \"GitHub-style workout history (6 months)\",\n          \"Self-hosting capability\",\n          \"Open source code access\",\n        ],\n        button: \"Your actual plan\",\n        footer_note: \"No signup required • Full access forever\",\n      },\n\n      premium: {\n        name: \"PREMIUM ⭐\",\n        price_label: \"€7.90/month or €49/year\",\n        badge: \"MOST POPULAR • For enthusiasts\",\n        description: \"All features + early access\",\n        footer_monthly: \"Join the passionate community! 🔥\",\n        footer_yearly: \"Thank you for the yearly support! 🙏\",\n        yearly_price_note: \"/month\",\n        features: [\n          \"...all of the free plan\",\n          \"No ads\",\n          \"Unlimited history (vs 6 months free)\",\n          \"Progress tracking with advanced statistics (volume, progression, PR)\",\n          \"Pre-designed training programs\",\n          \"Private 1:1 chat with a coach\",\n          \"Early access to new features\",\n        ],\n      },\n    },\n\n    // Buttons and Actions\n    actions: {\n      processing: \"Processing...\",\n      go_premium: \"Go Premium\",\n      sign_in_continue: \"Go Premium\",\n      upgrade_now: \"Upgrade Now\",\n      current_plan: \"Your actual plan\",\n    },\n\n    // Trust Elements\n    trust: {\n      gdpr_compliant: \"100% GDPR compliant\",\n      money_back: \"30-day money back guarantee\",\n      cancel_anytime: \"1 click to cancel, no commitment\",\n      secure_payment: \"100% secure payment via Stripe\",\n    },\n\n    // Feature Comparison\n    comparison: {\n      title: \"Detailed Feature Comparison\",\n      subtitle: \"Everything you need to know about what's included in each plan\",\n      features_label: \"Features\",\n      headers: {\n        features: \"Features\",\n        free: \"Free\",\n        premium: \"Premium\",\n      },\n      categories: {\n        equipment: \"Equipment & Exercises\",\n        tracking: \"Tracking & Analytics\",\n        programs: \"Programs & AI\",\n        community: \"Community & Sharing\",\n        support: \"Support & Project\",\n      },\n      features: {\n        exercise_library: \"Exercise library\",\n        custom_exercise: \"Custom exercise\",\n        video_tutorials: \"Video tutorials\",\n        workout_history: \"Workout history\",\n        progress_statistics: \"Progress statistics\",\n        personal_records: \"Personal records tracking\",\n        volume_analytics: \"Volume & progression analytics\",\n        predesigned_programs: \"Pre-designed programs\",\n        personalized_recommendations: \"Personalized recommendations\",\n        pro_templates: \"Pro templates (Powerlifting, bodybuilding, etc.)\",\n        community_access: \"Community access\",\n        discord_community: \"Discord community\",\n        private_chat: \"Private 1:1 chat with coach\",\n        community_support: \"Community support\",\n        priority_support: \"Priority support\",\n        early_access: \"Early access to features\",\n        beta_testing: \"Beta testing access\",\n      },\n      values: {\n        basic: \"Basic\",\n        complete: \"Complete\",\n        unlimited: \"Unlimited\",\n        professional: \"Professional\",\n        six_months: \"6 months\",\n        limited: \"Limited\",\n        all_programs: \"All programs\",\n        public: \"Public\",\n        vip_access: \"VIP access\",\n        private_channels: \"Private channels\",\n        soon: \"Soon\",\n        hd_slowmo: \"4K + Slow-mo\",\n        early_access: \"Early Access\",\n      },\n    },\n\n    // FAQ\n    faq: {\n      title: \"Frequently Asked Questions\",\n      subtitle: \"Everything you need to know about Workout.cool and our mission\",\n      items: [\n        {\n          question: \"Why pay if it's open-source?\",\n          answer:\n            \"Excellent question! The code will always remain free, but maintaining servers, database and infrastructure costs money. Your contribution helps us keep the tool free for everyone. It's a win-win model: you get premium features, the community keeps free access!\",\n        },\n        {\n          question: \"Can I self-host Workout.cool?\",\n          answer:\n            \"Absolutely! The entire codebase is available on GitHub under MIT license. You can deploy it on your own servers, customize it however you want, and use it completely free. Self-hosting gives you full control over your data and workout privacy.\",\n        },\n        {\n          question: \"Are my workout data secure?\",\n          answer:\n            \"Yes! We're GDPR compliant, use encrypted connections, and store your data securely. Plus, since we're open-source, you can audit our security practices. You can also export your data anytime or self-host for complete control.\",\n        },\n        {\n          question: \"Can I cancel my subscription anytime?\",\n          answer:\n            \"Of course! No contracts, no commitments. Cancel with one click anytime. You'll keep access until your current billing period ends, and you can always restart later. Your workout data remains accessible even if you downgrade to free.\",\n        },\n        {\n          question: \"Are there exercises for beginners?\",\n          answer:\n            \"Definitely! Our exercise library covers all fitness levels from complete beginners to advanced athletes. Videos and instructions help beginners find appropriate exercises, and our video tutorials show proper form.\",\n        },\n        {\n          question: \"How does progress tracking work?\",\n          answer:\n            \"Every set, rep, weight, and time is automatically logged. You get a GitHub-style workout history showing your consistency, plus detailed analytics on volume, progression, and personal records. Premium users get advanced charts and insights.\",\n        },\n        {\n          question: \"Can I import data from other apps?\",\n          answer:\n            \"Soon. We will support CSV imports for basic data (reps & weight). If you're switching from another fitness app, our support team can help migrate your workout history.\",\n        },\n        {\n          question: \"Does the app work offline?\",\n          answer:\n            \"The core workout tracking works offline. You can log sets and reps without internet connection for 10 workouts. Exercise videos and cloud sync require internet connection. All your offline data syncs automatically when you're back online.\",\n        },\n        {\n          question: \"Are there programs for women?\",\n          answer:\n            \"Absolutely! And there will be more programs in the future. We are working on it. Supporter and Premium plans will include all the future specialized programs for different goals: strength, toning, powerlifting, bodybuilding, and more !\",\n        },\n        {\n          question: \"Can I create my own programs?\",\n          answer: \"Unfortunately, no. We are working on it !\",\n        },\n      ],\n      additional_support: {\n        title: \"Still have questions?\",\n        description: \"Our fitness-focused community is here to help you succeed\",\n        community: \"Community support (discord or hello@workout.cool)\",\n        discussions: \"Open discussions (github/discord)\",\n        roadmap: \"Transparent roadmap (github)\",\n      },\n    },\n\n    // Final CTA\n    final_cta: {\n      motivation: \"Keep pushing! 💪\",\n      title: \"Ready to Support the Mission?\",\n      subtitle: \"Join thousands of fitness enthusiasts who believe in open-source training freedom\",\n      values: [\n        {\n          title: \"Community First\",\n          description: \"Built by and for the fitness community\",\n        },\n        {\n          title: \"Always Transparent\",\n          description: \"Open-source code, transparent funding\",\n        },\n        {\n          title: \"Labor of Love\",\n          description: \"15 years of passion !\",\n        },\n      ],\n      quote: {\n        text: \"We believe fitness tools should be accessible to everyone. Your support helps us maintain this vision while continuing to innovate.\",\n        author: \"— The Workout.cool Team\",\n      },\n    },\n\n    // Premium Active State\n    premium_active: {\n      title: \"Premium Active! 💪\",\n      supporting: \"Supporting the mission 💚\",\n    },\n\n    // Legacy translations (keeping for compatibility)\n    premium_active_title: \"Premium Active\",\n    premium_active_subtitle: \"All features unlocked\",\n    free_intro_title: \"You're already getting a lot for free...\",\n    free_intro_text:\n      \"Workout.cool is a free, open-source fitness app used daily by 60,000+ users. It's built with love (not VC money ^^) and it costs us real time and money to keep it running.\",\n    donation_story_text:\n      \"At first, we were running on donations. But as you can imagine, donations weren't sufficient to cover development and running costs. So we made you a package that will help us keep the lights on and unlock a few superpowers along the way.\",\n    health_upgrade_text: \"If Workout.cool helps you level up your health, please consider going Premium :D !\",\n    unlock_features_text: \"Unlock advanced features & support open-source fitness.\",\n    invest_yourself_quote: \"Never skimp on fitness & books — invest in yourself !\",\n    support_mission: \"Support the mission\",\n    best_value_badge: \"BEST VALUE\",\n    annual_plan: \"Annual\",\n    monthly_plan: \"Monthly\",\n    discount_badge: \"40% off\",\n    per_month: \"/month\",\n    feature_all_programs: \"All workout programs\",\n    feature_progress_tracking: \"Progress tracking\",\n    coming_soon: \"(soon)\",\n    feature_future_updates: \"All future programs & updates\",\n    feature_priority_support: \"Priority support\",\n    save_yearly: \"Save 40% yearly\",\n    processing: \"Processing...\",\n    cta_annual: \"I want to support + save 40%\",\n    cta_monthly: \"Let's unlock my full plan\",\n    thank_supporting: \"Thank you for supporting.\",\n    no_pressure: \"No pressure. You can upgrade anytime.\",\n    keep_pushing: \"keep pushing ! huhu\",\n    still_unsure: \"Still not sure? No worries. Workout.cool will always remain free and open-source.\",\n    support_helps: \"But if you believe in what we're building and you can afford it, your support will help 💚\",\n    self_hosting: \"Self-hosting\",\n    community: \"Community\",\n    mit_license: \"MIT License\",\n    pricing_year: \"year\",\n    pricing_month: \"month\",\n    conversion_flow_title: \"Redirecting...\",\n    conversion_flow_message: \"Successfully signed in! Redirecting to checkout...\",\n    redirecting_to_checkout: \"Redirecting to checkout\",\n\n    // Premium Gate\n    premium_feature: \"Premium Feature\",\n    upgrade_to_access_feature: \"Upgrade to premium to access this feature\",\n    unlock_all_features: \"Unlock all features and support development\",\n  },\n  breadcrumbs: {\n    home: \"Home\",\n  },\n  bottom_navigation: {\n    statistics: \"Statistics\",\n    statistics_tooltip: \"View your statistics\",\n    programs: \"Programs\",\n    programs_tooltip: \"Browse programs\",\n    workouts: \"Workouts\",\n    workouts_tooltip: \"Create your own workout\",\n    premium: \"Premium\",\n    premium_tooltip: \"Upgrade to Premium\",\n    leaderboard: \"Leaderboard\",\n    leaderboard_tooltip: \"View workout rankings\",\n    tools: \"Tools\",\n    tools_tooltip: \"Browse tools\",\n    profile: \"Profile\",\n    profile_tooltip: \"View your profile\",\n  },\n  tools: {\n    try_now: \"Try now\",\n    title: \"Fitness Tools\",\n    subtitle: \"Essential calculators to optimize your training and nutrition\",\n    moreComingSoon: \"More tools coming soon\",\n    meta: {\n      title: \"Fitness Tools - Calculators for Training & Nutrition\",\n      description:\n        \"Free fitness calculators: TDEE, macros, BMI, heart rate zones, 1RM and more. Optimize your training and nutrition with our essential tools.\",\n      keywords:\n        \"fitness calculator, calorie calculator, macro calculator, BMI calculator, TDEE calculator, heart rate zones, one rep max, fitness tools\",\n    },\n    \"calorie-calculator\": {\n      body_fat_percentage: \"Body Fat Percentage\",\n      body_fat_info_title:\n        \"Body fat percentage is essential for Katch-McArdle and Cunningham formulas as they calculate based on lean body mass. If you don't know your exact body fat %, use online visual guides or DEXA scans for accuracy.\",\n      title: \"Calorie Calculator\",\n      description: \"Calculate your daily caloric needs (TDEE) based on your activity level and goals\",\n      meta: {\n        title: \"Calorie Calculator - TDEE & Daily Caloric Needs\",\n        description:\n          \"Calculate your Total Daily Energy Expenditure (TDEE) and daily caloric needs. Get personalized recommendations for weight loss, maintenance, or muscle gain.\",\n        keywords:\n          \"calorie calculator, TDEE calculator, daily calories, weight loss calculator, caloric needs, BMR calculator, metabolism calculator\",\n      },\n      subtitle: \"Calculate your daily caloric needs based on the Mifflin-St Jeor equation\",\n      how_it_works: \"How does this calculator work?\",\n      how_it_works_description:\n        \"This calculator uses scientifically proven formulas to estimate your daily caloric needs based on your personal characteristics and lifestyle.\",\n      how_it_works_step1: \"We calculate your base metabolism (calories burned at rest)\",\n      how_it_works_step2: \"We adjust based on your activity level\",\n      how_it_works_step3: \"We personalize according to your goal (lose, maintain, or gain weight)\",\n      calculate: \"Calculate\",\n      calculating: \"Calculating...\",\n      tap_info_icons: \"Tap the ℹ️ icons for more information\",\n      gender: \"Gender\",\n      male: \"Male\",\n      female: \"Female\",\n      units: \"Units\",\n      metric: \"Metric\",\n      imperial: \"Imperial\",\n      age: \"Age\",\n      age_placeholder: \"Enter your age\",\n      years: \"years\",\n      height: \"Height\",\n      height_placeholder: \"Enter your height\",\n      weight: \"Weight\",\n      weight_placeholder: \"Enter your weight\",\n      cm: \"cm\",\n      kg: \"kg\",\n      lbs: \"lbs\",\n      feet: \"feet\",\n      inches: \"inches\",\n      activity_level: \"Activity Level\",\n      activity: {\n        sedentary: \"Sedentary\",\n        sedentary_desc: \"Little to no exercise, desk job, minimal walking\",\n        light: \"Lightly Active\",\n        light_desc: \"Light exercise 1-3 days/week, or daily walking\",\n        moderate: \"Moderately Active\",\n        moderate_desc: \"Moderate exercise 3-5 days/week, active lifestyle\",\n        active: \"Very Active\",\n        active_desc: \"Heavy exercise 6-7 days/week, very active job\",\n        very_active: \"Extremely Active\",\n        very_active_desc: \"Athlete, physical job + daily training\",\n      },\n      goal: \"Goal\",\n      goals: {\n        lose_fast: \"Lose Weight Fast\",\n        lose_fast_desc: \"Lose 2 lbs (1 kg) per week - Aggressive but effective\",\n        lose_slow: \"Lose Weight\",\n        lose_slow_desc: \"Lose 1 lb (0.5 kg) per week - Sustainable and healthy\",\n        maintain: \"Maintain Weight\",\n        maintain_desc: \"Stay at current weight - Perfect for maintaining your shape\",\n        gain_slow: \"Gain Weight\",\n        gain_slow_desc: \"Gain 1 lb (0.5 kg) per week - Clean muscle building\",\n        gain_fast: \"Gain Weight Fast\",\n        gain_fast_desc: \"Gain 2 lbs (1 kg) per week - Maximum muscle growth\",\n      },\n      results: {\n        title: \"Your Results\",\n        bmr: \"BMR\",\n        bmr_explanation:\n          \"Basal Metabolic Rate (BMR) is the number of calories your body burns at complete rest, just to maintain basic functions like breathing, circulation, and cell production. This is the minimum energy your body needs to survive.\",\n        tdee: \"TDEE\",\n        tdee_explanation:\n          \"Total Daily Energy Expenditure (TDEE) is your BMR plus the calories burned through daily activities and exercise. This is the total number of calories you burn in a day based on your activity level.\",\n        target: \"Target Calories\",\n        macros: \"Recommended Macros\",\n        macros_explanation:\n          \"Macronutrients (macros) are the three main nutrient groups your body needs: Proteins (for muscle building and repair), Carbohydrates (for energy), and Fats (for hormones and vitamin absorption). The percentages shown are a balanced distribution suitable for most fitness goals.\",\n        protein: \"Protein\",\n        carbs: \"Carbohydrates\",\n        fat: \"Fat\",\n        disclaimer:\n          \"These calculations are estimates based on average formulas. Actual caloric needs may vary based on individual factors. Consult with a healthcare professional or registered dietitian for personalized advice.\",\n      },\n      faq: {\n        title: \"Frequently Asked Questions\",\n        q1: \"Why is my calorie target different from other calculators?\",\n        a1: \"Different calculators may use different formulas or activity multipliers. We use the Mifflin-St Jeor equation, which is considered one of the most accurate for most people. However, individual metabolism can vary by 10-20% from these estimates.\",\n        q2: \"Should I eat exactly this many calories every day?\",\n        a2: \"These are average daily targets. It's normal to eat slightly more some days and less on others. Focus on your weekly average rather than being exact every single day. Listen to your body's hunger and fullness cues.\",\n        q3: \"What if I'm not seeing results after following these recommendations?\",\n        a3: \"If you're not seeing results after 2-3 weeks, you may need to adjust. Your actual metabolism might be higher or lower than calculated. Try adjusting by 100-200 calories and monitor for another 2 weeks. Also ensure you're tracking your food accurately.\",\n        q4: \"Are the macro recommendations suitable for everyone?\",\n        a4: \"The 30/40/30 split (protein/carbs/fat) is a balanced approach suitable for most people. However, athletes, people with medical conditions, or those following specific diets (keto, vegan, etc.) may need different ratios. Consult a nutritionist for personalized recommendations.\",\n      },\n    },\n    \"macro-calculator\": {\n      title: \"Macro Calculator\",\n      description: \"Find your optimal protein, carbs and fat distribution for your fitness goals\",\n    },\n    \"bmi-calculator\": {\n      title: \"BMI Calculator\",\n      description: \"Calculate your Body Mass Index and understand your weight category\",\n    },\n    \"heart-rate-calculator\": {\n      title: \"Heart Rate Zones\",\n      description: \"Discover your optimal training zones for fat burning and performance\",\n    },\n    \"heart-rate-zones\": {\n      title: \"Heart Rate Zones Calculator\",\n      description: \"Calculate your optimal heart rate training zones for maximum performance and fat burning\",\n      page_title: \"Heart Rate Zones Calculator\",\n      page_description:\n        \"Calculate your personalized heart rate training zones using scientifically proven formulas. Optimize your cardio workouts for fat burning, endurance, and performance.\",\n      meta: {\n        title: \"Heart Rate Zones Calculator - Target Heart Rate & Training Zones\",\n        description:\n          \"Calculate your maximum heart rate and personalized training zones. Use basic or Karvonen formulas to find your VO2 Max, Anaerobic, Aerobic, Fat Burn, and Warm Up zones.\",\n        keywords:\n          \"heart rate zones calculator, target heart rate, maximum heart rate, training zones, VO2 max zone, anaerobic zone, aerobic zone, fat burn zone, Karvonen formula, heart rate training\",\n      },\n      calculate: \"Calculate Zones\",\n      calculating: \"Calculating...\",\n      method: \"Calculation Method\",\n      method_info: \"Choose the formula that best suits your fitness level and available data\",\n      methods: {\n        basic: \"Basic by Age\",\n        basic_desc: \"Simple formula using age only - good for beginners\",\n        karvonen_age: \"Karvonen by Age & RHR\",\n        karvonen_age_desc: \"More accurate using age and resting heart rate\",\n        karvonen_custom: \"Karvonen by MHR & RHR\",\n        karvonen_custom_desc: \"Most accurate using measured max and resting heart rates\",\n      },\n      age: \"Age\",\n      age_placeholder: \"Enter your age\",\n      resting_heart_rate: \"Resting Heart Rate (RHR)\",\n      resting_heart_rate_placeholder: \"Enter your RHR\",\n      resting_heart_rate_info: \"Measure your heart rate when you wake up, before getting out of bed. Normal range is 60-100 bpm.\",\n      max_heart_rate: \"Maximum Heart Rate (MHR)\",\n      max_heart_rate_placeholder: \"Enter your MHR\",\n      max_heart_rate_info:\n        \"Your actual maximum heart rate from a stress test or max effort workout. More accurate than age-based estimates.\",\n      results: {\n        overview: \"Overview of your heart rate zones\",\n        title: \"Your Heart Rate Zones\",\n        max_heart_rate: \"Maximum Heart Rate\",\n        heart_rate_reserve: \"Heart Rate Reserve\",\n        target_zones: \"Target Training Zones\",\n        zone: \"Zone\",\n        intensity: \"Intensity\",\n        heart_rate_range: \"Heart Rate (bpm)\",\n        benefits: \"Benefits\",\n        duration: \"Typical Duration\",\n      },\n      zones: {\n        warm_up: {\n          name: \"Warm Up Zone\",\n          intensity: \"50-60%\",\n          benefits: \"🧘 Perfect warm-up\",\n          example: \"Slow walk\",\n          duration: \"5-10 minutes\",\n          description: \"Very light intensity for warming up and cooling down\",\n        },\n        fat_burn: {\n          name: \"Fat Burn Zone\",\n          intensity: \"60-70%\",\n          benefits: \"🔥 Maximum fat burning\",\n          example: \"Light jogging\",\n          duration: \"20-40 minutes\",\n          description: \"Light intensity, comfortable pace for longer workouts\",\n        },\n        aerobic: {\n          name: \"Aerobic Zone\",\n          intensity: \"70-80%\",\n          benefits: \"💪 Improves cardiovascular\",\n          example: \"Moderate run\",\n          duration: \"10-40 minutes\",\n          description: \"Moderate intensity, sustainable for extended periods\",\n        },\n        anaerobic: {\n          name: \"Anaerobic Zone\",\n          intensity: \"80-90%\",\n          benefits: \"⚡ Increases speed\",\n          example: \"Short sprint\",\n          duration: \"2-10 minutes\",\n          description: \"Hard intensity, challenging but sustainable for short periods\",\n        },\n        vo2_max: {\n          name: \"VO2 Max Zone\",\n          intensity: \"90-100%\",\n          benefits: \"🏆 Maximum performance\",\n          example: \"Sprint\",\n          duration: \"30 seconds - 2 minutes\",\n          description: \"Maximum intensity, only sustainable for very short bursts\",\n        },\n      },\n      formulas: {\n        basic_formula: \"Basic Formula\",\n        basic_explanation: \"THR = MHR × %Intensity\",\n        karvonen_formula: \"Karvonen Formula\",\n        karvonen_explanation: \"THR = [(MHR - RHR) × %Intensity] + RHR\",\n        mhr_calculation: \"MHR = 220 - Age\",\n      },\n      abbreviations: {\n        thr: \"THR = Target Heart Rate\",\n        mhr: \"MHR = Maximum Heart Rate\",\n        rhr: \"RHR = Resting Heart Rate\",\n        hrr: \"HRR = Heart Rate Reserve\",\n        bpm: \"bpm = Beats Per Minute\",\n      },\n      tips: {\n        title: \"Training Tips\",\n        tip1: \"Start with lower intensity zones if you're new to exercise\",\n        tip2: \"Mix different zones in your weekly training for best results\",\n        tip3: \"Use a heart rate monitor for accurate tracking during workouts\",\n        tip4: \"Your zones may change as your fitness improves - recalculate periodically\",\n      },\n      faq: {\n        title: \"Frequently Asked Questions\",\n        q1: \"Which calculation method should I use?\",\n        a1: \"If you're just starting, use the Basic method. If you know your resting heart rate, use Karvonen by Age for better accuracy. For the most precise zones, use Karvonen with measured MHR and RHR.\",\n        q2: \"How do I measure my resting heart rate?\",\n        a2: \"Measure your pulse for 60 seconds immediately after waking up, before getting out of bed. Do this for 3-5 days and use the average. Normal RHR is 60-100 bpm, with lower values indicating better fitness.\",\n        q3: \"What zone should I train in for weight loss?\",\n        a3: \"The Fat Burn Zone (60-70%) is optimal for burning fat as fuel. However, higher intensity zones burn more total calories. Mix zones for best results - include both fat burn and higher intensity workouts.\",\n        q4: \"How accurate is the 220-age formula?\",\n        a4: \"It's a general estimate that works for most people but can vary by ±10-15 bpm. For more accuracy, consider a supervised max heart rate test or use the Karvonen formula with your actual measurements.\",\n        q5: \"Can I train in the VO2 Max zone every day?\",\n        a5: \"No, the VO2 Max zone is extremely intense and should only be used 1-2 times per week for short intervals. Most training should be in the Aerobic and Fat Burn zones to build endurance and allow recovery.\",\n      },\n      guide: {\n        title: \"Complete Guide to Heart Rate Zones for Training\",\n        text1:\n          \"Heart rate zones are a scientific tool essential for optimizing your training and achieving your fitness goals. Whether you're looking to lose weight, improve endurance, or increase performance, understanding and using heart rate zones will transform your approach to exercise.\",\n        text2:\n          \"This calculator uses scientifically proven formulas to determine your personalized zones based on your age and, optionally, your resting heart rate. Each zone corresponds to a specific intensity and offers unique benefits for your cardiovascular health.\",\n      },\n      table: {\n        title: \"Reference Table of Heart Rates by Age\",\n        col1: \"Age\",\n        col2: \"FCM\",\n        col3: \"50% Intensity\",\n        col4: \"85% Intensity\",\n        avertiser: \"* These values are averages. Your actual FCM may vary by ±10-15 bpm.\",\n      },\n      details: {\n        title: \"The 5 Training Zones Explained in Detail\",\n        benefits: \"Benefits\",\n        zone1_title: \"Zone 1 : Warm Up (50-60% FCM)\",\n        zone1_content:\n          \"The warm up zone is ideal for starting a session, recovering between intervals, or finishing a workout. At this intensity, you can maintain a normal conversation without getting out of breath.\",\n        zone1_details_1: \"Improves blood circulation\",\n        zone1_details_2: \"Prepares muscles and joints\",\n        zone1_details_3: \"Reduces the risk of injury\",\n        zone1_details_4: \"Favorizes active recovery\",\n        zone1_duration: \"Recommended Duration\",\n        zone1_duration_value: \"5-10 minutes at the beginning/end of a session\",\n        zone1_duration_value_2: \"20-30 minutes for active recovery\",\n        zone2_title: \"Zone 2 : Fat Burn (60-70% FCM)\",\n        zone2_content:\n          \"In this zone, your body primarily uses fat as fuel. It's the optimal intensity for developing your aerobic base and improving metabolic efficiency.\",\n        zone2_details_1: \"Maximizes fat utilization\",\n        zone2_details_2: \"Develops aerobic endurance\",\n        zone2_details_3: \"Improves cardiac efficiency\",\n        zone2_details_4: \"Strengthens the immune system\",\n        zone2_duration: \"Recommended Duration\",\n        zone2_duration_value: \"30-90 minutes for endurance\",\n        zone2_duration_value_2: \"45-60 minutes for weight loss\",\n        zone3_title: \"Zone 3 : Aerobic (70-80% FCM)\",\n        zone3_content:\n          \"The aerobic zone significantly improves your cardiovascular capacity. You breathe harder but can still speak in short sentences. It's the main training zone for most athletes.\",\n        zone3_details_1: \"Increases pulmonary capacity\",\n        zone3_details_2: \"Improves cardiovascular endurance\",\n        zone3_details_3: \"Strengthens the heart\",\n        zone3_details_4: \"Optimizes oxygen utilization\",\n        zone3_duration: \"Recommended Duration\",\n        zone3_duration_value: \"20-60 minutes continuously\",\n        zone3_duration_value_2: \"Intervals of 5-15 minutes\",\n        zone4_title: \"Zone 4 : Anaerobic (80-90% FCM)\",\n        zone4_content:\n          \"In the anaerobic zone, your body produces lactic acid faster than it can eliminate it. This intensity develops power and speed but can't be sustained for long periods.\",\n        zone4_details_1: \"Increases muscle power\",\n        zone4_details_2: \"Improves lactate tolerance\",\n        zone4_details_3: \"Develops speed\",\n        zone4_details_4: \"Strengthens the mind\",\n        zone4_duration: \"Recommended Duration\",\n        zone4_duration_value: \"Intervals of 2-8 minutes\",\n        zone4_duration_value_2: \"Equal or double recovery\",\n        zone5_title: \"Zone 5 : VO2 Max (90-100% FCM)\",\n        zone5_content:\n          \"The VO2 Max zone represents the maximum effort. At this intensity, you can only pronounce a few words and the effort is unbearable beyond a few minutes. Reserved for experienced athletes.\",\n        zone5_details_1: \"Maximizes aerobic capacity\",\n        zone5_details_2: \"Improves running economy\",\n        zone5_details_3: \"Develops maximum power\",\n        zone5_details_4: \"Pushes mental limits\",\n        zone5_duration: \"Recommended Duration\",\n        zone5_duration_value: \"Intervals of 30s to 2 minutes\",\n        zone5_duration_value_2: \"Maximum 1-2 times per week\",\n      },\n      training_tips: {\n        title: \"Expert Training Tips to Optimize Your Training\",\n        tip1: {\n          title: \"Progressive Warm-up\",\n          description: \"Always start with 5-10 minutes in Zone 1 (50-60%) to prepare your cardiovascular system.\",\n        },\n        tip2: {\n          title: \"80/20 Rule\",\n          description: \"80% of your training in Zones 1-3 (aerobic), 20% in Zones 4-5 (anaerobic) for optimal development.\",\n        },\n        tip3: {\n          title: \"Active Recovery\",\n          description: \"After an intense effort, gradually return to Zone 1-2 for 5-10 minutes.\",\n        },\n        tip4: {\n          title: \"Constant Hydration\",\n          description: \"Drink before, during, and after exercise. Dehydration increases heart rate.\",\n        },\n        tip5: {\n          title: \"Restorative Sleep\",\n          description: \"7-9 hours of sleep allows better recovery and a lower resting heart rate.\",\n        },\n        tip6: {\n          title: \"Gradual Progression\",\n          description: \"Increase intensity or duration by 10% maximum per week to avoid overtraining.\",\n        },\n      },\n      training_tips_2: {\n        title: \"Practical Tips\",\n        title1: \"Find your zone\",\n        description1: \"Each zone has a different goal. Choose based on your goal!\",\n        title2: \"Recommended Duration\",\n        description2: \"The higher the intensity, the shorter the duration.\",\n        title3: \"Progression\",\n        description3: \"Start slowly and gradually increase the intensity.\",\n        title4: \"Listen to your body\",\n        description4: \"If you feel bad, slow down immediately.\",\n      },\n      quick_facts: {\n        title: \"Did you know?\",\n        fact1: \"220 - your age = approximate maximum heart rate\",\n        fact2: \"Measure your pulse in the morning to know your resting heart rate\",\n        fact3: \"A smartwatch can track your heart rate in real time\",\n        fact4: \"80% of your training should be in zones 1-3\",\n      },\n      weekly_plan: {\n        title: \"Typical Weekly Plan\",\n        description: \"An example of a balanced weekly training plan\",\n        monday: {\n          title: \"Zone 1-2\",\n          description: \"30-45 min\",\n        },\n        tuesday: {\n          title: \"Zone 2-3\",\n          description: \"45-60 min\",\n        },\n        wednesday: {\n          title: \"Repos\",\n          description: \"Recovery\",\n        },\n        thursday: {\n          title: \"Zone 3-4\",\n          description: \"30-40 min\",\n        },\n        friday: {\n          title: \"Zone 1-2\",\n          description: \"30 min\",\n        },\n        saturday: {\n          title: \"Zone 4-5\",\n          description: \"20-30 min\",\n        },\n        tips: \"💡 Adapt this plan according to your level and goals!\",\n        cta: \"⬆️ Calculate my zones now\",\n      },\n      seo_faq_title: \"Frequently Asked Questions about Heart Rate Zones\",\n      seo_faq_q1_question: \"What is the maximum heart rate (FCM)?\",\n      seo_faq_q1_answer:\n        \"The maximum heart rate is the maximum number of beats per minute your heart can reach during an intense physical effort. It is generally calculated with the formula: 220 - your age. However, this formula can vary by ±10-15 bpm depending on individuals.\",\n      seo_faq_q2_question: \"How to measure my resting heart rate?\",\n      seo_faq_q2_answer:\n        \"Measure your pulse for 60 seconds immediately after waking up, before getting out of bed. Count the beats for 60 seconds or 15 seconds and multiply by 4. Repeat for 3-5 days and use the average. A normal resting heart rate is between 60-100 bpm.\",\n      seo_faq_q3_question: \"What zone is best for weight loss?\",\n      seo_faq_q3_answer:\n        \"The Fat Burn Zone (60-70%) is optimal for burning fat as fuel. However, higher intensity zones burn more total calories. Mix zones for best results - include both fat burn and higher intensity workouts.\",\n      seo_faq_q4_question: \"Can I train in the VO2 Max zone every day?\",\n      seo_faq_q4_answer:\n        \"No, the VO2 Max zone is extremely intense and should only be used 1-2 times per week for short intervals (30 seconds to 2 minutes). Most of your training should be in the aerobic zones to build endurance and allow recovery.\",\n      seo_faq_q5_question: \"Is the 220-age formula accurate?\",\n      seo_faq_q5_answer:\n        \"It's a general estimate that works for most people but can vary by ±10-15 bpm. For more accuracy, consider a supervised max heart rate test or use the Karvonen formula with your actual measurements.\",\n      seo_faq_q6_question: \"How to know if I'm in the right zone?\",\n      seo_faq_q6_answer:\n        \"Use a heart rate monitor for the most accurate measurement. Without a device, use the speech test: Light zone = easy conversation, Moderate zone = short sentences, High zone = isolated words only.\",\n      seo_faq_q7_question: \"Do zones change with improving fitness?\",\n      seo_faq_q7_answer:\n        \"Yes, with training, your resting heart rate decreases and your cardiac efficiency improves. Recalculate your zones every 2-3 months to adjust your training.\",\n      seo_faq_q8_question: \"What is the difference between the Basic and Karvonen formulas?\",\n      seo_faq_q8_answer:\n        \"The Basic formula only uses age (THR = FCM × %Intensity). The Karvonen formula is more accurate because it takes your RHR into account: THR = [(FCM - RHR) × %Intensity] + RHR.\",\n      intern_links_title: \"Ready to Optimize Your Training?\",\n      intern_links_subtitle: \"Use our calculator to discover your personalized zones and transform your fitness\",\n      intern_links_button: \"Calculate My Zones Now\",\n      intern_links_bmi_title: \"BMI Calculator\",\n      intern_links_bmi_description: \"Evaluate your body mass index\",\n      intern_links_calorie_title: \"Calorie Calculator\",\n      intern_links_calorie_description: \"Determine your daily calorie needs\",\n      intern_links_macro_title: \"Macro Calculator\",\n      intern_links_macro_description: \"Optimize your nutritional distribution\",\n      medical_warning_title: \"Medical Warning\",\n      medical_warning_content:\n        \"This calculator provides estimates based on general formulas. Results may vary based on your physical condition, medications, and health status. Always consult a healthcare professional before starting a new exercise program, particularly if you have pre-existing medical conditions or experience unusual symptoms during exercise.\",\n      educational: {\n        title: \"Understanding Heart Rate Training\",\n        description: \"Visualize each training zone easily\",\n        what_are_zones: {\n          title: \"What Are Heart Rate Zones?\",\n          content:\n            \"Heart rate zones are ranges of heart beats per minute that correspond to different exercise intensities. Training in specific zones helps you achieve different fitness goals more effectively.\",\n        },\n        why_use_zones: {\n          title: \"Why Use Heart Rate Zones?\",\n          content:\n            \"Training with heart rate zones ensures you're exercising at the right intensity for your goals. It prevents overtraining, maximizes results, and helps you train more efficiently.\",\n        },\n        zone_distribution: {\n          title: \"Recommended Weekly Zone Distribution\",\n          content:\n            \"For balanced fitness: 80% in Zones 1-3 (aerobic base), 15% in Zone 4 (threshold), 5% in Zone 5 (VO2 max). Adjust based on your specific goals and fitness level.\",\n        },\n        monitoring: {\n          title: \"How to Monitor Your Heart Rate\",\n          content:\n            \"Use a chest strap for most accuracy, or a wrist-based monitor for convenience. Check your heart rate regularly during exercise and adjust intensity to stay in your target zone.\",\n        },\n      },\n    },\n    \"one-rep-max\": {\n      title: \"1RM Calculator\",\n      description: \"Estimate your one rep max and plan your strength training percentages\",\n    },\n    back_to_calculators: \"Back to calculators\",\n    body_fat_percentage: \"Body Fat Percentage\",\n    body_fat_info_title: \"What is Body Fat Percentage?\",\n    body_fat_info_content:\n      \"Body fat percentage is essential for Katch-McArdle and Cunningham formulas as they calculate based on lean body mass. If you don't know your exact body fat %, use online visual guides or DEXA scans for accuracy.\",\n    \"calorie-calculator-hub\": {\n      title: \"Calorie Calculator Formulas\",\n      subtitle: \"Choose the best formula for your needs and get accurate calorie calculations\",\n      meta: {\n        title: \"Calorie Calculator Formulas - BMR & TDEE Calculators\",\n        description:\n          \"Compare different BMR formulas: Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham, and Oxford. Choose the best calorie calculator for your needs.\",\n        keywords:\n          \"BMR formulas, calorie calculator comparison, Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham, Oxford, TDEE calculator\",\n      },\n      which_formula: \"Which Formula Should I Choose?\",\n      formula_explanation: \"Different formulas work better for different people. Here's a quick guide to help you choose:\",\n      recommendation_general: \"Best overall formula, most accurate for general population\",\n      recommendation_traditional: \"Classic formula, widely used but slightly less accurate\",\n      recommendation_bodyfat: \"Most accurate if you know your body fat percentage\",\n      since: \"Since\",\n      all_formulas: \"All formulas\",\n      popularity: \"Popularity\",\n      accuracy: \"Accuracy\",\n      accuracy_high: \"High\",\n      accuracy_good: \"Good\",\n      accuracy_medium: \"Medium\",\n      best_for: \"Best for\",\n      best_for_general: \"General use\",\n      best_for_traditional: \"Traditional\",\n      best_for_athletes: \"Athletes\",\n      best_for_bodybuilders: \"Bodybuilders\",\n      best_for_european: \"European population\",\n      best_for_comparison: \"Compare all\",\n      \"mifflin-st-jeor\": {\n        title: \"Mifflin-St Jeor (Recommended)\",\n        description: \"Most accurate formula for general population, developed in 1990. Currently the gold standard for BMR calculations.\",\n      },\n      \"harris-benedict\": {\n        title: \"Harris-Benedict (Classic)\",\n        description: \"Revised 1984 version of the classic formula. Widely used but tends to overestimate calories for some people.\",\n      },\n      \"katch-mcardle\": {\n        title: \"Katch-McArdle (Athletes)\",\n        description: \"Based on lean body mass. Most accurate for people who know their body fat percentage and are physically active.\",\n      },\n      cunningham: {\n        title: \"Cunningham (Bodybuilders)\",\n        description: \"Designed for very lean athletes and bodybuilders with low body fat. Uses lean body mass calculation.\",\n      },\n      oxford: {\n        title: \"Oxford (European)\",\n        description: \"More recent formula (2005) based on European populations. Takes age brackets into account.\",\n      },\n      comparison: {\n        title: \"Compare All Formulas\",\n        description: \"Compare results from all formulas side by side to see the differences and choose what works best for you.\",\n      },\n    },\n    \"mifflin-st-jeor\": {\n      title: \"Mifflin-St Jeor Calculator\",\n      subtitle: \"The gold standard for BMR calculation - most accurate for general population\",\n      meta: {\n        title: \"Mifflin-St Jeor Calculator - Most Accurate BMR & TDEE\",\n        description:\n          \"Calculate your BMR and TDEE using the Mifflin-St Jeor equation - the most accurate formula for general population. Get personalized calorie recommendations.\",\n        keywords: \"Mifflin-St Jeor calculator, BMR calculator, TDEE calculator, most accurate calorie calculator, metabolism calculator\",\n      },\n      how_it_works: \"How the Mifflin-St Jeor Formula Works\",\n      how_it_works_description:\n        \"Developed in 1990, this formula is considered the most accurate for calculating Basal Metabolic Rate (BMR) in healthy adults. It's more precise than the Harris-Benedict equation and is widely recommended by nutritionists and fitness professionals.\",\n    },\n    \"harris-benedict\": {\n      title: \"Harris-Benedict Calculator\",\n      subtitle: \"Classic BMR formula - the traditional approach to calorie calculation\",\n      meta: {\n        title: \"Harris-Benedict Calculator - Classic BMR & TDEE Formula\",\n        description:\n          \"Calculate your BMR and TDEE using the revised Harris-Benedict equation (1984). The classic formula that started modern calorie calculations.\",\n        keywords: \"Harris-Benedict calculator, classic BMR calculator, traditional TDEE calculator, revised Harris-Benedict formula\",\n      },\n      how_it_works: \"How the Harris-Benedict Formula Works\",\n      how_it_works_description:\n        \"Originally developed in 1919 and revised in 1984, the Harris-Benedict equation was one of the first formulas to calculate BMR. While slightly less accurate than newer formulas, it remains widely used and provides good estimates for most people.\",\n    },\n    \"katch-mcardle\": {\n      title: \"Katch-McArdle Calculator\",\n      subtitle: \"Precise BMR calculation based on lean body mass - ideal for athletes\",\n      meta: {\n        title: \"Katch-McArdle Calculator - Lean Body Mass BMR & TDEE\",\n        description:\n          \"Calculate your BMR and TDEE using the Katch-McArdle formula based on lean body mass. Most accurate for people who know their body fat percentage.\",\n        keywords: \"Katch-McArdle calculator, lean body mass BMR, body fat percentage calculator, athlete BMR calculator, precise TDEE\",\n      },\n      how_it_works: \"How the Katch-McArdle Formula Works\",\n      how_it_works_description:\n        \"This formula calculates BMR based on lean body mass rather than total body weight, making it more accurate for people who know their body fat percentage. It's particularly useful for athletes and physically active individuals.\",\n    },\n    cunningham: {\n      title: \"Cunningham Calculator\",\n      subtitle: \"BMR formula designed for very lean athletes and bodybuilders\",\n      meta: {\n        title: \"Cunningham Calculator - BMR for Lean Athletes & Bodybuilders\",\n        description:\n          \"Calculate your BMR and TDEE using the Cunningham formula, specifically designed for very lean athletes and bodybuilders with low body fat.\",\n        keywords:\n          \"Cunningham calculator, bodybuilder BMR calculator, lean athlete BMR, low body fat BMR calculator, competition prep calculator\",\n      },\n      how_it_works: \"How the Cunningham Formula Works\",\n      how_it_works_description:\n        \"Developed specifically for very lean individuals with low body fat percentages, this formula provides higher BMR estimates than other equations. It's most accurate for competitive athletes and bodybuilders in contest preparation.\",\n    },\n    oxford: {\n      title: \"Oxford Calculator\",\n      subtitle: \"Modern BMR formula based on European populations with age considerations\",\n      meta: {\n        title: \"Oxford Calculator - Modern BMR & TDEE Formula\",\n        description:\n          \"Calculate your BMR and TDEE using the Oxford equation (2005), a modern formula based on European populations with age-specific calculations.\",\n        keywords: \"Oxford calculator, modern BMR calculator, European BMR formula, age-specific BMR calculator, 2005 BMR equation\",\n      },\n      how_it_works: \"How the Oxford Formula Works\",\n      how_it_works_description:\n        \"Published in 2005, this is one of the more recent BMR formulas. It was developed using data from European populations and takes age brackets into account, providing different equations for people under and over 30 years old.\",\n    },\n    \"calorie-calculator-comparison\": {\n      title: \"Compare All BMR Formulas\",\n      subtitle: \"See how different BMR formulas calculate your calorie needs side by side\",\n      meta: {\n        title: \"BMR Formula Comparison - Compare All Calorie Calculators\",\n        description:\n          \"Compare Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham, and Oxford BMR formulas side by side. See which formula works best for you.\",\n        keywords:\n          \"BMR formula comparison, calorie calculator comparison, Mifflin vs Harris-Benedict, best BMR calculator, compare calorie formulas\",\n      },\n      how_it_works: \"How This Comparison Works\",\n      how_it_works_description:\n        \"Enter your details once and see how all major BMR formulas calculate your daily calorie needs. This helps you understand the differences and choose the most suitable formula for your goals.\",\n      input_details: \"Your Details\",\n      compare: \"Compare\",\n      results_comparison: \"Formula Comparison Results\",\n      vs_mifflin: \"vs Mifflin-St Jeor\",\n      summary: \"Summary & Recommendations\",\n      summary_explanation:\n        \"Different formulas can give varying results. Generally, differences of ±100-200 calories are normal and expected.\",\n      recommendation:\n        \"For most people, Mifflin-St Jeor provides the most accurate baseline. Athletes should consider Katch-McArdle if they know their body fat percentage.\",\n    },\n    \"bmi-calculator-hub\": {\n      title: \"BMI Calculator Tools\",\n      subtitle: \"Calculate your Body Mass Index with different methods and get personalized health insights\",\n      meta: {\n        title: \"BMI Calculator - Body Mass Index Tools & Health Assessment\",\n        description:\n          \"Calculate your BMI with our comprehensive tools. Standard BMI, adjusted for athletes, pediatric BMI, and comparison tools. Get health insights and recommendations.\",\n        keywords: \"BMI calculator, body mass index, health assessment, weight status, BMI tools, pediatric BMI, athlete BMI\",\n      },\n      understanding_bmi: \"Understanding BMI\",\n      bmi_explanation:\n        \"BMI is a screening tool that helps assess whether you're at a healthy weight for your height. Choose the right calculator for your needs:\",\n      recommendation_standard: \"Best for general population and initial health screening\",\n      recommendation_adjusted: \"More accurate for athletes and muscular individuals\",\n      recommendation_pediatric: \"Specialized for children and adolescents with age-specific percentiles\",\n      popularity: \"Popularity\",\n      accuracy: \"Accuracy\",\n      accuracy_high: \"High\",\n      accuracy_good: \"Good\",\n      accuracy_medium: \"Medium\",\n      best_for: \"Best for\",\n      best_for_general: \"General use\",\n      best_for_athletes: \"Athletes\",\n      best_for_children: \"Children\",\n      best_for_comparison: \"Compare all\",\n      category_standard: \"Standard\",\n      category_advanced: \"Advanced\",\n      category_specialized: \"Specialized\",\n      standard: {\n        title: \"Standard BMI Calculator\",\n        description: \"Classic BMI calculation using the standard WHO formula. Quick and easy assessment for general population.\",\n        page_title: \"Standard BMI Calculator\",\n        page_description:\n          \"Calculate your Body Mass Index using the standard WHO formula. Get instant results with health category and personalized recommendations.\",\n      },\n      adjusted: {\n        title: \"Adjusted BMI Calculator\",\n        description:\n          \"Enhanced BMI calculation that considers muscle mass and body composition for more accurate results in athletic individuals.\",\n      },\n      pediatric: {\n        title: \"Pediatric BMI Calculator\",\n        description: \"Specialized BMI calculator for children and adolescents using age and gender-specific percentiles and growth charts.\",\n      },\n      comparison: {\n        title: \"BMI Comparison Tool\",\n        description: \"Compare different BMI calculation methods side by side to understand how various factors affect your results.\",\n      },\n    },\n  },\n  \"bmi-calculator\": {\n    height: \"Height\",\n    weight: \"Weight\",\n    feet: \"ft\",\n    inches: \"in\",\n    cm: \"cm\",\n    kg: \"kg\",\n    lbs: \"lbs\",\n    height_placeholder: \"Enter height\",\n    weight_placeholder: \"Enter weight\",\n    calculate: \"Calculate BMI\",\n    your_bmi: \"Your BMI\",\n    bmi_prime: \"BMI Prime\",\n    ponderal_index: \"Ponderal Index\",\n    bmi_category: \"BMI Category\",\n    health_risk: \"Health Risk\",\n    recommendations_label: \"Recommendations\",\n    units: \"Units\",\n    metric: \"Metric (kg/cm)\",\n    imperial: \"Imperial (lbs/ft)\",\n\n    // Detailed BMI Categories (WHO)\n    category_severe_thinness: \"Severe Thinness\",\n    category_moderate_thinness: \"Moderate Thinness\",\n    category_mild_thinness: \"Mild Thinness\",\n    category_normal: \"Normal Weight\",\n    category_overweight: \"Overweight\",\n    category_obese_class_1: \"Obesity Class I\",\n    category_obese_class_2: \"Obesity Class II\",\n    category_obese_class_3: \"Obesity Class III\",\n\n    // Health Risks\n    risk_low: \"Low\",\n    risk_normal: \"Normal\",\n    risk_increased: \"Increased\",\n    risk_high: \"High\",\n    risk_very_high: \"Very High\",\n    risk_extremely_high: \"Extremely High\",\n\n    // Additional Information\n    bmi_range: \"BMI Range\",\n    ideal_weight: \"Ideal Weight Range\",\n    weight_to_lose: \"Weight to Lose\",\n    weight_to_gain: \"Weight to Gain\",\n    normal_range: \"Normal BMI range: 18.5 - 24.9\",\n\n    // BMI Prime\n    about_bmi_prime: \"About BMI Prime\",\n    bmi_prime_explanation:\n      \"BMI Prime is the ratio of your BMI to the upper limit of normal BMI (25). A value of 1.0 means you're at the upper limit of normal weight.\",\n    underweight: \"Underweight\",\n    normal: \"Normal\",\n    overweight: \"Overweight\",\n    obese: \"Obese\",\n\n    // Limitations\n    limitations_title: \"BMI Limitations\",\n    limitations_text:\n      \"BMI doesn't distinguish between muscle and fat mass. Athletes and very muscular individuals may have high BMI despite being healthy. Age, sex, ethnicity, and body composition also affect interpretation.\",\n\n    disclaimer: \"BMI is a screening tool and may not reflect body composition. Consult healthcare professionals for personalized advice.\",\n\n    // Recommendations\n    recommendations: {\n      severe_thinness: {\n        medical_consultation: \"Immediate medical consultation strongly recommended\",\n        nutritional_assessment: \"Comprehensive nutritional assessment needed\",\n        weight_gain_program: \"May require supervised weight gain program\",\n        screen_conditions: \"Screen for underlying medical conditions\",\n        psychological_evaluation: \"Consider psychological evaluation if eating disorder suspected\",\n      },\n      moderate_thinness: {\n        healthcare_provider: \"Consult with healthcare provider for evaluation\",\n        nutrient_dense_foods: \"Focus on nutrient-dense, calorie-rich foods\",\n        registered_dietitian: \"Consider working with a registered dietitian\",\n        monitor_malnutrition: \"Monitor for signs of malnutrition\",\n        gradual_weight_gain: \"Gradual, healthy weight gain recommended\",\n      },\n      mild_thinness: {\n        consider_healthcare: \"Consider consulting with a healthcare provider\",\n        nutrient_dense_foods: \"Focus on nutrient-dense foods to gain healthy weight\",\n        strength_training: \"Include strength training to build muscle mass\",\n        monitor_health: \"Monitor your health regularly\",\n        gradual_weight_gain: \"Aim for gradual weight gain (1-2 lbs per week)\",\n      },\n      normal: {\n        maintain_weight: \"Maintain your current healthy weight\",\n        physical_activity: \"Continue regular physical activity (150+ minutes per week)\",\n        balanced_diet: \"Eat a balanced, nutritious diet\",\n        health_checkups: \"Regular health check-ups\",\n        overall_wellness: \"Focus on overall wellness and body composition\",\n      },\n      overweight: {\n        gradual_weight_loss: \"Aim for gradual weight loss (1-2 lbs per week)\",\n        increase_activity: \"Increase physical activity to 150+ minutes per week\",\n        portion_control: \"Focus on portion control and balanced nutrition\",\n        healthcare_provider: \"Consider consulting with a healthcare provider\",\n        lifestyle_goals: \"Set realistic, sustainable lifestyle goals\",\n      },\n      obese_class_1: {\n        healthcare_provider: \"Consult with a healthcare provider for a weight management plan\",\n        weight_loss_target: \"Aim for 5-10% weight loss initially\",\n        diet_exercise: \"Combine diet and exercise interventions\",\n        nutritional_counseling: \"Consider professional nutritional counseling\",\n        screen_conditions: \"Screen for weight-related health conditions\",\n      },\n      obese_class_2: {\n        medical_supervision: \"Seek medical supervision for weight management\",\n        lifestyle_programs: \"Consider comprehensive lifestyle intervention programs\",\n        evaluate_conditions: \"Evaluate for weight-related health conditions\",\n        medical_treatments: \"May benefit from medical weight loss treatments\",\n        bariatric_surgery: \"Consider bariatric surgery evaluation if appropriate\",\n      },\n      obese_class_3: {\n        medical_consultation: \"Immediate medical consultation recommended\",\n        bariatric_surgery: \"Consider bariatric surgery evaluation\",\n        weight_management: \"Comprehensive medical weight management program\",\n        health_complications: \"Address weight-related health complications\",\n        multidisciplinary: \"Multidisciplinary approach with medical team\",\n      },\n    },\n\n    // Health Risks\n    health_risks: {\n      overweight: {\n        high_blood_pressure: \"High blood pressure\",\n        ldl_cholesterol: \"Higher levels of LDL cholesterol (bad cholesterol)\",\n        hdl_cholesterol: \"Lower levels of HDL cholesterol (good cholesterol)\",\n        triglycerides: \"High levels of triglycerides\",\n        type_2_diabetes: \"Type II diabetes\",\n        coronary_heart_disease: \"Coronary heart disease\",\n        stroke: \"Stroke\",\n        gallbladder_disease: \"Gallbladder disease\",\n        osteoarthritis: \"Osteoarthritis\",\n        sleep_apnea: \"Sleep apnea and breathing problems\",\n        certain_cancers: \"Certain cancers (endometrial, breast, colon, kidney, gallbladder, liver)\",\n        low_quality_life: \"Low quality of life\",\n        mental_illnesses: \"Mental illnesses such as clinical depression and anxiety\",\n        body_pains: \"Body pains and difficulty with physical functions\",\n        increased_mortality: \"Generally increased risk of mortality\",\n      },\n      underweight: {\n        malnutrition: \"Malnutrition and vitamin deficiencies\",\n        anemia: \"Anemia (lowered ability to carry oxygen in blood)\",\n        osteoporosis: \"Osteoporosis (increased risk of bone fractures)\",\n        immune_function: \"Decreased immune function\",\n        growth_development: \"Growth and development issues (especially in children)\",\n        reproductive_issues: \"Reproductive issues for women due to hormonal imbalances\",\n        miscarriage_risk: \"Higher chance of miscarriage in first trimester\",\n        surgery_complications: \"Potential complications during surgery\",\n        increased_mortality: \"Generally increased risk of mortality\",\n        underlying_conditions: \"May indicate underlying medical conditions\",\n      },\n    },\n\n    // Educational Content\n    educational: {\n      introduction_title: \"BMI Introduction\",\n      introduction_text:\n        \"BMI is a measurement of a person's leanness or corpulence based on their height and weight, and is intended to quantify tissue mass. It is widely used as a general indicator of whether a person has a healthy body weight for their height.\",\n      introduction_usage:\n        \"Specifically, the value obtained from the calculation of BMI is used to categorize whether a person is underweight, normal weight, overweight, or obese depending on what range the value falls between. These ranges of BMI vary based on factors such as region and age, and are sometimes further divided into subcategories such as severely underweight or very severely obese.\",\n\n      adult_table_title: \"BMI Table for Adults\",\n      adult_table_description:\n        \"This is the World Health Organization's (WHO) recommended body weight based on BMI values for adults. It is used for both men and women, age 20 or older.\",\n\n      children_table_title: \"BMI Table for Children and Teens, Age 2-20\",\n      children_table_description:\n        \"The Centers for Disease Control and Prevention (CDC) recommends BMI categorization for children and teens between age 2 and 20.\",\n\n      classification: \"Classification\",\n      bmi_range: \"BMI Range - kg/m²\",\n      category: \"Category\",\n      percentile_range: \"Percentile Range\",\n      underweight: \"Underweight\",\n      healthy_weight: \"Healthy Weight\",\n      at_risk_overweight: \"At Risk of Overweight\",\n      overweight: \"Overweight\",\n\n      overweight_risks_title: \"Risks Associated with Being Overweight\",\n      overweight_risks_intro:\n        \"Being overweight increases the risk of a number of serious diseases and health conditions. Below is a list of said risks, according to the Centers for Disease Control and Prevention (CDC):\",\n\n      cardiovascular_risks: \"Cardiovascular Risks\",\n      high_blood_pressure: \"High blood pressure\",\n      cholesterol_issues: \"Higher levels of LDL cholesterol, lower levels of HDL cholesterol, and high levels of triglycerides\",\n      coronary_heart_disease: \"Coronary heart disease\",\n      stroke: \"Stroke\",\n\n      metabolic_risks: \"Metabolic Risks\",\n      type_2_diabetes: \"Type II diabetes\",\n      gallbladder_disease: \"Gallbladder disease\",\n      sleep_apnea: \"Sleep apnea and breathing problems\",\n      osteoarthritis: \"Osteoarthritis, a type of joint disease caused by breakdown of joint cartilage\",\n\n      other_risks: \"Other Health Risks\",\n      certain_cancers: \"Certain cancers (endometrial, breast, colon, kidney, gallbladder, liver)\",\n      mental_health_issues: \"Mental illnesses such as clinical depression, anxiety, and others\",\n      reduced_quality_life: \"Low quality of life and body pains\",\n      increased_mortality: \"Generally, an increased risk of mortality compared to those with a healthy BMI\",\n\n      underweight_risks_title: \"Risks Associated with Being Underweight\",\n      underweight_risks_intro: \"Being underweight has its own associated risks, listed below:\",\n      malnutrition: \"Malnutrition, vitamin deficiencies, anemia (lowered ability to carry blood vessels)\",\n      osteoporosis: \"Osteoporosis, a disease that causes bone weakness, increasing the risk of breaking a bone\",\n      immune_function_decrease: \"A decrease in immune function\",\n      growth_development_issues: \"Growth and development issues, particularly in children and teenagers\",\n      reproductive_issues: \"Possible reproductive issues for women due to hormonal imbalances\",\n      surgery_complications: \"Potential complications as a result of surgery\",\n      increased_mortality_underweight: \"Generally, an increased risk of mortality compared to those with a healthy BMI\",\n\n      adults_limitations: \"In Adults\",\n      older_adults_fat: \"Older adults tend to have more body fat than younger adults with the same BMI\",\n      women_fat_difference: \"Women tend to have more body fat than men for an equivalent BMI\",\n      athletes_muscle_mass: \"Muscular individuals and highly trained athletes may have higher BMIs due to large muscle mass\",\n\n      children_limitations: \"In Children and Adolescents\",\n      height_maturation_influence: \"Height and level of sexual maturation can influence BMI and body fat among children\",\n      fat_free_mass_difference: \"BMI could be a result of increased levels of either fat or fat-free mass\",\n      population_accuracy: \"BMI is fairly indicative of body fat for 90-95% of the population\",\n\n      formulas_title: \"BMI Formula\",\n      metric_formula: \"Metric Formula\",\n      imperial_formula: \"Imperial Formula\",\n      example: \"Example\",\n\n      bmi_prime_formula: \"BMI Prime Formula\",\n      bmi_prime_description: \"Ratio of your BMI to the upper limit of normal BMI (25)\",\n\n      ponderal_index_title: \"Ponderal Index\",\n      ponderal_index_explanation:\n        \"The Ponderal Index (PI) is similar to BMI in that it measures the leanness or corpulence of a person based on their height and weight. The main difference between the PI and BMI is the cubing rather than squaring of the height in the formula. While BMI can be a useful tool when considering large populations, it is not reliable for determining leanness or corpulence in individuals.\",\n      ponderal_index_metric_description: \"Ponderal Index using metric units\",\n      ponderal_index_imperial_description: \"Ponderal Index using imperial units\",\n\n      medical_disclaimer_title: \"Medical Disclaimer\",\n    },\n  },\n  levels: {\n    BEGINNER: \"Beginner\",\n    INTERMEDIATE: \"Intermediate\",\n    ADVANCED: \"Advanced\",\n  },\n  email_sent: \"Email sent\",\n  cant_send_email: \"Can't send email\",\n  logout: \"Logout\",\n  verify_email: \"Verify your email. ⚠️ Don't forget to check your spam folder.\",\n  verify_email_subtitle: \"Please verify your email to continue.\",\n  resend_email: \"Resend email\",\n  resend_email_countdown: \"Resend email in {seconds} seconds\",\n  signin_error_subtitle: \"Please check your credentials and try again.\",\n  register_title: \"Create an account\",\n  register_description: \"Enter your information below to create your account\",\n  register_terms: \"By signing up, you agree to our\",\n  register_privacy: \"Privacy Policy\",\n  register_privacy_link: \"and our\",\n  register_privacy_link_2: \"Privacy Policy\",\n  password_forgot_title: \"Forgot password?\",\n  password_forgot_subtitle: \"Enter your email to reset your password\",\n  new_password: \"New password\",\n  new_password_placeholder: \"Enter your new password\",\n  current_password: \"Current password\",\n  current_password_placeholder: \"Enter your current password\",\n  confirm_password: \"Confirm password\",\n  confirm_password_placeholder: \"Confirm your password\",\n\n  success: {\n    feedback_sent: \"Feedback sent\",\n    password_forgot_success: \"Email sent\",\n    reset_password_success: \"Password reset successfully\",\n    password_updated_successfully: \"Password updated successfully\",\n  },\n\n  error: {\n    invalid_credentials: \"Invalid credentials or account does not exist\",\n    upload_failed: \"Upload failed\",\n    generic_error: \"Error during operation\",\n    sending_email: \"Error sending email\",\n  },\n\n  backend_errors: {\n    EMAIL_ALREADY_EXISTS: \"Email already exists\",\n    INVALID_FILE_TYPE: \"Invalid file type\",\n    FILE_TOO_LARGE: \"File too large\",\n    NO_FILE_UPLOADED: \"No file uploaded\",\n    IMAGE_PROCESSING_ERROR: \"Image processing error\",\n    upload_failed: \"Upload failed\",\n  },\n\n  profile: {\n    new_workout: \"New Workout\",\n    alert: {\n      title: \"Your progress is stored in your browser.\",\n      create_account: \"Create an account\",\n      log_in: \"Log in\",\n      to_ensure_it_is_not_getting_lost: \"to ensure it is not getting lost.\",\n    },\n  },\n\n  // Release Notes\n  release_notes: {\n    title: \"What's New\",\n    release_notes: \"Release Notes\",\n    notes: {\n      note_2025_10_29: {\n        title: \"🍑 New Booty Program Released!\",\n        content:\n          \"<li>A brand new <a href='/programs/booty-pump' class='text-blue-500 hover:underline'>Booty program</a> is now available!</li><li>Target and strengthen your glutes with specialized workouts</li><li>Designed for maximum results and muscle growth</li><li>Join the program today! 💪</li>\",\n      },\n      note_2025_08_18: {\n        title: \"🏆 New Leaderboard Feature!\",\n        content:\n          \"<li>New <strong>leaderboard</strong> to compete with other workout champions</li><li>View rankings by <strong>all-time, monthly, and weekly</strong> periods</li><li>Track your position among the top performers</li><li>Motivate yourself to climb the rankings! 🚀</li>\",\n      },\n      note_2025_07_09: {\n        title: \"🎯 Exercise Selection, Favorites & New Tools\",\n        content:\n          \"<li>New <strong>exercise selection</strong> during workout creation (step 3)</li><li><strong>Favorite exercises</strong> system to mark your preferred movements</li><li>New <em>fitness tools</em>: BMI calculator and heart rate zones</li><li>Improved program cards</li><li>New contributors join the project! 🚀</li>\",\n      },\n      note_2025_07_02: {\n        title: \"🛠️ Self-Hosting, Russian & New Tools\",\n        content:\n          \"Improved <strong>self-hosting</strong> capabilities, added <strong>Russian language</strong> support, and introduced new <em>fitness tools</em> including a calorie calculator. 🚀\",\n      },\n      note_2025_06_23: {\n        title: \"🇵🇹 Portuguese Support & Donation Banner\",\n        content:\n          \"The app now supports <strong>Portuguese</strong>! We've also added a <em>donation banner</em> to help support the ongoing costs of the project via <a href='https://github.com/sponsors/snouzy' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>GitHub Sponsors</a> or <a href='https://ko-fi.com/workoutcool' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Ko-fi</a>. 🙏\",\n      },\n      note_2025_06_22: {\n        title: \"🌍 New Languages & Performance Boost!\",\n        content:\n          \"The app is now available in Chinese and Russian! We've also improved drag'n'drop performance for a smoother experience. ⚡\",\n      },\n      note_2025_06_19: {\n        title: \"📱 Now Available as a PWA!\",\n        content:\n          \"Workout.cool v1.2 is now a Progressive Web App! Install it on your phone for a native app experience with offline access. 🚀\",\n      },\n      note_2025_06_18: {\n        title:\n          \"🚀 Featured #1 on <a href='https://news.ycombinator.com/item?id=44309320' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Hacker News</a>!\",\n        content:\n          \"Workout.cool reached the top spot on Hacker News! Thanks to everyone for the amazing support and welcome to all the new users! 💪\",\n      },\n      note_2025_06_01: {\n        title: \"🎉 New: Release Notes Dialog\",\n        content: \"You can now view what's new directly from the header! Stay tuned for more updates.\",\n      },\n      note_2025_05_20: {\n        title: \"UI Improvements\",\n        content: \"Improved mobile responsiveness and added subtle hover effects to buttons.\",\n      },\n    },\n  },\n\n  // Premium Upsell Alert\n  donation_alert: {\n    title: \"Unlock advanced features with Workout.cool Premium\",\n    or: \"or\",\n  },\n\n  // Donation Modal\n  donation_modal: {\n    support_via: \"Support via...\",\n    title: \"Support the project\",\n    congrats: \"Congratulations on your workout! 🎉\",\n    subtitle: \"This app helps you for free, but it has a real cost for me...\",\n    costs_title: \"The reality of costs\",\n    costs_description: \"Currently, donations don't even cover basic costs: servers, authentication, infrastructure, database, etc.\",\n    open_source_title: \"100% Open Source\",\n    open_source_description:\n      \"This app is completely free, ads free and open source. No profit is generated - it's a passion project to help the community and help people exercise.\",\n    no_ads: \"No ads\",\n    no_tracking: \"No tracking\",\n    impact_title: \"Your impact\",\n    impact_3_euros: \"• Even €3 covers 1 week of server\",\n    impact_support: \"• Your support keeps the app free for everyone\",\n    impact_footer: \"Every donation, even small, makes a real difference! 🙏\",\n    later_button: \"Later\",\n    support_button: \"Support the project\",\n  },\n\n  // Contact Support\n  contact_support: \"Contact Support\",\n  contact_support_subtitle: \"Describe your issue and we'll help you as soon as possible. You can also write to us directly at\",\n\n  // Social Platforms\n  social_platforms: {\n    x: \"X (Twitter)\",\n    facebook: \"Facebook\",\n    email: \"Email\",\n    whatsapp: \"WhatsApp\",\n    website: \"Website\",\n    phone: \"Phone\",\n    youtube: \"YouTube\",\n    linkedin: \"LinkedIn\",\n    snapchat: \"Snapchat\",\n    instagram: \"Instagram\",\n    tiktok: \"TikTok\",\n    threads: \"Threads\",\n  },\n\n  // Workout Builder\n  workout_builder: {\n    confirm_delete: \"Are you sure you want to delete this workout session?\",\n    steps: {\n      equipment: {\n        title: \"Equipment\",\n        description: \"Select your equipment\",\n      },\n      muscles: {\n        title: \"Muscles\",\n        description: \"Choose your training\",\n      },\n      exercises: {\n        title: \"Exercises\",\n        description: \"Customize your workout\",\n      },\n    },\n    muscles: {\n      back: \"Back\",\n      abdominals: \"Abdominals\",\n      abductors: \"Abductors\",\n      adductors: \"Adductors\",\n      biceps: \"Biceps\",\n      triceps: \"Triceps\",\n      chest: \"Chest\",\n      shoulders: \"Shoulders\",\n      quadriceps: \"Quadriceps\",\n      hamstrings: \"Hamstrings\",\n      glutes: \"Glutes\",\n      calves: \"Calves\",\n      forearms: \"Forearms\",\n      traps: \"Traps\",\n      obliques: \"Obliques\",\n      lats: \"Lats\",\n    },\n    exercise: {\n      watch_video: \"Watch video\",\n      shuffle: \"Shuffle\",\n      pick: \"Pick\",\n      remove: \"Remove\",\n      no_video_available: \"No video available.\",\n    },\n    loading: {\n      exercises: \"Loading exercises...\",\n    },\n    error: {\n      loading_exercises: \"Error loading exercises\",\n    },\n    no_exercises_found: \"No exercises found. Try to change your equipment or muscles selection.\",\n    addExercise: \"Add exercise\",\n    exerciseAdded: \"{name} added to workout\",\n    exercises: \"exercises\",\n    equipment: {\n      bodyweight: {\n        label: \"Bodyweight\",\n        description: \"Exercises using only your body weight\",\n      },\n      dumbbell: {\n        label: \"Dumbbell\",\n        description: \"Free weight exercises with dumbbells\",\n      },\n      barbell: {\n        label: \"Barbell\",\n        description: \"Compound movements with a barbell\",\n      },\n      kettlebell: {\n        label: \"Kettlebell\",\n        description: \"Dynamic exercises with kettlebells\",\n      },\n      band: {\n        label: \"Band\",\n        description: \"Resistance band exercises\",\n      },\n      plate: {\n        label: \"Plate\",\n        description: \"Exercises using weight plates\",\n      },\n      pullup_bar: {\n        label: \"Pull-up bar\",\n        description: \"Upper body exercises with a pull-up bar\",\n      },\n      bench: {\n        label: \"Bench\",\n        description: \"Bench exercises and support\",\n      },\n    },\n    navigation: {\n      previous: \"Previous\",\n      continue: \"Continue\",\n      complete: \"Complete\",\n    },\n    stats: {\n      \"muscle_selected#zero\": \"0 muscle selected\",\n      \"muscle_selected#one\": \"1 muscle selected\",\n      \"muscle_selected#other\": \"{count} muscles selected\",\n      \"equipment_selected#zero\": \"0 equipment selected\",\n      \"equipment_selected#one\": \"1 equipment selected\",\n      \"equipment_selected#other\": \"{count} equipments selected\",\n      selected: \"Selected\",\n      total: \"Total\",\n      equipment_ready: \"equipment ready\",\n      equipment_ready_plural: \"equipment ready\",\n    },\n    selection: {\n      choose_your_arsenal: \"Choose Your Arsenal\",\n      select_equipment_description: \"Select equipment to unlock personalized workouts\",\n      clear_all: \"Clear all\",\n      muscle_selection_coming_soon: \"Muscle Selection (Coming Soon)\",\n      muscle_selection_description: \"Select the muscle(s) you want to train by clicking on them.\",\n      exercise_selection_coming_soon: \"Exercise Selection (Coming Soon)\",\n      exercise_selection_description: \"This step will show you personalized exercise recommendations.\",\n    },\n    session: {\n      back_to_workout: \"Back to workout\",\n      congrats: \"Congratulations, workout finished! 🎉\",\n      congrats_subtitle: \"You've done it !\",\n      see_instructions: \"See instructions\",\n      finish_set: \"Finish Set\",\n      finish_session: \"Finish Session\",\n      bodyweight: \"Bodyweight\",\n      weight: \"Weight\",\n      reps: \"Reps\",\n      time: \"Time\",\n      next_exercise: \"Next Exercise\",\n      add_set: \"Add set\",\n      add_column: \"Add column\",\n      add_row: \"Add row\",\n      remove_column: \"Remove column\",\n      set_number: \"Set {number}\",\n      set_number_plural: \"Sets {number}\",\n      set_number_singular: \"Set {number}\",\n      set_number_plural_singular: \"Sets {number}\",\n      workout_in_progress: \"Workout in Progress\",\n      started_at: \"Started at\",\n      quit_workout: \"Quit Workout\",\n      elapsed_time: \"Elapsed Time\",\n      chronometer: \"Chronometer\",\n      exercise_progress: \"Exercise Progress\",\n      total_volume: \"Total Volume\",\n      current_exercise: \"Current Exercise\",\n      complete: \"Complete\",\n      active: \"Active\",\n      already_have_a_active_session: \"You already have an active session. Impossible to repeat without finishing or quitting the workout.\",\n      no_exercise_selected: \"No exercise selected\",\n      quit_workout_title: \"Quit Workout?\",\n      progress: \"Progress\",\n      quit_warning: \"Are you sure you want to quit? You can save your progress or lose it completely.\",\n      save_and_quit: \"Save & Quit\",\n      quit_without_save: \"Quit Without Saving\",\n      continue_workout: \"Continue Workout\",\n      history: \"Workout History [{count}]\",\n      no_workout_yet: \"No workout yet.\",\n      start: \"start\",\n      end: \"end\",\n      exercise: \"EXERCISE\",\n      repeat: \"Repeat\",\n      delete: \"Delete\",\n    },\n    attribute_value: {\n      bodyweight: \"Bodyweight\",\n      strength: \"Strength\",\n      powerlifting: \"Powerlifting\",\n      calisthenic: \"Calisthenics\",\n      plyometrics: \"Plyometrics\",\n      stretching: \"Stretching\",\n      strongman: \"Strongman\",\n      cardio: \"Cardio\",\n      stabilization: \"Stabilization\",\n      power: \"Power\",\n      resistance: \"Resistance\",\n      crossfit: \"CrossFit\",\n      weightlifting: \"Weightlifting\",\n      neck: \"Neck\",\n      lats: \"Lats\",\n      adductors: \"Adductors\",\n      abductors: \"Abductors\",\n      groin: \"Groin\",\n      full_body: \"Full body\",\n      rotator_cuff: \"Rotator cuff\",\n      hip_flexor: \"Hip flexor\",\n      achilles_tendon: \"Achilles tendon\",\n      fingers: \"Fingers\",\n      smith_machine: \"Smith machine\",\n      other: \"Other\",\n      ez_bar: \"EZ bar\",\n      machine: \"Machine\",\n      desk: \"Desk\",\n      none: \"None\",\n      cable: \"Cable\",\n      medicine_ball: \"Medicine ball\",\n      swiss_ball: \"Swiss ball\",\n      foam_roll: \"Foam roll\",\n      trx: \"TRX\",\n      box: \"Box\",\n      ropes: \"Ropes\",\n      spin_bike: \"Spin bike\",\n      step: \"Step\",\n      bosu: \"BOSU\",\n      tyre: \"Tyre\",\n      sandbag: \"Sandbag\",\n      pole: \"Pole\",\n      wall: \"Wall\",\n      bar: \"Bar\",\n      rack: \"Rack\",\n      car: \"Car\",\n      sled: \"Sled\",\n      chain: \"Chain\",\n      skierg: \"SkiErg\",\n      rope: \"Rope\",\n      na: \"N/A\",\n      isolation: \"Isolation\",\n      compound: \"Compound\",\n    },\n  },\n  commons: {\n    last_activity: \"Last activity\",\n    registered_on: \"Registered on\",\n    upgrade_to_premium: \"Upgrade to Premium\",\n    refresh: \"Refresh\",\n    just_now: \"just now\",\n    signup_with: \"Sign up with {provider}\",\n    signin_with: \"Sign in with {provider}\",\n    signup: \"Sign up\",\n    login: \"Login\",\n    connecting: \"Connecting...\",\n    login_to_your_account_title: \"Login to your account\",\n    login_to_your_account_subtitle: \"Enter your credentials below to login\",\n    password_forgot: \"Forgot password?\",\n    password_reset_success: \"Password reset successfully\",\n    dont_have_account: \"Don't have an account?\",\n    already_have_account: \"Already have an account?\",\n    or: \"Or\",\n    add: \"Add\",\n    your_feminine: \"your\",\n    password: \"Password\",\n    email: \"Email\",\n    logout: \"Logout\",\n    first_name: \"First name\",\n    last_name: \"Last name\",\n    verify_password: \"Verify password\",\n    submit: \"Submit\",\n    upload: \"Upload\",\n    cancel: \"Cancel\",\n    save_changes: \"Save changes\",\n    change: \"Change\",\n    subject: \"Subject\",\n    message: \"Message\",\n    saving: \"Saving...\",\n    edit: \"Edit\",\n    more_options: \"More options\",\n    open_link: \"Open link\",\n    hide: \"Hide\",\n    make_visible: \"Make visible\",\n    delete: \"Delete\",\n    share: \"Share\",\n    title: \"Title\",\n    subtitle: \"Subtitle\",\n    content: \"Content\",\n    save: \"Save\",\n    button: \"Button\",\n    card: \"Card\",\n    go_back: \"Go back\",\n    next: \"Next\",\n    choose_image: \"Choose image\",\n    soon: \"Soon\",\n    coming_soon_with_emoji: \"Coming soon 🤫\",\n    no_image: \"No image\",\n    description: \"Description\",\n    price: \"Price\",\n    duration: \"Duration\",\n    location: \"Location\",\n    schedule: \"Schedule\",\n    participants_info: \"Participants info\",\n    description_placeholder: \"Enter the description\",\n    title_placeholder: \"Enter the title\",\n    changes_saved: \"Changes saved\",\n    replace: \"Replace\",\n    loading: \"Loading...\",\n    image_deleted: \"The image has been deleted\",\n    discover_workoutcool: \"Discover Workout Cool\",\n    received_just_now: \"Received just now\",\n    copied: \"Copied\",\n    url_copied: \"The URL has been copied\",\n    copy_failed: \"Copy failed\",\n    accordion: \"Accordion\",\n    image: \"Image\",\n    other: \"Other\",\n    register: \"Register\",\n    instantly: \"instantly\",\n    immediately: \"immediately\",\n    link: \"Link\",\n    accept: \"Accept\",\n    deny: \"Deny\",\n    invalid_input: \"Invalid input. Please check the errors.\",\n    copy_url: \"Copy URL\",\n    page_url: \"Page URL\",\n    saving_short: \"Saving...\",\n    saved_short: \"OK\",\n    looks_like_you_are_lost: \"Looks like you are lost\",\n    the_page_you_are_looking_for_is_not_available: \"The page you are looking for is not available\",\n    go_to_home: \"Go to home\",\n    go_to_profile: \"Go to profile\",\n    terms: \"Terms of Service\",\n    privacy: \"Privacy Policy\",\n    sales_terms: \"Sales Terms\",\n    consent_banner: \"We use cookies to improve your experience. By clicking Accept, you agree to our use of cookies.\",\n    about: \"About us\",\n    profile: \"Profile\",\n    donate: \"Donate\",\n    my_account: \"My account\",\n    dashboard: \"Dashboard\",\n    home: \"Home\",\n    changelog: \"Changelog\",\n    stop_impersonation_button: \"Stop impersonation\",\n    impersonating_user_label: \"Impersonating user\",\n    re_hello: \"Re Hello\",\n    back_to_login: \"Back to login\",\n    sending: \"Sending...\",\n    send_me_link: \"Send me a link\",\n    subscription: \"Subscription\",\n    manage_subscription: \"Manage subscription\",\n    become_premium: \"Become Premium\",\n    remove_ads: \"Remove Ads\",\n    extremely_dissatisfied: \"Extremely dissatisfied\",\n    somewhat_dissatisfied: \"Somewhat dissatisfied\",\n    neutral: \"Neutral\",\n    satisfied: \"Satisfied\",\n    support: \"Support\",\n    change_language: \"Change language\",\n    in_progress: \"In progress\",\n    close: \"Close\",\n    premium: \"Premium\",\n    free: \"Free\",\n    new: \"New\",\n    coming_soon: \"Coming soon\",\n    monday: \"Monday\",\n    tuesday: \"Tuesday\",\n    wednesday: \"Wednesday\",\n    thursday: \"Thursday\",\n    friday: \"Friday\",\n    saturday: \"Saturday\",\n    sunday: \"Sunday\",\n    added_to_favorites: \"Added to favorites\",\n    add_to_favorites: \"Add to favorites\",\n    remove_from_favorites: \"Remove from favorites\",\n    favorites: \"Favorites\",\n  },\n  statistics: {\n    title: \"Statistics\",\n    page_subtitle: \"Track your fitness journey with advanced analytics and personalized insights.\",\n    select_exercise: \"Select Exercise\",\n    active_daily_users: \"Active Daily Users\",\n    success_rate: \"Success Rate\",\n    user_rating: \"User Rating\",\n\n    // Tabs\n    tabs: {\n      video: \"Video\",\n      statistics: \"Statistics\",\n    },\n\n    // Chart titles and labels\n    weight: \"Weight\",\n    volume: \"Volume\",\n    weight_progression: \"Weight Progression\",\n    weight_progression_chart: \"Weight progression chart\",\n    weekly_volume: \"Weekly Volume\",\n    volume_chart: \"Volume chart\",\n    estimated_1rm: \"Estimated 1 Rep Max (1RM)\",\n    one_rep_max_chart: \"One rep max chart\",\n    performance_over_time: \"Performance Over Time\",\n\n    // Form and controls\n    timeframe: \"Timeframe\",\n    timeframe_selector: \"Timeframe selector\",\n\n    // Timeframes\n    timeframes: {\n      \"4weeks\": \"4 Weeks\",\n      \"8weeks\": \"8 Weeks\",\n      \"12weeks\": \"12 Weeks\",\n      \"1year\": \"1 Year\",\n    },\n\n    // Error messages\n    error_loading_data: \"Error loading data\",\n    error_loading_weight_progression: \"Error loading weight progression\",\n    error_loading_1rm: \"Error loading 1RM data\",\n    error_loading_volume: \"Error loading volume data\",\n\n    // Empty states\n    no_data_yet: \"No data yet\",\n    start_tracking: \"Start tracking to see your progress\",\n    no_1rm_data: \"No 1RM data available\",\n    complete_sets_with_weight: \"Complete sets with weight to see your 1 Rep Max (1RM)\",\n    no_volume_data: \"No volume data available\",\n    complete_workouts: \"Complete workouts to see your volume\",\n\n    // Info and tooltips\n    \"1rm_formula_info\": \"1RM formula information\",\n    volume_calculation: \"Volume = Weight × Reps × Sets\",\n    last_updated: \"Last updated: {date}\",\n\n    // Premium\n    premium_required: \"Premium required to access statistics\",\n\n    // StatisticsPreviewOverlay\n    premium_statistics: \"Premium Statistics\",\n    premium_statistics_description: \"Get detailed insights into your fitness journey with advanced analytics for each exercise.\",\n    total_volume: \"Total Volume\",\n    pr_increase: \"PR Increase\",\n    weight_progress: \"Weight Progress\",\n    upgrade_now: \"Upgrade Now\",\n    rating: \"4.8/5 rating\",\n    no_ads: \"No ads\",\n    cancel_anytime: \"Cancel anytime\",\n    preview_notice: \"This is just a preview! 👀\",\n    preview_description: \"Unlock full access to detailed analytics, progress tracking, and personalized insights.\",\n    get_premium_access: \"Get Premium Access\",\n\n    // ExercisesBrowser\n    all_equipment: \"All Equipment\",\n    all_muscles: \"All Muscles\",\n    search_exercises: \"Search Exercises\",\n    error_loading_exercises: \"Error loading exercises\",\n    no_exercises_found: \"No exercises found\",\n    equipment_label: \"Equipment:\",\n    primary_muscle_label: \"Primary Muscle:\",\n    unknown: \"Unknown\",\n    no_image_available: \"No image available\",\n  },\n  heatmap: {\n    week_days_short: {\n      sunday: \"S\",\n      monday: \"M\",\n      tuesday: \"T\",\n      wednesday: \"W\",\n      thursday: \"T\",\n      friday: \"F\",\n      saturday: \"S\",\n    },\n    month_names_short: {\n      january: \"Jan\",\n      february: \"Feb\",\n      march: \"Mar\",\n      april: \"Apr\",\n      may: \"May\",\n      june: \"Jun\",\n      july: \"Jul\",\n      august: \"Aug\",\n      september: \"Sep\",\n      october: \"Oct\",\n      november: \"Nov\",\n      december: \"Dec\",\n    },\n    no_workout: \"No workout\",\n    one_workout_unit: \"workout\",\n    multiple_workouts_unit: \"workouts\",\n    \"workout#one\": \"workout\",\n    \"workout#other\": \"workouts\",\n  },\n} as const;\n"
  },
  {
    "path": "locales/es.ts",
    "content": "export default {\n  leaderboard: {\n    title: \"Clasificación\",\n    description: \"Campeones de entrenamientos\",\n    champion_badge: \"🏆 Campeón\",\n    runner_up_badge: \"🥈 Subcampeón\",\n    third_place_badge: \"🥉 Tercer lugar\",\n    second_place: \"2do lugar\",\n    third_place: \"3er lugar\",\n    workouts: \"entrenamientos\",\n    unable_to_load: \"No se pudo cargar la clasificación\",\n    try_again_later: \"Inténtalo de nuevo más tarde\",\n    no_champions_yet: \"Aún no hay campeones\",\n    complete_first_workout: \"¡Completa tu primer entrenamiento para reclamar el trono!\",\n    member_since: \"Miembro desde\",\n    workouts_per_week: \"entrenamientos/semana\",\n    last_workout: \"Último entrenamiento\",\n    page_title: \"Clasificación de Campeones\",\n    page_subtitle: \"Sube a la cima y conviértete en una leyenda de Workout.cool\",\n    period_all_time: \"Global\",\n    period_monthly: \"Mes\",\n    period_weekly: \"Semana\",\n    no_sessions_this_week: \"Sin sesiones esta semana\",\n    no_sessions_this_month: \"Sin sesiones este mes\",\n    registered_members_only: \"Solo miembros registrados\",\n    registered_members_description: \"Crea una cuenta para aparecer en la clasificación\",\n    reset_timezone: \"Reinicio Europa/París\",\n    reset_timezone_description: \"Las clasificaciones semanales y mensuales se reinician a medianoche hora de París\",\n  },\n  programs: {\n    available_programs: \"Programas disponibles\",\n    exercises_in_session: \"Ejercicios en sesión\",\n    start_session: \"Iniciar sesión\",\n    starting_session: \"Iniciando...\",\n    more_than: \"más de\",\n    my_progress: \"Mi progreso\",\n    session: \"sesión\",\n    completed_feminine: \"completadas\",\n    completed_sets: \"sesiones completadas\",\n    \"set#zero\": \"serie\",\n    \"set#one\": \"serie\",\n    \"set#other\": \"series\",\n    error_starting_session: \"Error al iniciar la sesión\",\n    premium_session: \"Sesión Premium\",\n    premium_session_description: \"Esta sesión es parte del contenido premium. Puedes ver los detalles pero no realizar el entrenamiento.\",\n    workout_description: \"Descripción de la sesión\",\n    connect_to_access: \"Conecta para acceder\",\n    become_premium: \"Torne-se Premium\",\n    back_to_program: \"Voltar ao programa\",\n    premium_session_exercises: \"Ejercicios incluidos\",\n    no_equipment: \"Ningún equipo\",\n    workout_programs_title: \"Programas de entrenamiento (+ en curso de creación)\",\n    workout_programs: \"Programas de entrenamiento\",\n    workout_programs_description: \"Elige tu desafío y hazte más fuerte! 💪\",\n    no_programs_available: \"No hay programas disponibles\",\n    no_programs_available_description: \"Los programas estarán disponibles pronto!\",\n    auth_required: \"Autenticación requerida\",\n    auth_required_description: \"Necesitas iniciar sesión para acceder a esta sesión de entrenamiento.\",\n    login_to_continue: \"Iniciar sesión para continuar\",\n    signup_to_continue: \"Regístrate para continuar\",\n    premium_required: \"Premium requerido\",\n    premium_required_description: \"Esta sesión es premium. Actualiza a Premium para acceder a todo el contenido premium.\",\n    upgrade_to_premium: \"Actualizar a Premium\",\n    completed: \"Completado\",\n    about: \"Acerca de\",\n    program: \"Programa\",\n    not_found: \"Programa no encontrado\",\n    characteristics: \"Características\",\n    weeks: \"semanas\",\n    sessions_per_week: \"sesiones/semana\",\n    session_duration: \"min/sesión\",\n    \"your_coach#zero\": \"Tu coach\",\n    \"your_coach#one\": \"Tu coach\",\n    \"your_coach#other\": \"Tus coaches\",\n    community: \"Comunidad activa\",\n    community_count: \"coolbuilders han rejoint\",\n    week_short: \"Sem.\",\n    week: \"Semana\",\n    exercises: \"ejercicios\",\n    min_short: \"min\",\n    premium: \"Premium\",\n    free: \"Gratis\",\n    continue: \"Continuar\",\n    join_cta: \"Inscribirse\",\n    program_completed: \"Programa terminado\",\n    sessions: \"Sesiones\",\n    check_out_program: \"Descubre este programa de entrenamiento!\",\n    share_success: \"Compartido con éxito!\",\n    copied_to_clipboard: \"Enlace copiado!\",\n    share_failed: \"Error al compartir\",\n    important_info: \"Información importante\",\n    donation_teaser:\n      \"Al principio, funcionábamos con donaciones. Pero como puedes imaginar, las donaciones no fueron suficientes para cubrir los costos de desarrollo y funcionamiento. Así que creamos un paquete que nos ayudará a mantener las luces encendidas y desbloquear algunos superpoderes en el camino.\",\n    new: \"NUEVO\",\n    more_programs_coming_title: \"Más programas pronto!\",\n    more_programs_coming_description:\n      \"Estamos trabajando duro para crear nuevos programas. Al pasar a premium ahora, los tendrás todos automáticamente. Gracias por tu apoyo. 🚀\",\n    coming_strength: \"Fuerza & Músculo\",\n    coming_cardio: \"Cardio HIIT\",\n    coming_yoga: \"Yoga & Movilidad\",\n    sessions_coming_soon: \"Sesiones pronto!\",\n    sessions_in_creation: \"Nuestra equipo está trabajando duro para crear sesiones de calidad para esta semana. ¡Vuelve pronto! 🚀\",\n    welcome_modal: {\n      welcome_title: \"¡Bienvenido a {programTitle}!\",\n      subtitle: \"¡Prepárate para superar tus límites! 💪\",\n      level_label: \"Nivel\",\n      duration_label: \"Duración\",\n      frequency_label: \"Frecuencia\",\n      later_button: \"Más tarde\",\n      start_button: \"¡Vamos!\",\n    },\n  },\n  premium: {\n    checkout_error: \"Error al iniciar la compra\",\n    premium_required_title: \"Premium Requerido\",\n    premium_required_subtitle: \"Este es un acceso premium. Actualiza a Premium para acceder a todo el contenido premium.\",\n    premium_required_button: \"Actualizar a Premium\",\n    already_premium: \"Estás disfrutando de Workout.cool Premium\",\n    no_ads: \"Sin anuncios\",\n    upgrade: \"Actualizar\",\n\n    pricing: {\n      month: \"mes\",\n      year: \"año\",\n      monthly: \"Mensual\",\n      yearly: \"Anual\",\n      discount: \"-48%\",\n    },\n\n    // Hero Section\n    hero: {\n      badge: \"Open-Source & Auto-hospedaje SIEMPRE gratis\",\n      title: \"Entrena libremente, apoya la misión\",\n      subtitle: \"Para aquellos que creen en el proyecto y quieren (re)creerse con power boosters !\",\n      stats: {\n        athletes: {\n          count: \"12.4K+\",\n          label: \"Atletas activos\",\n        },\n        series: {\n          count: \"1.2M+\",\n          label: \"Series registradas\",\n        },\n        rating: {\n          count: \"4.9/5\",\n          label: \"Calificación de la comunidad\",\n        },\n        progression: {\n          count: \"+23%\",\n          label: \"Progresión promedio\",\n        },\n      },\n\n      // Health Risks\n      health_risks: {\n        overweight: {\n          high_blood_pressure: \"Presión arterial alta\",\n          ldl_cholesterol: \"Niveles altos de colesterol LDL (colesterol malo)\",\n          hdl_cholesterol: \"Niveles bajos de colesterol HDL (colesterol bueno)\",\n          triglycerides: \"Niveles altos de triglicéridos\",\n          type_2_diabetes: \"Diabetes tipo II\",\n          coronary_heart_disease: \"Enfermedad coronaria\",\n          stroke: \"Accidente cerebrovascular\",\n          gallbladder_disease: \"Enfermedad de la vesícula biliar\",\n          osteoarthritis: \"Osteoartritis\",\n          sleep_apnea: \"Apnea del sueño y problemas respiratorios\",\n          certain_cancers: \"Ciertos cánceres (endometrial, mama, colon, riñón, vesícula biliar, hígado)\",\n          low_quality_life: \"Baja calidad de vida\",\n          mental_illnesses: \"Enfermedades mentales como depresión clínica y ansiedad\",\n          body_pains: \"Dolores corporales y dificultad con funciones físicas\",\n          increased_mortality: \"Riesgo generalmente aumentado de mortalidad\",\n        },\n        underweight: {\n          malnutrition: \"Desnutrición y deficiencias vitamínicas\",\n          anemia: \"Anemia (capacidad reducida para transportar oxígeno en la sangre)\",\n          osteoporosis: \"Osteoporosis (riesgo aumentado de fracturas óseas)\",\n          immune_function: \"Función inmune disminuida\",\n          growth_development: \"Problemas de crecimiento y desarrollo (especialmente en niños)\",\n          reproductive_issues: \"Problemas reproductivos en mujeres debido a desequilibrios hormonales\",\n          miscarriage_risk: \"Mayor probabilidad de aborto espontáneo en el primer trimestre\",\n          surgery_complications: \"Complicaciones potenciales durante cirugías\",\n          increased_mortality: \"Riesgo generalmente aumentado de mortalidad\",\n          underlying_conditions: \"Puede indicar condiciones médicas subyacentes\",\n        },\n      },\n    },\n\n    // Mission Banner\n    mission: {\n      supporters_count: \"234\",\n      supporters_text: \"seguidores apoyando la misión\",\n      limited: \"Limitado\",\n      early_access: \"plazas de acceso temprano\",\n    },\n\n    // Plans\n    plans: {\n      monthly: \"Mensual\",\n      yearly: \"Anual\",\n      yearly_discount: \"-48%\",\n      per_month: \"/mes\",\n      per_year: \"/año\",\n\n      free: {\n        name: \"GRATIS\",\n        price: \"€0\",\n        period: \"/para siempre\",\n        price_label: \"€0/para siempre\",\n        badge: \"Open-Source • SIEMPRE GRATIS\",\n        description: \"Todas las funciones esenciales para entrenar\",\n        features: [\n          \"Generador de ejercicios con videos\",\n          \"Historial de entrenamientos tipo GitHub (6 meses)\",\n          \"Auto-hospedaje posible\",\n          \"Código fuente disponible\",\n        ],\n        button: \"Tu plan actual\",\n        footer_note: \"No se requiere registro • Acceso completo para siempre\",\n      },\n\n      premium: {\n        name: \"PREMIUM ⭐\",\n        price_label: \"€7.90/mes o €49/año\",\n        badge: \"MOST POPULAR • Para entusiastas\",\n        description: \"Todas las funciones + acceso temprano\",\n        footer_monthly: \"¡Únete a la comunidad apasionada! 🔥\",\n        footer_yearly: \"¡Gracias por el apoyo anual! 🙏\",\n        yearly_price_note: \"/mes\",\n        features: [\n          \"...todo del plan Gratuit\",\n          \"Sin publicidad\",\n          \"Historial ilimitado (vs 6 meses gratuito)\",\n          \"Seguimiento de progreso con estadísticas avanzadas (volumen, progresión, PR)\",\n          \"Programas de entrenamiento prediseñados\",\n          \"Chat privado con un coach 1:1\",\n          \"Acceso temprano a nuevas funcionalidades\",\n        ],\n      },\n    },\n\n    // Buttons and Actions\n    actions: {\n      processing: \"Procesando...\",\n      go_premium: \"Torne-se Premium\",\n      sign_in_continue: \"Torne-se Premium\",\n      upgrade_now: \"Actualizar ahora\",\n      current_plan: \"Tu plan actual\",\n    },\n\n    // Trust Elements\n    trust: {\n      gdpr_compliant: \"100% compatible con GDPR\",\n      money_back: \"Garantía de devolución de 30 días\",\n      cancel_anytime: \"1 clic para cancelar, sin compromiso\",\n      secure_payment: \"100% pago seguro vía Stripe\",\n    },\n\n    // Feature Comparison\n    comparison: {\n      title: \"Comparación de características detallada\",\n      subtitle: \"Todo lo que necesitas saber sobre lo que incluye cada plan\",\n      features_label: \"Características\",\n      headers: {\n        features: \"Características\",\n        free: \"Gratis\",\n        premium: \"Premium\",\n      },\n      categories: {\n        equipment: \"Equipamiento & Ejercicios\",\n        tracking: \"Seguimiento & Análisis\",\n        programs: \"Programas & IA\",\n        community: \"Comunidad & Compartir\",\n        support: \"Soporte & Proyecto\",\n      },\n      features: {\n        exercise_library: \"Biblioteca de ejercicios\",\n        custom_exercise: \"Ejercicio personalizado\",\n        video_tutorials: \"Tutoriales en video\",\n        workout_history: \"Historial de entrenamientos\",\n        progress_statistics: \"Estadísticas de progreso\",\n        personal_records: \"Seguimiento de récords personales\",\n        volume_analytics: \"Análisis de volumen & progresión\",\n        predesigned_programs: \"Programas prediseñados\",\n        personalized_recommendations: \"Recomendaciones personalizadas\",\n        pro_templates: \"Plantillas profesionales (Powerlifting, bodybuilding, etc.)\",\n        community_access: \"Acceso a la comunidad\",\n        discord_community: \"Comunidad de Discord\",\n        private_chat: \"Chat privado 1:1 con el coach\",\n        community_support: \"Soporte comunitario\",\n        priority_support: \"Soporte prioritario\",\n        early_access: \"Acceso temprano a funcionalidades\",\n        beta_testing: \"Acceso a pruebas beta\",\n      },\n      values: {\n        basic: \"Básico\",\n        complete: \"Completo\",\n        unlimited: \"Ilimitado\",\n        professional: \"Profesional\",\n        six_months: \"6 meses\",\n        limited: \"Limitado\",\n        all_programs: \"Todos los programas\",\n        public: \"Público\",\n        vip_access: \"Acceso VIP\",\n        private_channels: \"Canales privados\",\n        soon: \"Pronto\",\n        hd_slowmo: \"4K + Slow-mo\",\n        early_access: \"Acceso temprano\",\n      },\n    },\n\n    // FAQ\n    faq: {\n      title: \"Preguntas frecuentes\",\n      subtitle: \"Todo lo que necesitas saber sobre Workout.cool y nuestra misión\",\n      items: [\n        {\n          question: \"¿Por qué pagar si es open-source?\",\n          answer:\n            \"¡Excelente pregunta! El código siempre permanecerá gratis, pero mantener servidores, base de datos y infraestructura cuesta dinero. Tu contribución nos ayuda a mantener la herramienta gratuita para todos. Es un modelo ganador: obtienes funciones premium, la comunidad mantiene acceso gratuito!\",\n        },\n        {\n          question: \"¿Puedo auto-alojar Workout.cool?\",\n          answer:\n            \"¡Absolutamente! Todo el código está disponible en GitHub bajo licencia MIT. Puedes desplegarlo en tus propios servidores, personalizarlo como quieras y usarlo completamente gratis. El auto-alojamiento te da control total sobre tus datos y privacidad del entrenamiento.\",\n        },\n        {\n          question: \"¿Están seguros mis datos de entrenamiento?\",\n          answer:\n            \"¡Sí! Somos compatibles con GDPR, usamos conexiones encriptadas y almacenamos tus datos de forma segura. Además, como somos open-source, puedes auditar nuestras prácticas de seguridad. También puedes exportar tus datos en cualquier momento o auto-alojar para tener control total.\",\n        },\n        {\n          question: \"¿Puedo cancelar mi suscripción en cualquier momento?\",\n          answer:\n            \"¡Claro! Sin contratos, sin compromisos. Cancela con un clic en cualquier momento. Mantendrás acceso hasta que finalice tu período de facturación actual, y siempre puedes reiniciar más tarde. Tus datos de entrenamiento permanecen accesibles incluso si bajas a gratis.\",\n        },\n        {\n          question: \"¿Hay ejercicios para principiantes?\",\n          answer:\n            \"¡Claro! Nuestra biblioteca de ejercicios cubre todos los niveles de aptitud desde los más principiantes hasta los atletas avanzados. Los vídeos y las instrucciones ayudan a los principiantes a encontrar ejercicios adecuados, y nuestros tutoriales en vídeo muestran la forma correcta.\",\n        },\n        {\n          question: \"¿Cómo funciona el seguimiento del progreso?\",\n          answer:\n            \"Cada serie, repetición, peso y tiempo se registra automáticamente. Obtienes un historial de entrenamientos estilo GitHub que muestra tu consistencia, además de análisis detallados sobre volumen, progresión y récords personales. Los usuarios Premium obtienen gráficos avanzados e insights.\",\n        },\n        {\n          question: \"¿Puedo importar datos de otras aplicaciones?\",\n          answer:\n            \"Pronto. Vamos a soportar la importación de datos en CSV para datos básicos (reps & peso). Si estás cambiando de otra aplicación de fitness, nuestro equipo de soporte puede ayudarte a migrar tu historial de entrenamientos.\",\n        },\n        {\n          question: \"¿Funciona la aplicación sin conexión?\",\n          answer:\n            \"El seguimiento del entrenamiento funciona sin conexión. Puedes registrar series y repeticiones sin conexión para 10 entrenamientos. Los vídeos de ejercicios y la sincronización en la nube requieren conexión a internet. Todos tus datos sin conexión se sincronizan automáticamente cuando vuelvas a estar en línea.\",\n        },\n        {\n          question: \"¿Hay programas para mujeres?\",\n          answer:\n            \"¡Claro! Y habrá más programas en el futuro. Estamos trabajando en ello. Los planes Supporter y Premium incluirán todos los programas especializados futuros para diferentes objetivos: fuerza, tonificación, powerlifting, bodybuilding y más !\",\n        },\n        {\n          question: \"¿Puedo crear mis propios programas?\",\n          answer: \"Desafortunadamente, no. Estamos trabajando en ello !\",\n        },\n      ],\n      additional_support: {\n        title: \"Still have questions?\",\n        description: \"Our fitness-focused community is here to help you succeed\",\n        community: \"Community support (discord or hello@workout.cool)\",\n        discussions: \"Open discussions (github/discord)\",\n        roadmap: \"Transparent roadmap (github)\",\n      },\n    },\n\n    // Final CTA\n    final_cta: {\n      motivation: \"¡Sigue empujando! 💪\",\n      title: \"¿Listo para apoyar la misión?\",\n      subtitle: \"Únete a miles de entusiastas del fitness que creen en la libertad de entrenamiento de código abierto\",\n      values: [\n        {\n          title: \"Comunidad primero\",\n          description: \"Construido por y para la comunidad fitness\",\n        },\n        {\n          title: \"Siempre transparente\",\n          description: \"Código abierto, financiación transparente\",\n        },\n        {\n          title: \"Proyecto de amor\",\n          description: \"¡15 años de pasión!\",\n        },\n      ],\n      quote: {\n        text: \"Creemos que las herramientas de fitness deben ser accesibles para todos. Tu apoyo nos ayuda a mantener esta visión mientras continuamos innovando.\",\n        author: \"— El equipo de Workout.cool\",\n      },\n    },\n\n    // Premium Active State\n    premium_active: {\n      title: \"¡Premium Activo! 💪\",\n      supporting: \"Apoyando la misión 💚\",\n    },\n\n    // Legacy translations (keeping for compatibility)\n    premium_active_title: \"Premium Activo\",\n    premium_active_subtitle: \"Todas las funciones desbloqueadas\",\n    free_intro_title: \"Ya estás obteniendo mucho gratis...\",\n    free_intro_text:\n      \"Workout.cool es una aplicación de fitness gratuita y de código abierto utilizada diariamente por más de 60,000 usuarios. Está construida con amor (no con dinero de VC ^^) y nos cuesta tiempo y dinero real mantenerla funcionando.\",\n    donation_story_text:\n      \"Al principio, funcionábamos con donaciones. Pero como puedes imaginar, las donaciones no fueron suficientes para cubrir los costos de desarrollo y funcionamiento. Así que creamos un paquete que nos ayudará a mantener las luces encendidas y desbloquear algunos superpoderes en el camino.\",\n    health_upgrade_text: \"Si Workout.cool te ayuda a mejorar tu salud, por favor considera pasarte a Premium :D !\",\n    unlock_features_text: \"Desbloquea funciones avanzadas y apoya el fitness de código abierto.\",\n    invest_yourself_quote: \"Nunca escatimes en fitness y libros — ¡invierte en ti mismo!\",\n    support_mission: \"Apoya la misión\",\n    best_value_badge: \"MEJOR VALOR\",\n    annual_plan: \"Anual\",\n    monthly_plan: \"Mensual\",\n    discount_badge: \"40% de descuento\",\n    per_month: \"/mes\",\n    feature_all_programs: \"Todos los programas de entrenamiento\",\n    feature_progress_tracking: \"Seguimiento del progreso\",\n    coming_soon: \"(pronto)\",\n    feature_future_updates: \"Todos los futuros programas y actualizaciones\",\n    feature_priority_support: \"Soporte prioritario\",\n    save_yearly: \"Ahorra 40% anual\",\n    processing: \"Procesando...\",\n    cta_annual: \"Quiero apoyar + ahorrar 40%\",\n    cta_monthly: \"Desbloquear mi plan completo\",\n    thank_supporting: \"Gracias por tu apoyo.\",\n    no_pressure: \"Sin presión. Puedes actualizar en cualquier momento.\",\n    keep_pushing: \"¡sigue empujando! huhu\",\n    still_unsure: \"¿Todavía no estás seguro? No te preocupes. Workout.cool siempre seguirá siendo gratuito y de código abierto.\",\n    support_helps: \"Pero si crees en lo que estamos construyendo y puedes permitírtelo, tu apoyo ayudará 💚\",\n    self_hosting: \"Auto-alojamiento\",\n    community: \"Comunidad\",\n    mit_license: \"Licencia MIT\",\n    pricing_year: \"año\",\n    pricing_month: \"mes\",\n    conversion_flow_title: \"Redirigiendo...\",\n    conversion_flow_message: \"¡Sesión iniciada exitosamente! Redirigiendo al checkout...\",\n    redirecting_to_checkout: \"Redirigiendo al checkout\",\n  },\n  breadcrumbs: {\n    home: \"Inicio\",\n  },\n  bottom_navigation: {\n    statistics: \"Estadísticas\",\n    statistics_tooltip: \"Ver tus estadísticas\",\n    programs: \"Programas\",\n    programs_tooltip: \"Explorar programas\",\n    workouts: \"Entrenamientos\",\n    workouts_tooltip: \"Crear tu propio entrenamiento\",\n    premium: \"Premium\",\n    premium_tooltip: \"Torne-se Premium\",\n    leaderboard: \"Clasificación\",\n    leaderboard_tooltip: \"Ver ranking de entrenamiento\",\n    tools: \"Herramientas\",\n    tools_tooltip: \"Explorar herramientas\",\n    profile: \"Perfil\",\n    profile_tooltip: \"Ver tu perfil\",\n  },\n  levels: {\n    BEGINNER: \"Principiante\",\n    INTERMEDIATE: \"Intermedio\",\n    ADVANCED: \"Avanzado\",\n  },\n  email_sent: \"Email enviado\",\n  cant_send_email: \"No se puede enviar el email\",\n  logout: \"Cerrar sesión\",\n  verify_email: \"Verificar tu email. ⚠️ No olvides revisar tu carpeta de spam.\",\n  verify_email_subtitle: \"Por favor verifica tu email para continuar.\",\n  resend_email: \"Reenviar email\",\n  resend_email_countdown: \"Reenviar email en {seconds} segundos\",\n  signin_error_subtitle: \"Por favor verifica tus credenciales e intenta de nuevo.\",\n  register_title: \"Crear una cuenta\",\n  register_description: \"Ingresa tu información para crear tu cuenta\",\n  register_terms: \"Al registrarte, aceptas nuestros\",\n  register_privacy: \"Política de privacidad\",\n  register_privacy_link: \"y nuestra\",\n  register_privacy_link_2: \"Política de privacidad\",\n  password_forgot_title: \"¿Olvidaste tu contraseña?\",\n  password_forgot_subtitle: \"Ingresa tu email para restablecer tu contraseña\",\n  new_password: \"Nueva contraseña\",\n  new_password_placeholder: \"Ingresa tu nueva contraseña\",\n  current_password: \"Contraseña actual\",\n  current_password_placeholder: \"Ingresa tu contraseña actual\",\n  confirm_password: \"Confirmar contraseña\",\n  confirm_password_placeholder: \"Confirma tu contraseña\",\n\n  success: {\n    feedback_sent: \"Comentario enviado\",\n    password_forgot_success: \"Email enviado\",\n    reset_password_success: \"Contraseña restablecida exitosamente\",\n    password_updated_successfully: \"Contraseña actualizada exitosamente\",\n  },\n\n  error: {\n    invalid_credentials: \"Credenciales inválidas o cuenta inexistente\",\n    upload_failed: \"Error al subir\",\n    generic_error: \"Error durante la operación\",\n    sending_email: \"Error al enviar el email\",\n  },\n\n  backend_errors: {\n    EMAIL_ALREADY_EXISTS: \"Email ya existente\",\n    INVALID_FILE_TYPE: \"Tipo de archivo inválido\",\n    FILE_TOO_LARGE: \"Archivo demasiado grande\",\n    NO_FILE_UPLOADED: \"Ningún archivo subido\",\n    IMAGE_PROCESSING_ERROR: \"Error al procesar la imagen\",\n    upload_failed: \"Error al subir\",\n  },\n\n  profile: {\n    new_workout: \"Nuevo entrenamiento\",\n    alert: {\n      title: \"Tu progreso está almacenado en tu navegador.\",\n      create_account: \"Crear una cuenta\",\n      log_in: \"Iniciar sesión\",\n      to_ensure_it_is_not_getting_lost: \"para asegurar que no se pierda.\",\n    },\n  },\n\n  // Release Notes\n  release_notes: {\n    title: \"Novedades\",\n    release_notes: \"Notas\",\n    notes: {\n      note_2025_10_29: {\n        title: \"🍑 ¡Nuevo Programa Booty Lanzado!\",\n        content:\n          \"<li>¡Un nuevo <a href='/programs/booty-pump' class='text-blue-500 hover:underline'>programa Booty</a> ya está disponible!</li><li>Trabaja y fortalece tus glúteos con entrenamientos especializados</li><li>Diseñado para resultados máximos y crecimiento muscular</li><li>¡Únete al programa hoy! 💪</li>\",\n      },\n      note_2025_08_18: {\n        title: \"🏆 ¡Nueva Funcionalidad de Clasificación!\",\n        content:\n          \"<li>Nueva <strong>clasificación</strong> para competir con otros campeones de entrenamiento</li><li>Ver rankings por períodos <strong>todos los tiempos, mensual y semanal</strong></li><li>Rastrea tu posición entre los mejores performers</li><li>¡Motívate para subir en la clasificación! 🚀</li>\",\n      },\n      note_2025_07_09: {\n        title: \"🎯 Selección de Ejercicios, Favoritos y Nuevas Herramientas\",\n        content:\n          \"<li>Nueva <strong>selección de ejercicios</strong> durante la creación de entrenamientos (paso 3)</li><li>Sistema de <strong>ejercicios favoritos</strong> para marcar tus movimientos preferidos</li><li>Nuevas <em>herramientas de fitness</em>: calculadora de IMC y zonas de frecuencia cardíaca</li><li>Tarjetas de programas mejoradas</li><li>¡Nuevos colaboradores se unen al proyecto! 🚀</li>\",\n      },\n      note_2025_07_02: {\n        title: \"🛠️ Auto-alojamiento, Ruso y Nuevas Herramientas\",\n        content:\n          \"Mejora del <strong>auto-alojamiento</strong>, añadido soporte para <strong>ruso</strong>, e introducción de nuevas <em>herramientas de fitness</em> incluyendo una calculadora de calorías. 🚀\",\n      },\n      note_2025_06_23: {\n        title: \"🇵🇹 Soporte de Portugués y Banner de Donación\",\n        content:\n          \"¡La app ahora soporta <strong>portugués</strong>! También hemos añadido un <em>banner de donación</em> para ayudar a cubrir los costos del proyecto via <a href='https://github.com/sponsors/snouzy' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>GitHub Sponsors</a> o <a href='https://ko-fi.com/workoutcool' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Ko-fi</a>. 🙏\",\n      },\n      note_2025_06_22: {\n        title: \"🌍 ¡Nuevos idiomas y mejora de rendimiento!\",\n        content:\n          \"¡La aplicación ahora está disponible en chino y ruso! También hemos mejorado el rendimiento del arrastrar y soltar para una experiencia más fluida. ⚡\",\n      },\n      note_2025_06_19: {\n        title: \"📱 ¡Ahora disponible como PWA!\",\n        content:\n          \"¡Workout.cool v1.2 ahora es una Progressive Web App! Instálala en tu teléfono para una experiencia de aplicación nativa con acceso sin conexión. 🚀\",\n      },\n      note_2025_06_18: {\n        title:\n          \"🚀 ¡Destacado #1 en <a href='https://news.ycombinator.com/item?id=44309320' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Hacker News</a>!\",\n        content:\n          \"¡Workout.cool alcanzó el primer lugar en Hacker News! ¡Gracias a todos por el increíble apoyo y bienvenidos a todos los nuevos usuarios! 💪\",\n      },\n      note_2025_06_01: {\n        title: \"🎉 Nuevo: Diálogo de notas de versión\",\n        content: \"¡Ahora puedes ver las novedades directamente desde la cabecera! Mantente atento para más actualizaciones.\",\n      },\n      note_2025_05_20: {\n        title: \"Mejoras de la interfaz\",\n        content: \"Mejora de la responsividad móvil y adición de efectos de hover sutiles a los botones.\",\n      },\n    },\n  },\n\n  // Premium Upsell Alert\n  donation_alert: {\n    title: \"Desbloquea funciones avanzadas con Workout.cool Premium\",\n    or: \"o\",\n  },\n\n  // Donation Modal\n  donation_modal: {\n    support_via: \"Apoyar via\",\n    title: \"Apoya el proyecto\",\n    congrats: \"¡Felicidades por tu entrenamiento! 🎉\",\n    subtitle: \"Esta app te ayuda gratis, pero tiene un costo real para mí...\",\n    costs_title: \"La realidad de los costos\",\n    costs_description:\n      \"Actualmente, las donaciones ni siquiera cubren los costos básicos: servidores, autenticación, infraestructura, base de datos, etc.\",\n    open_source_title: \"100% Open Source\",\n    open_source_description:\n      \"Esta app es completamente gratuita y de código abierto. No se genera ganancia - es un proyecto de pasión para ayudar a la comunidad y ayudar a las personas a hacer ejercicio.\",\n    no_ads: \"Sin publicidad\",\n    no_tracking: \"Sin rastreo\",\n    impact_title: \"Tu impacto\",\n    impact_3_euros: \"• Incluso €3 cubren 1 semana de servidor\",\n    impact_support: \"• Tu apoyo mantiene la app gratuita para todos\",\n    impact_footer: \"¡Cada donación, incluso pequeña, hace una diferencia real! 🙏\",\n    later_button: \"Más tarde\",\n    support_button: \"Apoyar el proyecto\",\n  },\n\n  // Contact Support\n  contact_support: \"Contactar soporte\",\n  contact_support_subtitle: \"Describe tu problema y te ayudaremos lo antes posible. También puedes escribirnos directamente a\",\n\n  // Social Platforms\n  social_platforms: {\n    x: \"X (Twitter)\",\n    facebook: \"Facebook\",\n    email: \"Email\",\n    whatsapp: \"WhatsApp\",\n    website: \"Sitio web\",\n    phone: \"Teléfono\",\n    youtube: \"YouTube\",\n    linkedin: \"LinkedIn\",\n    snapchat: \"Snapchat\",\n    instagram: \"Instagram\",\n    tiktok: \"TikTok\",\n    threads: \"Threads\",\n  },\n\n  // Workout Builder\n  workout_builder: {\n    confirm_delete: \"¿Estás seguro de que quieres eliminar esta sesión de entrenamiento?\",\n    steps: {\n      equipment: {\n        title: \"Equipo\",\n        description: \"Selecciona tu equipo\",\n      },\n      muscles: {\n        title: \"Músculos\",\n        description: \"Elige tu entrenamiento\",\n      },\n      exercises: {\n        title: \"Ejercicios\",\n        description: \"Personaliza tu sesión\",\n      },\n    },\n    muscles: {\n      abdominals: \"Abdominales\",\n      adductors: \"Adductores\",\n      abductors: \"Abductores\",\n      back: \"Espalda\",\n      biceps: \"Bíceps\",\n      triceps: \"Tríceps\",\n      chest: \"Pecho\",\n      shoulders: \"Hombros\",\n      quadriceps: \"Cuádriceps\",\n      hamstrings: \"Isquiotibiales\",\n      glutes: \"Glúteos\",\n      calves: \"Pantorrillas\",\n      forearms: \"Antebrazos\",\n      traps: \"Trapecios\",\n      obliques: \"Oblicuos\",\n    },\n    exercise: {\n      watch_video: \"Ver video\",\n      shuffle: \"Mezclar\",\n      pick: \"Elegir\",\n      remove: \"Eliminar\",\n      no_video_available: \"No hay video disponible.\",\n    },\n    loading: {\n      exercises: \"Cargando ejercicios...\",\n    },\n    error: {\n      loading_exercises: \"Error al cargar ejercicios\",\n    },\n    no_exercises_found: \"No se encontraron ejercicios. Intenta cambiar tu selección de equipos o músculos.\",\n    addExercise: \"Añadir ejercicio\",\n    exerciseAdded: \"{name} añadido al entrenamiento\",\n    exercises: \"ejercicios\",\n    equipment: {\n      bodyweight: {\n        label: \"Peso corporal\",\n        description: \"Ejercicios usando únicamente el peso de tu cuerpo\",\n      },\n      dumbbell: {\n        label: \"Mancuernas\",\n        description: \"Ejercicios de peso libre con mancuernas\",\n      },\n      barbell: {\n        label: \"Barra\",\n        description: \"Movimientos compuestos con una barra\",\n      },\n      kettlebell: {\n        label: \"Kettlebell\",\n        description: \"Ejercicios dinámicos con kettlebells\",\n      },\n      band: {\n        label: \"Banda elástica\",\n        description: \"Ejercicios con bandas de resistencia\",\n      },\n      plate: {\n        label: \"Discos\",\n        description: \"Ejercicios usando discos de peso\",\n      },\n      pullup_bar: {\n        label: \"Barra de dominadas\",\n        description: \"Ejercicios del tren superior con barra de dominadas\",\n      },\n      bench: {\n        label: \"Banco\",\n        description: \"Ejercicios en banco y soporte\",\n      },\n    },\n    navigation: {\n      previous: \"Anterior\",\n      continue: \"Continuar\",\n      complete: \"Completar\",\n    },\n    stats: {\n      \"muscle_selected#zero\": \"0 músculos seleccionados\",\n      \"muscle_selected#one\": \"1 músculo seleccionado\",\n      \"muscle_selected#other\": \"{count} músculos seleccionados\",\n      \"equipment_selected#zero\": \"0 equipos seleccionados\",\n      \"equipment_selected#one\": \"1 equipo seleccionado\",\n      \"equipment_selected#other\": \"{count} equipos seleccionados\",\n      selected: \"Seleccionado\",\n      total: \"Total\",\n      equipment_ready: \"equipo listo\",\n      equipment_ready_plural: \"equipos listos\",\n    },\n    selection: {\n      choose_your_arsenal: \"Elige tu arsenal\",\n      select_equipment_description: \"Selecciona el equipo para desbloquear entrenamientos personalizados\",\n      clear_all: \"Limpiar todo\",\n      muscle_selection_coming_soon: \"Selección de músculos (Próximamente)\",\n      muscle_selection_description: \"Selecciona el/los músculo(s) que quieres entrenar haciendo clic en ellos.\",\n      exercise_selection_coming_soon: \"Selección de ejercicios (Próximamente)\",\n      exercise_selection_description: \"Este paso te mostrará recomendaciones de ejercicios personalizadas.\",\n    },\n    session: {\n      back_to_workout: \"Volver al entrenamiento\",\n      congrats: \"¡Felicidades, entrenamiento terminado! 🎉\",\n      congrats_subtitle: \"¡Lo has logrado!\",\n      see_instructions: \"Ver instrucciones\",\n      finish_set: \"Terminar serie\",\n      finish_session: \"Terminar sesión\",\n      bodyweight: \"Peso corporal\",\n      weight: \"Peso\",\n      reps: \"Repeticiones\",\n      time: \"Tiempo\",\n      next_exercise: \"Siguiente ejercicio\",\n      add_set: \"Agregar serie\",\n      add_column: \"Agregar columna\",\n      add_row: \"Agregar fila de atributos\",\n      remove_column: \"Eliminar columna\",\n      set_number: \"Serie {number}\",\n      set_number_plural: \"Series {number}\",\n      set_number_singular: \"Serie {number}\",\n      set_number_plural_singular: \"Series {number}\",\n      workout_in_progress: \"Entrenamiento en progreso\",\n      started_at: \"Iniciado a las\",\n      quit_workout: \"Abandonar entrenamiento\",\n      elapsed_time: \"Tiempo transcurrido\",\n      chronometer: \"Cronómetro\",\n      total_workout_time: \"Tiempo total de entrenamiento\",\n      exercise_progress: \"Progreso\",\n      total_volume: \"Volumen Total\",\n      current_exercise: \"Ejercicio actual\",\n      complete: \"Completo\",\n      active: \"Activo\",\n      already_have_a_active_session: \"Ya tienes una sesión activa. Imposible repetir sin terminar o abandonar el entrenamiento.\",\n      no_exercise_selected: \"Ningún ejercicio seleccionado\",\n      quit_workout_title: \"¿Abandonar entrenamiento?\",\n      progress: \"Progreso\",\n      quit_warning: \"¿Estás seguro de que quieres abandonar? Puedes guardar tu progreso o perderlo completamente.\",\n      save_and_quit: \"Guardar y abandonar\",\n      quit_without_save: \"Abandonar sin guardar\",\n      continue_workout: \"Continuar entrenamiento\",\n      history: \"Historial de entrenamientos [{count}]\",\n      no_workout_yet: \"Ningún entrenamiento aún.\",\n      start: \"inicio\",\n      end: \"fin\",\n      exercise: \"EJERCICIO\",\n      repeat: \"Repetir\",\n      delete: \"Eliminar\",\n    },\n    attribute_value: {\n      bodyweight: \"Peso corporal\",\n      strength: \"Fuerza\",\n      powerlifting: \"Powerlifting\",\n      calisthenic: \"Calistenia\",\n      plyometrics: \"Pliometría\",\n      stretching: \"Estiramiento\",\n      strongman: \"Strongman\",\n      cardio: \"Cardio\",\n      stabilization: \"Estabilización\",\n      power: \"Potencia\",\n      resistance: \"Resistencia\",\n      crossfit: \"CrossFit\",\n      weightlifting: \"Halterofilia\",\n      neck: \"Cuello\",\n      lats: \"Dorsales\",\n      adductors: \"Aductores\",\n      abductors: \"Abductores\",\n      groin: \"Ingle\",\n      full_body: \"Cuerpo completo\",\n      rotator_cuff: \"Manguito rotador\",\n      hip_flexor: \"Flexor de cadera\",\n      achilles_tendon: \"Tendón de Aquiles\",\n      fingers: \"Dedos\",\n      smith_machine: \"Máquina Smith\",\n      other: \"Otro\",\n      ez_bar: \"Barra EZ\",\n      machine: \"Máquina\",\n      desk: \"Escritorio\",\n      none: \"Ninguno\",\n      cable: \"Cable\",\n      medicine_ball: \"Pelota medicinal\",\n      swiss_ball: \"Pelota suiza\",\n      foam_roll: \"Rodillo de espuma\",\n      trx: \"TRX\",\n      box: \"Cajón\",\n      ropes: \"Cuerdas\",\n      spin_bike: \"Bicicleta de spinning\",\n      step: \"Step\",\n      bosu: \"BOSU\",\n      tyre: \"Neumático\",\n      sandbag: \"Saco de arena\",\n      pole: \"Barra vertical\",\n      wall: \"Pared\",\n      bar: \"Barra\",\n      rack: \"Rack\",\n      car: \"Coche\",\n      sled: \"Trineo\",\n      chain: \"Cadena\",\n      skierg: \"SkiErg\",\n      rope: \"Cuerda\",\n      na: \"N/A\",\n      isolation: \"Aislamiento\",\n      compound: \"Compuesto\",\n    },\n  },\n  commons: {\n    upgrade_to_premium: \"Torne-se Premium\",\n    last_activity: \"Última actividad\",\n    registered_on: \"Inscrito el\",\n    just_now: \"ahora mismo\",\n    signup_with: \"Registrarse con {provider}\",\n    signin_with: \"Iniciar sesión con {provider}\",\n    signup: \"Registrarse\",\n    login: \"Iniciar sesión\",\n    connecting: \"Conectando...\",\n    password_reset_success: \"Contraseña restablecida exitosamente\",\n    login_to_your_account_title: \"Inicia sesión en tu cuenta\",\n    login_to_your_account_subtitle: \"Ingresa tus credenciales para iniciar sesión\",\n    password_forgot: \"¿Olvidaste tu contraseña?\",\n    dont_have_account: \"¿No tienes una cuenta?\",\n    already_have_account: \"¿Ya tienes una cuenta?\",\n    or: \"O\",\n    add: \"Agregar\",\n    your_feminine: \"tu\",\n    password: \"Contraseña\",\n    email: \"Email\",\n    logout: \"Cerrar sesión\",\n    first_name: \"Nombre\",\n    last_name: \"Apellido\",\n    verify_password: \"Verificar contraseña\",\n    submit: \"Enviar\",\n    upload: \"Subir\",\n    cancel: \"Cancelar\",\n    save_changes: \"Guardar cambios\",\n    change: \"Cambiar\",\n    subject: \"Asunto\",\n    message: \"Mensaje\",\n    saving: \"Guardando...\",\n    edit: \"Editar\",\n    more_options: \"Más opciones\",\n    open_link: \"Abrir enlace\",\n    hide: \"Ocultar\",\n    make_visible: \"Hacer visible\",\n    delete: \"Eliminar\",\n    share: \"Compartir\",\n    title: \"Título\",\n    subtitle: \"Subtítulo\",\n    content: \"Contenido\",\n    save: \"Guardar\",\n    button: \"Botón\",\n    card: \"Tarjeta\",\n    go_back: \"Volver\",\n    next: \"Siguiente\",\n    choose_image: \"Elegir imagen\",\n    soon: \"Pronto\",\n    coming_soon_with_emoji: \"Próximamente 🤫\",\n    no_image: \"Sin imagen\",\n    description: \"Descripción\",\n    price: \"Precio\",\n    duration: \"Duración\",\n    location: \"Ubicación\",\n    schedule: \"Horario\",\n    participants_info: \"Información de participantes\",\n    title_placeholder: \"Ingresa el título\",\n    description_placeholder: \"Ingresa la descripción\",\n    changes_saved: \"Cambios guardados\",\n    replace: \"Reemplazar\",\n    loading: \"Cargando...\",\n    image_deleted: \"La imagen ha sido eliminada\",\n    discover_workoutcool: \"Descubre gratis\",\n    received_just_now: \"Recibido ahora\",\n    copied: \"Copiado\",\n    url_copied: \"La URL ha sido copiada\",\n    copy_failed: \"Error al copiar la URL\",\n    accordion: \"Acordeón\",\n    image: \"Imagen\",\n    other: \"Otro\",\n    register: \"Registrarse\",\n    instantly: \"instantáneamente\",\n    immediately: \"inmediatamente\",\n    link: \"Enlace\",\n    accept: \"Aceptar\",\n    deny: \"Denegar\",\n    invalid_input: \"Entrada inválida. Por favor verifica los errores.\",\n    copy_url: \"Copiar URL\",\n    page_url: \"URL de la página\",\n    saving_short: \"Guardando...\",\n    saved_short: \"Guardado\",\n    looks_like_you_are_lost: \"Parece que estás perdido\",\n    the_page_you_are_looking_for_is_not_available: \"La página que buscas no está disponible\",\n    go_to_home: \"Ir al inicio\",\n    go_to_profile: \"Ir a mi perfil\",\n    terms: \"Términos de uso\",\n    privacy: \"Política de privacidad\",\n    sales_terms: \"Términos de venta\",\n    consent_banner: \"Usamos cookies para mejorar tu experiencia. Al hacer clic en Aceptar, aceptas nuestras cookies.\",\n    about: \"Acerca de\",\n    profile: \"Perfil\",\n    donate: \"Donar\",\n    my_account: \"Mi cuenta\",\n    dashboard: \"Panel\",\n    home: \"Inicio\",\n    changelog: \"Anuncios y notas de versión\",\n    stop_impersonation_button: \"Detener suplantación\",\n    impersonating_user_label: \"Suplantando usuario\",\n    re_hello: \"Hola de nuevo\",\n    back_to_login: \"Volver al inicio de sesión\",\n    sending: \"Enviando...\",\n    send_me_link: \"Enviarme enlace\",\n    extremely_dissatisfied: \"Muy insatisfecho\",\n    somewhat_dissatisfied: \"Insatisfecho\",\n    neutral: \"Neutral\",\n    satisfied: \"Satisfecho\",\n    support: \"Soporte\",\n    change_language: \"Cambiar idioma\",\n    subscription: \"Suscripción\",\n    manage_subscription: \"Gestionar suscripción\",\n    become_premium: \"Torne-se Premium\",\n    remove_ads: \"Eliminar anuncios\",\n    in_progress: \"En progreso\",\n    close: \"Cerrar\",\n    premium: \"Premium\",\n    free: \"Gratis\",\n    new: \"Nuevo\",\n    coming_soon: \"Próximamente\",\n    monday: \"Lunes\",\n    tuesday: \"Martes\",\n    wednesday: \"Miércoles\",\n    thursday: \"Jueves\",\n    friday: \"Viernes\",\n    saturday: \"Sábado\",\n    sunday: \"Domingo\",\n    added_to_favorites: \"Agregado a favoritos\",\n    add_to_favorites: \"Agregar a favoritos\",\n    remove_from_favorites: \"Eliminar de favoritos\",\n    favorites: \"Favoritos\",\n  },\n  tools: {\n    try_now: \"Probar ahora\",\n    title: \"Herramientas de Fitness\",\n    subtitle: \"Calculadoras esenciales para optimizar tu entrenamiento y nutrición\",\n    moreComingSoon: \"Más herramientas próximamente\",\n    meta: {\n      title: \"Herramientas de Fitness - Calculadoras para Entrenamiento y Nutrición\",\n      description:\n        \"Calculadoras de fitness gratuitas: TDEE, macros, IMC, zonas de frecuencia cardíaca, 1RM y más. Optimiza tu entrenamiento y nutrición con nuestras herramientas esenciales.\",\n      keywords:\n        \"calculadora fitness, calculadora calorías, calculadora macros, calculadora IMC, calculadora TDEE, zonas frecuencia cardíaca, repetición máxima, herramientas fitness\",\n    },\n    \"calorie-calculator\": {\n      title: \"Calculadora de Calorías\",\n      description: \"Calcula tus necesidades calóricas diarias (TDEE) basándote en tu nivel de actividad y objetivos\",\n      meta: {\n        title: \"Calculadora de Calorías - TDEE y Necesidades Calóricas Diarias\",\n        description:\n          \"Calcula tu Gasto Energético Total Diario (TDEE) y necesidades calóricas diarias. Obtén recomendaciones personalizadas para pérdida de peso, mantenimiento o ganancia muscular.\",\n        keywords:\n          \"calculadora calorías, calculadora TDEE, calorías diarias, calculadora pérdida peso, necesidades calóricas, calculadora TMB, calculadora metabolismo\",\n      },\n      subtitle: \"Calcula tus necesidades calóricas diarias basándote en la ecuación de Mifflin-St Jeor\",\n      how_it_works: \"¿Cómo funciona esta calculadora?\",\n      how_it_works_description:\n        \"Esta calculadora utiliza fórmulas científicamente probadas para estimar tus necesidades calóricas diarias basándose en tus características personales y estilo de vida.\",\n      how_it_works_step1: \"Calculamos tu metabolismo basal (calorías quemadas en reposo)\",\n      how_it_works_step2: \"Ajustamos según tu nivel de actividad\",\n      how_it_works_step3: \"Personalizamos según tu objetivo (perder, mantener o ganar peso)\",\n      calculate: \"Calcular\",\n      calculating: \"Calculando...\",\n      tap_info_icons: \"Toca los íconos ℹ️ para más información\",\n      gender: \"Género\",\n      male: \"Masculino\",\n      female: \"Femenino\",\n      units: \"Unidades\",\n      metric: \"Métrico\",\n      imperial: \"Imperial\",\n      age: \"Edad\",\n      age_placeholder: \"Ingresa tu edad\",\n      years: \"años\",\n      height: \"Altura\",\n      height_placeholder: \"Ingresa tu altura\",\n      weight: \"Peso\",\n      weight_placeholder: \"Ingresa tu peso\",\n      cm: \"cm\",\n      kg: \"kg\",\n      lbs: \"lbs\",\n      feet: \"pies\",\n      inches: \"pulgadas\",\n      activity_level: \"Nivel de Actividad\",\n      activity: {\n        sedentary: \"Sedentario\",\n        sedentary_desc: \"Poca o ninguna actividad física, trabajo en escritorio, caminatas mínimas\",\n        light: \"Ligeramente Activo\",\n        light_desc: \"Ejercicio ligero 1-3 veces por semana, o caminatas diarias\",\n        moderate: \"Moderadamente Activo\",\n        moderate_desc: \"Ejercicio moderado 3-5 veces por semana, estilo de vida activo\",\n        active: \"Muy Activo\",\n        active_desc: \"Ejercicio pesado 6-7 veces por semana, trabajo muy activo\",\n        very_active: \"Extremadamente Activo\",\n        very_active_desc: \"Atleta, trabajo físico + entrenamiento diario\",\n      },\n      goal: \"Objetivo\",\n      goals: {\n        lose_fast: \"Perder peso rápido\",\n        lose_fast_desc: \"Perder 2 lbs (1 kg) por semana - Agresivo pero efectivo\",\n        lose_slow: \"Perder peso\",\n        lose_slow_desc: \"Perder 1 lb (0.5 kg) por semana - Sostenible y saludable\",\n        maintain: \"Mantener peso\",\n        maintain_desc: \"Mantener el peso actual - Perfecto para mantener tu forma\",\n        gain_slow: \"Ganar peso\",\n        gain_slow_desc: \"Ganar 1 lb (0.5 kg) por semana - Construcción muscular limpia\",\n        gain_fast: \"Ganar peso rápido\",\n        gain_fast_desc: \"Ganar 2 lbs (1 kg) por semana - Máxima crecimiento muscular\",\n      },\n      results: {\n        overview: \"Resumen\",\n        title: \"Tus Resultados\",\n        bmr: \"TMB\",\n        bmr_explanation:\n          \"Tasa Metabólica Basal (TMB) es el número de calorías que tu cuerpo quema en reposo, solo para mantener funciones básicas como respirar, circulación y producción de células. Esta es la energía mínima que tu cuerpo necesita para sobrevivir.\",\n        tdee: \"GETD\",\n        tdee_explanation:\n          \"Gasto Energético Total Diario (GETD) es tu TMB más las calorías quemadas a través de actividades diarias y ejercicio. Este es el número total de calorías que quemas en un día basado en tu nivel de actividad.\",\n        target: \"Calorías Objetivo\",\n        macros: \"Macros Recomendados\",\n        macros_explanation:\n          \"Macronutrientes (macros) son los tres grupos principales de nutrientes que tu cuerpo necesita: Proteínas (para la construcción y reparación muscular), Carbohidratos (para la energía), y Grasas (para hormonas y absorción de vitaminas). Los porcentajes mostrados son una distribución equilibrada adecuada para la mayoría de los objetivos de fitness.\",\n        protein: \"Proteína\",\n        carbs: \"Carbohidratos\",\n        fat: \"Grasa\",\n        disclaimer:\n          \"These calculations are estimates based on average formulas. Actual caloric needs may vary based on individual factors. Consult with a healthcare professional or registered dietitian for personalized advice.\",\n      },\n      faq: {\n        title: \"Preguntas Frecuentes\",\n        q1: \"¿Por qué mi objetivo de calorías es diferente de otros calculadores?\",\n        a1: \"Diferentes calculadores pueden usar diferentes fórmulas o multiplicadores de actividad. Usamos la ecuación de Mifflin-St Jeor, que se considera una de las más precisas para la mayoría de las personas. Sin embargo, el metabolismo individual puede variar en un 10-20% de estas estimaciones.\",\n        q2: \"¿Debo comer exactamente esta cantidad de calorías todos los días?\",\n        a2: \"Estos son objetivos promedio. Es normal comer más o menos algunos días. Concéntrate en tu promedio semanal en lugar de ser exacto todos los días. Escucha a tus señales de hambre y saciedad.\",\n        q3: \"¿Qué pasa si no veo resultados después de seguir estas recomendaciones?\",\n        a3: \"Si no ves resultados después de 2-3 semanas, es posible que necesites ajustar. Tu metabolismo real puede ser más alto o más bajo que el calculado. Intenta ajustar entre 100-200 calorías y monitorea por otras 2 semanas. También asegúrate de estar rastreando tu alimentación con precisión.\",\n        q4: \"¿Son las recomendaciones de macros adecuadas para todos?\",\n        a4: \"La división 30/40/30 (proteína/carbohidratos/grasa) es un enfoque equilibrado adecuado para la mayoría de las personas. Sin embargo, atletas, personas con condiciones médicas o aquellos que siguen dietas específicas (keto, vegano, etc.) pueden necesitar diferentes proporciones. Consulta a un nutricionista para recomendaciones personalizadas.\",\n      },\n    },\n    \"macro-calculator\": {\n      title: \"Calculadora de Macros\",\n      description: \"Encuentra tu distribución óptima de proteínas, carbohidratos y grasas para tus objetivos de fitness\",\n    },\n    \"bmi-calculator\": {\n      title: \"Calculadora de IMC\",\n      description: \"Calcula tu Índice de Masa Corporal y comprende tu categoría de peso\",\n    },\n    \"heart-rate-calculator\": {\n      title: \"Zonas de Frecuencia Cardíaca\",\n      description: \"Descubre tus zonas de entrenamiento óptimas para quemar grasa y mejorar tu rendimiento\",\n    },\n    \"heart-rate-zones\": {\n      title: \"Calculadora de Zonas de Frecuencia Cardíaca\",\n      description: \"Calcula tus zonas de entrenamiento de frecuencia cardíaca óptimas para un rendimiento máximo y la quema de grasa\",\n      page_title: \"Calculadora de Zonas de Frecuencia Cardíaca\",\n      page_description:\n        \"Calcula tus zonas de entrenamiento de frecuencia cardíaca personalizadas usando fórmulas científicamente comprobadas. Optimiza tus entrenamientos cardio para quemar grasa, resistencia y rendimiento.\",\n      meta: {\n        title: \"Calculadora de Zonas de Frecuencia Cardíaca - Frecuencia Objetivo y Zonas de Entrenamiento\",\n        description:\n          \"Calcula tu frecuencia cardíaca máxima y tus zonas de entrenamiento personalizadas. Usa las fórmulas básicas o Karvonen para encontrar tus zonas de VO2 Máx, Anaeróbica, Aeróbica, Quema de Grasa y Calentamiento.\",\n        keywords:\n          \"calculadora zonas frecuencia cardíaca, frecuencia cardíaca objetivo, frecuencia cardíaca máxima, zonas entrenamiento, zona VO2 máx, zona anaeróbica, zona aeróbica, zona quema grasa, fórmula Karvonen, entrenamiento frecuencia cardíaca\",\n      },\n      calculate: \"Calcular Zonas\",\n      calculating: \"Calculando...\",\n      method: \"Método de Cálculo\",\n      method_info: \"Elige la fórmula que mejor se adapte a tu nivel de condición física y datos disponibles\",\n      methods: {\n        basic: \"Básico por Edad\",\n        basic_desc: \"Fórmula simple usando solo la edad - buena para principiantes\",\n        karvonen_age: \"Karvonen por Edad y FCR\",\n        karvonen_age_desc: \"Más preciso usando edad y frecuencia cardíaca en reposo\",\n        karvonen_custom: \"Karvonen por FCM y FCR\",\n        karvonen_custom_desc: \"El más preciso usando frecuencias cardíacas máxima y en reposo medidas\",\n      },\n      age: \"Edad\",\n      age_placeholder: \"Ingresa tu edad\",\n      resting_heart_rate: \"Frecuencia Cardíaca en Reposo (FCR)\",\n      resting_heart_rate_placeholder: \"Ingresa tu FCR\",\n      resting_heart_rate_info: \"Mide tu frecuencia cardíaca al despertar, antes de salir de la cama. El rango normal es 60-100 lpm.\",\n      max_heart_rate: \"Frecuencia Cardíaca Máxima (FCM)\",\n      max_heart_rate_placeholder: \"Ingresa tu FCM\",\n      max_heart_rate_info:\n        \"Tu frecuencia cardíaca máxima real de una prueba de esfuerzo o entrenamiento de esfuerzo máximo. Más preciso que las estimaciones basadas en edad.\",\n\n      results: {\n        title: \"Tus Zonas de Frecuencia Cardíaca\",\n        max_heart_rate: \"Frecuencia Cardíaca Máxima\",\n        heart_rate_reserve: \"Reserva de Frecuencia Cardíaca\",\n        target_zones: \"Zonas de Entrenamiento Objetivo\",\n        zone: \"Zona\",\n        intensity: \"Intensidad\",\n        heart_rate_range: \"Frecuencia Cardíaca (lpm)\",\n        benefits: \"Beneficios\",\n        duration: \"Duración Típica\",\n      },\n      zones: {\n        warm_up: {\n          name: \"Zona de Calentamiento\",\n          intensity: \"50-60%\",\n          benefits: \"🧘 Calentamiento perfecto\",\n          example: \"Caminata tranquila\",\n          duration: \"5-10 minutos\",\n          description: \"Intensidad muy ligera para calentamiento y recuperación\",\n        },\n        fat_burn: {\n          name: \"Zona de Quema de Grasa\",\n          intensity: \"60-70%\",\n          benefits: \"🔥 Quema grasa\",\n          example: \"Trote ligero\",\n          duration: \"20-40 minutos\",\n          description: \"Intensidad ligera, ritmo cómodo para entrenamientos más largos\",\n        },\n        aerobic: {\n          name: \"Zona Aeróbica\",\n          intensity: \"70-80%\",\n          benefits: \"💪 Mejora la resistencia\",\n          example: \"Carrera moderada\",\n          duration: \"10-40 minutos\",\n          description: \"Intensidad moderada, sostenible durante períodos prolongados\",\n        },\n        anaerobic: {\n          name: \"Zona Anaeróbica\",\n          intensity: \"80-90%\",\n          benefits: \"⚡ Aumenta la velocidad\",\n          example: \"Sprint corto\",\n          duration: \"2-10 minutos\",\n          description: \"Intensidad difícil, desafiante pero sostenible por períodos cortos\",\n        },\n        vo2_max: {\n          name: \"Zona VO2 Máx\",\n          intensity: \"90-100%\",\n          benefits: \"🏆 Rendimiento máx\",\n          example: \"Sprint intenso\",\n          duration: \"30 segundos - 2 minutos\",\n          description: \"Intensidad máxima, sostenible solo por períodos muy cortos\",\n        },\n      },\n      formulas: {\n        basic_formula: \"Fórmula Básica\",\n        basic_explanation: \"FCO = FCM × %Intensidad\",\n        karvonen_formula: \"Fórmula Karvonen\",\n        karvonen_explanation: \"FCO = [(FCM - FCR) × %Intensidad] + FCR\",\n        mhr_calculation: \"FCM = 220 - Edad\",\n      },\n      abbreviations: {\n        thr: \"FCO = Frecuencia Cardíaca Objetivo\",\n        mhr: \"FCM = Frecuencia Cardíaca Máxima\",\n        rhr: \"FCR = Frecuencia Cardíaca en Reposo\",\n        hrr: \"RFC = Reserva de Frecuencia Cardíaca\",\n        bpm: \"lpm = Latidos Por Minuto\",\n      },\n      tips: {\n        title: \"Consejos de Entrenamiento\",\n        tip1: \"Comienza con zonas de baja intensidad si eres principiante en el ejercicio\",\n        tip2: \"Mezcla diferentes zonas en tu entrenamiento semanal para mejores resultados\",\n        tip3: \"Usa un monitor de frecuencia cardíaca para un seguimiento preciso durante los entrenamientos\",\n        tip4: \"Tus zonas pueden cambiar a medida que mejora tu condición física - recalcula periódicamente\",\n      },\n      faq: {\n        title: \"Preguntas Frecuentes\",\n        q1: \"¿Qué método de cálculo debo usar?\",\n        a1: \"Si eres principiante, usa el método Básico. Si conoces tu frecuencia cardíaca en reposo, usa Karvonen por Edad para mayor precisión. Para las zonas más precisas, usa Karvonen con FCM y FCR medidas.\",\n        q2: \"¿Cómo medir mi frecuencia cardíaca en reposo?\",\n        a2: \"Mide tu pulso durante 60 segundos inmediatamente después de despertar, antes de salir de la cama. Hazlo durante 3-5 días y usa el promedio. La FCR normal es 60-100 lpm, valores más bajos indican mejor condición física.\",\n        q3: \"¿En qué zona debo entrenar para perder peso?\",\n        a3: \"La Zona de Quema de Grasa (60-70%) es óptima para quemar grasa como combustible. Sin embargo, las zonas de mayor intensidad queman más calorías totales. Mezcla las zonas para mejores resultados - incluye tanto entrenamientos de quema de grasa como de alta intensidad.\",\n        q4: \"¿Qué tan precisa es la fórmula 220-edad?\",\n        a4: \"Es una estimación general que funciona para la mayoría de las personas pero puede variar ±10-15 lpm. Para mayor precisión, considera una prueba supervisada de frecuencia cardíaca máxima o usa la fórmula Karvonen con tus mediciones reales.\",\n        q5: \"¿Puedo entrenar en la zona VO2 Máx todos los días?\",\n        a5: \"No, la zona VO2 Máx es extremadamente intensa y solo debe usarse 1-2 veces por semana para intervalos cortos. La mayor parte del entrenamiento debe estar en las zonas Aeróbica y Quema de Grasa para construir resistencia y permitir la recuperación.\",\n      },\n      guide: {\n        title: \"Guía Completa de Zonas de Frecuencia Cardíaca para el Entrenamiento\",\n        text1:\n          \"Las zonas de frecuencia cardíaca son una herramienta científica esencial para optimizar tus entrenamientos y alcanzar tus objetivos fitness. Ya sea que busques perder peso, mejorar tu resistencia o aumentar tu rendimiento, entender y usar las zonas cardíacas transformará tu enfoque del ejercicio.\",\n        text2:\n          \"Esta calculadora utiliza fórmulas validadas científicamente para determinar tus zonas personalizadas basadas en tu edad y, opcionalmente, tu frecuencia cardíaca en reposo. Cada zona corresponde a una intensidad específica y ofrece beneficios únicos para tu salud cardiovascular.\",\n      },\n      table: {\n        title: \"Tabla de Referencia de Frecuencias Cardíacas por Edad\",\n        col1: \"Edad\",\n        col2: \"FCM\",\n        col3: \"50% Intensidad\",\n        col4: \"85% Intensidad\",\n        avertiser: \"* Estos valores son promedios. Tu FCM real puede variar ±10-15 lpm.\",\n      },\n      details: {\n        title: \"Las 5 Zonas de Entrenamiento Explicadas en Detalle\",\n        benefits: \"Beneficios\",\n        zone1_title: \"Zona 1: Calentamiento (50-60% FCM)\",\n        zone1_content:\n          \"La zona de calentamiento es ideal para comenzar una sesión, recuperar entre intervalos o terminar un entrenamiento. A esta intensidad, puedes mantener una conversación normal sin quedarte sin aliento.\",\n        zone1_details_1: \"Mejora la circulación sanguínea\",\n        zone1_details_2: \"Prepara músculos y articulaciones\",\n        zone1_details_3: \"Reduce el riesgo de lesiones\",\n        zone1_details_4: \"Favorece la recuperación activa\",\n        zone1_duration: \"Duración recomendada\",\n        zone1_duration_value: \"5-10 minutos al inicio/final de la sesión\",\n        zone1_duration_value_2: \"20-30 minutos para recuperación activa\",\n        zone2_title: \"Zona 2: Quema de Grasa (60-70% FCM)\",\n        zone2_content:\n          \"En esta zona, tu cuerpo utiliza principalmente las grasas como fuente de energía. Es la intensidad óptima para desarrollar la resistencia base y mejorar la eficiencia metabólica.\",\n        zone2_details_1: \"Maximiza el uso de grasas\",\n        zone2_details_2: \"Desarrolla la resistencia aeróbica\",\n        zone2_details_3: \"Mejora la eficiencia cardíaca\",\n        zone2_details_4: \"Fortalece el sistema inmunológico\",\n        zone2_duration: \"Duración recomendada\",\n        zone2_duration_value: \"30-90 minutos para resistencia\",\n        zone2_duration_value_2: \"45-60 minutos para pérdida de peso\",\n        zone3_title: \"Zona 3: Aeróbica (70-80% FCM)\",\n        zone3_content:\n          \"La zona aeróbica mejora significativamente tu capacidad cardiovascular. Respiras más fuerte pero aún puedes pronunciar frases cortas. Es la zona de entrenamiento principal para la mayoría de los atletas.\",\n        zone3_details_1: \"Aumenta la capacidad pulmonar\",\n        zone3_details_2: \"Mejora la resistencia cardiovascular\",\n        zone3_details_3: \"Fortalece el corazón\",\n        zone3_details_4: \"Optimiza el uso del oxígeno\",\n        zone3_duration: \"Duración recomendada\",\n        zone3_duration_value: \"20-60 minutos continuos\",\n        zone3_duration_value_2: \"Intervalos de 5-15 minutos\",\n        zone4_title: \"Zona 4: Anaeróbica (80-90% FCM)\",\n        zone4_content:\n          \"En la zona anaeróbica, tu cuerpo produce ácido láctico más rápido de lo que puede eliminarlo. Esta intensidad desarrolla la potencia y velocidad pero no puede mantenerse mucho tiempo.\",\n        zone4_details_1: \"Aumenta la potencia muscular\",\n        zone4_details_2: \"Mejora la tolerancia al lactato\",\n        zone4_details_3: \"Desarrolla la velocidad\",\n        zone4_details_4: \"Fortalece la mente\",\n        zone4_duration: \"Duración recomendada\",\n        zone4_duration_value: \"Intervalos de 2-8 minutos\",\n        zone4_duration_value_2: \"Recuperación igual o doble\",\n        zone5_title: \"Zona 5: VO2 Máx (90-100% FCM)\",\n        zone5_content:\n          \"La zona VO2 Máx representa el esfuerzo máximo. A esta intensidad, solo puedes pronunciar pocas palabras y el esfuerzo es insostenible más allá de unos minutos. Reservada para atletas experimentados.\",\n        zone5_details_1: \"Maximiza la capacidad aeróbica\",\n        zone5_details_2: \"Mejora la economía de carrera\",\n        zone5_details_3: \"Desarrolla la potencia máxima\",\n        zone5_details_4: \"Empuja los límites mentales\",\n        zone5_duration: \"Duración recomendada\",\n        zone5_duration_value: \"Intervalos de 30s a 2 minutos\",\n        zone5_duration_value_2: \"Máximo 1-2 veces por semana\",\n      },\n      educational: {\n        title: \"Entendiendo el Entrenamiento por Frecuencia Cardíaca\",\n        description: \"Visualiza fácilmente cada zona de entrenamiento\",\n        what_are_zones: {\n          title: \"¿Qué Son las Zonas de Frecuencia Cardíaca?\",\n          content:\n            \"Las zonas de frecuencia cardíaca son rangos de latidos por minuto que corresponden a diferentes intensidades de ejercicio. Entrenar en zonas específicas te ayuda a alcanzar diferentes objetivos de condición física más efectivamente.\",\n        },\n        why_use_zones: {\n          title: \"¿Por Qué Usar las Zonas de Frecuencia Cardíaca?\",\n          content:\n            \"Entrenar con zonas de frecuencia cardíaca garantiza que ejercites a la intensidad correcta para tus objetivos. Previene el sobreentrenamiento, maximiza los resultados y te ayuda a entrenar más eficientemente.\",\n        },\n        zone_distribution: {\n          title: \"Distribución Semanal Recomendada de Zonas\",\n          content:\n            \"Para una condición física equilibrada: 80% en Zonas 1-3 (base aeróbica), 15% en Zona 4 (umbral), 5% en Zona 5 (VO2 máx). Ajusta según tus objetivos específicos y nivel de condición física.\",\n        },\n        monitoring: {\n          title: \"Cómo Monitorear Tu Frecuencia Cardíaca\",\n          content:\n            \"Usa una banda de pecho para mayor precisión, o un monitor de muñeca por comodidad. Verifica regularmente tu frecuencia cardíaca durante el ejercicio y ajusta la intensidad para mantenerte en tu zona objetivo.\",\n        },\n      },\n      training_tips: {\n        title: \"Consejos de Experto para Optimizar tu Entrenamiento\",\n        tip1: {\n          title: \"Calentamiento progresivo\",\n          description: \"Siempre comienza con 5-10 minutos en zona 1 (50-60%) para preparar tu sistema cardiovascular.\",\n        },\n        tip2: {\n          title: \"Regla del 80/20\",\n          description: \"80% de tu entrenamiento en zonas 1-3 (aeróbico), 20% en zonas 4-5 (anaeróbico) para desarrollo óptimo.\",\n        },\n        tip3: {\n          title: \"Recuperación activa\",\n          description: \"Después de un esfuerzo intenso, baja progresivamente a zona 1-2 durante 5-10 minutos.\",\n        },\n        tip4: {\n          title: \"Hidratación constante\",\n          description: \"Bebe antes, durante y después del ejercicio. La deshidratación aumenta la frecuencia cardíaca.\",\n        },\n        tip5: {\n          title: \"Sueño reparador\",\n          description: \"7-9 horas de sueño permiten mejor recuperación y una FCR más baja.\",\n        },\n        tip6: {\n          title: \"Progresión gradual\",\n          description: \"Aumenta la intensidad o duración máximo 10% por semana para evitar el sobreentrenamiento.\",\n        },\n      },\n      training_tips_2: {\n        title: \"Consejos prácticos\",\n        title1: \"Encuentra tu zona\",\n        description1: \"Cada zona tiene un objetivo diferente. ¡Elige según tu meta!\",\n        title2: \"Duración recomendada\",\n        description2: \"Mientras más alta la intensidad, más corta debe ser la duración.\",\n        title3: \"Progresión\",\n        description3: \"Comienza suavemente y aumenta progresivamente la intensidad.\",\n        title4: \"Escucha tu cuerpo\",\n        description4: \"Si te sientes mal, reduce la velocidad inmediatamente.\",\n      },\n      quick_facts: {\n        title: \"¿Sabías que?\",\n        fact1: \"220 - tu edad = Frecuencia cardíaca máxima aproximada\",\n        fact2: \"Mide tu pulso al despertar para conocer tu frecuencia en reposo\",\n        fact3: \"Un reloj inteligente puede seguir tu frecuencia en tiempo real\",\n        fact4: \"80% de tu entrenamiento debería estar en zonas 1-3\",\n      },\n      weekly_plan: {\n        title: \"Plan semanal tipo\",\n        description: \"Un ejemplo de semana de entrenamiento equilibrada\",\n        monday: {\n          title: \"Zona 1-2\",\n          description: \"30-45 min\",\n        },\n        tuesday: {\n          title: \"Zona 2-3\",\n          description: \"45-60 min\",\n        },\n        wednesday: {\n          title: \"Descanso\",\n          description: \"Recuperación\",\n        },\n        thursday: {\n          title: \"Zona 3-4\",\n          description: \"30-40 min\",\n        },\n        friday: {\n          title: \"Zona 1-2\",\n          description: \"30 min\",\n        },\n        saturday: {\n          title: \"Zona 4-5\",\n          description: \"20-30 min\",\n        },\n        tips: \"💡 ¡Adapta este plan según tu nivel y objetivos!\",\n        cta: \"⬆️ Calcular mis zonas ahora\",\n      },\n      seo_faq_title: \"Preguntas Frecuentes sobre las Zonas de Frecuencia Cardíaca\",\n      seo_faq_q1_question: \"¿Qué es la frecuencia cardíaca máxima (FCM)?\",\n      seo_faq_q1_answer:\n        \"La frecuencia cardíaca máxima es el número máximo de latidos por minuto que tu corazón puede alcanzar durante un esfuerzo físico intenso. Generalmente se calcula con la fórmula: 220 - tu edad. Sin embargo, esta fórmula puede variar ±10-15 lpm según los individuos.\",\n      seo_faq_q2_question: \"¿Cómo medir mi frecuencia cardíaca en reposo?\",\n      seo_faq_q2_answer:\n        \"Mide tu pulso al despertar, antes de salir de la cama. Cuenta los latidos durante 60 segundos o durante 15 segundos y multiplica por 4. Repite durante 3-5 días y usa el promedio. Una FCR normal está entre 60-100 lpm.\",\n      seo_faq_q3_question: \"¿Cuál zona es la mejor para perder peso?\",\n      seo_faq_q3_answer:\n        \"La zona de quema de grasa (60-70% FCM) es óptima para quemar grasa como combustible. Sin embargo, las zonas más intensas queman más calorías totales. Para una pérdida de peso efectiva, alterna entre diferentes zonas.\",\n      seo_faq_q4_question: \"¿Puedo entrenar en la zona VO2 Máx todos los días?\",\n      seo_faq_q4_answer:\n        \"No, la zona VO2 Máx (90-100% FCM) es extremadamente intensa y solo debe usarse 1-2 veces por semana para períodos cortos (30 segundos a 2 minutos). La mayoría de tu entrenamiento debe estar en las zonas aeróbicas.\",\n      seo_faq_q5_question: \"¿Es precisa la fórmula 220-edad?\",\n      seo_faq_q5_answer:\n        \"Es una estimación general que funciona para la mayoría de las personas pero puede variar ±10-15 lpm. Para mayor precisión, usa la fórmula de Karvonen con tu FCR o haz una prueba de esfuerzo supervisada.\",\n      seo_faq_q6_question: \"¿Cómo saber si estoy en la zona correcta?\",\n      seo_faq_q6_answer:\n        \"Usa un monitor de frecuencia cardíaca para una medición precisa. Sin dispositivo, usa la prueba del habla: Zona ligera = conversación fácil, Zona moderada = frases cortas, Zona intensa = palabras sueltas solamente.\",\n      seo_faq_q7_question: \"¿Las zonas cambian con la mejora de mi condición física?\",\n      seo_faq_q7_answer:\n        \"Sí, con el entrenamiento, tu frecuencia cardíaca en reposo disminuye y tu eficiencia cardíaca mejora. Recalcula tus zonas cada 2-3 meses para ajustar tu entrenamiento.\",\n      seo_faq_q8_question: \"¿Cuál es la diferencia entre las fórmulas Básica y Karvonen?\",\n      seo_faq_q8_answer:\n        \"La fórmula Básica usa solo la edad (FCO = FCM × %Intensidad). La fórmula Karvonen es más precisa porque toma en cuenta tu FCR: FCO = [(FCM - FCR) × %Intensidad] + FCR.\",\n      intern_links_title: \"¿Listo para Optimizar tus Entrenamientos?\",\n      intern_links_subtitle: \"Usa nuestra calculadora para descubrir tus zonas personalizadas y transforma tu fitness\",\n      intern_links_button: \"Calcular Mis Zonas Ahora\",\n      intern_links_bmi_title: \"Calculadora de IMC\",\n      intern_links_bmi_description: \"Evalúa tu índice de masa corporal\",\n      intern_links_calorie_title: \"Calculadora de Calorías\",\n      intern_links_calorie_description: \"Determina tus necesidades calóricas diarias\",\n      intern_links_macro_title: \"Calculadora de Macros\",\n      intern_links_macro_description: \"Optimiza tu distribución nutricional\",\n      cta: {\n        title: \"¿Listo para Optimizar tus Entrenamientos?\",\n        subtitle: \"Usa nuestra calculadora para descubrir tus zonas personalizadas y transforma tu fitness\",\n        button: \"Calcular Mis Zonas Ahora\",\n        bmi_title: \"Calculadora de IMC\",\n        bmi_description: \"Evalúa tu índice de masa corporal\",\n        calorie_title: \"Calculadora de Calorías\",\n        calorie_description: \"Determina tus necesidades calóricas diarias\",\n        macro_title: \"Calculadora de Macros\",\n        macro_description: \"Optimiza tu distribución nutricional\",\n      },\n      medical_warning_title: \"Advertencia Médica Importante\",\n      medical_warning_content:\n        \"Esta calculadora proporciona estimaciones basadas en fórmulas generales. Los resultados pueden variar según tu condición física, medicamentos y estado de salud. Siempre consulta a un profesional de salud antes de comenzar un nuevo programa de ejercicio, particularmente si tienes condiciones médicas preexistentes o si experimentas síntomas inusuales durante el ejercicio.\",\n    },\n    \"one-rep-max\": {\n      title: \"Calculadora de 1RM\",\n      description: \"Estima tu 1RM y planifica tus porcentajes de entrenamiento de fuerza\",\n    },\n    back_to_calculators: \"Volver a las calculadoras\",\n    body_fat_percentage: \"Porcentaje de Grasa Corporal\",\n    body_fat_info_title: \"¿Qué es el Porcentaje de Grasa Corporal?\",\n    body_fat_info_content:\n      \"El porcentaje de grasa corporal es esencial para las fórmulas de Katch-McArdle y Cunningham ya que calculan basándose en la masa corporal magra. Si no conoces tu % de grasa corporal exacto, utiliza guías visuales en línea o escaneos DEXA para mayor precisión.\",\n    \"calorie-calculator-hub\": {\n      title: \"Fórmulas de Calculadora de Calorías\",\n      subtitle: \"Elige la mejor fórmula para tus necesidades y obtén cálculos calóricos precisos\",\n      meta: {\n        title: \"Fórmulas de Calculadora de Calorías - Calculadoras TMB y TDEE\",\n        description:\n          \"Compara diferentes fórmulas de TMB: Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham y Oxford. Elige la mejor calculadora de calorías para tus necesidades.\",\n        keywords:\n          \"fórmulas TMB, comparación calculadoras calorías, Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham, Oxford, calculadora TDEE\",\n      },\n      which_formula: \"¿Qué Fórmula Debo Elegir?\",\n      formula_explanation:\n        \"Diferentes fórmulas funcionan mejor para diferentes personas. Aquí tienes una guía rápida para ayudarte a elegir:\",\n      recommendation_general: \"Mejor fórmula general, más precisa para la población general\",\n      recommendation_traditional: \"Fórmula clásica, ampliamente utilizada pero ligeramente menos precisa\",\n      recommendation_bodyfat: \"Más precisa si conoces tu porcentaje de grasa corporal\",\n      since: \"Desde\",\n      all_formulas: \"Todas las fórmulas\",\n      popularity: \"Popularidad\",\n      accuracy: \"Precisión\",\n      accuracy_high: \"Alta\",\n      accuracy_good: \"Buena\",\n      accuracy_medium: \"Media\",\n      best_for: \"Mejor para\",\n      best_for_general: \"Uso general\",\n      best_for_traditional: \"Tradicional\",\n      best_for_athletes: \"Atletas\",\n      best_for_bodybuilders: \"Culturistas\",\n      best_for_european: \"Población europea\",\n      best_for_comparison: \"Comparar todas\",\n      \"mifflin-st-jeor\": {\n        title: \"Mifflin-St Jeor (Recomendada)\",\n        description:\n          \"Fórmula más precisa para la población general, desarrollada en 1990. Actualmente el estándar dorado para cálculos de TMB.\",\n      },\n      \"harris-benedict\": {\n        title: \"Harris-Benedict (Clásica)\",\n        description:\n          \"Versión revisada de 1984 de la fórmula clásica. Ampliamente utilizada pero tiende a sobreestimar las calorías para algunas personas.\",\n      },\n      \"katch-mcardle\": {\n        title: \"Katch-McArdle (Atletas)\",\n        description:\n          \"Basada en masa corporal magra. Más precisa para personas que conocen su porcentaje de grasa corporal y son físicamente activas.\",\n      },\n      cunningham: {\n        title: \"Cunningham (Culturistas)\",\n        description: \"Diseñada para atletas muy magros y culturistas con baja grasa corporal. Utiliza cálculo de masa corporal magra.\",\n      },\n      oxford: {\n        title: \"Oxford (Europea)\",\n        description: \"Fórmula más reciente (2005) basada en poblaciones europeas. Toma en cuenta grupos de edad.\",\n      },\n      comparison: {\n        title: \"Comparar Todas las Fórmulas\",\n        description: \"Compara resultados de todas las fórmulas lado a lado para ver las diferencias y elegir lo que mejor te funcione.\",\n      },\n    },\n    \"mifflin-st-jeor\": {\n      title: \"Calculadora Mifflin-St Jeor\",\n      subtitle: \"El estándar dorado para el cálculo de TMB - más precisa para la población general\",\n      meta: {\n        title: \"Calculadora Mifflin-St Jeor - TMB y TDEE Más Precisos\",\n        description:\n          \"Calcula tu TMB y TDEE usando la ecuación de Mifflin-St Jeor - la fórmula más precisa para la población general. Obtén recomendaciones calóricas personalizadas.\",\n        keywords:\n          \"calculadora Mifflin-St Jeor, calculadora TMB, calculadora TDEE, calculadora calorías más precisa, calculadora metabolismo\",\n      },\n      how_it_works: \"Cómo Funciona la Fórmula Mifflin-St Jeor\",\n      how_it_works_description:\n        \"Desarrollada en 1990, esta fórmula se considera la más precisa para calcular la Tasa Metabólica Basal (TMB) en adultos sanos. Es más precisa que la ecuación de Harris-Benedict y es ampliamente recomendada por nutricionistas y profesionales del fitness.\",\n    },\n    \"harris-benedict\": {\n      title: \"Calculadora Harris-Benedict\",\n      subtitle: \"Fórmula TMB clásica - el enfoque tradicional para el cálculo de calorías\",\n      meta: {\n        title: \"Calculadora Harris-Benedict - Fórmula TMB y TDEE Clásica\",\n        description:\n          \"Calcula tu TMB y TDEE usando la ecuación revisada de Harris-Benedict (1984). La fórmula clásica que inició los cálculos calóricos modernos.\",\n        keywords: \"calculadora Harris-Benedict, calculadora TMB clásica, calculadora TDEE tradicional, fórmula Harris-Benedict revisada\",\n      },\n      how_it_works: \"Cómo Funciona la Fórmula Harris-Benedict\",\n      how_it_works_description:\n        \"Originalmente desarrollada en 1919 y revisada en 1984, la ecuación de Harris-Benedict fue una de las primeras fórmulas para calcular el TMB. Aunque ligeramente menos precisa que las fórmulas más nuevas, sigue siendo ampliamente utilizada y proporciona buenas estimaciones para la mayoría de las personas.\",\n    },\n    \"katch-mcardle\": {\n      title: \"Calculadora Katch-McArdle\",\n      subtitle: \"Cálculo preciso de TMB basado en masa corporal magra - ideal para atletas\",\n      meta: {\n        title: \"Calculadora Katch-McArdle - TMB y TDEE de Masa Corporal Magra\",\n        description:\n          \"Calcula tu TMB y TDEE usando la fórmula de Katch-McArdle basada en masa corporal magra. Más precisa para personas que conocen su porcentaje de grasa corporal.\",\n        keywords:\n          \"calculadora Katch-McArdle, TMB masa corporal magra, calculadora porcentaje grasa corporal, calculadora TMB atletas, TDEE preciso\",\n      },\n      how_it_works: \"Cómo Funciona la Fórmula Katch-McArdle\",\n      how_it_works_description:\n        \"Esta fórmula calcula el TMB basándose en la masa corporal magra en lugar del peso corporal total, haciéndola más precisa para personas que conocen su porcentaje de grasa corporal. Es particularmente útil para atletas e individuos físicamente activos.\",\n    },\n    cunningham: {\n      title: \"Calculadora Cunningham\",\n      subtitle: \"Fórmula TMB diseñada para atletas muy magros y culturistas\",\n      meta: {\n        title: \"Calculadora Cunningham - TMB para Atletas Magros y Culturistas\",\n        description:\n          \"Calcula tu TMB y TDEE usando la fórmula de Cunningham, específicamente diseñada para atletas muy magros y culturistas con baja grasa corporal.\",\n        keywords:\n          \"calculadora Cunningham, calculadora TMB culturistas, TMB atletas magros, calculadora baja grasa corporal, calculadora preparación competencia\",\n      },\n      how_it_works: \"Cómo Funciona la Fórmula Cunningham\",\n      how_it_works_description:\n        \"Desarrollada específicamente para individuos muy magros con porcentajes bajos de grasa corporal, esta fórmula proporciona estimaciones de TMB más altas que otras ecuaciones. Es más precisa para atletas competitivos y culturistas en preparación para competencias.\",\n    },\n    oxford: {\n      title: \"Calculadora Oxford\",\n      subtitle: \"Fórmula TMB moderna basada en poblaciones europeas con consideraciones de edad\",\n      meta: {\n        title: \"Calculadora Oxford - Fórmula TMB y TDEE Moderna\",\n        description:\n          \"Calcula tu TMB y TDEE usando la ecuación de Oxford (2005), una fórmula moderna basada en poblaciones europeas con cálculos específicos por edad.\",\n        keywords:\n          \"calculadora Oxford, calculadora TMB moderna, fórmula TMB europea, calculadora TMB específica por edad, ecuación TMB 2005\",\n      },\n      how_it_works: \"Cómo Funciona la Fórmula Oxford\",\n      how_it_works_description:\n        \"Publicada en 2005, esta es una de las fórmulas de TMB más recientes. Fue desarrollada usando datos de poblaciones europeas y toma en cuenta grupos de edad, proporcionando ecuaciones diferentes para personas menores y mayores de 30 años.\",\n    },\n    \"calorie-calculator-comparison\": {\n      title: \"Comparar todas las fórmulas BMR\",\n      subtitle: \"Ve cómo diferentes fórmulas BMR calculan tus necesidades calóricas lado a lado\",\n      meta: {\n        title: \"Comparación de fórmulas BMR - Comparar todos los calculadores de calorías\",\n        description:\n          \"Compara las fórmulas Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham y Oxford BMR lado a lado. Ve qué fórmula funciona mejor para ti.\",\n        keywords:\n          \"comparación fórmula BMR, comparación calculador calorías, Mifflin vs Harris-Benedict, mejor calculador BMR, comparar fórmulas calorías\",\n      },\n      how_it_works: \"Cómo funciona esta comparación\",\n      how_it_works_description:\n        \"Ingresa tus detalles una vez y ve cómo todas las principales fórmulas BMR calculan tus necesidades calóricas diarias. Esto te ayuda a entender las diferencias y elegir la fórmula más adecuada para tus objetivos.\",\n      input_details: \"Tus detalles\",\n      compare: \"Comparar\",\n      results_comparison: \"Resultados de la comparación de fórmulas\",\n      vs_mifflin: \"vs Mifflin-St Jeor\",\n      summary: \"Resumen y recomendaciones\",\n      summary_explanation:\n        \"Diferentes fórmulas pueden dar resultados variables. Generalmente, diferencias de ±100-200 calorías son normales y esperadas.\",\n      recommendation:\n        \"Para la mayoría de las personas, Mifflin-St Jeor proporciona la base más precisa. Los atletas deberían considerar Katch-McArdle si conocen su porcentaje de grasa corporal.\",\n    },\n    \"bmi-calculator-hub\": {\n      title: \"Herramientas Calculadora IMC\",\n      subtitle: \"Calcula tu Índice de Masa Corporal con diferentes métodos y obtén información de salud personalizada\",\n      meta: {\n        title: \"Calculadora IMC - Herramientas de Índice de Masa Corporal y Evaluación de Salud\",\n        description:\n          \"Calcula tu IMC con nuestras herramientas integrales. IMC estándar, ajustado para atletas, IMC pediátrico, y herramientas de comparación. Obtén información de salud y recomendaciones.\",\n        keywords: \"calculadora IMC, índice masa corporal, evaluación salud, estado peso, herramientas IMC, IMC pediátrico, IMC atleta\",\n      },\n      understanding_bmi: \"Entendiendo el IMC\",\n      bmi_explanation:\n        \"El IMC es una herramienta de detección que ayuda a evaluar si tienes un peso saludable para tu altura. Elige la calculadora adecuada para tus necesidades:\",\n      recommendation_standard: \"Mejor para la población general y detección inicial de salud\",\n      recommendation_adjusted: \"Más preciso para atletas e individuos musculosos\",\n      recommendation_pediatric: \"Especializado para niños y adolescentes con percentiles específicos por edad\",\n      popularity: \"Popularidad\",\n      accuracy: \"Precisión\",\n      accuracy_high: \"Alta\",\n      accuracy_good: \"Buena\",\n      accuracy_medium: \"Media\",\n      best_for: \"Mejor para\",\n      best_for_general: \"Uso general\",\n      best_for_athletes: \"Atletas\",\n      best_for_children: \"Niños\",\n      best_for_comparison: \"Comparar todo\",\n      category_standard: \"Estándar\",\n      category_advanced: \"Avanzado\",\n      category_specialized: \"Especializado\",\n      standard: {\n        title: \"Calculadora IMC Estándar\",\n        description: \"Cálculo IMC clásico usando la fórmula estándar de la OMS. Evaluación rápida y fácil para la población general.\",\n        page_title: \"Calculadora IMC Estándar\",\n        page_description:\n          \"Calcula tu Índice de Masa Corporal usando la fórmula estándar de la OMS. Obtén resultados instantáneos con categoría de salud y recomendaciones personalizadas.\",\n      },\n      adjusted: {\n        title: \"Calculadora IMC Ajustada\",\n        description:\n          \"Cálculo IMC mejorado que considera la masa muscular y composición corporal para resultados más precisos en individuos atléticos.\",\n      },\n      pediatric: {\n        title: \"Calculadora IMC Pediátrica\",\n        description:\n          \"Calculadora IMC especializada para niños y adolescentes usando percentiles específicos por edad y sexo y gráficos de crecimiento.\",\n      },\n      comparison: {\n        title: \"Herramienta de Comparación IMC\",\n        description: \"Compara diferentes métodos de cálculo IMC lado a lado para entender cómo varios factores afectan tus resultados.\",\n      },\n    },\n  },\n  \"bmi-calculator\": {\n    educational: {\n      introduction_title: \"Introducción al IMC\",\n      introduction_text:\n        \"El IMC es una medida de la delgadez o corpulencia de una persona basada en su altura y peso, y está destinado a cuantificar la masa tisular. Se utiliza ampliamente como indicador general de si una persona tiene un peso corporal saludable para su altura.\",\n      introduction_usage:\n        \"Específicamente, el valor obtenido del cálculo del IMC se utiliza para categorizar si una persona tiene bajo peso, peso normal, sobrepeso u obesidad dependiendo del rango en el que caiga el valor. Estos rangos de IMC varían según factores como la región y la edad, y a veces se subdividen en subcategorías como bajo peso severo u obesidad muy severa.\",\n\n      adult_table_title: \"Tabla de IMC para Adultos\",\n      adult_table_description:\n        \"Esta es la recomendación de peso corporal de la Organización Mundial de la Salud (OMS) basada en valores de IMC para adultos. Se utiliza tanto para hombres como mujeres, de 20 años o más.\",\n\n      children_table_title: \"Tabla de IMC para Niños y Adolescentes, Edad 2-20\",\n      children_table_description:\n        \"Los Centros para el Control y la Prevención de Enfermedades (CDC) recomiendan la categorización del IMC para niños y adolescentes entre 2 y 20 años.\",\n\n      classification: \"Clasificación\",\n      bmi_range: \"Rango de IMC - kg/m²\",\n      category: \"Categoría\",\n      percentile_range: \"Rango de Percentil\",\n      underweight: \"Bajo peso\",\n      healthy_weight: \"Peso Saludable\",\n      at_risk_overweight: \"En Riesgo de Sobrepeso\",\n      overweight: \"Sobrepeso\",\n\n      overweight_risks_title: \"Riesgos Asociados con el Sobrepeso\",\n      overweight_risks_intro:\n        \"El sobrepeso aumenta el riesgo de varias enfermedades graves y condiciones de salud. A continuación se presenta una lista de dichos riesgos, según los Centros para el Control y la Prevención de Enfermedades (CDC):\",\n\n      cardiovascular_risks: \"Riesgos Cardiovasculares\",\n      high_blood_pressure: \"Presión arterial alta\",\n      cholesterol_issues: \"Niveles más altos de colesterol LDL, niveles más bajos de colesterol HDL y niveles altos de triglicéridos\",\n      coronary_heart_disease: \"Enfermedad coronaria\",\n      stroke: \"Accidente cerebrovascular\",\n\n      metabolic_risks: \"Riesgos Metabólicos\",\n      type_2_diabetes: \"Diabetes tipo II\",\n      gallbladder_disease: \"Enfermedad de la vesícula biliar\",\n      sleep_apnea: \"Apnea del sueño y problemas respiratorios\",\n      osteoarthritis: \"Osteoartritis, un tipo de enfermedad articular causada por la degradación del cartílago articular\",\n\n      other_risks: \"Otros Riesgos de Salud\",\n      certain_cancers: \"Ciertos cánceres (endometrial, mama, colon, riñón, vesícula biliar, hígado)\",\n      mental_health_issues: \"Enfermedades mentales como depresión clínica, ansiedad y otras\",\n      reduced_quality_life: \"Baja calidad de vida y dolores corporales\",\n      increased_mortality: \"En general, un mayor riesgo de mortalidad comparado con aquellos con un IMC saludable\",\n\n      underweight_risks_title: \"Riesgos Asociados con el Bajo Peso\",\n      underweight_risks_intro: \"El bajo peso tiene sus propios riesgos asociados, listados a continuación:\",\n      malnutrition: \"Desnutrición, deficiencias vitamínicas, anemia (capacidad reducida para transportar oxígeno en la sangre)\",\n      osteoporosis: \"Osteoporosis, una enfermedad que causa debilidad ósea, aumentando el riesgo de fractura de huesos\",\n      immune_function_decrease: \"Una disminución en la función inmune\",\n      growth_development_issues: \"Problemas de crecimiento y desarrollo, particularmente en niños y adolescentes\",\n      reproductive_issues: \"Posibles problemas reproductivos para mujeres debido a desequilibrios hormonales\",\n      surgery_complications: \"Complicaciones potenciales como resultado de cirugía\",\n      increased_mortality_underweight: \"En general, un mayor riesgo de mortalidad comparado con aquellos con un IMC saludable\",\n\n      adults_limitations: \"En Adultos\",\n      older_adults_fat: \"Los adultos mayores tienden a tener más grasa corporal que los adultos más jóvenes con el mismo IMC\",\n      women_fat_difference: \"Las mujeres tienden a tener más grasa corporal que los hombres para un IMC equivalente\",\n      athletes_muscle_mass: \"Individuos musculosos y atletas altamente entrenados pueden tener IMCs más altos debido a gran masa muscular\",\n\n      children_limitations: \"En Niños y Adolescentes\",\n      height_maturation_influence: \"La altura y el nivel de maduración sexual pueden influir en el IMC y la grasa corporal entre los niños\",\n      fat_free_mass_difference: \"El IMC podría ser resultado de niveles aumentados de grasa o masa libre de grasa\",\n      population_accuracy: \"El IMC es bastante indicativo de la grasa corporal para el 90-95% de la población\",\n\n      formulas_title: \"Fórmula del IMC\",\n      metric_formula: \"Fórmula Métrica\",\n      imperial_formula: \"Fórmula Imperial\",\n      example: \"Ejemplo\",\n\n      bmi_prime_formula: \"Fórmula del IMC Prime\",\n      bmi_prime_description: \"Relación de tu IMC con el límite superior del IMC normal (25)\",\n\n      ponderal_index_title: \"Índice Ponderal\",\n      ponderal_index_explanation:\n        \"El Índice Ponderal (IP) es similar al IMC en que mide la delgadez o corpulencia de una persona basada en su altura y peso. La principal diferencia entre el IP y el IMC es el cubo en lugar del cuadrado de la altura en la fórmula. Mientras que el IMC puede ser una herramienta útil al considerar grandes poblaciones, no es confiable para determinar la delgadez o corpulencia en individuos.\",\n      ponderal_index_metric_description: \"Índice Ponderal usando unidades métricas\",\n      ponderal_index_imperial_description: \"Índice Ponderal usando unidades imperiales\",\n\n      medical_disclaimer_title: \"Descargo de Responsabilidad Médica\",\n    },\n    height: \"Altura\",\n    weight: \"Peso\",\n    feet: \"pies\",\n    inches: \"pulg\",\n    cm: \"cm\",\n    kg: \"kg\",\n    lbs: \"lbs\",\n    height_placeholder: \"Ingresa la altura\",\n    weight_placeholder: \"Ingresa el peso\",\n    calculate: \"Calcular IMC\",\n    your_bmi: \"Tu IMC\",\n    bmi_prime: \"IMC Prime\",\n    ponderal_index: \"Índice Ponderal\",\n    bmi_category: \"Categoría IMC\",\n    health_risk: \"Riesgo de Salud\",\n    recommendations_label: \"Recomendaciones\",\n    units: \"Unidades\",\n    metric: \"Métrico (kg/cm)\",\n    imperial: \"Imperial (lbs/pies)\",\n\n    // Detailed BMI Categories (WHO)\n    category_severe_thinness: \"Delgadez Severa\",\n    category_moderate_thinness: \"Delgadez Moderada\",\n    category_mild_thinness: \"Delgadez Leve\",\n    category_normal: \"Peso Normal\",\n    category_overweight: \"Sobrepeso\",\n    category_obese_class_1: \"Obesidad Clase I\",\n    category_obese_class_2: \"Obesidad Clase II\",\n    category_obese_class_3: \"Obesidad Clase III\",\n\n    // Health Risks\n    risk_low: \"Bajo\",\n    risk_normal: \"Normal\",\n    risk_increased: \"Aumentado\",\n    risk_high: \"Alto\",\n    risk_very_high: \"Muy Alto\",\n    risk_extremely_high: \"Extremadamente Alto\",\n\n    // Additional Information\n    bmi_range: \"Rango IMC\",\n    ideal_weight: \"Rango de Peso Ideal\",\n    weight_to_lose: \"Peso a Perder\",\n    weight_to_gain: \"Peso a Ganar\",\n    normal_range: \"Rango IMC normal: 18,5 - 24,9\",\n\n    // BMI Prime\n    about_bmi_prime: \"Acerca del IMC Prime\",\n    bmi_prime_explanation:\n      \"El IMC Prime es la relación entre tu IMC y el límite superior del IMC normal (25). Un valor de 1,0 significa que estás en el límite superior del peso normal.\",\n    underweight: \"Bajo peso\",\n    normal: \"Normal\",\n    overweight: \"Sobrepeso\",\n    obese: \"Obeso\",\n\n    // Limitations\n    limitations_title: \"Limitaciones del IMC\",\n    limitations_text:\n      \"El IMC no distingue entre masa muscular y masa grasa. Los atletas y personas muy musculosas pueden tener un IMC alto a pesar de estar saludables. La edad, sexo, etnia y composición corporal también afectan la interpretación.\",\n\n    disclaimer:\n      \"El IMC es una herramienta de detección y puede no reflejar la composición corporal. Consulta profesionales de la salud para consejos personalizados.\",\n\n    // Recommendations\n    recommendations: {\n      severe_thinness: {\n        medical_consultation: \"Consulta médica inmediata fuertemente recomendada\",\n        nutritional_assessment: \"Evaluación nutricional integral necesaria\",\n        weight_gain_program: \"Puede requerir programa supervisado de aumento de peso\",\n        screen_conditions: \"Detectar condiciones médicas subyacentes\",\n        psychological_evaluation: \"Considerar evaluación psicológica si se sospecha trastorno alimentario\",\n      },\n      moderate_thinness: {\n        healthcare_provider: \"Consultar con profesional de la salud para evaluación\",\n        nutrient_dense_foods: \"Enfocarse en alimentos densos en nutrientes y calorías\",\n        registered_dietitian: \"Considerar trabajar con un dietista registrado\",\n        monitor_malnutrition: \"Monitorear signos de desnutrición\",\n        gradual_weight_gain: \"Aumento de peso gradual y saludable recomendado\",\n      },\n      mild_thinness: {\n        consider_healthcare: \"Considerar consultar con un profesional de la salud\",\n        nutrient_dense_foods: \"Enfocarse en alimentos densos en nutrientes para ganar peso saludablemente\",\n        strength_training: \"Incluir entrenamiento de fuerza para desarrollar masa muscular\",\n        monitor_health: \"Monitorear tu salud regularmente\",\n        gradual_weight_gain: \"Apuntar a un aumento de peso gradual (0.5-1 kg por semana)\",\n      },\n      normal: {\n        maintain_weight: \"Mantener tu peso saludable actual\",\n        physical_activity: \"Continuar actividad física regular (150+ minutos por semana)\",\n        balanced_diet: \"Seguir una dieta equilibrada y nutritiva\",\n        health_checkups: \"Chequeos de salud regulares\",\n        overall_wellness: \"Enfocarse en el bienestar general y composición corporal\",\n      },\n      overweight: {\n        gradual_weight_loss: \"Apuntar a pérdida de peso gradual (0.5-1 kg por semana)\",\n        increase_activity: \"Aumentar actividad física a 150+ minutos por semana\",\n        portion_control: \"Enfocarse en control de porciones y nutrición equilibrada\",\n        healthcare_provider: \"Considerar consultar con un profesional de la salud\",\n        lifestyle_goals: \"Establecer metas de estilo de vida realistas y sostenibles\",\n      },\n      obese_class_1: {\n        healthcare_provider: \"Consultar con profesional de la salud para plan de manejo de peso\",\n        weight_loss_target: \"Apuntar a pérdida de peso del 5-10% inicialmente\",\n        diet_exercise: \"Combinar intervenciones de dieta y ejercicio\",\n        nutritional_counseling: \"Considerar asesoramiento nutricional profesional\",\n        screen_conditions: \"Detectar condiciones de salud relacionadas con el peso\",\n      },\n      obese_class_2: {\n        medical_supervision: \"Buscar supervisión médica para manejo de peso\",\n        lifestyle_programs: \"Considerar programas integrales de intervención de estilo de vida\",\n        evaluate_conditions: \"Evaluar condiciones de salud relacionadas con el peso\",\n        medical_treatments: \"Puede beneficiarse de tratamientos médicos para pérdida de peso\",\n        bariatric_surgery: \"Considerar evaluación de cirugía bariátrica si es apropiado\",\n      },\n      obese_class_3: {\n        medical_consultation: \"Consulta médica inmediata recomendada\",\n        bariatric_surgery: \"Considerar evaluación de cirugía bariátrica\",\n        weight_management: \"Programa médico integral de manejo de peso\",\n        health_complications: \"Abordar complicaciones de salud relacionadas con el peso\",\n        multidisciplinary: \"Enfoque multidisciplinario con equipo médico\",\n      },\n    },\n  },\n  statistics: {\n    title: \"Estadísticas\",\n    page_subtitle: \"Sigue tu viaje fitness con análisis avanzados y perspectivas personalizadas.\",\n    select_exercise: \"Seleccionar Ejercicio\",\n    active_daily_users: \"Usuarios Activos Diarios\",\n    success_rate: \"Tasa de Éxito\",\n    user_rating: \"Calificación de Usuario\",\n\n    // Tabs\n    tabs: {\n      video: \"Video\",\n      statistics: \"Estadísticas\",\n    },\n\n    // Chart titles and labels\n    weight: \"Peso\",\n    volume: \"Volumen\",\n    weight_progression: \"Progresión de Peso\",\n    weight_progression_chart: \"Gráfico de progresión de peso\",\n    weekly_volume: \"Volumen Semanal\",\n    volume_chart: \"Gráfico de volumen\",\n    estimated_1rm: \"1 Rep Máx Estimado (1RM)\",\n    one_rep_max_chart: \"Gráfico de repetición máxima\",\n    performance_over_time: \"Rendimiento a lo Largo del Tiempo\",\n\n    // Form and controls\n    timeframe: \"Período de Tiempo\",\n    timeframe_selector: \"Selector de período de tiempo\",\n\n    // Timeframes\n    timeframes: {\n      \"4weeks\": \"4 Semanas\",\n      \"8weeks\": \"8 Semanas\",\n      \"12weeks\": \"12 Semanas\",\n      \"1year\": \"1 Año\",\n    },\n\n    // Error messages\n    error_loading_data: \"Error al cargar datos\",\n    error_loading_weight_progression: \"Error al cargar la progresión de peso\",\n    error_loading_1rm: \"Error al cargar datos de 1RM\",\n    error_loading_volume: \"Error al cargar datos de volumen\",\n\n    // Empty states\n    no_data_yet: \"Sin datos aún\",\n    start_tracking: \"Comienza a registrar para ver tu progreso\",\n    no_1rm_data: \"Sin datos de 1RM disponibles\",\n    complete_sets_with_weight: \"Completa series con peso para ver tu 1 Rep Máx (1RM)\",\n    no_volume_data: \"Sin datos de volumen disponibles\",\n    complete_workouts: \"Completa entrenamientos para ver tu volumen\",\n\n    // Info and tooltips\n    \"1rm_formula_info\": \"Información de fórmula 1RM\",\n    volume_calculation: \"Volumen = Peso × Reps × Series\",\n    last_updated: \"Última actualización: {date}\",\n\n    // Premium\n    premium_required: \"Se requiere Premium para acceder a las estadísticas\",\n\n    // StatisticsPreviewOverlay\n    premium_statistics: \"Estadísticas Premium\",\n    premium_statistics_description: \"Obtén información detallada sobre tu viaje fitness con análisis avanzados para cada ejercicio.\",\n    total_volume: \"Volumen Total\",\n    pr_increase: \"Aumento de PR\",\n    weight_progress: \"Progreso de Peso\",\n    upgrade_now: \"Actualizar Ahora\",\n    rating: \"Calificación 4.8/5\",\n    no_ads: \"Sin anuncios\",\n    cancel_anytime: \"Cancelar en cualquier momento\",\n    preview_notice: \"¡Esto es solo una vista previa! 👀\",\n    preview_description: \"Desbloquea el acceso completo a análisis detallados, seguimiento de progreso e información personalizada.\",\n    get_premium_access: \"Obtener Acceso Premium\",\n\n    // ExercisesBrowser\n    all_equipment: \"Todo el Equipo\",\n    all_muscles: \"Todos los Músculos\",\n    search_exercises: \"Buscar Ejercicios\",\n    error_loading_exercises: \"Error al cargar ejercicios\",\n    no_exercises_found: \"No se encontraron ejercicios\",\n    equipment_label: \"Equipo:\",\n    primary_muscle_label: \"Músculo Principal:\",\n    unknown: \"Desconocido\",\n    no_image_available: \"No hay imagen disponible\",\n  },\n  heatmap: {\n    week_days_short: {\n      sunday: \"D\", // domingo\n      monday: \"L\", // lunes\n      tuesday: \"M\", // martes\n      wednesday: \"M\", // miércoles\n      thursday: \"J\", // jueves\n      friday: \"V\", // viernes\n      saturday: \"S\", // sábado\n    },\n    month_names_short: {\n      january: \"Ene\", // enero\n      february: \"Feb\", // febrero\n      march: \"Mar\", // marzo\n      april: \"Abr\", // abril\n      may: \"May\", // mayo (same)\n      june: \"Jun\", // junio\n      july: \"Jul\", // julio\n      august: \"Ago\", // agosto\n      september: \"Sep\", // septiembre\n      october: \"Oct\", // octubre\n      november: \"Nov\", // noviembre\n      december: \"Dic\", // diciembre\n    },\n    \"workout#one\": \"entrenamiento\",\n    \"workout#other\": \"entrenamientos\",\n  },\n} as const;\n"
  },
  {
    "path": "locales/fr.ts",
    "content": "export default {\n  leaderboard: {\n    title: \"Classification\",\n    description: \"Champions des entraînements\",\n    champion_badge: \"🏆 Champion\",\n    runner_up_badge: \"🥈 Finaliste\",\n    third_place_badge: \"🥉 Troisième place\",\n    second_place: \"2ème place\",\n    third_place: \"3ème place\",\n    workouts: \"séances\",\n    unable_to_load: \"Impossible de charger le classement\",\n    try_again_later: \"Veuillez réessayer plus tard\",\n    no_champions_yet: \"Pas encore de champions\",\n    complete_first_workout: \"Complétez votre premier entraînement pour revendiquer le trône !\",\n    member_since: \"Membre depuis\",\n    workouts_per_week: \"entraînements/semaine\",\n    last_workout: \"Dernier entraînement\",\n    page_title: \"Classement des Champions\",\n    page_subtitle: \"Grimpez au sommet et devenez une légende Workout.cool\",\n    period_all_time: \"Global\",\n    period_monthly: \"Mois\",\n    period_weekly: \"Semaine\",\n    no_sessions_this_week: \"Aucune séance cette semaine\",\n    no_sessions_this_month: \"Aucune séance ce mois\",\n    registered_members_only: \"Membres inscrits uniquement\",\n    registered_members_description: \"Créez un compte pour apparaître dans le classement et apparaître\",\n    reset_timezone: \"Réinitialisation Europe/Paris\",\n    reset_timezone_description: \"Les classements hebdo et mensuel se réinitialisent à minuit, heure de Paris\",\n  },\n  programs: {\n    available_programs: \"Programmes disponibles\",\n    exercises_in_session: \"Exercices dans la séance\",\n    start_session: \"Démarrer la séance\",\n    starting_session: \"Démarrage...\",\n    more_than: \"+ de\",\n    my_progress: \"Mon progrès\",\n    session: \"séance\",\n    completed_feminine: \"complétées\",\n    completed_sets: \"séances complétées\",\n    \"set#zero\": \"série\",\n    \"set#one\": \"série\",\n    \"set#other\": \"séries\",\n    error_starting_session: \"Erreur lors du démarrage de la séance\",\n    premium_session: \"Séance Premium\",\n    premium_session_description:\n      \"Cette séance fait partie du contenu premium. Vous pouvez voir les détails mais pas effectuer l'entraînement.\",\n    premium_session_exercises: \"Exercices inclus\",\n    workout_description: \"Description de la séance\",\n    connect_to_access: \"Connectez-vous pour accéder\",\n    become_premium: \"Devenir Premium\",\n    back_to_program: \"Retour au programme\",\n    no_equipment: \"Aucun équipement\",\n    workout_programs_title: \"Programmes d'entraînement\",\n    workout_programs: \"Programmes d'entraînement\",\n    workout_programs_description: \"Choisissez votre défi et devenez plus fort ! 💪\",\n    no_programs_available: \"Aucun programme disponible\",\n    no_programs_available_description: \"Les programmes seront bientôt disponibles !\",\n    completed: \"Terminé\",\n    about: \"Présentation\",\n    program: \"Programme\",\n    not_found: \"Programme non trouvé\",\n    characteristics: \"Caractéristiques\",\n    weeks: \"semaines\",\n    sessions_per_week: \"séances/semaine\",\n    session_duration: \"min/séance\",\n    \"your_coach#zero\": \"Ton coach cool\",\n    \"your_coach#one\": \"Ton coach cool\",\n    \"your_coach#other\": \"Tes coachs cool\",\n    community: \"Communauté active\",\n    community_count: \"coolbuilders ont rejoint\",\n    week_short: \"Sem.\",\n    week: \"Semaine\",\n    exercises: \"exercices\",\n    min_short: \"min\",\n    premium: \"Premium\",\n    free: \"Gratuit\",\n    join_cta: \"Rejoindre le défi\",\n    continue: \"Continuer\",\n    sessions: \"Les séances\",\n    auth_required: \"Connexion Requise\",\n    auth_required_description: \"Vous devez vous connecter pour accéder à cette séance d'entraînement.\",\n    login_to_continue: \"Se connecter pour continuer\",\n    signup_to_continue: \"S'inscrire pour continuer\",\n    premium_required: \"Accès premium\",\n    premium_required_description:\n      \"Cette séance est réservée aux membres premium. Soutenez le projet et passez à premium pour accéder à tout le contenu premium.\",\n    upgrade_to_premium: \"Passer à Premium\",\n    program_completed: \"Programme Terminé\",\n    check_out_program: \"Découvre ce programme d'entraînement !\",\n    share_success: \"Partagé avec succès !\",\n    copied_to_clipboard: \"Lien copié !\",\n    share_failed: \"Échec du partage\",\n    important_info: \"Informations importantes\",\n    donation_teaser:\n      \"Au début, nous fonctionnions grâce aux dons. Mais comme tu peux l'imaginer, les dons n'étaient pas suffisants pour couvrir les coûts de développement et de fonctionnement. Nous avons donc créé un package qui nous aidera à garder les lumières allumées et à toi, de débloquer quelques super-pouvoirs en cours de route. :)\",\n    new: \"NOUVEAU\",\n    more_programs_coming_title: \"Encore plus de programmes en préparation !\",\n    more_programs_coming_description:\n      \"On bosse dur pour créer de nouveaux programmes. En passant premium maintenant, tu les auras tous automatiquement. Merci pour ton soutien. 🚀\",\n    coming_strength: \"Force & Muscle\",\n    coming_cardio: \"Cardio HIIT\",\n    coming_yoga: \"Yoga & Mobilité\",\n    sessions_coming_soon: \"Séances bientôt disponibles !\",\n    sessions_in_creation: \"Notre équipe est en train de créer des séances de qualité pour cette semaine. Reviens très bientôt ! 🚀\",\n    welcome_modal: {\n      welcome_title: \"Bienvenue dans {programTitle} !\",\n      subtitle: \"Prépare-toi à repousser tes limites ! 💪\",\n      level_label: \"Niveau\",\n      duration_label: \"Durée\",\n      frequency_label: \"Fréquence\",\n      later_button: \"Plus tard\",\n      start_button: \"C'est parti !\",\n    },\n  },\n  premium: {\n    checkout_error: \"Erreur lors de la commande\",\n    premium_required_title: \"Premium Requis\",\n    premium_required_subtitle: \"Ceci est un accès premium. Passez Premium pour accéder à tout le contenu premium.\",\n    premium_required_button: \"Passer Premium\",\n    already_premium: \"Vous profitez de Workout.cool Premium\",\n    no_ads: \"Sans publicités\",\n    upgrade: \"Passer Premium\",\n\n    // Checkout\n    checkout: {\n      processing: \"Traitement...\",\n    },\n\n    // Pricing\n    pricing: {\n      month: \"mois\",\n      year: \"année\",\n      monthly: \"Mensuel\",\n      yearly: \"Annuel\",\n      discount: \"-48%\",\n    },\n\n    // Hero Section\n    hero: {\n      badge: \"Open-Source & Auto-hébergement TOUJOURS gratuits\",\n      title: \"Entraînez-vous librement, soutenez la mission\",\n      subtitle: \"Pour ceux qui croient au projet et qui veulent (re)-croire en eux avec des power boosters !\",\n      stats: {\n        athletes: {\n          count: \"12.4K+\",\n          label: \"Athlètes actifs\",\n        },\n        series: {\n          count: \"1.2M+\",\n          label: \"Séries enregistrées\",\n        },\n        rating: {\n          count: \"4.9/5\",\n          label: \"Note de la communauté\",\n        },\n        progression: {\n          count: \"+23%\",\n          label: \"Progression moyenne\",\n        },\n      },\n    },\n\n    // Mission Banner\n    mission: {\n      supporters_count: \"234\",\n      supporters_text: \"supporters aident la mission\",\n      limited: \"Limité\",\n      early_access: \"places d'accès anticipé\",\n    },\n\n    // Plans\n    plans: {\n      monthly: \"Mensuel\",\n      yearly: \"Annuel\",\n      yearly_discount: \"-48%\",\n      per_month: \"/mois\",\n      per_year: \"/an\",\n\n      free: {\n        name: \"GRATUIT\",\n        price: \"0€\",\n        period: \"/pour toujours\",\n        price_label: \"0€/pour toujours\",\n        badge: \"Open-Source • Toujours Gratuit\",\n        description: \"Toutes les fonctions essentielles pour s'entraîner\",\n        features: [\n          \"Génération d'exercices avec vidéos\",\n          \"Historique type GitHub de tes séances (6 mois)\",\n          \"Partage et reprise de séances (bientôt)\",\n          \"Auto-hébergement possible\",\n          \"Code source disponible\",\n        ],\n        button: \"Votre plan actuel\",\n        footer_note: \"Aucune inscription requise • Accès complet pour toujours\",\n      },\n\n      premium: {\n        name: \"PREMIUM ⭐\",\n        price_label: \"7,90€/mois ou 49€/an\",\n        badge: \"PLUS POPULAIRE • Pour les passionnés\",\n        description: \"Toutes les fonctions + accès anticipé\",\n        footer_monthly: \"Rejoignez la communauté passionnée ! 🔥\",\n        footer_yearly: \"Merci pour le soutien annuel ! 🙏\",\n        yearly_price_note: \"/mois\",\n        features: [\n          \"...tout du plan Gratuit\",\n          \"Pas de publicités\",\n          \"Historique illimité (vs 6 mois gratuit)\",\n          \"Suivi des progrès avec statistiques avancées (volume, progression, PR)\",\n          \"Programmes d'entraînement pré-conçus\",\n          \"Chat privé avec un coach 1:1\",\n          \"Accès anticipé aux nouvelles features\",\n        ],\n      },\n    },\n\n    // Buttons and Actions\n    actions: {\n      processing: \"Traitement...\",\n      go_premium: \"Passer Premium\",\n      sign_in_continue: \"Passer Premium\",\n      upgrade_now: \"Mettre à niveau maintenant\",\n      current_plan: \"Votre plan actuel\",\n    },\n\n    // Trust Elements\n    trust: {\n      gdpr_compliant: \"100% conforme RGPD\",\n      money_back: \"Garantie satisfait ou remboursé 30 jours\",\n      cancel_anytime: \"1 clic pour annuler, aucun engagement\",\n      secure_payment: \"100% paiement sécurisé via Stripe\",\n    },\n\n    // Feature Comparison\n    comparison: {\n      title: \"Comparaison détaillée des fonctionnalités\",\n      subtitle: \"Tout ce que vous devez savoir sur ce qui est inclus dans chaque plan\",\n      features_label: \"Fonctionnalités\",\n      headers: {\n        features: \"Fonctionnalités\",\n        free: \"Gratuit\",\n        premium: \"Premium\",\n      },\n      categories: {\n        equipment: \"Équipements & Exercices\",\n        tracking: \"Suivi & Analyses\",\n        programs: \"Programmes & IA\",\n        community: \"Communauté & Partage\",\n        support: \"Support & Projet\",\n      },\n      features: {\n        exercise_library: \"Bibliothèque d'exercices\",\n        custom_exercise: \"Exercices personnalisés\",\n        video_tutorials: \"Tutoriels vidéo\",\n        workout_history: \"Historique d'entraînement\",\n        progress_statistics: \"Statistiques de progression\",\n        personal_records: \"Suivi des records personnels\",\n        volume_analytics: \"Analyses de volume & progression\",\n        predesigned_programs: \"Programmes pré-conçus\",\n        personalized_recommendations: \"Recommandations personnalisées\",\n        pro_templates: \"Templates pro (Powerlifting, bodybuilding, etc.)\",\n        community_access: \"Accès communauté\",\n        discord_community: \"Communauté Discord\",\n        private_chat: \"Chat privé 1:1 avec coach\",\n        community_support: \"Support communautaire\",\n        priority_support: \"Support prioritaire\",\n        early_access: \"Accès anticipé aux fonctionnalités\",\n        beta_testing: \"Accès aux tests bêta\",\n      },\n      values: {\n        basic: \"Basique\",\n        complete: \"Complet\",\n        unlimited: \"Illimité\",\n        professional: \"Professionnel\",\n        six_months: \"6 mois\",\n        limited: \"Limité\",\n        all_programs: \"Tous les programmes\",\n        public: \"Public\",\n        vip_access: \"Accès VIP\",\n        private_channels: \"Canaux privés\",\n        soon: \"Bientôt\",\n        hd_slowmo: \"4K + Ralenti\",\n        early_access: \"Accès Anticipé\",\n      },\n    },\n\n    // FAQ\n    faq: {\n      title: \"Questions fréquemment posées\",\n      subtitle: \"Tout ce que vous devez savoir sur Workout.cool et notre mission\",\n      items: [\n        {\n          question: \"Pourquoi payer si c'est open-source ?\",\n          answer:\n            \"Excellente question ! Le code restera toujours gratuit, mais maintenir les serveurs, la base de données et l'infrastructure coûte de l'argent. Votre contribution nous aide à garder l'outil gratuit pour tous. C'est un modèle gagnant-gagnant : vous obtenez les fonctionnalités premium, la communauté garde l'accès gratuit !\",\n        },\n        {\n          question: \"Puis-je auto-héberger Workout.cool ?\",\n          answer:\n            \"Absolument ! Toute la base de code est disponible sur GitHub sous licence MIT. Vous pouvez la déployer sur vos propres serveurs, la personnaliser comme vous voulez et l'utiliser complètement gratuitement. L'auto-hébergement vous donne un contrôle total sur vos données et votre confidentialité d'entraînement.\",\n        },\n        {\n          question: \"Mes données d'entraînement sont-elles sécurisées ?\",\n          answer:\n            \"Oui ! Nous sommes conformes RGPD, utilisons des connexions chiffrées et stockons vos données en sécurité. De plus, comme nous sommes open-source, vous pouvez auditer nos pratiques de sécurité. Vous pouvez aussi exporter vos données à tout moment ou auto-héberger pour un contrôle complet.\",\n        },\n        {\n          question: \"Puis-je annuler mon abonnement à tout moment ?\",\n          answer:\n            \"Bien sûr ! Aucun contrat, aucun engagement. Annulez en un clic à tout moment. Vous gardez l'accès jusqu'à la fin de votre période de facturation actuelle, et vous pouvez toujours redémarrer plus tard. Vos données d'entraînement restent accessibles même si vous repassez en gratuit.\",\n        },\n        {\n          question: \"Y a-t-il des exercices pour les débutants ?\",\n          answer:\n            \"Définitivement ! Notre bibliothèque d'exercices couvre tous les niveaux de fitness, des débutants complets aux athlètes avancés. Les vidéos et instructions aident les débutants à trouver les exercices appropriés, et nos tutoriels vidéo montrent la bonne forme.\",\n        },\n        {\n          question: \"Comment fonctionne le suivi des progrès ?\",\n          answer:\n            \"Chaque série, répétition, poids et temps est automatiquement enregistré. Vous obtenez un historique d'entraînement style GitHub montrant votre régularité, plus des analyses détaillées sur le volume, la progression et les records personnels. Les utilisateurs Premium obtiennent des graphiques et insights avancés.\",\n        },\n        {\n          question: \"Puis-je importer des données d'autres apps ?\",\n          answer:\n            \"Bientôt. Nous supporterons les imports CSV pour les données de base (reps & poids). Si vous changez d'une autre app fitness, notre équipe support peut aider à migrer votre historique d'entraînement.\",\n        },\n        {\n          question: \"L'app fonctionne-t-elle hors ligne ?\",\n          answer:\n            \"Le suivi d'entraînement principal fonctionne hors ligne. Vous pouvez enregistrer séries et reps sans connexion internet pour 10 entraînements. Les vidéos d'exercices et la synchronisation cloud nécessitent internet. Toutes vos données hors ligne se synchronisent automatiquement quand vous êtes de nouveau en ligne.\",\n        },\n        {\n          question: \"Y a-t-il des programmes pour les femmes ?\",\n          answer:\n            \"Absolument ! Et il y aura plus de programmes à l'avenir. Nous y travaillons. Les plans Supporter et Premium incluront tous les futurs programmes spécialisés pour différents objectifs : force, tonification, powerlifting, bodybuilding, et plus !\",\n        },\n        {\n          question: \"Puis-je créer mes propres programmes ?\",\n          answer: \"Malheureusement, non. Nous y travaillons !\",\n        },\n      ],\n      additional_support: {\n        title: \"Vous avez encore des questions ?\",\n        description: \"Notre communauté axée fitness est là pour vous aider à réussir\",\n        community: \"Support communautaire (discord ou hello@workout.cool)\",\n        discussions: \"Discussions ouvertes (github/discord)\",\n        roadmap: \"Roadmap transparente (github)\",\n      },\n    },\n\n    // Final CTA\n    final_cta: {\n      motivation: \"Continue à pousser ! 💪\",\n      title: \"Prêt à soutenir la mission ?\",\n      subtitle: \"Rejoignez des milliers de passionnés de fitness qui croient en la liberté d'entraînement open-source\",\n      values: [\n        {\n          title: \"Communauté d'abord\",\n          description: \"Construit par et pour la communauté fitness\",\n        },\n        {\n          title: \"Toujours transparent\",\n          description: \"Code open-source, financement transparent\",\n        },\n        {\n          title: \"Projet passion\",\n          description: \"15 ans de passion !\",\n        },\n      ],\n      quote: {\n        text: \"Nous croyons que les outils fitness doivent être accessibles à tous. Votre soutien nous aide à maintenir cette vision tout en continuant à innover.\",\n        author: \"— L'équipe Workout.cool\",\n      },\n    },\n\n    // Premium Active State\n    premium_active: {\n      title: \"Premium Actif ! 💪\",\n      supporting: \"Mission soutenue 💚\",\n    },\n\n    // Legacy translations (keeping for compatibility)\n    premium_active_title: \"Premium Actif\",\n    premium_active_subtitle: \"Toutes les fonctionnalités débloquées\",\n    free_intro_title: \"Tu as déjà beaucoup gratuitement...\",\n    free_intro_text:\n      \"Workout.cool est une application de fitness gratuite et open-source utilisée quotidiennement par plus de 60 000 utilisateurs. Elle est construite avec amour (pas avec l'argent des VCs ^^) et nous coûte du temps et de l'argent réels pour la maintenir en ligne.\",\n    donation_story_text:\n      \"Au début, nous fonctionnions grâce aux dons. Mais comme vous pouvez l'imaginer, les dons n'étaient pas suffisants pour couvrir les coûts de développement et de fonctionnement. Nous avons donc créé un package qui nous aidera à garder les lumières allumées et à débloquer quelques super-pouvoirs en cours de route.\",\n    health_upgrade_text: \"Si Workout.cool vous aide à améliorer votre santé, pensez à passer Premium :D !\",\n    unlock_features_text: \"Débloquez des fonctionnalités avancées et soutenez le fitness open-source.\",\n    invest_yourself_quote: \"Ne lésinez jamais sur le fitness et les livres — investissez en vous-même !\",\n    support_mission: \"Soutenir la mission\",\n    best_value_badge: \"MEILLEURE VALEUR\",\n    annual_plan: \"Annuel\",\n    monthly_plan: \"Mensuel\",\n    discount_badge: \"40% de réduction\",\n    per_month: \"/mois\",\n    feature_all_programs: \"Tous les programmes d'entraînement\",\n    feature_progress_tracking: \"Suivi des progrès\",\n    coming_soon: \"(bientôt)\",\n    feature_future_updates: \"Tous les futurs programmes et mises à jour\",\n    feature_priority_support: \"Support prioritaire\",\n    save_yearly: \"Économisez 40% par an\",\n    processing: \"Traitement...\",\n    cta_annual: \"Soutenir + économiser 40%\",\n    cta_monthly: \"Débloquer mon plan complet\",\n    thank_supporting: \"Merci de votre soutien.\",\n    no_pressure: \"Aucune pression. Vous pouvez passer à Premium à tout moment.\",\n    keep_pushing: \"continue à pousser ! huhu\",\n    still_unsure: \"Toujours pas sûr ? Pas de souci. Workout.cool restera toujours gratuit et open-source.\",\n    support_helps: \"Mais si vous croyez en ce que nous construisons et que vous pouvez vous le permettre, votre soutien aidera 💚\",\n    self_hosting: \"Auto-hébergement\",\n    community: \"Communauté\",\n    mit_license: \"Licence MIT\",\n    pricing_year: \"an\",\n    pricing_month: \"mois\",\n    conversion_flow_title: \"Redirection en cours...\",\n    conversion_flow_message: \"Vous avez été connecté avec succès ! Redirection vers le checkout...\",\n    redirecting_to_checkout: \"Redirection vers le checkout\",\n\n    // Premium Gate\n    premium_feature: \"Fonctionnalité Premium\",\n    upgrade_to_access_feature: \"Passez à premium pour accéder à cette fonctionnalité\",\n    unlock_all_features: \"Débloquez toutes les fonctionnalités et soutenez le développement\",\n  },\n  breadcrumbs: {\n    home: \"Accueil\",\n  },\n  bottom_navigation: {\n    statistics: \"Statistiques\",\n    statistics_tooltip: \"Voir vos statistiques\",\n    programs: \"Programmes\",\n    programs_tooltip: \"Parcourir les programmes\",\n    workouts: \"Séances\",\n    workouts_tooltip: \"Créer votre propre entraînement\",\n    premium: \"Premium\",\n    premium_tooltip: \"Passer à Premium\",\n    leaderboard: \"Classement\",\n    leaderboard_tooltip: \"Voir le classement d'entraînements\",\n    tools: \"Outils\",\n    tools_tooltip: \"Parcourir les outils\",\n    profile: \"Profil\",\n    profile_tooltip: \"Voir votre profil\",\n  },\n  tools: {\n    try_now: \"Essayer maintenant\",\n    title: \"Outils Fitness\",\n    subtitle: \"Calculateurs essentiels pour optimiser votre entraînement et nutrition\",\n    moreComingSoon: \"Plus d'outils bientôt disponibles\",\n    meta: {\n      title: \"Outils Fitness - Calculateurs pour Entraînement & Nutrition\",\n      description:\n        \"Calculateurs fitness gratuits : TDEE, macros, IMC, zones de fréquence cardiaque, 1RM et plus. Optimisez votre entraînement et nutrition avec nos outils essentiels.\",\n      keywords:\n        \"calculateur fitness, calculateur calories, calculateur macros, calculateur IMC, calculateur TDEE, zones fréquence cardiaque, one rep max, outils fitness\",\n    },\n    \"calorie-calculator\": {\n      title: \"Calculateur de calories\",\n      description: \"Calculez vos besoins caloriques quotidiens (TDEE) selon votre activité et vos objectifs\",\n      meta: {\n        title: \"Calculateur de calories - TDEE & Besoins caloriques\",\n        description:\n          \"Calculez votre dépense énergétique journalière totale (TDEE) et vos besoins caloriques. Obtenez des recommandations personnalisées pour la perte de poids, le maintien ou la prise de muscle.\",\n        keywords:\n          \"calculateur calories, calculateur TDEE, calories quotidiennes, calculateur perte de poids, besoins caloriques, calculateur métabolisme de base\",\n      },\n      subtitle: \"Calculez vos besoins caloriques quotidiens basés sur l'équation de Mifflin-St Jeor\",\n      how_it_works: \"Comment fonctionne ce calculateur ?\",\n      how_it_works_description:\n        \"Ce calculateur utilise des formules scientifiquement prouvées pour estimer vos besoins caloriques quotidiens selon vos caractéristiques personnelles et votre mode de vie.\",\n      how_it_works_step1: \"Nous calculons votre métabolisme de base (calories brûlées au repos)\",\n      how_it_works_step2: \"Nous ajustons selon votre niveau d'activité\",\n      how_it_works_step3: \"Nous personnalisons selon votre objectif (perdre, maintenir ou prendre du poids)\",\n      calculate: \"Calculer\",\n      calculating: \"Calcul en cours...\",\n      tap_info_icons: \"Appuyez sur les icônes ℹ️ pour plus d'informations\",\n      gender: \"Sexe\",\n      male: \"Homme\",\n      female: \"Femme\",\n      units: \"Unités\",\n      metric: \"Métrique\",\n      imperial: \"Impérial\",\n      age: \"Âge\",\n      age_placeholder: \"Entrez votre âge\",\n      years: \"ans\",\n      height: \"Taille\",\n      height_placeholder: \"Entrez votre taille\",\n      weight: \"Poids\",\n      weight_placeholder: \"Entrez votre poids\",\n      cm: \"cm\",\n      kg: \"kg\",\n      lbs: \"lbs\",\n      feet: \"pieds\",\n      inches: \"pouces\",\n      activity_level: \"Niveau d'Activité\",\n      activity: {\n        sedentary: \"Sédentaire\",\n        sedentary_desc: \"Peu ou pas d'exercice, travail de bureau, marche minimale\",\n        light: \"Légèrement Actif\",\n        light_desc: \"Exercice léger 1-3 jours/semaine, ou marche quotidienne\",\n        moderate: \"Modérément Actif\",\n        moderate_desc: \"Exercice modéré 3-5 jours/semaine, mode de vie actif\",\n        active: \"Très Actif\",\n        active_desc: \"Exercice intense 6-7 jours/semaine, travail très actif\",\n        very_active: \"Extrêmement Actif\",\n        very_active_desc: \"Athlète, travail physique + entraînement quotidien\",\n      },\n      goal: \"Objectif\",\n      goals: {\n        lose_fast: \"Perdre du Poids Rapidement\",\n        lose_fast_desc: \"Perdre 1 kg par semaine - Agressif mais efficace\",\n        lose_slow: \"Perdre du Poids\",\n        lose_slow_desc: \"Perdre 0,5 kg par semaine - Durable et sain\",\n        maintain: \"Maintenir le Poids\",\n        maintain_desc: \"Rester au poids actuel - Parfait pour maintenir votre forme\",\n        gain_slow: \"Prendre du Poids\",\n        gain_slow_desc: \"Prendre 0,5 kg par semaine - Construction musculaire propre\",\n        gain_fast: \"Prendre du Poids Rapidement\",\n        gain_fast_desc: \"Prendre 1 kg par semaine - Croissance musculaire maximale\",\n      },\n      results: {\n        overview: \"Vue d'ensemble de vos zones\",\n        title: \"Vos Résultats\",\n        bmr: \"MB\",\n        bmr_explanation:\n          \"Le Métabolisme de Base (MB) est le nombre de calories que votre corps brûle au repos complet, juste pour maintenir les fonctions vitales comme la respiration, la circulation et la production cellulaire. C'est l'énergie minimale dont votre corps a besoin pour survivre.\",\n        tdee: \"TDEE\",\n        tdee_explanation:\n          \"La Dépense Énergétique Journalière Totale (TDEE) est votre MB plus les calories brûlées par vos activités quotidiennes et l'exercice. C'est le nombre total de calories que vous brûlez en une journée selon votre niveau d'activité.\",\n        target: \"Calories Cibles\",\n        macros: \"Macros Recommandées\",\n        macros_explanation:\n          \"Les macronutriments (macros) sont les trois groupes de nutriments principaux dont votre corps a besoin : Protéines (pour la construction et réparation musculaire), Glucides (pour l'énergie), et Lipides (pour les hormones et l'absorption des vitamines). Les pourcentages affichés sont une répartition équilibrée adaptée à la plupart des objectifs fitness.\",\n        protein: \"Protéines\",\n        carbs: \"Glucides\",\n        fat: \"Lipides\",\n        disclaimer:\n          \"Ces calculs sont des estimations basées sur des formules moyennes. Les besoins caloriques réels peuvent varier selon les facteurs individuels. Consultez un professionnel de santé ou un diététicien pour des conseils personnalisés.\",\n      },\n      faq: {\n        title: \"Questions Fréquemment Posées\",\n        q1: \"Pourquoi mon objectif calorique est différent des autres calculateurs ?\",\n        a1: \"Différents calculateurs peuvent utiliser des formules ou multiplicateurs d'activité différents. Nous utilisons l'équation de Mifflin-St Jeor, considérée comme l'une des plus précises pour la plupart des gens. Cependant, le métabolisme individuel peut varier de 10-20% de ces estimations.\",\n        q2: \"Dois-je manger exactement ce nombre de calories chaque jour ?\",\n        a2: \"Ce sont des objectifs quotidiens moyens. Il est normal de manger un peu plus certains jours et moins d'autres. Concentrez-vous sur votre moyenne hebdomadaire plutôt que d'être exact chaque jour. Écoutez les signaux de faim et de satiété de votre corps.\",\n        q3: \"Que faire si je ne vois pas de résultats après avoir suivi ces recommandations ?\",\n        a3: \"Si vous ne voyez pas de résultats après 2-3 semaines, vous devrez peut-être ajuster. Votre métabolisme réel pourrait être plus élevé ou plus bas que calculé. Essayez d'ajuster de 100-200 calories et surveillez pendant 2 semaines supplémentaires. Assurez-vous également de suivre votre alimentation avec précision.\",\n        q4: \"Les recommandations de macros conviennent-elles à tout le monde ?\",\n        a4: \"La répartition 30/40/30 (protéines/glucides/lipides) est une approche équilibrée adaptée à la plupart des gens. Cependant, les athlètes, les personnes avec des conditions médicales, ou ceux suivant des régimes spécifiques (kéto, végan, etc.) peuvent avoir besoin de ratios différents. Consultez un nutritionniste pour des recommandations personnalisées.\",\n      },\n    },\n    \"macro-calculator\": {\n      title: \"Calculateur de macros\",\n      description: \"Trouvez votre répartition optimale en protéines, glucides et lipides pour vos objectifs\",\n    },\n    \"bmi-calculator\": {\n      title: \"Calculateur d'IMC\",\n      description: \"Calculez votre Indice de Masse Corporelle et comprenez votre catégorie de poids\",\n    },\n    \"heart-rate-calculator\": {\n      title: \"Zones de fréquence cardiaque\",\n      description: \"Découvrez vos zones d'entraînement optimales pour brûler des graisses et performer\",\n    },\n    \"heart-rate-zones\": {\n      title: \"Calculateur de Zones de Fréquence Cardiaque\",\n      description:\n        \"Calculez vos zones d'entraînement de fréquence cardiaque optimales pour une performance maximale et la combustion des graisses\",\n      page_title: \"Calculateur de Zones de Fréquence Cardiaque\",\n      page_description:\n        \"Calculez vos zones d'entraînement de fréquence cardiaque personnalisées en utilisant des formules scientifiquement prouvées. Optimisez vos entraînements cardio pour brûler des graisses, l'endurance et la performance.\",\n      meta: {\n        title: \"Calculateur de Zones de Fréquence Cardiaque - Fréquence Cible et Zones d'Entraînement\",\n        description:\n          \"Calculez votre fréquence cardiaque maximale et vos zones d'entraînement personnalisées. Utilisez les formules de base ou Karvonen pour trouver vos zones VO2 Max, Anaérobie, Aérobie, Combustion des Graisses et Échauffement.\",\n        keywords:\n          \"calculateur zones fréquence cardiaque, fréquence cardiaque cible, fréquence cardiaque maximale, zones entraînement, zone VO2 max, zone anaérobie, zone aérobie, zone combustion graisses, formule Karvonen, entraînement fréquence cardiaque\",\n      },\n      calculate: \"Calculer les Zones\",\n      calculating: \"Calcul en cours...\",\n      method: \"Méthode de Calcul\",\n      method_info: \"Choisissez la formule qui convient le mieux à votre niveau de forme physique et aux données disponibles\",\n      methods: {\n        basic: \"Basique par Âge\",\n        basic_desc: \"Formule simple utilisant uniquement l'âge - bonne pour les débutants\",\n        karvonen_age: \"Karvonen par Âge et FCR\",\n        karvonen_age_desc: \"Plus précis en utilisant l'âge et la fréquence cardiaque au repos\",\n        karvonen_custom: \"Karvonen par FCM et FCR\",\n        karvonen_custom_desc: \"Le plus précis en utilisant les fréquences cardiaques maximale et au repos mesurées\",\n      },\n      age: \"Âge\",\n      age_placeholder: \"Entrez votre âge\",\n      resting_heart_rate: \"Fréquence Cardiaque au Repos (FCR)\",\n      resting_heart_rate_placeholder: \"Entrez votre FCR\",\n      resting_heart_rate_info: \"Mesurez votre fréquence cardiaque au réveil, avant de sortir du lit. La plage normale est de 60-100 bpm.\",\n      max_heart_rate: \"Fréquence Cardiaque Maximale (FCM)\",\n      max_heart_rate_placeholder: \"Entrez votre FCM\",\n      max_heart_rate_info:\n        \"Votre fréquence cardiaque maximale réelle d'un test d'effort ou d'un entraînement à effort maximal. Plus précis que les estimations basées sur l'âge.\",\n\n      results: {\n        title: \"Vos Zones de Fréquence Cardiaque\",\n        max_heart_rate: \"Fréquence Cardiaque Maximale\",\n        heart_rate_reserve: \"Réserve de Fréquence Cardiaque\",\n        target_zones: \"Zones d'Entraînement Cibles\",\n        zone: \"Zone\",\n        intensity: \"Intensité\",\n        heart_rate_range: \"Fréquence Cardiaque (bpm)\",\n        benefits: \"Bénéfices\",\n        duration: \"Durée Typique\",\n      },\n      zones: {\n        warm_up: {\n          name: \"Zone d'Échauffement\",\n          intensity: \"50-60%\",\n          benefits: \"🧘 Échauffement parfait\",\n          example: \"Marche tranquille\",\n          duration: \"5-10 minutes\",\n          description: \"Intensité très légère pour l'échauffement et la récupération\",\n        },\n        fat_burn: {\n          name: \"Zone de Combustion des Graisses\",\n          intensity: \"60-70%\",\n          benefits: \"🔥 Brûle les graisses\",\n          example: \"Jogging léger\",\n          duration: \"20-40 minutes\",\n          description: \"Intensité légère, rythme confortable pour des entraînements plus longs\",\n        },\n        aerobic: {\n          name: \"Zone Aérobie\",\n          intensity: \"70-80%\",\n          benefits: \"💪 Améliore l'endurance\",\n          example: \"Course modérée\",\n          duration: \"10-40 minutes\",\n          description: \"Intensité modérée, soutenable pendant des périodes prolongées\",\n        },\n        anaerobic: {\n          name: \"Zone Anaérobie\",\n          intensity: \"80-90%\",\n          benefits: \"⚡ Augmente la vitesse\",\n          example: \"Sprint court\",\n          duration: \"2-10 minutes\",\n          description: \"Intensité difficile, stimulante mais soutenable pour de courtes périodes\",\n        },\n        vo2_max: {\n          name: \"Zone VO2 Max\",\n          intensity: \"90-100%\",\n          benefits: \"🏆 Performance max\",\n          example: \"Sprint intense\",\n          duration: \"30 secondes - 2 minutes\",\n          description: \"Intensité maximale, soutenable uniquement pour de très courtes périodes\",\n        },\n      },\n      formulas: {\n        basic_formula: \"Formule de Base\",\n        basic_explanation: \"FCC = FCM × %Intensité\",\n        karvonen_formula: \"Formule Karvonen\",\n        karvonen_explanation: \"FCC = [(FCM - FCR) × %Intensité] + FCR\",\n        mhr_calculation: \"FCM = 220 - Âge\",\n      },\n      abbreviations: {\n        thr: \"FCC = Fréquence Cardiaque Cible\",\n        mhr: \"FCM = Fréquence Cardiaque Maximale\",\n        rhr: \"FCR = Fréquence Cardiaque au Repos\",\n        hrr: \"RFC = Réserve de Fréquence Cardiaque\",\n        bpm: \"bpm = Battements Par Minute\",\n      },\n      tips: {\n        title: \"Conseils d'Entraînement\",\n        tip1: \"Commencez par des zones de faible intensité si vous débutez l'exercice\",\n        tip2: \"Mélangez différentes zones dans votre entraînement hebdomadaire pour de meilleurs résultats\",\n        tip3: \"Utilisez un moniteur de fréquence cardiaque pour un suivi précis pendant les entraînements\",\n        tip4: \"Vos zones peuvent changer à mesure que votre condition physique s'améliore - recalculez périodiquement\",\n      },\n      faq: {\n        title: \"Questions Fréquemment Posées\",\n        q1: \"Quelle méthode de calcul dois-je utiliser ?\",\n        a1: \"Si vous débutez, utilisez la méthode Basique. Si vous connaissez votre fréquence cardiaque au repos, utilisez Karvonen par Âge pour plus de précision. Pour les zones les plus précises, utilisez Karvonen avec FCM et FCR mesurées.\",\n        q2: \"Comment mesurer ma fréquence cardiaque au repos ?\",\n        a2: \"Mesurez votre pouls pendant 60 secondes immédiatement après le réveil, avant de sortir du lit. Faites-le pendant 3-5 jours et utilisez la moyenne. La FCR normale est de 60-100 bpm, les valeurs plus basses indiquant une meilleure condition physique.\",\n        q3: \"Dans quelle zone dois-je m'entraîner pour perdre du poids ?\",\n        a3: \"La Zone de Combustion des Graisses (60-70%) est optimale pour brûler les graisses comme carburant. Cependant, les zones de plus haute intensité brûlent plus de calories totales. Mélangez les zones pour de meilleurs résultats - incluez à la fois des entraînements de combustion des graisses et de haute intensité.\",\n        q4: \"Quelle est la précision de la formule 220-âge ?\",\n        a4: \"C'est une estimation générale qui fonctionne pour la plupart des gens mais peut varier de ±10-15 bpm. Pour plus de précision, envisagez un test supervisé de fréquence cardiaque maximale ou utilisez la formule Karvonen avec vos mesures réelles.\",\n        q5: \"Puis-je m'entraîner dans la zone VO2 Max tous les jours ?\",\n        a5: \"Non, la zone VO2 Max est extrêmement intense et ne devrait être utilisée que 1-2 fois par semaine pour de courts intervalles. La plupart de l'entraînement devrait être dans les zones Aérobie et Combustion des Graisses pour construire l'endurance et permettre la récupération.\",\n      },\n      guide: {\n        title: \"Guide Complet des Zones de Fréquence Cardiaque pour l'Entraînement\",\n        text1:\n          \"Les zones de fréquence cardiaque sont un outil scientifique essentiel pour optimiser vos entraînements et atteindre vos objectifs fitness. Que vous cherchiez à perdre du poids, améliorer votre endurance ou augmenter vos performances, comprendre et utiliser les zones cardiaques transformera votre approche de l'exercice.\",\n        text2:\n          \"Ce calculateur utilise des formules validées scientifiquement pour déterminer vos zones personnalisées basées sur votre âge et, optionnellement, votre fréquence cardiaque au repos. Chaque zone correspond à une intensité spécifique et offre des bénéfices uniques pour votre santé cardiovasculaire.\",\n      },\n      table: {\n        title: \"Tableau de Référence des Fréquences Cardiaques par Âge\",\n        col1: \"Âge\",\n        col2: \"FCM\",\n        col3: \"50% Intensité\",\n        col4: \"85% Intensité\",\n        avertiser: \"* Ces valeurs sont des moyennes. Votre FCM réelle peut varier de ±10-15 bpm.\",\n      },\n      details: {\n        title: \"Les 5 Zones d'Entraînement Expliquées en Détail\",\n        benefits: \"Bénéfices\",\n        zone1_title: \"Zone 1 : Échauffement (50-60% FCM)\",\n        zone1_content:\n          \"La zone d'échauffement est idéale pour débuter une séance, récupérer entre les intervalles ou terminer un entraînement. À cette intensité, vous pouvez maintenir une conversation normale sans essoufflement.\",\n        zone1_details_1: \"Améliore la circulation sanguine\",\n        zone1_details_2: \"Prépare les muscles et articulations\",\n        zone1_details_3: \"Réduit le risque de blessures\",\n        zone1_details_4: \"Favorise la récupération active\",\n        zone1_duration: \"Durée recommandée\",\n        zone1_duration_value: \"5-10 minutes en début/fin de séance\",\n        zone1_duration_value_2: \"20-30 minutes pour la récupération active\",\n        zone2_title: \"Zone 2 : Combustion des Graisses (60-70% FCM)\",\n        zone2_content:\n          \"Dans cette zone, votre corps utilise principalement les graisses comme source d'énergie. C'est l'intensité optimale pour développer l'endurance de base et améliorer l'efficacité métabolique.\",\n        zone2_details_1: \"Maximise l'utilisation des graisses\",\n        zone2_details_2: \"Développe l'endurance aérobie\",\n        zone2_details_3: \"Améliore l'efficacité cardiaque\",\n        zone2_details_4: \"Renforce le système immunitaire\",\n        zone2_duration: \"Durée recommandée\",\n        zone2_duration_value: \"30-90 minutes pour l'endurance\",\n        zone2_duration_value_2: \"45-60 minutes pour la perte de poids\",\n        zone3_title: \"Zone 3 : Aérobie (70-80% FCM)\",\n        zone3_content:\n          \"La zone aérobie améliore significativement votre capacité cardiovasculaire. Vous respirez plus fort mais pouvez encore prononcer des phrases courtes. C'est la zone d'entraînement principale pour la plupart des athlètes.\",\n        zone3_details_1: \"Augmente la capacité pulmonaire\",\n        zone3_details_2: \"Améliore l'endurance cardiovasculaire\",\n        zone3_details_3: \"Renforce le cœur\",\n        zone3_details_4: \"Optimise l'utilisation de l'oxygène\",\n        zone3_duration: \"Durée recommandée\",\n        zone3_duration_value: \"20-60 minutes en continu\",\n        zone3_duration_value_2: \"Intervalles de 5-15 minutes\",\n        zone4_title: \"Zone 4 : Anaérobie (80-90% FCM)\",\n        zone4_content:\n          \"Dans la zone anaérobie, votre corps produit de l'acide lactique plus rapidement qu'il ne peut l'éliminer. Cette intensité développe la puissance et la vitesse mais ne peut être maintenue longtemps.\",\n        zone4_details_1: \"Augmente la puissance musculaire\",\n        zone4_details_2: \"Améliore la tolérance au lactate\",\n        zone4_details_3: \"Développe la vitesse\",\n        zone4_details_4: \"Renforce le mental\",\n        zone4_duration: \"Durée recommandée\",\n        zone4_duration_value: \"Intervalles de 2-8 minutes\",\n        zone4_duration_value_2: \"Récupération égale ou double\",\n        zone5_title: \"Zone 5 : VO2 Max (90-100% FCM)\",\n        zone5_content:\n          \"La zone VO2 Max représente l'effort maximal. À cette intensité, vous ne pouvez prononcer que quelques mots et l'effort est insoutenable au-delà de quelques minutes. Réservée aux athlètes expérimentés.\",\n        zone5_details_1: \"Maximise la capacité aérobie\",\n        zone5_details_2: \"Améliore l'économie de course\",\n        zone5_details_3: \"Développe la puissance maximale\",\n        zone5_details_4: \"Repousse les limites mentales\",\n        zone5_duration: \"Durée recommandée\",\n        zone5_duration_value: \"Intervalles de 30s à 2 minutes\",\n        zone5_duration_value_2: \"Maximum 1-2 fois par semaine\",\n      },\n      educational: {\n        title: \"Comprendre l'Entraînement par Fréquence Cardiaque\",\n        description: \"Visualisez facilement chaque zone d'entraînement\",\n        what_are_zones: {\n          title: \"Que Sont les Zones de Fréquence Cardiaque ?\",\n          content:\n            \"Les zones de fréquence cardiaque sont des plages de battements par minute qui correspondent à différentes intensités d'exercice. S'entraîner dans des zones spécifiques vous aide à atteindre différents objectifs de forme physique plus efficacement.\",\n        },\n        why_use_zones: {\n          title: \"Pourquoi Utiliser les Zones de Fréquence Cardiaque ?\",\n          content:\n            \"S'entraîner avec des zones de fréquence cardiaque garantit que vous vous exercez à la bonne intensité pour vos objectifs. Cela prévient le surentraînement, maximise les résultats et vous aide à vous entraîner plus efficacement.\",\n        },\n        zone_distribution: {\n          title: \"Distribution Hebdomadaire Recommandée des Zones\",\n          content:\n            \"Pour une forme physique équilibrée : 80% dans les Zones 1-3 (base aérobie), 15% dans la Zone 4 (seuil), 5% dans la Zone 5 (VO2 max). Ajustez en fonction de vos objectifs spécifiques et de votre niveau de forme physique.\",\n        },\n        monitoring: {\n          title: \"Comment Surveiller Votre Fréquence Cardiaque\",\n          content:\n            \"Utilisez une ceinture thoracique pour plus de précision, ou un moniteur au poignet pour la commodité. Vérifiez régulièrement votre fréquence cardiaque pendant l'exercice et ajustez l'intensité pour rester dans votre zone cible.\",\n        },\n      },\n      training_tips: {\n        title: \"Conseils d'Expert pour Optimiser votre Entraînement\",\n        tip1: {\n          title: \"Échauffement progressif\",\n          description: \"Commencez toujours par 5-10 minutes en zone 1 (50-60%) pour préparer votre système cardiovasculaire.\",\n        },\n        tip2: {\n          title: \"Règle du 80/20\",\n          description: \"80% de votre entraînement en zones 1-3 (aérobie), 20% en zones 4-5 (anaérobie) pour un développement optimal.\",\n        },\n        tip3: {\n          title: \"Récupération active\",\n          description: \"Après un effort intense, redescendez progressivement en zone 1-2 pendant 5-10 minutes.\",\n        },\n        tip4: {\n          title: \"Hydratation constante\",\n          description: \"Buvez avant, pendant et après l'exercice. La déshydratation augmente la fréquence cardiaque.\",\n        },\n        tip5: {\n          title: \"Sommeil réparateur\",\n          description: \"7-9 heures de sommeil permettent une meilleure récupération et une FCR plus basse.\",\n        },\n        tip6: {\n          title: \"Progression graduelle\",\n          description: \"Augmentez l'intensité ou la durée de 10% maximum par semaine pour éviter le surentraînement.\",\n        },\n      },\n      training_tips_2: {\n        title: \"Conseils pratiques\",\n        title1: \"Trouvez votre zone\",\n        description1: \"Chaque zone a un objectif différent. Choisissez selon votre but !\",\n        title2: \"Durée recommandée\",\n        description2: \"Plus l'intensité est élevée, plus la durée doit être courte.\",\n        title3: \"Progression\",\n        description3: \"Commencez doucement et augmentez progressivement l'intensité.\",\n        title4: \"Écoutez votre corps\",\n        description4: \"Si vous vous sentez mal, ralentissez immédiatement.\",\n      },\n      quick_facts: {\n        title: \"Le saviez-vous ?\",\n        fact1: \"220 - votre âge = Fréquence cardiaque maximale approximative\",\n        fact2: \"Mesurez votre pouls au réveil pour connaître votre fréquence au repos\",\n        fact3: \"Une montre connectée peut suivre votre fréquence en temps réel\",\n        fact4: \"80% de votre entraînement devrait être en zones 1-3\",\n      },\n      weekly_plan: {\n        title: \"Plan hebdomadaire type\",\n        description: \"Un exemple de semaine d'entraînement équilibrée\",\n        monday: {\n          title: \"Zone 1-2\",\n          description: \"30-45 min\",\n        },\n        tuesday: {\n          title: \"Zone 2-3\",\n          description: \"45-60 min\",\n        },\n        wednesday: {\n          title: \"Repos\",\n          description: \"Récupération\",\n        },\n        thursday: {\n          title: \"Zone 3-4\",\n          description: \"30-40 min\",\n        },\n        friday: {\n          title: \"Zone 1-2\",\n          description: \"30 min\",\n        },\n        saturday: {\n          title: \"Zone 4-5\",\n          description: \"20-30 min\",\n        },\n        tips: \"💡 Adaptez ce plan selon votre niveau et vos objectifs !\",\n        cta: \"⬆️ Calculer mes zones maintenant\",\n      },\n      seo_faq_title: \"Questions Fréquentes sur les Zones de Fréquence Cardiaque\",\n      seo_faq_q1_question: \"Qu'est-ce que la fréquence cardiaque maximale (FCM) ?\",\n      seo_faq_q1_answer:\n        \"La fréquence cardiaque maximale est le nombre maximal de battements par minute que votre cœur peut atteindre lors d'un effort physique intense. Elle est généralement calculée avec la formule : 220 - votre âge. Cependant, cette formule peut varier de ±10-15 bpm selon les individus.\",\n      seo_faq_q2_question: \"Comment mesurer ma fréquence cardiaque au repos ?\",\n      seo_faq_q2_answer:\n        \"Mesurez votre pouls au réveil, avant de sortir du lit. Comptez les battements pendant 60 secondes ou pendant 15 secondes et multipliez par 4. Répétez pendant 3-5 jours et utilisez la moyenne. Une FCR normale est entre 60-100 bpm.\",\n      seo_faq_q3_question: \"Quelle zone est la meilleure pour perdre du poids ?\",\n      seo_faq_q3_answer:\n        \"La zone de combustion des graisses (60-70% FCM) est optimale pour brûler les graisses comme carburant. Cependant, les zones plus intenses brûlent plus de calories totales. Pour une perte de poids efficace, alternez entre différentes zones.\",\n      seo_faq_q4_question: \"Puis-je m'entraîner dans la zone VO2 Max tous les jours ?\",\n      seo_faq_q4_answer:\n        \"Non, la zone VO2 Max (90-100% FCM) est extrêmement intense et ne devrait être utilisée que 1-2 fois par semaine pour de courtes périodes (30 secondes à 2 minutes). La majorité de votre entraînement devrait être dans les zones aérobiques.\",\n      seo_faq_q5_question: \"La formule 220-âge est-elle précise ?\",\n      seo_faq_q5_answer:\n        \"C'est une estimation générale qui fonctionne pour la plupart des gens mais peut varier de ±10-15 bpm. Pour plus de précision, utilisez la formule de Karvonen avec votre FCR ou faites un test d'effort supervisé.\",\n      seo_faq_q6_question: \"Comment savoir si je suis dans la bonne zone ?\",\n      seo_faq_q6_answer:\n        \"Utilisez un cardiofréquencemètre pour une mesure précise. Sans appareil, utilisez le test de la parole : Zone légère = conversation facile, Zone modérée = phrases courtes, Zone intense = mots isolés seulement.\",\n      seo_faq_q7_question: \"Les zones changent-elles avec l'amélioration de ma condition physique ?\",\n      seo_faq_q7_answer:\n        \"Oui, avec l'entraînement, votre fréquence cardiaque au repos diminue et votre efficacité cardiaque s'améliore. Recalculez vos zones tous les 2-3 mois pour ajuster votre entraînement.\",\n      seo_faq_q8_question: \"Quelle est la différence entre les formules Basic et Karvonen ?\",\n      seo_faq_q8_answer:\n        \"La formule Basic utilise seulement l'âge (THR = FCM × %Intensité). La formule Karvonen est plus précise car elle prend en compte votre FCR : THR = [(FCM - FCR) × %Intensité] + FCR.\",\n      intern_links_title: \"Prêt à Optimiser vos Entraînements ?\",\n      intern_links_subtitle: \"Utilisez notre calculateur pour découvrir vos zones personnalisées et transformez votre fitness\",\n      intern_links_button: \"Calculer Mes Zones Maintenant\",\n      intern_links_bmi_title: \"Calculateur d'IMC\",\n      intern_links_bmi_description: \"Évaluez votre indice de masse corporelle\",\n      intern_links_calorie_title: \"Calculateur de Calories\",\n      intern_links_calorie_description: \"Déterminez vos besoins caloriques quotidiens\",\n      intern_links_macro_title: \"Calculateur de Macros\",\n      intern_links_macro_description: \"Optimisez votre répartition nutritionnelle\",\n      cta: {\n        title: \"Prêt à Optimiser vos Entraînements ?\",\n        subtitle: \"Utilisez notre calculateur pour découvrir vos zones personnalisées et transformez votre fitness\",\n        button: \"Calculer Mes Zones Maintenant\",\n        bmi_title: \"Calculateur d'IMC\",\n        bmi_description: \"Évaluez votre indice de masse corporelle\",\n        calorie_title: \"Calculateur de Calories\",\n        calorie_description: \"Déterminez vos besoins caloriques quotidiens\",\n        macro_title: \"Calculateur de Macros\",\n        macro_description: \"Optimisez votre répartition nutritionnelle\",\n      },\n      medical_warning_title: \"Avertissement Médical Important\",\n      medical_warning_content:\n        \"Ce calculateur fournit des estimations basées sur des formules générales. Les résultats peuvent varier selon votre condition physique, vos médicaments et votre état de santé. Consultez toujours un professionnel de santé avant de commencer un nouveau programme d'exercice, particulièrement si vous avez des conditions médicales préexistantes ou si vous ressentez des symptômes inhabituels pendant l'exercice.\",\n    },\n    \"one-rep-max\": {\n      title: \"Calculateur 1RM\",\n      description: \"Estimez votre max sur une répétition et planifiez vos pourcentages d'entraînement\",\n    },\n    back_to_calculators: \"Retour aux calculateurs\",\n    body_fat_percentage: \"Pourcentage de Graisse Corporelle\",\n    body_fat_info_title: \"Qu'est-ce que le Pourcentage de Graisse Corporelle ?\",\n    body_fat_info_content:\n      \"Le pourcentage de graisse corporelle est essentiel pour les formules Katch-McArdle et Cunningham car elles calculent basé sur la masse maigre. Si vous ne connaissez pas votre % de graisse exact, utilisez des guides visuels en ligne ou des scans DEXA pour plus de précision.\",\n    \"calorie-calculator-hub\": {\n      title: \"Formules de Calculateur de calories\",\n      subtitle: \"Choisissez la meilleure formule pour vos besoins et obtenez des calculs caloriques précis\",\n      meta: {\n        title: \"Formules de Calculateur de calories - Calculateurs BMR & TDEE\",\n        description:\n          \"Comparez différentes formules BMR : Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham et Oxford. Choisissez le meilleur calculateur de calories pour vos besoins.\",\n        keywords:\n          \"formules BMR, comparaison calculateur calories, Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham, Oxford, calculateur TDEE\",\n      },\n      which_formula: \"Quelle Formule Dois-je Choisir ?\",\n      formula_explanation:\n        \"Différentes formules fonctionnent mieux pour différentes personnes. Voici un guide rapide pour vous aider à choisir :\",\n      recommendation_general: \"Meilleure formule globale, plus précise pour la population générale\",\n      recommendation_traditional: \"Formule classique, largement utilisée mais légèrement moins précise\",\n      recommendation_bodyfat: \"Plus précise si vous connaissez votre pourcentage de graisse corporelle\",\n      since: \"Depuis\",\n      all_formulas: \"Toutes les formules\",\n      popularity: \"Popularité\",\n      accuracy: \"Précision\",\n      accuracy_high: \"Élevée\",\n      accuracy_good: \"Bonne\",\n      accuracy_medium: \"Moyenne\",\n      best_for: \"Idéal pour\",\n      best_for_general: \"Usage général\",\n      best_for_traditional: \"Traditionnel\",\n      best_for_athletes: \"Athlètes\",\n      best_for_bodybuilders: \"Culturistes\",\n      best_for_european: \"Population européenne\",\n      best_for_comparison: \"Comparer tout\",\n      \"mifflin-st-jeor\": {\n        title: \"Mifflin-St Jeor (Recommandé)\",\n        description:\n          \"Formule la plus précise pour la population générale, développée en 1990. Actuellement l'étalon-or pour les calculs BMR.\",\n      },\n      \"harris-benedict\": {\n        title: \"Harris-Benedict (Classique)\",\n        description:\n          \"Version révisée 1984 de la formule classique. Largement utilisée mais tend à surestimer les calories pour certaines personnes.\",\n      },\n      \"katch-mcardle\": {\n        title: \"Katch-McArdle (Athlètes)\",\n        description:\n          \"Basée sur la masse maigre. Plus précise pour les personnes qui connaissent leur pourcentage de graisse corporelle et sont physiquement actives.\",\n      },\n      cunningham: {\n        title: \"Cunningham (Culturistes)\",\n        description:\n          \"Conçue pour les athlètes très maigres et les culturistes avec peu de graisse corporelle. Utilise le calcul de masse maigre.\",\n      },\n      oxford: {\n        title: \"Oxford (Européenne)\",\n        description: \"Formule plus récente (2005) basée sur les populations européennes. Prend en compte les tranches d'âge.\",\n      },\n      comparison: {\n        title: \"Comparer Toutes les Formules\",\n        description:\n          \"Comparez les résultats de toutes les formules côte à côte pour voir les différences et choisir ce qui fonctionne le mieux pour vous.\",\n      },\n    },\n    \"mifflin-st-jeor\": {\n      title: \"Calculateur Mifflin-St Jeor\",\n      subtitle: \"L'étalon-or pour le calcul BMR - le plus précis pour la population générale\",\n      meta: {\n        title: \"Calculateur Mifflin-St Jeor - BMR & TDEE les Plus Précis\",\n        description:\n          \"Calculez votre BMR et TDEE en utilisant l'équation Mifflin-St Jeor - la formule la plus précise pour la population générale. Obtenez des recommandations caloriques personnalisées.\",\n        keywords:\n          \"calculateur Mifflin-St Jeor, calculateur BMR, calculateur TDEE, calculateur calories le plus précis, calculateur métabolisme\",\n      },\n      how_it_works: \"Comment Fonctionne la Formule Mifflin-St Jeor\",\n      how_it_works_description:\n        \"Développée en 1990, cette formule est considérée comme la plus précise pour calculer le Taux Métabolique de Base (BMR) chez les adultes en bonne santé. Elle est plus précise que l'équation Harris-Benedict et est largement recommandée par les nutritionnistes et professionnels du fitness.\",\n    },\n    \"harris-benedict\": {\n      title: \"Calculateur Harris-Benedict\",\n      subtitle: \"Formule BMR classique - l'approche traditionnelle du calcul des calories\",\n      meta: {\n        title: \"Calculateur Harris-Benedict - Formule BMR & TDEE Classique\",\n        description:\n          \"Calculez votre BMR et TDEE en utilisant l'équation Harris-Benedict révisée (1984). La formule classique qui a initié les calculs caloriques modernes.\",\n        keywords: \"calculateur Harris-Benedict, calculateur BMR classique, calculateur TDEE traditionnel, formule Harris-Benedict révisée\",\n      },\n      how_it_works: \"Comment Fonctionne la Formule Harris-Benedict\",\n      how_it_works_description:\n        \"Développée à l'origine en 1919 et révisée en 1984, l'équation Harris-Benedict était l'une des premières formules pour calculer le BMR. Bien que légèrement moins précise que les formules plus récentes, elle reste largement utilisée et fournit de bonnes estimations pour la plupart des gens.\",\n    },\n    \"katch-mcardle\": {\n      title: \"Calculateur Katch-McArdle\",\n      subtitle: \"Calcul BMR précis basé sur la masse maigre - idéal pour les athlètes\",\n      meta: {\n        title: \"Calculateur Katch-McArdle - BMR & TDEE Masse Maigre\",\n        description:\n          \"Calculez votre BMR et TDEE en utilisant la formule Katch-McArdle basée sur la masse maigre. Le plus précis pour les personnes qui connaissent leur pourcentage de graisse corporelle.\",\n        keywords:\n          \"calculateur Katch-McArdle, BMR masse maigre, calculateur pourcentage graisse corporelle, calculateur BMR athlète, TDEE précis\",\n      },\n      how_it_works: \"Comment Fonctionne la Formule Katch-McArdle\",\n      how_it_works_description:\n        \"Cette formule calcule le BMR basé sur la masse maigre plutôt que sur le poids corporel total, la rendant plus précise pour les personnes qui connaissent leur pourcentage de graisse corporelle. Elle est particulièrement utile pour les athlètes et les individus physiquement actifs.\",\n    },\n    cunningham: {\n      title: \"Calculateur Cunningham\",\n      subtitle: \"Formule BMR conçue pour les athlètes très maigres et les culturistes\",\n      meta: {\n        title: \"Calculateur Cunningham - BMR pour Athlètes Maigres & Culturistes\",\n        description:\n          \"Calculez votre BMR et TDEE en utilisant la formule Cunningham, spécialement conçue pour les athlètes très maigres et les culturistes avec peu de graisse corporelle.\",\n        keywords:\n          \"calculateur Cunningham, calculateur BMR culturiste, BMR athlète maigre, calculateur BMR faible graisse corporelle, calculateur préparation compétition\",\n      },\n      how_it_works: \"Comment Fonctionne la Formule Cunningham\",\n      how_it_works_description:\n        \"Développée spécifiquement pour les individus très maigres avec de faibles pourcentages de graisse corporelle, cette formule fournit des estimations BMR plus élevées que les autres équations. Elle est plus précise pour les athlètes de compétition et les culturistes en préparation de concours.\",\n    },\n    oxford: {\n      title: \"Calculateur Oxford\",\n      subtitle: \"Formule BMR moderne basée sur les populations européennes avec considérations d'âge\",\n      meta: {\n        title: \"Calculateur Oxford - Formule BMR & TDEE Moderne\",\n        description:\n          \"Calculez votre BMR et TDEE en utilisant l'équation Oxford (2005), une formule moderne basée sur les populations européennes avec des calculs spécifiques à l'âge.\",\n        keywords: \"calculateur Oxford, calculateur BMR moderne, formule BMR européenne, calculateur BMR spécifique âge, équation BMR 2005\",\n      },\n      how_it_works: \"Comment Fonctionne la Formule Oxford\",\n      how_it_works_description:\n        \"Publiée en 2005, c'est l'une des formules BMR les plus récentes. Elle a été développée en utilisant des données de populations européennes et prend en compte les tranches d'âge, fournissant différentes équations pour les personnes de moins et plus de 30 ans.\",\n    },\n    \"calorie-calculator-comparison\": {\n      title: \"Comparer toutes les formules BMR\",\n      subtitle: \"Voyez comment différentes formules BMR calculent vos besoins caloriques côte à côte\",\n      meta: {\n        title: \"Comparaison des formules BMR - Comparer tous les calculateurs de calories\",\n        description:\n          \"Comparez les formules Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham et Oxford BMR côte à côte. Voyez quelle formule fonctionne le mieux pour vous.\",\n        keywords:\n          \"comparaison formule BMR, comparaison calculateur calories, Mifflin vs Harris-Benedict, meilleur calculateur BMR, comparer formules calories\",\n      },\n      how_it_works: \"Comment fonctionne cette comparaison\",\n      how_it_works_description:\n        \"Entrez vos détails une fois et voyez comment toutes les principales formules BMR calculent vos besoins caloriques quotidiens. Cela vous aide à comprendre les différences et à choisir la formule la plus adaptée à vos objectifs.\",\n      input_details: \"Vos détails\",\n      compare: \"Comparer\",\n      results_comparison: \"Résultats de la comparaison des formules\",\n      vs_mifflin: \"vs Mifflin-St Jeor\",\n      summary: \"Résumé et recommandations\",\n      summary_explanation:\n        \"Différentes formules peuvent donner des résultats variables. Généralement, des différences de ±100-200 calories sont normales et attendues.\",\n      recommendation:\n        \"Pour la plupart des gens, Mifflin-St Jeor fournit la base la plus précise. Les athlètes devraient considérer Katch-McArdle s'ils connaissent leur pourcentage de graisse corporelle.\",\n    },\n    \"bmi-calculator-hub\": {\n      title: \"Outils Calculateur IMC\",\n      subtitle: \"Calculez votre Indice de Masse Corporelle avec différentes méthodes et obtenez des conseils santé personnalisés\",\n      meta: {\n        title: \"Calculateur IMC - Outils d'Indice de Masse Corporelle et Évaluation Santé\",\n        description:\n          \"Calculez votre IMC avec nos outils complets. IMC standard, ajusté pour les athlètes, IMC pédiatrique, et outils de comparaison. Obtenez des conseils santé et recommandations.\",\n        keywords: \"calculateur IMC, indice masse corporelle, évaluation santé, statut poids, outils IMC, IMC pédiatrique, IMC athlète\",\n      },\n      understanding_bmi: \"Comprendre l'IMC\",\n      bmi_explanation:\n        \"L'IMC est un outil de dépistage qui aide à évaluer si vous avez un poids santé par rapport à votre taille. Choisissez le bon calculateur pour vos besoins :\",\n      recommendation_standard: \"Idéal pour la population générale et le dépistage initial\",\n      recommendation_adjusted: \"Plus précis pour les athlètes et personnes musclées\",\n      recommendation_pediatric: \"Spécialisé pour les enfants et adolescents avec percentiles spécifiques à l'âge\",\n      popularity: \"Popularité\",\n      accuracy: \"Précision\",\n      accuracy_high: \"Élevée\",\n      accuracy_good: \"Bonne\",\n      accuracy_medium: \"Moyenne\",\n      best_for: \"Idéal pour\",\n      best_for_general: \"Usage général\",\n      best_for_athletes: \"Athlètes\",\n      best_for_children: \"Enfants\",\n      best_for_comparison: \"Comparer tout\",\n      category_standard: \"Standard\",\n      category_advanced: \"Avancé\",\n      category_specialized: \"Spécialisé\",\n      standard: {\n        title: \"Calculateur IMC Standard\",\n        description: \"Calcul IMC classique utilisant la formule standard OMS. Évaluation rapide et facile pour la population générale.\",\n        page_title: \"Calculateur IMC Standard\",\n        page_description:\n          \"Calculez votre Indice de Masse Corporelle en utilisant la formule standard de l'OMS. Obtenez des résultats instantanés avec catégorie de santé et recommandations personnalisées.\",\n      },\n      adjusted: {\n        title: \"Calculateur IMC Ajusté\",\n        description:\n          \"Calcul IMC amélioré qui considère la masse musculaire et la composition corporelle pour des résultats plus précis chez les individus athlétiques.\",\n      },\n      pediatric: {\n        title: \"Calculateur IMC Pédiatrique\",\n        description:\n          \"Calculateur IMC spécialisé pour enfants et adolescents utilisant des percentiles spécifiques à l'âge et au sexe et des courbes de croissance.\",\n      },\n      comparison: {\n        title: \"Outil de Comparaison IMC\",\n        description:\n          \"Comparez différentes méthodes de calcul IMC côte à côte pour comprendre comment divers facteurs affectent vos résultats.\",\n      },\n    },\n  },\n  \"bmi-calculator\": {\n    height: \"Taille\",\n    weight: \"Poids\",\n    feet: \"pi\",\n    inches: \"po\",\n    cm: \"cm\",\n    kg: \"kg\",\n    lbs: \"lbs\",\n    height_placeholder: \"Entrez la taille\",\n    weight_placeholder: \"Entrez le poids\",\n    calculate: \"Calculer l'IMC\",\n    your_bmi: \"Votre IMC\",\n    bmi_prime: \"IMC Prime\",\n    ponderal_index: \"Indice Pondéral\",\n    bmi_category: \"Catégorie IMC\",\n    health_risk: \"Risque Santé\",\n    recommendations_label: \"Recommandations\",\n    units: \"Unités\",\n    metric: \"Métrique (kg/cm)\",\n    imperial: \"Impérial (lbs/pi)\",\n\n    // Detailed BMI Categories (WHO)\n    category_severe_thinness: \"Maigreur Sévère\",\n    category_moderate_thinness: \"Maigreur Modérée\",\n    category_mild_thinness: \"Maigreur Légère\",\n    category_normal: \"Poids Normal\",\n    category_overweight: \"Surpoids\",\n    category_obese_class_1: \"Obésité Classe I\",\n    category_obese_class_2: \"Obésité Classe II\",\n    category_obese_class_3: \"Obésité Classe III\",\n\n    // Health Risks\n    risk_low: \"Faible\",\n    risk_normal: \"Normal\",\n    risk_increased: \"Augmenté\",\n    risk_high: \"Élevé\",\n    risk_very_high: \"Très Élevé\",\n    risk_extremely_high: \"Extrêmement Élevé\",\n\n    // Additional Information\n    bmi_range: \"Plage IMC\",\n    ideal_weight: \"Plage de Poids Idéal\",\n    weight_to_lose: \"Poids à Perdre\",\n    weight_to_gain: \"Poids à Prendre\",\n    normal_range: \"Plage IMC normale : 18,5 - 24,9\",\n\n    // BMI Prime\n    about_bmi_prime: \"À Propos de l'IMC Prime\",\n    bmi_prime_explanation:\n      \"L'IMC Prime est le rapport entre votre IMC et la limite supérieure de l'IMC normal (25). Une valeur de 1,0 signifie que vous êtes à la limite supérieure du poids normal.\",\n    underweight: \"Insuffisant\",\n    normal: \"Normal\",\n    overweight: \"Surpoids\",\n    obese: \"Obèse\",\n\n    // Limitations\n    limitations_title: \"Limites de l'IMC\",\n    limitations_text:\n      \"L'IMC ne fait pas la distinction entre la masse musculaire et la masse graisseuse. Les athlètes et les personnes très musclées peuvent avoir un IMC élevé tout en étant en bonne santé. L'âge, le sexe, l'origine ethnique et la composition corporelle affectent également l'interprétation.\",\n\n    disclaimer:\n      \"L'IMC est un outil de dépistage et peut ne pas refléter la composition corporelle. Consultez des professionnels de santé pour des conseils personnalisés.\",\n\n    // Recommendations\n    recommendations: {\n      severe_thinness: {\n        medical_consultation: \"Consultation médicale immédiate fortement recommandée\",\n        nutritional_assessment: \"Évaluation nutritionnelle complète nécessaire\",\n        weight_gain_program: \"Peut nécessiter un programme de prise de poids supervisé\",\n        screen_conditions: \"Dépistage des conditions médicales sous-jacentes\",\n        psychological_evaluation: \"Envisager une évaluation psychologique si trouble alimentaire suspecté\",\n      },\n      moderate_thinness: {\n        healthcare_provider: \"Consulter un professionnel de santé pour évaluation\",\n        nutrient_dense_foods: \"Se concentrer sur des aliments riches en nutriments et calories\",\n        registered_dietitian: \"Envisager de travailler avec un diététicien agréé\",\n        monitor_malnutrition: \"Surveiller les signes de malnutrition\",\n        gradual_weight_gain: \"Prise de poids graduelle et saine recommandée\",\n      },\n      mild_thinness: {\n        consider_healthcare: \"Envisager de consulter un professionnel de santé\",\n        nutrient_dense_foods: \"Se concentrer sur des aliments riches en nutriments pour prendre du poids sainement\",\n        strength_training: \"Inclure l'entraînement en force pour développer la masse musculaire\",\n        monitor_health: \"Surveiller votre santé régulièrement\",\n        gradual_weight_gain: \"Viser une prise de poids graduelle (0,5-1 kg par semaine)\",\n      },\n      normal: {\n        maintain_weight: \"Maintenir votre poids santé actuel\",\n        physical_activity: \"Continuer l'activité physique régulière (150+ minutes par semaine)\",\n        balanced_diet: \"Adopter une alimentation équilibrée et nutritive\",\n        health_checkups: \"Bilans de santé réguliers\",\n        overall_wellness: \"Se concentrer sur le bien-être général et la composition corporelle\",\n      },\n      overweight: {\n        gradual_weight_loss: \"Viser une perte de poids graduelle (0,5-1 kg par semaine)\",\n        increase_activity: \"Augmenter l'activité physique à 150+ minutes par semaine\",\n        portion_control: \"Se concentrer sur le contrôle des portions et une nutrition équilibrée\",\n        healthcare_provider: \"Envisager de consulter un professionnel de santé\",\n        lifestyle_goals: \"Fixer des objectifs de mode de vie réalistes et durables\",\n      },\n      obese_class_1: {\n        healthcare_provider: \"Consulter un professionnel de santé pour un plan de gestion du poids\",\n        weight_loss_target: \"Viser une perte de poids de 5-10% initialement\",\n        diet_exercise: \"Combiner interventions alimentaires et exercice\",\n        nutritional_counseling: \"Envisager un conseil nutritionnel professionnel\",\n        screen_conditions: \"Dépistage des conditions de santé liées au poids\",\n      },\n      obese_class_2: {\n        medical_supervision: \"Rechercher une supervision médicale pour la gestion du poids\",\n        lifestyle_programs: \"Envisager des programmes d'intervention de mode de vie complets\",\n        evaluate_conditions: \"Évaluer les conditions de santé liées au poids\",\n        medical_treatments: \"Peut bénéficier de traitements médicaux de perte de poids\",\n        bariatric_surgery: \"Envisager une évaluation de chirurgie bariatrique si approprié\",\n      },\n      obese_class_3: {\n        medical_consultation: \"Consultation médicale immédiate recommandée\",\n        bariatric_surgery: \"Envisager une évaluation de chirurgie bariatrique\",\n        weight_management: \"Programme médical complet de gestion du poids\",\n        health_complications: \"Traiter les complications de santé liées au poids\",\n        multidisciplinary: \"Approche multidisciplinaire avec équipe médicale\",\n      },\n    },\n\n    // Health Risks\n    health_risks: {\n      overweight: {\n        high_blood_pressure: \"Hypertension artérielle\",\n        ldl_cholesterol: \"Niveaux élevés de cholestérol LDL (mauvais cholestérol)\",\n        hdl_cholesterol: \"Niveaux faibles de cholestérol HDL (bon cholestérol)\",\n        triglycerides: \"Niveaux élevés de triglycérides\",\n        type_2_diabetes: \"Diabète de type II\",\n        coronary_heart_disease: \"Maladie coronarienne\",\n        stroke: \"Accident vasculaire cérébral\",\n        gallbladder_disease: \"Maladie de la vésicule biliaire\",\n        osteoarthritis: \"Arthrose\",\n        sleep_apnea: \"Apnée du sommeil et problèmes respiratoires\",\n        certain_cancers: \"Certains cancers (endomètre, sein, côlon, rein, vésicule biliaire, foie)\",\n        low_quality_life: \"Faible qualité de vie\",\n        mental_illnesses: \"Maladies mentales comme la dépression clinique et l'anxiété\",\n        body_pains: \"Douleurs corporelles et difficultés avec les fonctions physiques\",\n        increased_mortality: \"Risque généralement accru de mortalité\",\n      },\n      underweight: {\n        malnutrition: \"Malnutrition et carences vitaminiques\",\n        anemia: \"Anémie (capacité réduite à transporter l'oxygène dans le sang)\",\n        osteoporosis: \"Ostéoporose (risque accru de fractures osseuses)\",\n        immune_function: \"Fonction immunitaire diminuée\",\n        growth_development: \"Problèmes de croissance et de développement (surtout chez les enfants)\",\n        reproductive_issues: \"Problèmes reproductifs chez les femmes dus aux déséquilibres hormonaux\",\n        miscarriage_risk: \"Risque plus élevé de fausse couche au premier trimestre\",\n        surgery_complications: \"Complications potentielles lors de chirurgies\",\n        increased_mortality: \"Risque généralement accru de mortalité\",\n        underlying_conditions: \"Peut indiquer des conditions médicales sous-jacentes\",\n      },\n    },\n\n    // Educational Content\n    educational: {\n      introduction_title: \"Introduction à l'IMC\",\n      introduction_text:\n        \"L'IMC est une mesure de la maigreur ou de la corpulence d'une personne basée sur sa taille et son poids, et vise à quantifier la masse tissulaire. Il est largement utilisé comme indicateur général pour déterminer si une personne a un poids santé par rapport à sa taille.\",\n      introduction_usage:\n        \"Spécifiquement, la valeur obtenue du calcul de l'IMC est utilisée pour catégoriser si une personne est en insuffisance pondérale, poids normal, surpoids ou obèse selon la plage dans laquelle la valeur se situe. Ces plages d'IMC varient selon des facteurs comme la région et l'âge, et sont parfois subdivisées en sous-catégories comme insuffisance pondérale sévère ou obésité très sévère.\",\n\n      adult_table_title: \"Tableau IMC pour Adultes\",\n      adult_table_description:\n        \"Voici les recommandations de l'Organisation Mondiale de la Santé (OMS) pour le poids corporel basé sur les valeurs d'IMC pour les adultes. Il est utilisé pour les hommes et les femmes, âgés de 20 ans ou plus.\",\n\n      children_table_title: \"Tableau IMC pour Enfants et Adolescents, Âge 2-20\",\n      children_table_description:\n        \"Les Centres de Contrôle et de Prévention des Maladies (CDC) recommandent la catégorisation IMC pour les enfants et adolescents entre 2 et 20 ans.\",\n\n      classification: \"Classification\",\n      bmi_range: \"Plage IMC - kg/m²\",\n      category: \"Catégorie\",\n      percentile_range: \"Plage de Percentile\",\n      underweight: \"Insuffisance pondérale\",\n      healthy_weight: \"Poids Santé\",\n      at_risk_overweight: \"À Risque de Surpoids\",\n      overweight: \"Surpoids\",\n\n      overweight_risks_title: \"Risques Associés au Surpoids\",\n      overweight_risks_intro:\n        \"Le surpoids augmente le risque de nombreuses maladies graves et conditions de santé. Voici une liste de ces risques, selon les Centres de Contrôle et de Prévention des Maladies (CDC) :\",\n\n      cardiovascular_risks: \"Risques Cardiovasculaires\",\n      high_blood_pressure: \"Hypertension artérielle\",\n      cholesterol_issues: \"Niveaux élevés de cholestérol LDL, niveaux faibles de cholestérol HDL, et niveaux élevés de triglycérides\",\n      coronary_heart_disease: \"Maladie coronarienne\",\n      stroke: \"Accident vasculaire cérébral\",\n\n      metabolic_risks: \"Risques Métaboliques\",\n      type_2_diabetes: \"Diabète de type II\",\n      gallbladder_disease: \"Maladie de la vésicule biliaire\",\n      sleep_apnea: \"Apnée du sommeil et problèmes respiratoires\",\n      osteoarthritis: \"Arthrose, un type de maladie articulaire causée par la dégradation du cartilage articulaire\",\n\n      other_risks: \"Autres Risques de Santé\",\n      certain_cancers: \"Certains cancers (endomètre, sein, côlon, rein, vésicule biliaire, foie)\",\n      mental_health_issues: \"Maladies mentales comme la dépression clinique, l'anxiété et autres\",\n      reduced_quality_life: \"Qualité de vie réduite et douleurs corporelles\",\n      increased_mortality: \"Généralement, un risque accru de mortalité comparé à ceux avec un IMC sain\",\n\n      underweight_risks_title: \"Risques Associés à l'Insuffisance Pondérale\",\n      underweight_risks_intro: \"L'insuffisance pondérale a ses propres risques associés, listés ci-dessous :\",\n      malnutrition: \"Malnutrition, carences vitaminiques, anémie (capacité réduite de transport sanguin)\",\n      osteoporosis: \"Ostéoporose, une maladie qui cause la faiblesse osseuse, augmentant le risque de fracture\",\n      immune_function_decrease: \"Diminution de la fonction immunitaire\",\n      growth_development_issues: \"Problèmes de croissance et développement, particulièrement chez les enfants et adolescents\",\n      reproductive_issues: \"Problèmes reproductifs possibles pour les femmes dus aux déséquilibres hormonaux\",\n      surgery_complications: \"Complications potentielles résultant de chirurgie\",\n      increased_mortality_underweight: \"Généralement, un risque accru de mortalité comparé à ceux avec un IMC sain\",\n\n      adults_limitations: \"Chez les Adultes\",\n      older_adults_fat: \"Les adultes âgés ont tendance à avoir plus de graisse corporelle que les jeunes adultes avec le même IMC\",\n      women_fat_difference: \"Les femmes ont tendance à avoir plus de graisse corporelle que les hommes pour un IMC équivalent\",\n      athletes_muscle_mass:\n        \"Les individus musclés et athlètes très entraînés peuvent avoir des IMC plus élevés dus à une grande masse musculaire\",\n\n      children_limitations: \"Chez les Enfants et Adolescents\",\n      height_maturation_influence:\n        \"La taille et le niveau de maturation sexuelle peuvent influencer l'IMC et la graisse corporelle chez les enfants\",\n      fat_free_mass_difference: \"L'IMC pourrait résulter de niveaux accrus soit de graisse soit de masse maigre\",\n      population_accuracy: \"L'IMC est assez indicatif de la graisse corporelle pour 90-95% de la population\",\n\n      formulas_title: \"Formule IMC\",\n      metric_formula: \"Formule Métrique\",\n      imperial_formula: \"Formule Impériale\",\n      example: \"Exemple\",\n\n      bmi_prime_formula: \"Formule IMC Prime\",\n      bmi_prime_description: \"Rapport de votre IMC à la limite supérieure de l'IMC normal (25)\",\n\n      ponderal_index_title: \"Indice Pondéral\",\n      ponderal_index_explanation:\n        \"L'Indice Pondéral (IP) est similaire à l'IMC en ce qu'il mesure la maigreur ou la corpulence d'une personne basée sur sa taille et son poids. La principale différence entre l'IP et l'IMC est l'élévation au cube plutôt qu'au carré de la taille dans la formule. Bien que l'IMC puisse être un outil utile pour considérer de grandes populations, il n'est pas fiable pour déterminer la maigreur ou la corpulence chez les individus.\",\n      ponderal_index_metric_description: \"Indice Pondéral utilisant les unités métriques\",\n      ponderal_index_imperial_description: \"Indice Pondéral utilisant les unités impériales\",\n\n      medical_disclaimer_title: \"Avertissement Médical\",\n    },\n  },\n  levels: {\n    BEGINNER: \"Débutant\",\n    INTERMEDIATE: \"Intermédiaire\",\n    ADVANCED: \"Avancé\",\n  },\n  email_sent: \"Email envoyé\",\n  cant_send_email: \"Impossible d'envoyer l'email\",\n  logout: \"Déconnexion\",\n  verify_email: \"Vérifier votre email. ⚠️ Pensez à vérifier votre dossier SPAM.\",\n  verify_email_subtitle: \"Veuillez vérifier votre email pour continuer.\",\n  resend_email: \"Renvoyer l'email\",\n  resend_email_countdown: \"Renvoyer l'email dans {seconds} secondes\",\n  signin_error_subtitle: \"Veuillez vérifier vos identifiants et réessayer.\",\n  register_title: \"Créer un compte\",\n  register_description: \"Entrez vos informations ci-dessous pour créer votre compte\",\n  register_terms: \"En vous inscrivant, vous acceptez nos\",\n  register_privacy: \"Politique de confidentialité\",\n  register_privacy_link: \"et notre\",\n  register_privacy_link_2: \"Politique de confidentialité\",\n  password_forgot_title: \"Forgot password?\",\n  password_forgot_subtitle: \"Enter your email to reset your password\",\n  new_password: \"Nouveau mot de passe\",\n  new_password_placeholder: \"Entrez votre nouveau mot de passe\",\n  current_password: \"Mot de passe actuel\",\n  current_password_placeholder: \"Entrez votre mot de passe actuel\",\n  confirm_password: \"Confirmer le mot de passe\",\n  confirm_password_placeholder: \"Confirmez votre mot de passe\",\n\n  success: {\n    feedback_sent: \"Feedback envoyé\",\n    password_forgot_success: \"Email envoyé\",\n    reset_password_success: \"Mot de passe réinitialisé avec succès\",\n    password_updated_successfully: \"Mot de passe mis à jour avec succès\",\n  },\n\n  error: {\n    invalid_credentials: \"Identifiants invalides ou compte inexistant\",\n    upload_failed: \"Erreur lors du téléchargement\",\n    generic_error: \"Erreur lors de l'opération\",\n    sending_email: \"Erreur lors de l'envoi de l'email\",\n  },\n\n  backend_errors: {\n    EMAIL_ALREADY_EXISTS: \"Email déjà existant\",\n    INVALID_FILE_TYPE: \"Type de fichier invalide\",\n    FILE_TOO_LARGE: \"Fichier trop grand\",\n    NO_FILE_UPLOADED: \"Aucun fichier téléchargé\",\n    IMAGE_PROCESSING_ERROR: \"Erreur lors du traitement de l'image\",\n    upload_failed: \"Erreur lors du téléchargement\",\n  },\n\n  profile: {\n    new_workout: \"Nouvelle séance\",\n    alert: {\n      title: \"Votre progression est stockée dans votre navigateur.\",\n      create_account: \"Créer un compte\",\n      log_in: \"Se connecter\",\n      to_ensure_it_is_not_getting_lost: \"pour la sauvegarder.\",\n    },\n  },\n\n  // Release Notes\n  release_notes: {\n    title: \"Nouveautés\",\n    release_notes: \"Notes\",\n    notes: {\n      note_2025_10_29: {\n        title: \"🍑 Nouveau Programme Booty Disponible !\",\n        content:\n          \"<li>Un tout nouveau <a href='/programs/booty-pump' class='text-blue-500 hover:underline'>programme Booty</a> est maintenant disponible !</li><li>Ciblez et renforcez vos fessiers avec des entraînements spécialisés</li><li>Conçu pour des résultats maximaux et une croissance musculaire</li><li>Rejoignez le programme dès aujourd'hui ! 💪</li>\",\n      },\n      note_2025_08_18: {\n        title: \"🏆 Nouvelle Fonctionnalité Classement !\",\n        content:\n          \"<li>Nouveau <strong>classement</strong> pour concourir avec les autres champions d'entraînement</li><li>Voir les classements par périodes <strong>tous temps, mensuel et hebdomadaire</strong></li><li>Suivez votre position parmi les meilleurs performers</li><li>Motivez-vous pour gravir le classement ! 🚀</li>\",\n      },\n      note_2025_07_09: {\n        title: \"🎯 Sélection d'exercices, Favoris & Nouveaux Outils\",\n        content:\n          \"<li>Nouvelle <strong>sélection d'exercices</strong> lors de la création d'entraînements (étape 3)</li><li>Système d'<strong>exercices favoris</strong> pour marquer vos mouvements préférés</li><li>Nouveaux <em>outils fitness</em> : calculateur d'IMC et zones de fréquence cardiaque</li><li>Cartes de programmes améliorées</li><li>Nouveaux contributeurs rejoignent le projet ! 🚀</li>\",\n      },\n      note_2025_07_02: {\n        title: \"🛠️ Auto-hébergement, Russe & Nouveaux Outils\",\n        content:\n          \"Amélioration de l'<strong>auto-hébergement</strong>, ajout du support <strong>russe</strong>, et introduction de nouveaux <em>outils fitness</em> dont un calculateur de calories. 🚀\",\n      },\n      note_2025_06_23: {\n        title: \"🇵🇹 Support Portugais & Bannière de Don\",\n        content:\n          \"L'app supporte maintenant le <strong>portugais</strong> ! Nous avons aussi ajouté une <em>bannière de don</em> pour aider à supporter les coûts du projet via <a href='https://github.com/sponsors/snouzy' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>GitHub Sponsors</a> ou <a href='https://ko-fi.com/workoutcool' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Ko-fi</a>.\",\n      },\n      note_2025_06_22: {\n        title: \"🌍 Nouvelles langues & amélioration des performances !\",\n        content:\n          \"L'application est maintenant disponible en chinois et en russe ! Nous avons aussi amélioré les performances du glisser-déposer pour une expérience plus fluide. ⚡\",\n      },\n      note_2025_06_19: {\n        title: \"📱 Maintenant disponible en PWA !\",\n        content:\n          \"Workout.cool v1.2 est maintenant une Progressive Web App ! Installez-la sur votre téléphone pour une expérience d'application native avec accès hors ligne.\",\n      },\n      note_2025_06_18: {\n        title:\n          \"🚀 Numéro #1 sur <a href='https://news.ycombinator.com/item?id=44309320' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Hacker News</a> !\",\n        content:\n          \"Workout.cool a atteint le <strong>top spot</strong> sur Hacker News ! Merci à tous pour le support incroyable — bienvenue à tous les nouveaux utilisateurs ! 💪\",\n      },\n      note_2025_06_01: {\n        title: \"🎉 Nouveau : Dialogue des notes de version\",\n        content: \"Vous pouvez maintenant voir les nouveautés directement depuis l'en-tête ! Restez à l'écoute pour plus de mises à jour.\",\n      },\n      note_2025_05_20: {\n        title: \"Améliorations de l'interface\",\n        content: \"Amélioration de la réactivité mobile et ajout d'effets de survol subtils aux boutons.\",\n      },\n    },\n  },\n\n  // Premium Upsell Alert\n  donation_alert: {\n    title: \"Débloquez des fonctionnalités avancées avec Workout.cool Premium\",\n    or: \"ou\",\n  },\n\n  // Donation Modal\n  donation_modal: {\n    support_via: \"Soutenir via...\",\n    title: \"Soutenez le projet\",\n    congrats: \"Félicitations pour la séance ! 🎉\",\n    subtitle: \"Cette app vous aide gratuitement, mais elle a un coût réel pour moi...\",\n    costs_title: \"La réalité des coûts\",\n    costs_description:\n      \"Actuellement, les donations ne couvrent même pas les coûts de base : serveurs, authentification, infrastructure, base de données, etc.\",\n    open_source_title: \"100% Open Source\",\n    open_source_description:\n      \"Cette app est entièrement gratuite et open source. Aucun profit n'est généré - c'est un projet de passion pour aider la communauté et aider les gens à faire du sport.\",\n    no_ads: \"Pas de pub\",\n    no_tracking: \"Pas de tracking\",\n    impact_title: \"Votre impact\",\n    impact_3_euros: \"• Même 3€ couvrent 1 semaine de serveur\",\n    impact_support: \"• Votre soutien garde l'app gratuite pour tous\",\n    impact_footer: \"Chaque don, même petit, fait une vraie différence ! 🙏\",\n    later_button: \"Plus tard\",\n    support_button: \"Soutenir le projet\",\n  },\n\n  // Contact Support\n  contact_support: \"Contacter le support\",\n  contact_support_subtitle: \"Décrivez votre problème et nous vous aiderons dès que possible. Vous pouvez aussi nous écrire directement à\",\n\n  // Social Platforms\n  social_platforms: {\n    x: \"X (Twitter)\",\n    facebook: \"Facebook\",\n    email: \"Email\",\n    whatsapp: \"WhatsApp\",\n    website: \"Site web\",\n    phone: \"Téléphone\",\n    youtube: \"YouTube\",\n    linkedin: \"LinkedIn\",\n    snapchat: \"Snapchat\",\n    instagram: \"Instagram\",\n    tiktok: \"TikTok\",\n    threads: \"Threads\",\n  },\n\n  // Workout Builder\n  workout_builder: {\n    confirm_delete: \"Êtes-vous sûr de vouloir supprimer cette séance ?\",\n    steps: {\n      equipment: {\n        title: \"Équipement\",\n        description: \"Sélectionnez votre équipement\",\n      },\n      muscles: {\n        title: \"Muscles\",\n        description: \"Choisissez votre entraînement\",\n      },\n      exercises: {\n        title: \"Exercices\",\n        description: \"Personnalisez votre séance\",\n      },\n    },\n    muscles: {\n      abdominals: \"Abdominaux\",\n      adductors: \"Adducteurs\",\n      abductors: \"Abducteurs\",\n      back: \"Dos\",\n      biceps: \"Biceps\",\n      triceps: \"Triceps\",\n      chest: \"Pectoraux\",\n      shoulders: \"Épaules\",\n      quadriceps: \"Quadriceps\",\n      hamstrings: \"Ischio-jambiers\",\n      glutes: \"Fessiers\",\n      calves: \"Mollets\",\n      forearms: \"Avant-bras\",\n      traps: \"Trapèzes\",\n      obliques: \"Obliques\",\n      lats: \"Grands dorsaux\",\n    },\n    exercise: {\n      watch_video: \"Voir la vidéo\",\n      shuffle: \"Mélanger\",\n      pick: \"Choisir\",\n      remove: \"Supprimer\",\n      no_video_available: \"Aucune vidéo disponible.\",\n    },\n    loading: {\n      exercises: \"Chargement des exercices...\",\n    },\n    error: {\n      loading_exercises: \"Erreur lors du chargement des exercices\",\n    },\n    no_exercises_found: \"Aucun exercice trouvé. Essayez de changer vos équipements ou vos muscles sélectionnés.\",\n    addExercise: \"Ajouter un exercice\",\n    exerciseAdded: \"{name} ajouté à l'entraînement\",\n    exercises: \"exercices\",\n    equipment: {\n      bodyweight: {\n        label: \"Poids du corps\",\n        description: \"Exercices utilisant uniquement le poids de votre corps\",\n      },\n      dumbbell: {\n        label: \"Haltères\",\n        description: \"Exercices de poids libres avec haltères\",\n      },\n      barbell: {\n        label: \"Barre\",\n        description: \"Mouvements composés avec une barre\",\n      },\n      kettlebell: {\n        label: \"Kettlebell\",\n        description: \"Exercices dynamiques avec kettlebells\",\n      },\n      band: {\n        label: \"Élastique\",\n        description: \"Exercices avec bandes de résistance\",\n      },\n      plate: {\n        label: \"Disques\",\n        description: \"Exercices utilisant des disques de poids\",\n      },\n      pullup_bar: {\n        label: \"Barre de traction\",\n        description: \"Exercices du haut du corps avec barre de traction\",\n      },\n      bench: {\n        label: \"Banc\",\n        description: \"Exercices sur banc et support\",\n      },\n    },\n    navigation: {\n      previous: \"Précédent\",\n      continue: \"Continuer\",\n      complete: \"Terminer\",\n    },\n    stats: {\n      \"muscle_selected#zero\": \"0 muscle sélectionné\",\n      \"muscle_selected#one\": \"1 muscle sélectionné\",\n      \"muscle_selected#other\": \"{count} muscles sélectionnés\",\n      \"equipment_selected#zero\": \"0 équipement sélectionné\",\n      \"equipment_selected#one\": \"1 équipement sélectionné\",\n      \"equipment_selected#other\": \"{count} équipements sélectionnés\",\n      selected: \"Sélectionné\",\n      total: \"Total\",\n      equipment_ready: \"équipement prêt\",\n      equipment_ready_plural: \"équipements prêts\",\n    },\n    selection: {\n      choose_your_arsenal: \"Choisissez votre arsenal\",\n      select_equipment_description: \"Sélectionnez l'équipement pour débloquer des entraînements personnalisés\",\n      clear_all: \"Tout effacer\",\n      muscle_selection_coming_soon: \"Sélection des muscles (Bientôt disponible)\",\n      muscle_selection_description: \"Sélectionnez le(s) muscle(s) que vous voulez entraîner en cliquant dessus.\",\n      exercise_selection_coming_soon: \"Sélection des exercices (Bientôt disponible)\",\n      exercise_selection_description: \"Cette étape vous montrera des recommandations d'exercices personnalisées.\",\n    },\n    session: {\n      back_to_workout: \"Retour à l'entraînement\",\n      congrats: \"Bravo, séance terminée ! 🎉\",\n      congrats_subtitle: \"Tu l'as fait !\",\n      see_instructions: \"Voir les instructions\",\n      finish_set: \"Valider la série\",\n      finish_session: \"Terminer la séance\",\n      bodyweight: \"Poids du corps\",\n      weight: \"Poids\",\n      reps: \"Répétitions\",\n      time: \"Temps\",\n      next_exercise: \"Exercice suivant\",\n      add_set: \"Ajouter une série\",\n      add_column: \"Ajouter une colonne\",\n      add_row: \"Ajouter une ligne d'attributs\",\n      remove_column: \"Supprimer une colonne\",\n      set_number: \"Série {number}\",\n      set_number_plural: \"Séries {number}\",\n      set_number_singular: \"Série {number}\",\n      set_number_plural_singular: \"Séries {number}\",\n      workout_in_progress: \"Entraînement en cours\",\n      started_at: \"Débuté à\",\n      quit_workout: \"Quitter l'entraînement\",\n      elapsed_time: \"Temps écoulé\",\n      chronometer: \"Chronomètre\",\n      total_workout_time: \"Temps total d'entraînement\",\n      exercise_progress: \"Progression\",\n      total_volume: \"Volume Total\",\n      current_exercise: \"Exercice actuel\",\n      complete: \"Terminé\",\n      active: \"Actif\",\n      already_have_a_active_session: \"Vous avez déjà une séance active. Impossible de répéter sans terminer ou quitter l'entraînement.\",\n      no_exercise_selected: \"Aucun exercice sélectionné\",\n      quit_workout_title: \"Quitter l'entraînement ?\",\n      progress: \"Progression\",\n      quit_warning: \"Êtes-vous sûr de vouloir quitter ? Vous pouvez sauvegarder votre progression ou la perdre complètement.\",\n      save_and_quit: \"Sauvegarder & Quitter\",\n      quit_without_save: \"Quitter sans sauvegarder\",\n      continue_workout: \"Continuer l'entraînement\",\n      history: \"Historique des séances [{count}]\",\n      no_workout_yet: \"Aucune séance enregistrée.\",\n      start: \"début\",\n      end: \"fin\",\n      exercise: \"EXERCICE\",\n      repeat: \"Répéter\",\n      delete: \"Supprimer\",\n    },\n    attribute_value: {\n      bodyweight: \"Poids du corps\",\n      strength: \"Force\",\n      powerlifting: \"Powerlifting\",\n      calisthenic: \"Calisthénie\",\n      plyometrics: \"Plyométrie\",\n      stretching: \"Étirement\",\n      strongman: \"Strongman\",\n      cardio: \"Cardio\",\n      stabilization: \"Stabilisation\",\n      power: \"Puissance\",\n      resistance: \"Résistance\",\n      crossfit: \"CrossFit\",\n      weightlifting: \"Haltérophilie\",\n      neck: \"Cou\",\n      lats: \"Grands dorsaux\",\n      adductors: \"Adducteurs\",\n      abductors: \"Abducteurs\",\n      groin: \"Aine\",\n      full_body: \"Corps entier\",\n      rotator_cuff: \"Coiffe des rotateurs\",\n      hip_flexor: \"Fléchisseur de hanche\",\n      achilles_tendon: \"Tendon d'Achille\",\n      fingers: \"Doigts\",\n      smith_machine: \"Smith machine\",\n      other: \"Autre\",\n      ez_bar: \"Barre EZ\",\n      machine: \"Machine\",\n      desk: \"Bureau\",\n      none: \"Aucun\",\n      cable: \"Câble\",\n      medicine_ball: \"Medecine ball\",\n      swiss_ball: \"Swiss ball\",\n      foam_roll: \"Foam roll\",\n      trx: \"TRX\",\n      box: \"Box\",\n      ropes: \"Cordes\",\n      spin_bike: \"Vélo de spinning\",\n      step: \"Step\",\n      bosu: \"BOSU\",\n      tyre: \"Pneu\",\n      sandbag: \"Sac de sable\",\n      pole: \"Barre verticale\",\n      wall: \"Mur\",\n      bar: \"Barre\",\n      rack: \"Rack\",\n      car: \"Voiture\",\n      sled: \"Luge\",\n      chain: \"Chaîne\",\n      skierg: \"SkiErg\",\n      rope: \"Corde\",\n      na: \"N/A\",\n      isolation: \"Isolation\",\n      compound: \"Polyarticulaire\",\n    },\n  },\n  commons: {\n    upgrade_to_premium: \"Devenir Premium\",\n    last_activity: \"Dernière activité\",\n    registered_on: \"Inscrit le\",\n    refresh: \"Actualiser\",\n    just_now: \"à l'instant\",\n    signup_with: \"S'inscrire avec {provider}\",\n    signin_with: \"Se connecter avec {provider}\",\n    signup: \"S'inscrire\",\n    login: \"Se connecter\",\n    connecting: \"Connexion...\",\n    password_reset_success: \"Le mot de passe a été réinitialisé avec succès\",\n    login_to_your_account_title: \"Connectez-vous à votre compte\",\n    login_to_your_account_subtitle: \"Entrez vos identifiants ci-dessous pour vous connecter\",\n    password_forgot: \"Mot de passe oublié ?\",\n    dont_have_account: \"Vous n'avez pas de compte ?\",\n    already_have_account: \"Vous avez déjà un compte ?\",\n    or: \"Ou\",\n    add: \"Ajouter\",\n    your_feminine: \"ta\",\n    password: \"Mot de passe\",\n    email: \"Email\",\n    logout: \"Déconnexion\",\n    first_name: \"Prénom\",\n    last_name: \"Nom\",\n    verify_password: \"Vérifier le mot de passe\",\n    submit: \"Envoyer\",\n    upload: \"Télécharger\",\n    cancel: \"Annuler\",\n    save_changes: \"Enregistrer les modifications\",\n    change: \"Changer\",\n    subject: \"Sujet\",\n    message: \"Message\",\n    saving: \"Enregistrement...\",\n    edit: \"Modifier\",\n    more_options: \"Plus d'options\",\n    open_link: \"Ouvrir le lien\",\n    hide: \"Masquer\",\n    make_visible: \"Rendre visible\",\n    delete: \"Supprimer\",\n    share: \"Partager\",\n    title: \"Titre\",\n    subtitle: \"Sous-titre\",\n    content: \"Contenu\",\n    save: \"Enregistrer\",\n    button: \"Bouton\",\n    card: \"Carte\",\n    go_back: \"Retour\",\n    next: \"Suivant\",\n    choose_image: \"Choisir une image\",\n    soon: \"Bientôt\",\n    coming_soon_with_emoji: \"Bientôt disponible 🤫\",\n    no_image: \"Aucune image\",\n    description: \"Description\",\n    price: \"Prix\",\n    duration: \"Durée\",\n    location: \"Lieu\",\n    schedule: \"Horaire\",\n    participants_info: \"Informations sur les participants\",\n    title_placeholder: \"Entrez le titre\",\n    description_placeholder: \"Entrez la description\",\n    changes_saved: \"Les modifications ont été sauvegardées\",\n    replace: \"Remplacer\",\n    loading: \"Chargement...\",\n    image_deleted: \"L'image a été supprimée\",\n    discover_workoutcool: \"Découvrir gratuitement\",\n    received_just_now: \"Reçu à l'instant\",\n    copied: \"Copié\",\n    url_copied: \"L'URL a été copiée\",\n    copy_failed: \"Erreur lors de la copie de l'URL\",\n    accordion: \"Accordéon\",\n    image: \"Image\",\n    other: \"Autre\",\n    register: \"S'inscrire\",\n    instantly: \"instantanément\",\n    immediately: \"immédiatement\",\n    link: \"Lien\",\n    accept: \"Accepter\",\n    deny: \"Refuser\",\n    invalid_input: \"Saisie invalide. Veuillez vérifier les erreurs.\",\n    copy_url: \"Copier l'URL\",\n    page_url: \"URL de la page\",\n    saving_short: \"Enregistrement...\",\n    saved_short: \"Sauvegardé\",\n    looks_like_you_are_lost: \"Il semble que vous soyez perdu\",\n    the_page_you_are_looking_for_is_not_available: \"La page que vous cherchez n'est pas disponible\",\n    go_to_home: \"Retour à l'accueil\",\n    go_to_profile: \"Aller à mon profil\",\n    terms: \"Conditions d'utilisation\",\n    privacy: \"Politique de confidentialité\",\n    sales_terms: \"Conditions de vente\",\n    consent_banner: \"Nous utilisons des cookies pour améliorer votre expérience. En cliquant sur Accepter, vous acceptez nos cookies.\",\n    about: \"À propos\",\n    profile: \"Profil\",\n    donate: \"Faire un don\",\n    my_account: \"Mon compte\",\n    dashboard: \"Tableau de bord\",\n    home: \"Accueil\",\n    changelog: \"Annonces & notes de version\",\n    stop_impersonation_button: \"Arrêter l'impersonnalisation\",\n    impersonating_user_label: \"Impersonnification en cours\",\n    re_hello: \"Re Hello\",\n    back_to_login: \"Retour à la connexion\",\n    sending: \"Envoi...\",\n    send_me_link: \"Envoyer un lien\",\n    subscription: \"Abonnement\",\n    manage_subscription: \"Gérer abonnement\",\n    become_premium: \"Devenir Premium\",\n    remove_ads: \"Supprimer les pubs\",\n    coming_soon: \"Bientôt disponible\",\n    extremely_dissatisfied: \"Très insatisfait\",\n    somewhat_dissatisfied: \"Insatisfait\",\n    neutral: \"Neutre\",\n    satisfied: \"Satisfait\",\n    support: \"Support\",\n    change_language: \"Changer de langue\",\n    in_progress: \"En cours\",\n    close: \"Fermer\",\n    premium: \"Premium\",\n    free: \"Gratuit\",\n    new: \"Nouveau\",\n    monday: \"Lundi\",\n    tuesday: \"Mardi\",\n    wednesday: \"Mercredi\",\n    thursday: \"Jeudi\",\n    friday: \"Vendredi\",\n    saturday: \"Samedi\",\n    sunday: \"Dimanche\",\n    added_to_favorites: \"Ajouté aux favoris\",\n    add_to_favorites: \"Ajouter aux favoris\",\n    remove_from_favorites: \"Retirer des favoris\",\n    favorites: \"Favoris\",\n  },\n  statistics: {\n    title: \"Statistiques\",\n    page_subtitle: \"Suivez votre parcours fitness avec des analyses avancées et des informations personnalisées.\",\n    select_exercise: \"Sélectionner un exercice\",\n    active_daily_users: \"Utilisateurs actifs quotidiens\",\n    success_rate: \"Taux de réussite\",\n    user_rating: \"Note des utilisateurs\",\n\n    // Tabs\n    tabs: {\n      video: \"Vidéo\",\n      statistics: \"Statistiques\",\n    },\n\n    // Chart titles and labels\n    weight: \"Poids\",\n    volume: \"Volume\",\n    weight_progression: \"Progression du Poids\",\n    weight_progression_chart: \"Graphique de progression du poids\",\n    weekly_volume: \"Volume Hebdomadaire\",\n    volume_chart: \"Graphique de volume\",\n    estimated_1rm: \"1 Rep Max Estimé (1RM)\",\n    one_rep_max_chart: \"Graphique de répétition maximale\",\n    performance_over_time: \"Performance au Fil du Temps\",\n\n    // Form and controls\n    timeframe: \"Période\",\n    timeframe_selector: \"Sélecteur de période\",\n\n    // Timeframes\n    timeframes: {\n      \"4weeks\": \"4 semaines\",\n      \"8weeks\": \"8 semaines\",\n      \"12weeks\": \"12 semaines\",\n      \"1year\": \"1 an\",\n    },\n\n    // Error messages\n    error_loading_data: \"Erreur de chargement des données\",\n    error_loading_weight_progression: \"Erreur de chargement de la progression du poids\",\n    error_loading_1rm: \"Erreur de chargement des données 1RM\",\n    error_loading_volume: \"Erreur de chargement des données de volume\",\n\n    // Empty states\n    no_data_yet: \"Pas encore de données\",\n    start_tracking: \"Commencez à suivre pour voir votre progression\",\n    no_1rm_data: \"Aucune donnée 1RM disponible\",\n    complete_sets_with_weight: \"Complétez des séries avec poids pour voir votre 1 Rep Max (1RM)\",\n    no_volume_data: \"Aucune donnée de volume disponible\",\n    complete_workouts: \"Complétez des entraînements pour voir votre volume\",\n\n    // Info and tooltips\n    \"1rm_formula_info\": \"Informations sur la formule 1RM\",\n    volume_calculation: \"Volume = Poids × Reps × Séries\",\n    last_updated: \"Dernière mise à jour : {date}\",\n\n    // Premium\n    premium_required: \"Premium requis pour accéder aux statistiques\",\n\n    // StatisticsPreviewOverlay\n    premium_statistics: \"Statistiques Premium\",\n    premium_statistics_description:\n      \"Obtenez des informations détaillées sur votre parcours fitness avec des analyses avancées pour chaque exercice.\",\n    total_volume: \"Volume Total\",\n    pr_increase: \"Augmentation PR\",\n    weight_progress: \"Progression du Poids\",\n    upgrade_now: \"Mettre à Niveau Maintenant\",\n    rating: \"Note 4.8/5\",\n    no_ads: \"Pas de publicités\",\n    cancel_anytime: \"Annuler à tout moment\",\n    preview_notice: \"Ceci n'est qu'un aperçu ! 👀\",\n    preview_description: \"Débloquez l'accès complet aux analyses détaillées, au suivi des progrès et aux informations personnalisées.\",\n    get_premium_access: \"Obtenir l'Accès Premium\",\n\n    // ExercisesBrowser\n    all_equipment: \"Tous les équipements\",\n    all_muscles: \"Tous les muscles\",\n    search_exercises: \"Rechercher des exercices\",\n    error_loading_exercises: \"Erreur lors du chargement des exercices\",\n    no_exercises_found: \"Aucun exercice trouvé\",\n    equipment_label: \"Équipement\",\n    primary_muscle_label: \"Muscle principal\",\n    unknown: \"Inconnu\",\n    no_image_available: \"Aucune image disponible\",\n  },\n  heatmap: {\n    week_days_short: {\n      sunday: \"D\", // dimanche\n      monday: \"L\", // lundi\n      tuesday: \"M\", // mardi\n      wednesday: \"M\", // mercredi\n      thursday: \"J\", // jeudi\n      friday: \"V\", // vendredi\n      saturday: \"S\", // samedi\n    },\n    month_names_short: {\n      january: \"Jan\",\n      february: \"Fév\",\n      march: \"Mar\",\n      april: \"Avr\",\n      may: \"Mai\",\n      june: \"Juin\",\n      july: \"Juil\",\n      august: \"Août\",\n      september: \"Sep\",\n      october: \"Oct\",\n      november: \"Nov\",\n      december: \"Déc\",\n    },\n    \"workout#one\": \"séance\",\n    \"workout#other\": \"séances\",\n  },\n} as const;\n"
  },
  {
    "path": "locales/heart-rate-zones-translations.ts",
    "content": "// SEO-optimized translations for heart rate zones calculator\n\nexport const heartRateZonesTranslations = {\n  en: {\n    meta: {\n      title: \"Heart Rate Zones Calculator - Target Heart Rate & Training Zones\",\n      description: \"Calculate your personalized heart rate training zones with our free calculator. Basic & Karvonen formulas, age-based chart, complete guide to optimize your cardio workouts.\",\n      keywords: \"heart rate zones calculator, target heart rate, maximum heart rate, training zones, VO2 max zone, anaerobic zone, aerobic zone, fat burn zone, Karvonen formula, heart rate training, THR calculator, MHR calculator, cardio zones, fitness calculator, heart rate by age\"\n    },\n    page_title: \"Heart Rate Zones Calculator\",\n    page_subtitle: \"Discover your personalized training zones to optimize performance, burn more fat, and improve cardiovascular fitness\",\n    seo_content: {\n      intro_title: \"Complete Guide to Heart Rate Training Zones\",\n      intro_text: \"Heart rate zones are a scientific tool essential for optimizing your workouts and achieving your fitness goals. Whether you're looking to lose weight, improve endurance, or increase performance, understanding and using heart rate zones will transform your approach to exercise.\",\n      age_chart_title: \"Heart Rate Reference Chart by Age\",\n      zones_detail_title: \"The 5 Training Zones Explained in Detail\",\n      expert_tips_title: \"Expert Tips to Optimize Your Training\",\n      faq_title: \"Frequently Asked Questions About Heart Rate Zones\",\n      medical_warning_title: \"Important Medical Warning\"\n    }\n  },\n  es: {\n    meta: {\n      title: \"Calculadora de Zonas de Frecuencia Cardíaca - FC Máxima y Zonas de Entrenamiento\",\n      description: \"Calcula tus zonas de frecuencia cardíaca personalizadas con nuestra calculadora gratuita. Fórmulas Basic y Karvonen, tabla por edad, guía completa para optimizar tu cardio.\",\n      keywords: \"calculadora zonas frecuencia cardiaca, frecuencia cardiaca objetivo, frecuencia cardiaca máxima, zonas entrenamiento, zona VO2 max, zona anaeróbica, zona aeróbica, zona quema grasa, fórmula Karvonen, entrenamiento frecuencia cardiaca, calculadora FCM, zonas cardio, calculadora fitness\"\n    },\n    page_title: \"Calculadora de Zonas de Frecuencia Cardíaca\",\n    page_subtitle: \"Descubre tus zonas de entrenamiento personalizadas para optimizar el rendimiento, quemar más grasa y mejorar tu condición cardiovascular\",\n    seo_content: {\n      intro_title: \"Guía Completa de Zonas de Entrenamiento por Frecuencia Cardíaca\",\n      intro_text: \"Las zonas de frecuencia cardíaca son una herramienta científica esencial para optimizar tus entrenamientos y alcanzar tus objetivos fitness. Ya sea que busques perder peso, mejorar la resistencia o aumentar el rendimiento, comprender y usar las zonas cardíacas transformará tu enfoque del ejercicio.\",\n      age_chart_title: \"Tabla de Referencia de Frecuencia Cardíaca por Edad\",\n      zones_detail_title: \"Las 5 Zonas de Entrenamiento Explicadas en Detalle\",\n      expert_tips_title: \"Consejos de Expertos para Optimizar tu Entrenamiento\",\n      faq_title: \"Preguntas Frecuentes sobre las Zonas de Frecuencia Cardíaca\",\n      medical_warning_title: \"Advertencia Médica Importante\"\n    }\n  },\n  fr: {\n    meta: {\n      title: \"Calculateur de Zones de Fréquence Cardiaque - FCM et Zones d'Entraînement\",\n      description: \"Calculez vos zones de fréquence cardiaque personnalisées avec notre calculateur gratuit. Formules Basic et Karvonen, tableau par âge, guide complet pour optimiser vos entraînements cardio.\",\n      keywords: \"calculateur zones fréquence cardiaque, fréquence cardiaque cible, fréquence cardiaque maximale, zones entraînement, zone VO2 max, zone anaérobie, zone aérobie, zone combustion graisses, formule Karvonen, entraînement fréquence cardiaque, calculateur FCM, zones cardio, calculateur fitness\"\n    },\n    page_title: \"Calculateur de Zones de Fréquence Cardiaque\",\n    page_subtitle: \"Découvrez vos zones d'entraînement personnalisées pour optimiser vos performances, brûler plus de graisses et améliorer votre condition cardiovasculaire\",\n    seo_content: {\n      intro_title: \"Guide Complet des Zones de Fréquence Cardiaque pour l'Entraînement\",\n      intro_text: \"Les zones de fréquence cardiaque sont un outil scientifique essentiel pour optimiser vos entraînements et atteindre vos objectifs fitness. Que vous cherchiez à perdre du poids, améliorer votre endurance ou augmenter vos performances, comprendre et utiliser les zones cardiaques transformera votre approche de l'exercice.\",\n      age_chart_title: \"Tableau de Référence des Fréquences Cardiaques par Âge\",\n      zones_detail_title: \"Les 5 Zones d'Entraînement Expliquées en Détail\",\n      expert_tips_title: \"Conseils d'Expert pour Optimiser votre Entraînement\",\n      faq_title: \"Questions Fréquentes sur les Zones de Fréquence Cardiaque\",\n      medical_warning_title: \"Avertissement Médical Important\"\n    }\n  },\n  pt: {\n    meta: {\n      title: \"Calculadora de Zonas de Frequência Cardíaca - FC Máxima e Zonas de Treino\",\n      description: \"Calcule suas zonas de frequência cardíaca personalizadas com nossa calculadora gratuita. Fórmulas Basic e Karvonen, tabela por idade, guia completo para otimizar seu treino cardio.\",\n      keywords: \"calculadora zonas frequência cardíaca, frequência cardíaca alvo, frequência cardíaca máxima, zonas treino, zona VO2 max, zona anaeróbica, zona aeróbica, zona queima gordura, fórmula Karvonen, treino frequência cardíaca, calculadora FCM, zonas cardio, calculadora fitness\"\n    },\n    page_title: \"Calculadora de Zonas de Frequência Cardíaca\",\n    page_subtitle: \"Descubra suas zonas de treino personalizadas para otimizar o desempenho, queimar mais gordura e melhorar sua condição cardiovascular\",\n    seo_content: {\n      intro_title: \"Guia Completo das Zonas de Treino por Frequência Cardíaca\",\n      intro_text: \"As zonas de frequência cardíaca são uma ferramenta científica essencial para otimizar seus treinos e alcançar seus objetivos fitness. Seja para perder peso, melhorar a resistência ou aumentar o desempenho, compreender e usar as zonas cardíacas transformará sua abordagem do exercício.\",\n      age_chart_title: \"Tabela de Referência de Frequência Cardíaca por Idade\",\n      zones_detail_title: \"As 5 Zonas de Treino Explicadas em Detalhe\",\n      expert_tips_title: \"Dicas de Especialistas para Otimizar seu Treino\",\n      faq_title: \"Perguntas Frequentes sobre as Zonas de Frequência Cardíaca\",\n      medical_warning_title: \"Aviso Médico Importante\"\n    }\n  },\n  ru: {\n    meta: {\n      title: \"Калькулятор Зон Пульса - Целевой Пульс и Тренировочные Зоны\",\n      description: \"Рассчитайте персональные зоны пульса с помощью нашего бесплатного калькулятора. Формулы Basic и Карвонена, таблица по возрасту, полное руководство для оптимизации кардио тренировок.\",\n      keywords: \"калькулятор зон пульса, целевой пульс, максимальный пульс, тренировочные зоны, зона VO2 max, анаэробная зона, аэробная зона, зона жиросжигания, формула Карвонена, тренировка по пульсу, калькулятор ЧСС, кардио зоны, фитнес калькулятор\"\n    },\n    page_title: \"Калькулятор Зон Пульса\",\n    page_subtitle: \"Откройте персональные тренировочные зоны для оптимизации результатов, сжигания жира и улучшения сердечно-сосудистой системы\",\n    seo_content: {\n      intro_title: \"Полное Руководство по Тренировочным Зонам Пульса\",\n      intro_text: \"Зоны пульса - это научный инструмент, необходимый для оптимизации тренировок и достижения фитнес-целей. Хотите ли вы похудеть, улучшить выносливость или повысить производительность, понимание и использование зон пульса изменит ваш подход к упражнениям.\",\n      age_chart_title: \"Таблица Пульса по Возрасту\",\n      zones_detail_title: \"5 Тренировочных Зон - Подробное Объяснение\",\n      expert_tips_title: \"Советы Экспертов для Оптимизации Тренировок\",\n      faq_title: \"Часто Задаваемые Вопросы о Зонах Пульса\",\n      medical_warning_title: \"Важное Медицинское Предупреждение\"\n    }\n  },\n  \"zh-CN\": {\n    meta: {\n      title: \"心率区间计算器 - 目标心率和训练区间\",\n      description: \"使用我们的免费计算器计算您的个性化心率训练区间。Basic和Karvonen公式，按年龄分类表，优化有氧运动的完整指南。\",\n      keywords: \"心率区间计算器, 目标心率, 最大心率, 训练区间, VO2最大值区间, 无氧区间, 有氧区间, 燃脂区间, Karvonen公式, 心率训练, 心率计算器, 有氧区间, 健身计算器\"\n    },\n    page_title: \"心率区间计算器\",\n    page_subtitle: \"发现您的个性化训练区间，优化运动表现，燃烧更多脂肪，改善心血管健康\",\n    seo_content: {\n      intro_title: \"心率训练区间完整指南\",\n      intro_text: \"心率区间是优化锻炼和实现健身目标的重要科学工具。无论您是想减肥、提高耐力还是提升运动表现，理解和使用心率区间将改变您的运动方式。\",\n      age_chart_title: \"按年龄分类的心率参考表\",\n      zones_detail_title: \"5个训练区间详细解释\",\n      expert_tips_title: \"优化训练的专家建议\",\n      faq_title: \"关于心率区间的常见问题\",\n      medical_warning_title: \"重要医疗警告\"\n    }\n  }\n};"
  },
  {
    "path": "locales/pt.ts",
    "content": "export default {\n  leaderboard: {\n    title: \"Classificação\",\n    description: \"Campeões dos treinos\",\n    champion_badge: \"🏆 Campeão\",\n    runner_up_badge: \"🥈 Vice-campeão\",\n    third_place_badge: \"🥉 Terceiro lugar\",\n    workouts: \"treinos\",\n    second_place: \"2º lugar\",\n    third_place: \"3º lugar\",\n    unable_to_load: \"Não foi possível carregar a classificação\",\n    try_again_later: \"Tente novamente mais tarde\",\n    no_champions_yet: \"Ainda não há campeões\",\n    complete_first_workout: \"Complete o seu primeiro treino para reivindicar o trono!\",\n    member_since: \"Membro desde\",\n    workouts_per_week: \"treinos/semana\",\n    last_workout: \"Último treino\",\n    page_title: \"Classificação dos Campeões\",\n    page_subtitle: \"Suba ao topo e torne-se uma lenda do Workout.cool\",\n    period_all_time: \"Global\",\n    period_monthly: \"Mês\",\n    period_weekly: \"Semana\",\n    no_sessions_this_week: \"Sem sessões esta semana\",\n    no_sessions_this_month: \"Sem sessões este mês\",\n    registered_members_only: \"Apenas membros registados\",\n    registered_members_description: \"Crie uma conta para aparecer na classificação\",\n    reset_timezone: \"Reinicialização Europa/Paris\",\n    reset_timezone_description: \"As classificações semanais e mensais reiniciam à meia-noite hora de Paris\",\n  },\n  programs: {\n    available_programs: \"Programas disponíveis\",\n    exercises_in_session: \"Exercícios na sessão\",\n    start_session: \"Iniciar sessão\",\n    starting_session: \"Iniciando...\",\n    more_than: \"mais de\",\n    my_progress: \"Meu progresso\",\n    session: \"sessão\",\n    completed_feminine: \"concluídas\",\n    completed_sets: \"sessões concluídas\",\n    \"set#zero\": \"série\",\n    \"set#one\": \"série\",\n    \"set#other\": \"séries\",\n    error_starting_session: \"Erro ao iniciar a sessão\",\n    premium_session: \"Sessão Premium\",\n    premium_session_description: \"Esta sessão é parte do conteúdo premium. Pode ver os detalhes mas não pode realizar o treino.\",\n    premium_session_exercises: \"Exercícios incluídos\",\n    workout_description: \"Descrição da sessão\",\n    connect_to_access: \"Conecte-se para acessar\",\n    become_premium: \"Torne-se Premium\",\n    back_to_program: \"Voltar ao programa\",\n    no_equipment: \"Nenhum equipamento\",\n    workout_programs_title: \"Programas de treino (+ em curso de criação)\",\n    workout_programs: \"Programas de treino\",\n    workout_programs_description: \"Escolha o seu desafio e torne-se mais forte! 💪\",\n    no_programs_available: \"Nenhum programa disponível\",\n    no_programs_available_description: \"Os programas estarão disponíveis em breve!\",\n    auth_required: \"Autenticação requerida\",\n    auth_required_description: \"Precisa de iniciar sessão para acessar esta sessão de treino.\",\n    login_to_continue: \"Iniciar sessão para continuar\",\n    signup_to_continue: \"Registar para continuar\",\n    premium_required: \"Premium requerido\",\n    premium_required_description: \"Esta sessão é premium. Atualize para acessar todo o conteúdo premium.\",\n    upgrade_to_premium: \"Atualizar para Premium\",\n    completed: \"Concluído\",\n    about: \"Sobre\",\n    program: \"Programa\",\n    not_found: \"Programa não encontrado\",\n    characteristics: \"Características\",\n    weeks: \"semanas\",\n    sessions_per_week: \"sessões/semana\",\n    session_duration: \"min/sessão\",\n    \"your_coach#zero\": \"Seu coach\",\n    \"your_coach#one\": \"Seu coach\",\n    \"your_coach#other\": \"Seus coaches\",\n    community: \"Comunidade ativa\",\n    community_count: \"coolbuilders têm rejeitado\",\n    week_short: \"Sem.\",\n    week: \"Semana\",\n    exercises: \"exercícios\",\n    min_short: \"min\",\n    premium: \"Premium\",\n    free: \"Gratuito\",\n    continue: \"Continuar\",\n    join_cta: \"Inscrever-se\",\n    sessions: \"Sessões\",\n    check_out_program: \"Descubra este programa de treino!\",\n    program_completed: \"Programa terminado\",\n    share_success: \"Compartido com sucesso!\",\n    copied_to_clipboard: \"Link copiado!\",\n    share_failed: \"Erro ao compartilhar\",\n    important_info: \"Informações importantes\",\n    donation_teaser:\n      \"No início, funcionávamos com doações. Mas como pode imaginar, as doações não foram suficientes para cobrir os custos de desenvolvimento e funcionamento. Então criámos um pacote que nos ajudará a manter as luzes acesas e desbloquear alguns superpoderes pelo caminho.\",\n    new: \"NOVO\",\n    more_programs_coming_title: \"Mais programas em breve!\",\n    more_programs_coming_description:\n      \"Estamos a trabalhar duro para criar novos programas. Ao passar a premium agora, terá eles automaticamente. Obrigado pelo seu apoio. 🚀\",\n    coming_strength: \"Força & Músculo\",\n    coming_cardio: \"Cardio HIIT\",\n    coming_yoga: \"Yoga & Mobilidade\",\n    sessions_coming_soon: \"Sessões em breve!\",\n    sessions_in_creation: \"A nossa equipa está a trabalhar em sessões de qualidade para esta semana. Volte em breve! 🚀\",\n    welcome_modal: {\n      welcome_title: \"Bem-vindo ao {programTitle}!\",\n      subtitle: \"Prepare-se para superar seus limites! 💪\",\n      level_label: \"Nível\",\n      duration_label: \"Duração\",\n      frequency_label: \"Frequência\",\n      later_button: \"Mais tarde\",\n      start_button: \"Vamos lá!\",\n    },\n  },\n  premium: {\n    checkout_error: \"Erro ao processar o pagamento\",\n    premium_required_title: \"Premium Obrigatório\",\n    premium_required_subtitle: \"Este é um acesso premium. Atualize para Premium para aceder a todo o conteúdo premium.\",\n    premium_required_button: \"Atualizar para Premium\",\n    already_premium: \"Está a desfrutar do Workout.cool Premium\",\n    no_ads: \"Sem anúncios\",\n    upgrade: \"Atualizar\",\n\n    pricing: {\n      month: \"month\",\n      year: \"year\",\n      monthly: \"Monthly\",\n      yearly: \"Yearly\",\n      discount: \"-48%\",\n    },\n\n    // Hero Section\n    hero: {\n      badge: \"Open-Source & Auto-hospedagem SEMPRE gratuitos\",\n      title: \"Treine livremente, apoie a missão\",\n      subtitle: \"Para aqueles que acreditam no projeto e querem (re)acreditar em si mesmos com power boosters !\",\n      stats: {\n        athletes: {\n          count: \"12.4K+\",\n          label: \"Atletas ativos\",\n        },\n        series: {\n          count: \"1.2M+\",\n          label: \"Séries registadas\",\n        },\n        rating: {\n          count: \"4.9/5\",\n          label: \"Avaliação da comunidade\",\n        },\n        progression: {\n          count: \"+23%\",\n          label: \"Progressão média\",\n        },\n      },\n\n      // Health Risks\n      health_risks: {\n        overweight: {\n          high_blood_pressure: \"Pressão arterial alta\",\n          ldl_cholesterol: \"Níveis elevados de colesterol LDL (colesterol mau)\",\n          hdl_cholesterol: \"Níveis baixos de colesterol HDL (colesterol bom)\",\n          triglycerides: \"Níveis elevados de triglicéridos\",\n          type_2_diabetes: \"Diabetes tipo II\",\n          coronary_heart_disease: \"Doença coronária\",\n          stroke: \"Acidente vascular cerebral\",\n          gallbladder_disease: \"Doença da vesícula biliar\",\n          osteoarthritis: \"Osteoartrite\",\n          sleep_apnea: \"Apneia do sono e problemas respiratórios\",\n          certain_cancers: \"Certos cancros (endometrial, mama, cólon, rim, vesícula biliar, fígado)\",\n          low_quality_life: \"Baixa qualidade de vida\",\n          mental_illnesses: \"Doenças mentais como depressão clínica e ansiedade\",\n          body_pains: \"Dores corporais e dificuldade com funções físicas\",\n          increased_mortality: \"Risco geralmente aumentado de mortalidade\",\n        },\n        underweight: {\n          malnutrition: \"Desnutrição e deficiências vitamínicas\",\n          anemia: \"Anemia (capacidade reduzida para transportar oxigénio no sangue)\",\n          osteoporosis: \"Osteoporose (risco aumentado de fraturas ósseas)\",\n          immune_function: \"Função imunitária diminuída\",\n          growth_development: \"Problemas de crescimento e desenvolvimento (especialmente em crianças)\",\n          reproductive_issues: \"Problemas reprodutivos em mulheres devido a desequilíbrios hormonais\",\n          miscarriage_risk: \"Maior probabilidade de aborto espontâneo no primeiro trimestre\",\n          surgery_complications: \"Complicações potenciais durante cirurgias\",\n          increased_mortality: \"Risco geralmente aumentado de mortalidade\",\n          underlying_conditions: \"Pode indicar condições médicas subjacentes\",\n        },\n      },\n    },\n\n    // Mission Banner\n    mission: {\n      supporters_count: \"234\",\n      supporters_text: \"suportadores a ajudar a missão\",\n      limited: \"Limitado\",\n      early_access: \"lugares de acesso antecipado\",\n    },\n\n    // Plans\n    plans: {\n      monthly: \"Mensal\",\n      yearly: \"Anual\",\n      yearly_discount: \"-48%\",\n      per_month: \"/mês\",\n      per_year: \"/ano\",\n\n      free: {\n        name: \"GRATUITO\",\n        price: \"€0\",\n        period: \"/para sempre\",\n        price_label: \"€0/para sempre\",\n        badge: \"Open-Source • Sempre Gratuito\",\n        description: \"Todas as funções essenciais para treinar\",\n        features: [\n          \"Gerador de exercícios com vídeos\",\n          \"Histórico de treinos tipo GitHub (6 meses)\",\n          \"Partilhar e repetir sessões (em breve)\",\n          \"Auto-hospedagem possível\",\n          \"Código fonte disponível\",\n        ],\n        button: \"O seu plano atual\",\n        footer_note: \"Não é necessário registo • Acesso completo para sempre\",\n      },\n\n      premium: {\n        name: \"PREMIUM ⭐\",\n        price_label: \"€7.90/mês ou €49/ano\",\n        badge: \"MAIS POPULAR • Para entusiastas\",\n        description: \"Todas as funcionalidades + acesso antecipado\",\n        footer_monthly: \"Junte-se à comunidade apaixonada! 🔥\",\n        footer_yearly: \"Obrigado pelo apoio anual! 🙏\",\n        yearly_price_note: \"/mês\",\n        features: [\n          \"...todo do plano Gratuito\",\n          \"Sem publicidade\",\n          \"Histórico ilimitado (vs 6 meses gratuito)\",\n          \"Acompanhamento de progresso com estatísticas avançadas (volume, progressão, PR)\",\n          \"Programas de treino pré-concebidos\",\n          \"Chat privado com um coach 1:1\",\n          \"Acesso antecipado a novas funcionalidades\",\n        ],\n      },\n    },\n\n    // Buttons and Actions\n    actions: {\n      processing: \"A processar...\",\n      go_premium: \"Tornar-se Premium\",\n      sign_in_continue: \"Tornar-se Premium\",\n      upgrade_now: \"Atualizar agora\",\n      current_plan: \"O seu plano atual\",\n    },\n\n    // Trust Elements\n    trust: {\n      gdpr_compliant: \"100% conforme com o RGPD\",\n      money_back: \"30 dias de garantia de devolução de dinheiro\",\n      cancel_anytime: \"1 clique para cancelar, sem compromisso\",\n      secure_payment: \"Pagamento seguro via Stripe\",\n    },\n\n    // Feature Comparison\n    comparison: {\n      title: \"Comparação de funcionalidades detalhada\",\n      subtitle: \"Tudo o que precisa de saber sobre o que está incluído em cada plano\",\n      features_label: \"Funcionalidades\",\n      headers: {\n        features: \"Funcionalidades\",\n        free: \"Gratuito\",\n        premium: \"Premium\",\n      },\n      categories: {\n        equipment: \"Equipamento & Exercícios\",\n        tracking: \"Acompanhamento & Análises\",\n        programs: \"Programas & IA\",\n        community: \"Comunidade & Compartilhamento\",\n        support: \"Suporte & Projeto\",\n      },\n      features: {\n        exercise_library: \"Biblioteca de exercícios\",\n        custom_exercise: \"Exercício personalizado\",\n        video_tutorials: \"Tutoriais em vídeo\",\n        workout_history: \"Histórico de treinos\",\n        progress_statistics: \"Estatísticas de progresso\",\n        personal_records: \"Acompanhamento de recordes pessoais\",\n        volume_analytics: \"Análises de volume & progresso\",\n        predesigned_programs: \"Programas pré-designados\",\n        personalized_recommendations: \"Recomendações personalizadas\",\n        pro_templates: \"Modelos profissionais (Powerlifting, bodybuilding, etc.)\",\n        community_access: \"Acesso à comunidade\",\n        discord_community: \"Comunidade Discord\",\n        private_chat: \"Chat privado 1:1 com o coach\",\n        community_support: \"Suporte comunitário\",\n        priority_support: \"Suporte prioritário\",\n        early_access: \"Acesso antecipado a funcionalidades\",\n        beta_testing: \"Teste beta\",\n      },\n      values: {\n        basic: \"Básico\",\n        complete: \"Completo\",\n        unlimited: \"Ilimitado\",\n        professional: \"Profissional\",\n        six_months: \"6 meses\",\n        limited: \"Limitado\",\n        all_programs: \"Todos os programas\",\n        public: \"Público\",\n        vip_access: \"Acesso VIP\",\n        private_channels: \"Canais privados\",\n        soon: \"Em breve\",\n        hd_slowmo: \"4K + Slow-mo\",\n        early_access: \"Acesso antecipado\",\n      },\n    },\n\n    // FAQ\n    faq: {\n      title: \"Perguntas frequentes\",\n      subtitle: \"Tudo o que precisa de saber sobre Workout.cool e a nossa missão\",\n      items: [\n        {\n          question: \"Porque pagar se é open-source?\",\n          answer:\n            \"Excelente pergunta! O código sempre permanecerá gratuito, mas manter servidores, base de dados e infraestrutura custa dinheiro. A sua contribuição ajuda-nos a manter a ferramenta gratuita para todos. É um modelo vantajoso: você obtém funcionalidades premium, a comunidade mantém acesso gratuito!\",\n        },\n        {\n          question: \"Posso auto-hospedar o Workout.cool?\",\n          answer:\n            \"Absolutamente! Todo o código está disponível no GitHub sob licença MIT. Pode implantá-lo em seus próprios servidores, personalizá-lo como quiser e usá-lo completamente gratuitamente. Auto-hospedagem dá-lhe controlo total sobre os seus dados e privacidade do treino.\",\n        },\n        {\n          question: \"As minhas dados de treino estão seguros?\",\n          answer:\n            \"Sim! Somos conformes com o RGPD, usamos conexões encriptadas e armazenamos os seus dados com segurança. Além disso, como somos open-source, pode auditar as nossas práticas de segurança. Também pode exportar os seus dados a qualquer momento ou auto-hospedar para ter controlo total.\",\n        },\n        {\n          question: \"Posso cancelar a minha subscrição a qualquer momento?\",\n          answer:\n            \"Claro! Sem contratos, sem compromissos. Pode cancelar com um clique a qualquer momento. Manterá acesso até o fim do seu período de faturação atual, e pode sempre reiniciar mais tarde. Os seus dados de treino permanecem acessíveis mesmo se downgrade para gratuito.\",\n        },\n        {\n          question: \"Existem exercícios para iniciantes?\",\n          answer:\n            \"Claro! A nossa biblioteca de exercícios cobre todos os níveis de aptidão desde os mais iniciantes até aos atletas avançados. Vídeos e instruções ajudam os iniciantes a encontrar exercícios apropriados, e os nossos tutoriais em vídeo mostram a forma correta.\",\n        },\n        {\n          question: \"Como funciona o acompanhamento do progresso?\",\n          answer:\n            \"Cada série, repetição, peso e tempo é automaticamente registado. Obtém um histórico de treinos estilo GitHub que mostra a sua consistência, mais análises detalhadas sobre volume, progressão e recordes pessoais. Os utilizadores Premium obtêm gráficos avançados e insights.\",\n        },\n        {\n          question: \"Can I import data from other apps?\",\n          answer:\n            \"Em breve. Vamos suportar a importação de dados em CSV para dados básicos (reps & peso). Se está a mudar de outra aplicação de fitness, a nossa equipa de suporte pode ajudar a migrar o seu histórico de treinos.\",\n        },\n        {\n          question: \"Does the app work offline?\",\n          answer:\n            \"O acompanhamento do treino funciona offline. Pode registar séries e repetições sem ligação à internet para 10 treinos. Vídeos de exercícios e sincronização na nuvem requerem ligação à internet. Todos os seus dados offline sincronizam automaticamente quando volta online.\",\n        },\n        {\n          question: \"Are there programs for women?\",\n          answer:\n            \"Claro! E haverá mais programas no futuro. Estamos a trabalhar nisso. Os planos Suporte e Premium incluirão todos os programas especializados futuros para diferentes objetivos: força, tonificação, levantamento de peso, musculação e mais!\",\n        },\n        {\n          question: \"Can I create my own programs?\",\n          answer: \"Infelizmente, não. Estamos a trabalhar nisso!\",\n        },\n      ],\n      additional_support: {\n        title: \"Ainda tem perguntas?\",\n        description: \"A nossa comunidade focada no fitness está aqui para ajudar-lhe a ter sucesso\",\n        community: \"Suporte comunitário (discord ou hello@workout.cool)\",\n        discussions: \"Discussões abertas (github/discord)\",\n        roadmap: \"Roadmap transparente (github)\",\n      },\n    },\n\n    // Final CTA\n    final_cta: {\n      motivation: \"Continue a esforçar-se! 💪\",\n      title: \"Pronto para apoiar a missão?\",\n      subtitle: \"Junte-se a milhares de entusiastas do fitness que acreditam na liberdade de treino open-source\",\n      values: [\n        {\n          title: \"Comunidade primeiro\",\n          description: \"Construído por e para a comunidade fitness\",\n        },\n        {\n          title: \"Sempre transparente\",\n          description: \"Código open-source, financiamento transparente\",\n        },\n        {\n          title: \"Projeto de amor\",\n          description: \"15 anos de paixão!\",\n        },\n      ],\n      quote: {\n        text: \"Acreditamos que as ferramentas de fitness devem ser acessíveis a todos. O seu apoio ajuda-nos a manter esta visão enquanto continuamos a inovar.\",\n        author: \"— A equipa Workout.cool\",\n      },\n    },\n\n    // Premium Active State\n    premium_active: {\n      title: \"Premium Ativo! 💪\",\n      supporting: \"Apoiando a missão\",\n    },\n\n    // Legacy translations (keeping for compatibility)\n    premium_active_title: \"Premium Ativo\",\n    premium_active_subtitle: \"Todos os recursos desbloqueados\",\n    free_intro_title: \"Já está a receber muito gratuitamente...\",\n    free_intro_text:\n      \"Workout.cool é uma aplicação de fitness gratuita e open source usada diariamente por mais de 60.000 utilizadores. É construída com amor (não com dinheiro de VC ^^) e custa-nos tempo e dinheiro real mantê-la a funcionar.\",\n    donation_story_text:\n      \"No início, funcionávamos com doações. Mas como pode imaginar, as doações não foram suficientes para cobrir os custos de desenvolvimento e funcionamento. Então criámos um pacote que nos ajudará a manter as luzes acesas e desbloquear alguns superpoderes pelo caminho.\",\n    health_upgrade_text: \"Se o Workout.cool o ajuda a melhorar a sua saúde, por favor considere tornar-se Premium :D !\",\n    unlock_features_text: \"Desbloqueie recursos avançados e apoie o fitness open source\",\n    invest_yourself_quote: \"Nunca poupe em fitness e livros — invista em si mesmo!\",\n    support_mission: \"Apoiar a missão\",\n    best_value_badge: \"MELHOR VALOR\",\n    annual_plan: \"Anual\",\n    monthly_plan: \"Mensal\",\n    discount_badge: \"40% de desconto\",\n    per_month: \"/mês\",\n    feature_all_programs: \"Todos os programas de treino\",\n    feature_progress_tracking: \"Acompanhamento do progresso\",\n    coming_soon: \"(em breve)\",\n    feature_future_updates: \"Todos os futuros programas e atualizações\",\n    feature_priority_support: \"Suporte prioritário\",\n    save_yearly: \"Poupe 40% anualmente\",\n    processing: \"A processar...\",\n    cta_annual: \"Quero apoiar + poupar 40%\",\n    cta_monthly: \"Desbloquear plano completo\",\n    thank_supporting: \"Obrigado pelo seu apoio.\",\n    no_pressure: \"Sem pressão. Pode fazer upgrade a qualquer momento.\",\n    keep_pushing: \"continue a esforçar-se! huhu\",\n    still_unsure: \"Ainda não tem certeza? Sem problemas. O Workout.cool permanecerá sempre gratuito e open source.\",\n    support_helps: \"Mas se acredita no que estamos a construir e pode pagar, o seu apoio ajudará 💚\",\n    self_hosting: \"Auto-hospedagem\",\n    community: \"Comunidade\",\n    mit_license: \"Licença MIT\",\n    pricing_year: \"ano\",\n    pricing_month: \"mês\",\n    conversion_flow_title: \"A redirecionar...\",\n    conversion_flow_message: \"Sessão iniciada com sucesso! A redirecionar para o checkout...\",\n    redirecting_to_checkout: \"A redirecionar para o checkout\",\n  },\n  breadcrumbs: {\n    home: \"Início\",\n  },\n  bottom_navigation: {\n    statistics: \"Estatísticas\",\n    statistics_tooltip: \"Ver as suas estatísticas\",\n    programs: \"Programas\",\n    programs_tooltip: \"Pesquisar programas\",\n    workouts: \"Treinos\",\n    workouts_tooltip: \"Criar o seu próprio treino\",\n    premium: \"Premium\",\n    premium_tooltip: \"Torne-se Premium\",\n    leaderboard: \"Classificação\",\n    leaderboard_tooltip: \"Ver ranking de treinos\",\n    tools: \"Ferramentas\",\n    tools_tooltip: \"Explorar ferramentas\",\n    profile: \"Perfil\",\n    profile_tooltip: \"Ver o seu perfil\",\n  },\n  tools: {\n    try_now: \"Experimentar agora\",\n    title: \"Ferramentas de Fitness\",\n    subtitle: \"Calculadoras essenciais para otimizar o seu treino e nutrição\",\n    moreComingSoon: \"Mais ferramentas em breve\",\n    meta: {\n      title: \"Ferramentas de Fitness - Calculadoras para Treino e Nutrição\",\n      description:\n        \"Calculadoras gratuitas de fitness: TDEE, macros, IMC, zonas de frequência cardíaca, 1RM e mais. Otimize o seu treino e nutrição com as nossas ferramentas essenciais.\",\n      keywords:\n        \"calculadora fitness, calculadora calorias, calculadora macros, calculadora IMC, calculadora TDEE, zonas frequência cardíaca, repetição máxima, ferramentas fitness\",\n    },\n    \"calorie-calculator\": {\n      title: \"Calculadora de Calorias\",\n      description: \"Calcule as suas necessidades calóricas diárias (TDEE) baseadas no seu nível de atividade e objetivos\",\n      meta: {\n        title: \"Calculadora de Calorias - TDEE e Necessidades Calóricas Diárias\",\n        description:\n          \"Calcule o seu Gasto Energético Total Diário (TDEE) e necessidades calóricas diárias. Obtenha recomendações personalizadas para perda de peso, manutenção ou ganho muscular.\",\n        keywords:\n          \"calculadora calorias, calculadora TDEE, calorias diárias, calculadora perda peso, necessidades calóricas, calculadora TMB, calculadora metabolismo\",\n      },\n      subtitle: \"Calcule as suas necessidades calóricas diárias baseadas na equação de Mifflin-St Jeor\",\n      how_it_works: \"Como funciona esta calculadora?\",\n      how_it_works_description:\n        \"Esta calculadora usa fórmulas cientificamente comprovadas para estimar as suas necessidades calóricas diárias baseadas nas suas características pessoais e estilo de vida.\",\n      how_it_works_step1: \"Calculamos o seu metabolismo basal (calorias queimadas em repouso)\",\n      how_it_works_step2: \"Ajustamos baseado no seu nível de atividade\",\n      how_it_works_step3: \"Personalizamos de acordo com o seu objetivo (perder, manter ou ganhar peso)\",\n      calculate: \"Calcular\",\n      calculating: \"A calcular...\",\n      tap_info_icons: \"Toque nos ícones ℹ️ para mais informações\",\n      gender: \"Género\",\n      male: \"Masculino\",\n      female: \"Feminino\",\n      units: \"Unidades\",\n      metric: \"Métrico\",\n      imperial: \"Imperial\",\n      age: \"Idade\",\n      age_placeholder: \"Insira a sua idade\",\n      years: \"anos\",\n      height: \"Altura\",\n      height_placeholder: \"Insira a sua altura\",\n      weight: \"Peso\",\n      weight_placeholder: \"Insira o seu peso\",\n      cm: \"cm\",\n      kg: \"kg\",\n      lbs: \"lbs\",\n      feet: \"pés\",\n      inches: \"polegadas\",\n      activity_level: \"Nível de Atividade\",\n      activity: {\n        sedentary: \"Sedentário\",\n        sedentary_desc: \"Pouco ou nenhum exercício, trabalho de secretária, caminhada mínima\",\n        light: \"Ligeiramente Ativo\",\n        light_desc: \"Exercício leve 1-3 dias/semana, ou caminhada diária\",\n        moderate: \"Moderadamente Ativo\",\n        moderate_desc: \"Exercício moderado 3-5 dias/semana, estilo de vida ativo\",\n        active: \"Muito Ativo\",\n        active_desc: \"Exercício intenso 6-7 dias/semana, trabalho muito ativo\",\n        very_active: \"Extremamente Ativo\",\n        very_active_desc: \"Atleta, trabalho físico + treino diário\",\n      },\n      goal: \"Objetivo\",\n      goals: {\n        lose_fast: \"Perder Peso Rapidamente\",\n        lose_fast_desc: \"Perder 2 lbs (1 kg) por semana - Agressivo mas eficaz\",\n        lose_slow: \"Perder Peso\",\n        lose_slow_desc: \"Perder 1 lb (0,5 kg) por semana - Sustentável e saudável\",\n        maintain: \"Manter Peso\",\n        maintain_desc: \"Manter o peso atual - Perfeito para manter a forma\",\n        gain_slow: \"Ganhar Peso\",\n        gain_slow_desc: \"Ganhar 1 lb (0,5 kg) por semana - Construção muscular limpa\",\n        gain_fast: \"Ganhar Peso Rapidamente\",\n        gain_fast_desc: \"Ganhar 2 lbs (1 kg) por semana - Crescimento muscular máximo\",\n      },\n      results: {\n        title: \"Os Seus Resultados\",\n        bmr: \"TMB\",\n        bmr_explanation:\n          \"Taxa Metabólica Basal (TMB) é o número de calorias que o seu corpo queima em repouso completo, apenas para manter funções básicas como respiração, circulação e produção celular. Esta é a energia mínima que o seu corpo precisa para sobreviver.\",\n        tdee: \"TDEE\",\n        tdee_explanation:\n          \"Gasto Energético Total Diário (TDEE) é a sua TMB mais as calorias queimadas através de atividades diárias e exercícios. Este é o número total de calorias que queima num dia baseado no seu nível de atividade.\",\n        target: \"Calorias Alvo\",\n        macros: \"Macros Recomendados\",\n        macros_explanation:\n          \"Macronutrientes (macros) são os três principais grupos de nutrientes que o seu corpo precisa: Proteínas (para construção e reparação muscular), Hidratos de Carbono (para energia) e Gorduras (para hormonas e absorção de vitaminas). As percentagens mostradas são uma distribuição equilibrada adequada para a maioria dos objetivos de fitness.\",\n        protein: \"Proteína\",\n        carbs: \"Hidratos de Carbono\",\n        fat: \"Gordura\",\n        disclaimer:\n          \"Estes cálculos são estimativas baseadas em fórmulas médias. As necessidades calóricas reais podem variar baseadas em fatores individuais. Consulte um profissional de saúde ou nutricionista registado para conselhos personalizados.\",\n      },\n      faq: {\n        title: \"Perguntas Frequentes\",\n        q1: \"Porque é que o meu alvo calórico é diferente de outras calculadoras?\",\n        a1: \"Diferentes calculadoras podem usar diferentes fórmulas ou multiplicadores de atividade. Nós usamos a equação de Mifflin-St Jeor, que é considerada uma das mais precisas para a maioria das pessoas. No entanto, o metabolismo individual pode variar 10-20% destas estimativas.\",\n        q2: \"Devo comer exatamente estas calorias todos os dias?\",\n        a2: \"Estes são alvos médios diários. É normal comer ligeiramente mais alguns dias e menos noutros. Foque-se na sua média semanal em vez de ser exato todos os dias. Ouça os sinais de fome e saciedade do seu corpo.\",\n        q3: \"E se não estiver a ver resultados após seguir estas recomendações?\",\n        a3: \"Se não estiver a ver resultados após 2-3 semanas, pode precisar de ajustar. O seu metabolismo real pode ser mais alto ou baixo que o calculado. Tente ajustar em 100-200 calorias e monitore por mais 2 semanas. Também certifique-se de que está a registar a sua comida com precisão.\",\n        q4: \"As recomendações de macros são adequadas para todos?\",\n        a4: \"A divisão 30/40/30 (proteína/hidratos/gordura) é uma abordagem equilibrada adequada para a maioria das pessoas. No entanto, atletas, pessoas com condições médicas, ou aqueles que seguem dietas específicas (keto, vegana, etc.) podem precisar de rácios diferentes. Consulte um nutricionista para recomendações personalizadas.\",\n      },\n    },\n    \"macro-calculator\": {\n      title: \"Calculadora de Macros\",\n      description: \"Encontre a distribuição ótima de proteínas, hidratos de carbono e gorduras para os seus objetivos de fitness\",\n    },\n    \"bmi-calculator\": {\n      title: \"Calculadora de IMC\",\n      description: \"Calcule o seu Índice de Massa Corporal e compreenda a sua categoria de peso\",\n    },\n    \"heart-rate-calculator\": {\n      title: \"Zonas de Frequência Cardíaca\",\n      description: \"Descubra as suas zonas de treino ótimas para queima de gordura e performance\",\n    },\n    \"heart-rate-zones\": {\n      title: \"Calculadora de Zonas de Frequência Cardíaca\",\n      description: \"Calcule suas zonas de treino de frequência cardíaca ideais para desempenho máximo e queima de gordura\",\n      page_title: \"Calculadora de Zonas de Frequência Cardíaca\",\n      page_description:\n        \"Calcule suas zonas de treino de frequência cardíaca personalizadas usando fórmulas cientificamente comprovadas. Otimize seus treinos de cardio para queima de gordura, resistência e desempenho.\",\n      meta: {\n        title: \"Calculadora de Zonas de Frequência Cardíaca - Frequência Alvo e Zonas de Treino\",\n        description:\n          \"Calcule sua frequência cardíaca máxima e suas zonas de treino personalizadas. Use as fórmulas básicas ou de Karvonen para encontrar suas zonas de VO2 Máx, Anaeróbia, Aeróbia, Queima de Gordura e Aquecimento.\",\n        keywords:\n          \"calculadora zonas frequência cardíaca, frequência cardíaca alvo, frequência cardíaca máxima, zonas de treino, zona VO2 máx, zona anaeróbia, zona aeróbia, zona queima de gordura, fórmula Karvonen, treino frequência cardíaca\",\n      },\n      calculate: \"Calcular Zonas\",\n      calculating: \"Calculando...\",\n      method: \"Método de Cálculo\",\n      method_info: \"Escolha a fórmula que melhor se adapta ao seu nível de condicionamento e aos dados disponíveis\",\n      methods: {\n        basic: \"Básica por Idade\",\n        basic_desc: \"Fórmula simples usando apenas a idade – boa para iniciantes\",\n        karvonen_age: \"Karvonen por Idade e FCR\",\n        karvonen_age_desc: \"Mais precisa usando idade e frequência cardíaca de repouso\",\n        karvonen_custom: \"Karvonen por FCM e FCR\",\n        karvonen_custom_desc: \"A mais precisa usando frequências cardíacas máxima e de repouso medidas\",\n      },\n      age: \"Idade\",\n      age_placeholder: \"Insira sua idade\",\n      resting_heart_rate: \"Frequência Cardíaca de Repouso (FCR)\",\n      resting_heart_rate_placeholder: \"Insira sua FCR\",\n      resting_heart_rate_info: \"Meça sua frequência cardíaca ao acordar, antes de se levantar. A faixa normal é 60–100 bpm.\",\n      max_heart_rate: \"Frequência Cardíaca Máxima (FCM)\",\n      max_heart_rate_placeholder: \"Insira sua FCM\",\n      max_heart_rate_info:\n        \"Sua frequência cardíaca máxima real de um teste de esforço ou treino em intensidade máxima. Mais precisa que estimativas baseadas na idade.\",\n\n      results: {\n        overview: \"Visão Geral\",\n        title: \"Suas Zonas de Frequência Cardíaca\",\n        max_heart_rate: \"Frequência Cardíaca Máxima\",\n        heart_rate_reserve: \"Reserva de Frequência Cardíaca\",\n        target_zones: \"Zonas de Treino Alvo\",\n        zone: \"Zona\",\n        intensity: \"Intensidade\",\n        heart_rate_range: \"Frequência Cardíaca (bpm)\",\n        benefits: \"Benefícios\",\n        duration: \"Duração Típica\",\n      },\n      zones: {\n        warm_up: {\n          name: \"Zona de Aquecimento\",\n          intensity: \"50–60%\",\n          benefits: \"🧘 Aquecimento ideal\",\n          example: \"Caminhada leve\",\n          duration: \"5–10 minutos\",\n          description: \"Intensidade muito leve para aquecimento e recuperação\",\n        },\n        fat_burn: {\n          name: \"Zona de Queima de Gordura\",\n          intensity: \"60–70%\",\n          benefits: \"🔥 Queima gordura\",\n          example: \"Corrida leve\",\n          duration: \"20–40 minutos\",\n          description: \"Intensidade leve, ritmo confortável para treinos mais longos\",\n        },\n        aerobic: {\n          name: \"Zona Aeróbia\",\n          intensity: \"70–80%\",\n          benefits: \"💪 Melhora a resistência\",\n          example: \"Corrida moderada\",\n          duration: \"10–40 minutos\",\n          description: \"Intensidade moderada, sustentável por períodos prolongados\",\n        },\n        anaerobic: {\n          name: \"Zona Anaeróbia\",\n          intensity: \"80–90%\",\n          benefits: \"⚡ Aumenta a velocidade\",\n          example: \"Sprint curto\",\n          duration: \"2–10 minutos\",\n          description: \"Intensidade alta, desafiadora mas sustentável por curtos períodos\",\n        },\n        vo2_max: {\n          name: \"Zona VO2 Máx\",\n          intensity: \"90–100%\",\n          benefits: \"🏆 Desempenho máximo\",\n          example: \"Sprint intenso\",\n          duration: \"30 segundos–2 minutos\",\n          description: \"Intensidade máxima, sustentável apenas por períodos muito curtos\",\n        },\n      },\n      formulas: {\n        basic_formula: \"Fórmula Básica\",\n        basic_explanation: \"FCC = FCM × %Intensidade\",\n        karvonen_formula: \"Fórmula de Karvonen\",\n        karvonen_explanation: \"FCC = [(FCM – FCR) × %Intensidade] + FCR\",\n        mhr_calculation: \"FCM = 220 – Idade\",\n      },\n      abbreviations: {\n        thr: \"FCC = Frequência Cardíaca Alvo\",\n        mhr: \"FCM = Frequência Cardíaca Máxima\",\n        rhr: \"FCR = Frequência Cardíaca de Repouso\",\n        hrr: \"RFC = Reserva de Frequência Cardíaca\",\n        bpm: \"bpm = Batimentos Por Minuto\",\n      },\n      tips: {\n        title: \"Dicas de Treino\",\n        tip1: \"Comece por zonas de baixa intensidade se for iniciante\",\n        tip2: \"Misture diferentes zonas no seu treino semanal para melhores resultados\",\n        tip3: \"Use um monitor de frequência cardíaca para acompanhamento preciso durante os treinos\",\n        tip4: \"Suas zonas podem mudar conforme seu condicionamento melhora – recalcule periodicamente\",\n      },\n      faq: {\n        title: \"Perguntas Frequentes\",\n        q1: \"Qual método de cálculo devo usar?\",\n        a1: \"Se for iniciante, use o método Básico. Se conhecer sua frequência de repouso, use Karvonen por Idade para mais precisão. Para as zonas mais precisas, use Karvonen com FCM e FCR medidas.\",\n        q2: \"Como medir minha frequência cardíaca de repouso?\",\n        a2: \"Meça seu pulso por 60 segundos logo ao acordar, antes de se levantar. Faça isso por 3–5 dias e use a média. FCR normal é 60–100 bpm, valores mais baixos indicam melhor condicionamento.\",\n        q3: \"Em qual zona devo treinar para perder peso?\",\n        a3: \"A Zona de Queima de Gordura (60–70%) é ótima para usar gordura como combustível. Mas zonas de maior intensidade queimam mais calorias totais. Misture as zonas para melhores resultados – inclua treinos de queima de gordura e alta intensidade.\",\n        q4: \"Qual a precisão da fórmula 220–idade?\",\n        a4: \"É uma estimativa geral que funciona para a maioria, mas pode variar ±10–15 bpm. Para mais precisão, considere um teste supervisionado de FCM ou use Karvonen com suas medidas reais.\",\n        q5: \"Posso treinar na zona VO2 Máx todos os dias?\",\n        a5: \"Não, a zona VO2 Máx é muito intensa e deve ser usada apenas 1–2 vezes por semana em intervalos curtos. A maior parte do treino deve ser nas zonas Aeróbia e Queima de Gordura para construir resistência e permitir recuperação.\",\n      },\n      guide: {\n        title: \"Guia Completo das Zonas de Frequência Cardíaca para Treino\",\n        text1:\n          \"As zonas de frequência cardíaca são uma ferramenta científica essencial para otimizar seus treinos e atingir seus objetivos de fitness. Quer você queira perder peso, melhorar sua resistência ou aumentar seu desempenho, compreender e usar as zonas cardíacas vai transformar sua abordagem ao exercício.\",\n        text2:\n          \"Esta calculadora usa fórmulas validadas cientificamente para determinar suas zonas personalizadas com base em sua idade e, opcionalmente, em sua frequência cardíaca de repouso. Cada zona corresponde a uma intensidade específica e oferece benefícios únicos para sua saúde cardiovascular.\",\n      },\n      table: {\n        title: \"Tabela de Referência de Frequências Cardíacas por Idade\",\n        col1: \"Idade\",\n        col2: \"FCM\",\n        col3: \"50% Intensidade\",\n        col4: \"85% Intensidade\",\n        avertiser: \"* Estes valores são médias. Sua FCM real pode variar ±10–15 bpm.\",\n      },\n      details: {\n        title: \"As 5 Zonas de Treino Explicadas em Detalhes\",\n        benefits: \"Benefícios\",\n        zone1_title: \"Zona 1 : Aquecimento (50–60% FCM)\",\n        zone1_content:\n          \"A zona de aquecimento é ideal para iniciar uma sessão, recuperar entre intervalos ou terminar um treino. Nesta intensidade, você pode manter uma conversa sem ficar sem fôlego.\",\n        zone1_details_1: \"Melhora a circulação sanguínea\",\n        zone1_details_2: \"Prepara músculos e articulações\",\n        zone1_details_3: \"Reduz risco de lesões\",\n        zone1_details_4: \"Favorece recuperação ativa\",\n        zone1_duration: \"Duração recomendada\",\n        zone1_duration_value: \"5–10 minutos no início/fim\",\n        zone1_duration_value_2: \"20–30 minutos para recuperação ativa\",\n        zone2_title: \"Zona 2 : Queima de Gordura (60–70% FCM)\",\n        zone2_content:\n          \"Nesta zona, seu corpo usa principalmente gordura como fonte de energia. É a intensidade ideal para desenvolver resistência básica e melhorar eficiência metabólica.\",\n        zone2_details_1: \"Maximiza uso de gordura\",\n        zone2_details_2: \"Desenvolve resistência aeróbia\",\n        zone2_details_3: \"Melhora eficiência cardíaca\",\n        zone2_details_4: \"Reforça sistema imunológico\",\n        zone2_duration: \"Duração recomendada\",\n        zone2_duration_value: \"30–90 minutos para resistência\",\n        zone2_duration_value_2: \"45–60 minutos para perda de peso\",\n        zone3_title: \"Zona 3 : Aeróbia (70–80% FCM)\",\n        zone3_content:\n          \"A zona aeróbia melhora significativamente sua capacidade cardiovascular. Você respira mais forte mas ainda consegue falar frases curtas. É a zona principal de treino para a maioria dos atletas.\",\n        zone3_details_1: \"Aumenta capacidade pulmonar\",\n        zone3_details_2: \"Melhora resistência cardiovascular\",\n        zone3_details_3: \"Fortalece o coração\",\n        zone3_details_4: \"Otimiza uso de oxigênio\",\n        zone3_duration: \"Duração recomendada\",\n        zone3_duration_value: \"20–60 minutos contínuos\",\n        zone3_duration_value_2: \"Intervalos de 5–15 minutos\",\n        zone4_title: \"Zona 4 : Anaeróbia (80–90% FCM)\",\n        zone4_content:\n          \"Na zona anaeróbia, seu corpo produz lactato mais rápido do que pode eliminar. Essa intensidade desenvolve potência e velocidade, mas não pode ser mantida por muito tempo.\",\n        zone4_details_1: \"Aumenta potência muscular\",\n        zone4_details_2: \"Melhora tolerância ao lactato\",\n        zone4_details_3: \"Desenvolve velocidade\",\n        zone4_details_4: \"Reforça o mental\",\n        zone4_duration: \"Duração recomendada\",\n        zone4_duration_value: \"Intervalos de 2–8 minutos\",\n        zone4_duration_value_2: \"Recuperação igual ou dupla\",\n        zone5_title: \"Zona 5 : VO2 Máx (90–100% FCM)\",\n        zone5_content:\n          \"A zona VO2 Máx representa o esforço máximo. Nesta intensidade, você só consegue dizer poucas palavras e não aguenta além de alguns minutos. Reservada a atletas experientes.\",\n        zone5_details_1: \"Maximiza capacidade aeróbia\",\n        zone5_details_2: \"Melhora economia de corrida\",\n        zone5_details_3: \"Desenvolve potência máxima\",\n        zone5_details_4: \"Empurra limites mentais\",\n        zone5_duration: \"Duração recomendada\",\n        zone5_duration_value: \"Intervalos de 30s–2 minutos\",\n        zone5_duration_value_2: \"Máximo 1–2 vezes por semana\",\n      },\n      educational: {\n        title: \"Entendendo o Treino por Frequência Cardíaca\",\n        description: \"Visualiza fácilmente cada zona de treino\",\n        what_are_zones: {\n          title: \"O Que São Zonas de Frequência Cardíaca?\",\n          content:\n            \"As zonas de frequência cardíaca são faixas de batimentos por minuto que correspondem a diferentes intensidades de exercício. Treinar em zonas específicas ajuda a alcançar objetivos de fitness de forma mais eficiente.\",\n        },\n        why_use_zones: {\n          title: \"Por Que Usar Zonas de Frequência Cardíaca?\",\n          content:\n            \"Treinar com zonas de frequência cardíaca garante que você esteja se exercitando na intensidade certa para seus objetivos. Isso previne overtraining, maximiza resultados e ajuda a treinar de forma mais eficiente.\",\n        },\n        zone_distribution: {\n          title: \"Distribuição Semanal Recomendada de Zonas\",\n          content:\n            \"Para um condicionamento equilibrado: 80% nas Zonas 1–3 (base aeróbia), 15% na Zona 4 (limiar), 5% na Zona 5 (VO2 máx). Ajuste conforme seus objetivos e nível de condicionamento.\",\n        },\n        monitoring: {\n          title: \"Como Monitorar Sua Frequência Cardíaca\",\n          content:\n            \"Use uma cinta peitoral para mais precisão ou um monitor de pulso para conveniência. Verifique regularmente sua frequência durante o exercício e ajuste a intensidade para permanecer na zona alvo.\",\n        },\n      },\n      training_tips: {\n        title: \"Dicas de Especialista para Otimizar Seu Treino\",\n        tip1: {\n          title: \"Aquecimento Progressivo\",\n          description: \"Comece sempre com 5–10 minutos na zona 1 (50–60%) para preparar seu sistema cardiovascular.\",\n        },\n        tip2: {\n          title: \"Regra 80/20\",\n          description: \"80% do treino em zonas 1–3 (aeróbia), 20% em zonas 4–5 (anaeróbia) para desenvolvimento ideal.\",\n        },\n        tip3: {\n          title: \"Recuperação Ativa\",\n          description: \"Após esforço intenso, reduza gradualmente para zona 1–2 por 5–10 minutos.\",\n        },\n        tip4: {\n          title: \"Hidratação Contínua\",\n          description: \"Beba antes, durante e após o exercício. Desidratação aumenta a frequência cardíaca.\",\n        },\n        tip5: {\n          title: \"Sono Reparador\",\n          description: \"7–9 horas de sono para melhor recuperação e FCR mais baixa.\",\n        },\n        tip6: {\n          title: \"Progressão Gradual\",\n          description: \"Aumente intensidade ou duração no máximo 10% por semana para evitar overtraining.\",\n        },\n      },\n      training_tips_2: {\n        title: \"Dicas Práticas\",\n        title1: \"Encontre sua Zona\",\n        description1: \"Cada zona tem um objetivo diferente. Escolha conforme sua meta!\",\n        title2: \"Duração Recomendada\",\n        description2: \"Quanto maior a intensidade, mais curta deve ser a duração.\",\n        title3: \"Progressão\",\n        description3: \"Comece devagar e aumente a intensidade gradualmente.\",\n        title4: \"Ouça seu Corpo\",\n        description4: \"Se sentir mal, diminua imediatamente.\",\n      },\n      quick_facts: {\n        title: \"Você Sabia?\",\n        fact1: \"220 – sua idade = Frequência Cardíaca Máxima aproximada\",\n        fact2: \"Meça seu pulso ao acordar para conhecer sua frequência de repouso\",\n        fact3: \"Um relógio inteligente pode acompanhar sua frequência em tempo real\",\n        fact4: \"80% do treino deve ser nas zonas 1–3\",\n      },\n      weekly_plan: {\n        title: \"Plano Semanal Exemplo\",\n        description: \"Um exemplo de semana de treino equilibrado\",\n        monday: {\n          title: \"Zona 1–2\",\n          description: \"30–45 min\",\n        },\n        tuesday: {\n          title: \"Zona 2–3\",\n          description: \"45–60 min\",\n        },\n        wednesday: {\n          title: \"Descanso\",\n          description: \"Recuperação\",\n        },\n        thursday: {\n          title: \"Zona 3–4\",\n          description: \"30–40 min\",\n        },\n        friday: {\n          title: \"Zona 1–2\",\n          description: \"30 min\",\n        },\n        saturday: {\n          title: \"Zona 4–5\",\n          description: \"20–30 min\",\n        },\n        tips: \"💡 Adapte este plano ao seu nível e objetivos!\",\n        cta: \"⬆️ Calcular minhas zonas agora\",\n      },\n      seo_faq_title: \"Perguntas Frequentes sobre Zonas de Frequência Cardíaca\",\n      seo_faq_q1_question: \"O que é a frequência cardíaca máxima (FCM)?\",\n      seo_faq_q1_answer:\n        \"A frequência cardíaca máxima é o número máximo de batimentos por minuto que seu coração pode alcançar durante esforço intenso. Geralmente calculada como: 220 – sua idade. Pode variar ±10–15 bpm.\",\n      seo_faq_q2_question: \"Como medir minha frequência cardíaca de repouso?\",\n      seo_faq_q2_answer:\n        \"Meça seu pulso ao acordar, antes de se levantar. Conte os batimentos por 60 segundos ou por 15 segundos e multiplique por 4. Repita por 3–5 dias e use a média. FCR normal é 60–100 bpm.\",\n      seo_faq_q3_question: \"Qual zona é melhor para perder peso?\",\n      seo_faq_q3_answer:\n        \"A zona de queima de gordura (60–70% FCM) é ótima para usar gordura como combustível. Entretanto, zonas mais intensas queimam mais calorias totais. Para perda de peso eficaz, alterne entre diferentes zonas.\",\n      seo_faq_q4_question: \"Posso treinar na zona VO2 Máx todos os dias?\",\n      seo_faq_q4_answer:\n        \"Não, a zona VO2 Máx (90–100% FCM) é extremamente intensa e deve ser usada apenas 1–2 vezes por semana em curtos períodos (30s–2 min). A maior parte do treino deve ser em zonas aeróbias.\",\n      seo_faq_q5_question: \"A fórmula 220–idade é precisa?\",\n      seo_faq_q5_answer:\n        \"É uma estimativa geral que funciona para a maioria, mas pode variar ±10–15 bpm. Para mais precisão, use a fórmula de Karvonen com sua FCR ou faça um teste de esforço supervisionado.\",\n      seo_faq_q6_question: \"Como saber se estou na zona certa?\",\n      seo_faq_q6_answer:\n        \"Use um monitor cardíaco para maior precisão. Sem equipamento, faça o teste da fala: zona leve = conversa fácil, zona moderada = frases curtas, zona intensa = palavras isoladas.\",\n      seo_faq_q7_question: \"As zonas mudam com a melhora do condicionamento?\",\n      seo_faq_q7_answer:\n        \"Sim, com o treino sua frequência de repouso diminui e a eficiência cardíaca melhora. Recalcule suas zonas a cada 2–3 meses para ajustar o treino.\",\n      seo_faq_q8_question: \"Qual a diferença entre as fórmulas Basic e Karvonen?\",\n      seo_faq_q8_answer:\n        \"A fórmula Basic usa apenas a idade (THR = FCM × %Intensidade). A fórmula Karvonen é mais precisa porque considera sua FCR: THR = [(FCM – FCR) × %Intensidade] + FCR.\",\n      intern_links_title: \"Pronto para Otimizar seus Treinos?\",\n      intern_links_subtitle: \"Use nossa calculadora para descobrir suas zonas personalizadas e transforme seu fitness\",\n      intern_links_button: \"Calcular Minhas Zonas Agora\",\n      intern_links_bmi_title: \"Calculadora de IMC\",\n      intern_links_bmi_description: \"Avalie seu índice de massa corporal\",\n      intern_links_calorie_title: \"Calculadora de Calorias\",\n      intern_links_calorie_description: \"Determine suas necessidades calóricas diárias\",\n      intern_links_macro_title: \"Calculadora de Macronutrientes\",\n      intern_links_macro_description: \"Otimize sua distribuição nutricional\",\n      cta: {\n        title: \"Pronto para Otimizar seus Treinos?\",\n        subtitle: \"Use nossa calculadora para descobrir suas zonas personalizadas e transforme seu fitness\",\n        button: \"Calcular Minhas Zonas Agora\",\n        bmi_title: \"Calculadora de IMC\",\n        bmi_description: \"Avalie seu índice de massa corporal\",\n        calorie_title: \"Calculadora de Calorias\",\n        calorie_description: \"Determine suas necessidades calóricas diárias\",\n        macro_title: \"Calculadora de Macronutrientes\",\n        macro_description: \"Otimize sua distribuição nutricional\",\n      },\n      medical_warning_title: \"Aviso Médico Importante\",\n      medical_warning_content:\n        \"Esta calculadora fornece estimativas baseadas em fórmulas gerais. Os resultados podem variar conforme seu condicionamento, medicamentos e estado de saúde. Consulte sempre um profissional de saúde antes de iniciar um novo programa de exercícios, especialmente se tiver condições médicas prévias ou sentir sintomas incomuns durante o exercício.\",\n    },\n    \"one-rep-max\": {\n      title: \"Calculadora 1RM\",\n      description: \"Estime a sua repetição máxima e planeie as percentagens do seu treino de força\",\n    },\n    back_to_calculators: \"Voltar às calculadoras\",\n    body_fat_percentage: \"Percentagem de Gordura Corporal\",\n    body_fat_info_title: \"O que é a Percentagem de Gordura Corporal?\",\n    body_fat_info_content:\n      \"A percentagem de gordura corporal é essencial para as fórmulas de Katch-McArdle e Cunningham pois calculam baseadas na massa magra corporal. Se não conhece a sua % de gordura corporal exata, use guias visuais online ou scans DEXA para precisão.\",\n    \"calorie-calculator-hub\": {\n      title: \"Fórmulas da Calculadora de Calorias\",\n      subtitle: \"Escolha a melhor fórmula para as suas necessidades e obtenha cálculos de calorias precisos\",\n      meta: {\n        title: \"Fórmulas da Calculadora de Calorias - Calculadoras TMB e TDEE\",\n        description:\n          \"Compare diferentes fórmulas de TMB: Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham e Oxford. Escolha a melhor calculadora de calorias para as suas necessidades.\",\n        keywords:\n          \"fórmulas TMB, comparação calculadora calorias, Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham, Oxford, calculadora TDEE\",\n      },\n      which_formula: \"Que Fórmula Devo Escolher?\",\n      formula_explanation:\n        \"Diferentes fórmulas funcionam melhor para diferentes pessoas. Aqui está um guia rápido para o ajudar a escolher:\",\n      recommendation_general: \"Melhor fórmula geral, mais precisa para a população geral\",\n      recommendation_traditional: \"Fórmula clássica, amplamente usada mas ligeiramente menos precisa\",\n      recommendation_bodyfat: \"Mais precisa se conhecer a sua percentagem de gordura corporal\",\n      since: \"Desde\",\n      all_formulas: \"Todas as fórmulas\",\n      popularity: \"Popularidade\",\n      accuracy: \"Precisão\",\n      accuracy_high: \"Alta\",\n      accuracy_good: \"Boa\",\n      accuracy_medium: \"Média\",\n      best_for: \"Melhor para\",\n      best_for_general: \"Uso geral\",\n      best_for_traditional: \"Tradicional\",\n      best_for_athletes: \"Atletas\",\n      best_for_bodybuilders: \"Fisiculturistas\",\n      best_for_european: \"População europeia\",\n      best_for_comparison: \"Comparar todas\",\n      \"mifflin-st-jeor\": {\n        title: \"Mifflin-St Jeor (Recomendada)\",\n        description: \"Fórmula mais precisa para a população geral, desenvolvida em 1990. Atualmente o padrão ouro para cálculos de TMB.\",\n      },\n      \"harris-benedict\": {\n        title: \"Harris-Benedict (Clássica)\",\n        description: \"Versão revista de 1984 da fórmula clássica. Amplamente usada mas tende a sobrestimar calorias para algumas pessoas.\",\n      },\n      \"katch-mcardle\": {\n        title: \"Katch-McArdle (Atletas)\",\n        description:\n          \"Baseada na massa magra corporal. Mais precisa para pessoas que conhecem a sua percentagem de gordura corporal e são fisicamente ativas.\",\n      },\n      cunningham: {\n        title: \"Cunningham (Fisiculturistas)\",\n        description:\n          \"Projetada para atletas muito magros e fisiculturistas com baixa gordura corporal. Usa cálculo da massa magra corporal.\",\n      },\n      oxford: {\n        title: \"Oxford (Europeia)\",\n        description: \"Fórmula mais recente (2005) baseada em populações europeias. Tem em conta faixas etárias.\",\n      },\n      comparison: {\n        title: \"Comparar Todas as Fórmulas\",\n        description: \"Compare resultados de todas as fórmulas lado a lado para ver as diferenças e escolher o que funciona melhor para si.\",\n      },\n    },\n    \"mifflin-st-jeor\": {\n      title: \"Calculadora Mifflin-St Jeor\",\n      subtitle: \"O padrão ouro para cálculo de TMB - mais precisa para a população geral\",\n      meta: {\n        title: \"Calculadora Mifflin-St Jeor - TMB e TDEE Mais Precisos\",\n        description:\n          \"Calcule a sua TMB e TDEE usando a equação Mifflin-St Jeor - a fórmula mais precisa para a população geral. Obtenha recomendações calóricas personalizadas.\",\n        keywords:\n          \"calculadora Mifflin-St Jeor, calculadora TMB, calculadora TDEE, calculadora calorias mais precisa, calculadora metabolismo\",\n      },\n      how_it_works: \"Como Funciona a Fórmula Mifflin-St Jeor\",\n      how_it_works_description:\n        \"Desenvolvida em 1990, esta fórmula é considerada a mais precisa para calcular a Taxa Metabólica Basal (TMB) em adultos saudáveis. É mais precisa que a equação Harris-Benedict e é amplamente recomendada por nutricionistas e profissionais de fitness.\",\n    },\n    \"harris-benedict\": {\n      title: \"Calculadora Harris-Benedict\",\n      subtitle: \"Fórmula clássica de TMB - a abordagem tradicional para cálculo de calorias\",\n      meta: {\n        title: \"Calculadora Harris-Benedict - Fórmula Clássica TMB e TDEE\",\n        description:\n          \"Calcule a sua TMB e TDEE usando a equação Harris-Benedict revista (1984). A fórmula clássica que iniciou os cálculos modernos de calorias.\",\n        keywords: \"calculadora Harris-Benedict, calculadora TMB clássica, calculadora TDEE tradicional, fórmula Harris-Benedict revista\",\n      },\n      how_it_works: \"Como Funciona a Fórmula Harris-Benedict\",\n      how_it_works_description:\n        \"Originalmente desenvolvida em 1919 e revista em 1984, a equação Harris-Benedict foi uma das primeiras fórmulas para calcular TMB. Embora ligeiramente menos precisa que fórmulas mais recentes, continua amplamente usada e fornece boas estimativas para a maioria das pessoas.\",\n    },\n    \"katch-mcardle\": {\n      title: \"Calculadora Katch-McArdle\",\n      subtitle: \"Cálculo preciso de TMB baseado na massa magra corporal - ideal para atletas\",\n      meta: {\n        title: \"Calculadora Katch-McArdle - TMB e TDEE da Massa Magra\",\n        description:\n          \"Calcule a sua TMB e TDEE usando a fórmula Katch-McArdle baseada na massa magra corporal. Mais precisa para pessoas que conhecem a sua percentagem de gordura corporal.\",\n        keywords:\n          \"calculadora Katch-McArdle, TMB massa magra, calculadora percentagem gordura corporal, calculadora TMB atleta, TDEE preciso\",\n      },\n      how_it_works: \"Como Funciona a Fórmula Katch-McArdle\",\n      how_it_works_description:\n        \"Esta fórmula calcula TMB baseada na massa magra corporal em vez do peso corporal total, tornando-a mais precisa para pessoas que conhecem a sua percentagem de gordura corporal. É particularmente útil para atletas e indivíduos fisicamente ativos.\",\n    },\n    cunningham: {\n      title: \"Calculadora Cunningham\",\n      subtitle: \"Fórmula de TMB projetada para atletas muito magros e fisiculturistas\",\n      meta: {\n        title: \"Calculadora Cunningham - TMB para Atletas Magros e Fisiculturistas\",\n        description:\n          \"Calcule a sua TMB e TDEE usando a fórmula Cunningham, especificamente projetada para atletas muito magros e fisiculturistas com baixa gordura corporal.\",\n        keywords:\n          \"calculadora Cunningham, calculadora TMB fisiculturista, TMB atleta magro, calculadora baixa gordura corporal, calculadora preparação competição\",\n      },\n      how_it_works: \"Como Funciona a Fórmula Cunningham\",\n      how_it_works_description:\n        \"Desenvolvida especificamente para indivíduos muito magros com baixas percentagens de gordura corporal, esta fórmula fornece estimativas de TMB mais altas que outras equações. É mais precisa para atletas competitivos e fisiculturistas em preparação para competições.\",\n    },\n    oxford: {\n      title: \"Calculadora Oxford\",\n      subtitle: \"Fórmula moderna de TMB baseada em populações europeias com considerações de idade\",\n      meta: {\n        title: \"Calculadora Oxford - Fórmula Moderna TMB e TDEE\",\n        description:\n          \"Calcule a sua TMB e TDEE usando a equação Oxford (2005), uma fórmula moderna baseada em populações europeias com cálculos específicos por idade.\",\n        keywords: \"calculadora Oxford, calculadora TMB moderna, fórmula TMB europeia, calculadora TMB específica idade, equação TMB 2005\",\n      },\n      how_it_works: \"Como Funciona a Fórmula Oxford\",\n      how_it_works_description:\n        \"Publicada em 2005, esta é uma das fórmulas de TMB mais recentes. Foi desenvolvida usando dados de populações europeias e tem em conta faixas etárias, fornecendo equações diferentes para pessoas com menos e mais de 30 anos.\",\n    },\n    \"calorie-calculator-comparison\": {\n      title: \"Comparar todas as fórmulas BMR\",\n      subtitle: \"Veja como diferentes fórmulas BMR calculam as suas necessidades calóricas lado a lado\",\n      meta: {\n        title: \"Comparação de fórmulas BMR - Comparar todos os calculadores de calorias\",\n        description:\n          \"Compare as fórmulas Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham e Oxford BMR lado a lado. Veja qual fórmula funciona melhor para si.\",\n        keywords:\n          \"comparação fórmula BMR, comparação calculador calorias, Mifflin vs Harris-Benedict, melhor calculador BMR, comparar fórmulas calorias\",\n      },\n      how_it_works: \"Como funciona esta comparação\",\n      how_it_works_description:\n        \"Insira os seus detalhes uma vez e veja como todas as principais fórmulas BMR calculam as suas necessidades calóricas diárias. Isto ajuda-o a compreender as diferenças e escolher a fórmula mais adequada para os seus objetivos.\",\n      input_details: \"Os seus detalhes\",\n      compare: \"Comparar\",\n      results_comparison: \"Resultados da comparação de fórmulas\",\n      vs_mifflin: \"vs Mifflin-St Jeor\",\n      summary: \"Resumo e recomendações\",\n      summary_explanation:\n        \"Diferentes fórmulas podem dar resultados variáveis. Geralmente, diferenças de ±100-200 calorias são normais e esperadas.\",\n      recommendation:\n        \"Para a maioria das pessoas, Mifflin-St Jeor fornece a base mais precisa. Os atletas devem considerar Katch-McArdle se conhecerem a sua percentagem de gordura corporal.\",\n    },\n    \"bmi-calculator-hub\": {\n      title: \"Ferramentas Calculadora IMC\",\n      subtitle: \"Calcule o seu Índice de Massa Corporal com diferentes métodos e obtenha informações de saúde personalizadas\",\n      meta: {\n        title: \"Calculadora IMC - Ferramentas de Índice de Massa Corporal e Avaliação de Saúde\",\n        description:\n          \"Calcule o seu IMC com as nossas ferramentas abrangentes. IMC padrão, ajustado para atletas, IMC pediátrico, e ferramentas de comparação. Obtenha informações de saúde e recomendações.\",\n        keywords: \"calculadora IMC, índice massa corporal, avaliação saúde, estado peso, ferramentas IMC, IMC pediátrico, IMC atleta\",\n      },\n      understanding_bmi: \"Compreender o IMC\",\n      bmi_explanation:\n        \"O IMC é uma ferramenta de rastreio que ajuda a avaliar se tem um peso saudável para a sua altura. Escolha a calculadora certa para as suas necessidades:\",\n      recommendation_standard: \"Melhor para a população geral e rastreio inicial de saúde\",\n      recommendation_adjusted: \"Mais preciso para atletas e indivíduos musculosos\",\n      recommendation_pediatric: \"Especializado para crianças e adolescentes com percentis específicos por idade\",\n      popularity: \"Popularidade\",\n      accuracy: \"Precisão\",\n      accuracy_high: \"Alta\",\n      accuracy_good: \"Boa\",\n      accuracy_medium: \"Média\",\n      best_for: \"Melhor para\",\n      best_for_general: \"Uso geral\",\n      best_for_athletes: \"Atletas\",\n      best_for_children: \"Crianças\",\n      best_for_comparison: \"Comparar tudo\",\n      category_standard: \"Padrão\",\n      category_advanced: \"Avançado\",\n      category_specialized: \"Especializado\",\n      standard: {\n        title: \"Calculadora IMC Padrão\",\n        description: \"Cálculo IMC clássico usando a fórmula padrão da OMS. Avaliação rápida e fácil para a população geral.\",\n        page_title: \"Calculadora IMC Padrão\",\n        page_description:\n          \"Calcule o seu Índice de Massa Corporal usando a fórmula padrão da OMS. Obtenha resultados instantâneos com categoria de saúde e recomendações personalizadas.\",\n      },\n      adjusted: {\n        title: \"Calculadora IMC Ajustada\",\n        description:\n          \"Cálculo IMC melhorado que considera a massa muscular e composição corporal para resultados mais precisos em indivíduos atléticos.\",\n      },\n      pediatric: {\n        title: \"Calculadora IMC Pediátrica\",\n        description:\n          \"Calculadora IMC especializada para crianças e adolescentes usando percentis específicos por idade e sexo e gráficos de crescimento.\",\n      },\n      comparison: {\n        title: \"Ferramenta de Comparação IMC\",\n        description:\n          \"Compare diferentes métodos de cálculo IMC lado a lado para compreender como vários fatores afetam os seus resultados.\",\n      },\n    },\n  },\n  \"bmi-calculator\": {\n    educational: {\n      introduction_title: \"Introdução ao IMC\",\n      introduction_text:\n        \"O IMC é uma medida da magreza ou corpulência de uma pessoa baseada na sua altura e peso, e destina-se a quantificar a massa tecidual. É amplamente utilizado como indicador geral de se uma pessoa tem um peso corporal saudável para a sua altura.\",\n      introduction_usage:\n        \"Especificamente, o valor obtido do cálculo do IMC é usado para categorizar se uma pessoa tem baixo peso, peso normal, excesso de peso ou obesidade dependendo da faixa em que o valor se enquadra. Estas faixas de IMC variam com base em fatores como região e idade, e são por vezes subdivididas em subcategorias como baixo peso severo ou obesidade muito severa.\",\n\n      adult_table_title: \"Tabela de IMC para Adultos\",\n      adult_table_description:\n        \"Esta é a recomendação de peso corporal da Organização Mundial de Saúde (OMS) baseada em valores de IMC para adultos. É utilizada tanto para homens como mulheres, com 20 anos ou mais.\",\n\n      children_table_title: \"Tabela de IMC para Crianças e Adolescentes, Idade 2-20\",\n      children_table_description:\n        \"Os Centros de Controlo e Prevenção de Doenças (CDC) recomendam a categorização do IMC para crianças e adolescentes entre os 2 e 20 anos.\",\n\n      classification: \"Classificação\",\n      bmi_range: \"Faixa de IMC - kg/m²\",\n      category: \"Categoria\",\n      percentile_range: \"Faixa de Percentil\",\n      underweight: \"Baixo peso\",\n      healthy_weight: \"Peso Saudável\",\n      at_risk_overweight: \"Em Risco de Excesso de Peso\",\n      overweight: \"Excesso de Peso\",\n\n      overweight_risks_title: \"Riscos Associados ao Excesso de Peso\",\n      overweight_risks_intro:\n        \"O excesso de peso aumenta o risco de várias doenças graves e condições de saúde. Abaixo está uma lista de tais riscos, segundo os Centros de Controlo e Prevenção de Doenças (CDC):\",\n\n      cardiovascular_risks: \"Riscos Cardiovasculares\",\n      high_blood_pressure: \"Pressão arterial alta\",\n      cholesterol_issues: \"Níveis mais altos de colesterol LDL, níveis mais baixos de colesterol HDL e níveis altos de triglicéridos\",\n      coronary_heart_disease: \"Doença coronária\",\n      stroke: \"Acidente vascular cerebral\",\n\n      metabolic_risks: \"Riscos Metabólicos\",\n      type_2_diabetes: \"Diabetes tipo II\",\n      gallbladder_disease: \"Doença da vesícula biliar\",\n      sleep_apnea: \"Apneia do sono e problemas respiratórios\",\n      osteoarthritis: \"Osteoartrite, um tipo de doença articular causada pela degradação da cartilagem articular\",\n\n      other_risks: \"Outros Riscos de Saúde\",\n      certain_cancers: \"Certos cancros (endometrial, mama, cólon, rim, vesícula biliar, fígado)\",\n      mental_health_issues: \"Doenças mentais como depressão clínica, ansiedade e outras\",\n      reduced_quality_life: \"Baixa qualidade de vida e dores corporais\",\n      increased_mortality: \"Geralmente, um risco aumentado de mortalidade comparado com aqueles com um IMC saudável\",\n\n      underweight_risks_title: \"Riscos Associados ao Baixo Peso\",\n      underweight_risks_intro: \"O baixo peso tem os seus próprios riscos associados, listados abaixo:\",\n      malnutrition: \"Desnutrição, deficiências vitamínicas, anemia (capacidade reduzida para transportar oxigénio no sangue)\",\n      osteoporosis: \"Osteoporose, uma doença que causa fraqueza óssea, aumentando o risco de fractura de ossos\",\n      immune_function_decrease: \"Uma diminuição na função imunitária\",\n      growth_development_issues: \"Problemas de crescimento e desenvolvimento, particularmente em crianças e adolescentes\",\n      reproductive_issues: \"Possíveis problemas reprodutivos para mulheres devido a desequilíbrios hormonais\",\n      surgery_complications: \"Complicações potenciais como resultado de cirurgia\",\n      increased_mortality_underweight: \"Geralmente, um risco aumentado de mortalidade comparado com aqueles com um IMC saudável\",\n\n      adults_limitations: \"Em Adultos\",\n      older_adults_fat: \"Adultos mais velhos tendem a ter mais gordura corporal que adultos mais jovens com o mesmo IMC\",\n      women_fat_difference: \"Mulheres tendem a ter mais gordura corporal que homens para um IMC equivalente\",\n      athletes_muscle_mass: \"Indivíduos musculosos e atletas altamente treinados podem ter IMCs mais altos devido a grande massa muscular\",\n\n      children_limitations: \"Em Crianças e Adolescentes\",\n      height_maturation_influence: \"Altura e nível de maturação sexual podem influenciar o IMC e gordura corporal entre crianças\",\n      fat_free_mass_difference: \"O IMC pode ser resultado de níveis aumentados de gordura ou massa livre de gordura\",\n      population_accuracy: \"O IMC é bastante indicativo de gordura corporal para 90-95% da população\",\n\n      formulas_title: \"Fórmula do IMC\",\n      metric_formula: \"Fórmula Métrica\",\n      imperial_formula: \"Fórmula Imperial\",\n      example: \"Exemplo\",\n\n      bmi_prime_formula: \"Fórmula do IMC Prime\",\n      bmi_prime_description: \"Relação do seu IMC com o limite superior do IMC normal (25)\",\n\n      ponderal_index_title: \"Índice Ponderal\",\n      ponderal_index_explanation:\n        \"O Índice Ponderal (IP) é similar ao IMC em que mede a magreza ou corpulência de uma pessoa baseada na sua altura e peso. A principal diferença entre o IP e o IMC é o cubo em vez do quadrado da altura na fórmula. Enquanto o IMC pode ser uma ferramenta útil ao considerar grandes populações, não é confiável para determinar magreza ou corpulência em indivíduos.\",\n      ponderal_index_metric_description: \"Índice Ponderal usando unidades métricas\",\n      ponderal_index_imperial_description: \"Índice Ponderal usando unidades imperiais\",\n\n      medical_disclaimer_title: \"Aviso Médico\",\n    },\n    height: \"Altura\",\n    weight: \"Peso\",\n    feet: \"pés\",\n    inches: \"pol\",\n    cm: \"cm\",\n    kg: \"kg\",\n    lbs: \"lbs\",\n    height_placeholder: \"Insira a altura\",\n    weight_placeholder: \"Insira o peso\",\n    calculate: \"Calcular IMC\",\n    your_bmi: \"O seu IMC\",\n    bmi_prime: \"IMC Prime\",\n    ponderal_index: \"Índice Ponderal\",\n    bmi_category: \"Categoria IMC\",\n    health_risk: \"Risco de Saúde\",\n    recommendations_label: \"Recomendações\",\n    units: \"Unidades\",\n    metric: \"Métrico (kg/cm)\",\n    imperial: \"Imperial (lbs/pés)\",\n\n    // Detailed BMI Categories (WHO)\n    category_severe_thinness: \"Magreza Severa\",\n    category_moderate_thinness: \"Magreza Moderada\",\n    category_mild_thinness: \"Magreza Ligeira\",\n    category_normal: \"Peso Normal\",\n    category_overweight: \"Excesso de Peso\",\n    category_obese_class_1: \"Obesidade Classe I\",\n    category_obese_class_2: \"Obesidade Classe II\",\n    category_obese_class_3: \"Obesidade Classe III\",\n\n    // Health Risks\n    risk_low: \"Baixo\",\n    risk_normal: \"Normal\",\n    risk_increased: \"Aumentado\",\n    risk_high: \"Alto\",\n    risk_very_high: \"Muito Alto\",\n    risk_extremely_high: \"Extremamente Alto\",\n\n    // Additional Information\n    bmi_range: \"Intervalo IMC\",\n    ideal_weight: \"Intervalo de Peso Ideal\",\n    weight_to_lose: \"Peso a Perder\",\n    weight_to_gain: \"Peso a Ganhar\",\n    normal_range: \"Intervalo IMC normal: 18,5 - 24,9\",\n\n    // BMI Prime\n    about_bmi_prime: \"Sobre o IMC Prime\",\n    bmi_prime_explanation:\n      \"O IMC Prime é a relação entre o seu IMC e o limite superior do IMC normal (25). Um valor de 1,0 significa que está no limite superior do peso normal.\",\n    underweight: \"Baixo peso\",\n    normal: \"Normal\",\n    overweight: \"Excesso de peso\",\n    obese: \"Obeso\",\n\n    // Limitations\n    limitations_title: \"Limitações do IMC\",\n    limitations_text:\n      \"O IMC não distingue entre massa muscular e massa gorda. Atletas e pessoas muito musculosas podem ter um IMC alto apesar de estarem saudáveis. A idade, sexo, etnia e composição corporal também afetam a interpretação.\",\n\n    disclaimer:\n      \"O IMC é uma ferramenta de rastreio e pode não refletir a composição corporal. Consulte profissionais de saúde para conselhos personalizados.\",\n\n    // Recommendations\n    recommendations: {\n      severe_thinness: {\n        medical_consultation: \"Consulta médica imediata fortemente recomendada\",\n        nutritional_assessment: \"Avaliação nutricional abrangente necessária\",\n        weight_gain_program: \"Pode necessitar de programa supervisionado de ganho de peso\",\n        screen_conditions: \"Rastreio de condições médicas subjacentes\",\n        psychological_evaluation: \"Considerar avaliação psicológica se suspeita de distúrbio alimentar\",\n      },\n      moderate_thinness: {\n        healthcare_provider: \"Consultar profissional de saúde para avaliação\",\n        nutrient_dense_foods: \"Focar em alimentos ricos em nutrientes e calorias\",\n        registered_dietitian: \"Considerar trabalhar com nutricionista registado\",\n        monitor_malnutrition: \"Monitorizar sinais de desnutrição\",\n        gradual_weight_gain: \"Ganho de peso gradual e saudável recomendado\",\n      },\n      mild_thinness: {\n        consider_healthcare: \"Considerar consultar profissional de saúde\",\n        nutrient_dense_foods: \"Focar em alimentos ricos em nutrientes para ganhar peso saudavelmente\",\n        strength_training: \"Incluir treino de força para desenvolver massa muscular\",\n        monitor_health: \"Monitorizar a sua saúde regularmente\",\n        gradual_weight_gain: \"Visar ganho de peso gradual (0,5-1 kg por semana)\",\n      },\n      normal: {\n        maintain_weight: \"Manter o seu peso saudável atual\",\n        physical_activity: \"Continuar atividade física regular (150+ minutos por semana)\",\n        balanced_diet: \"Seguir dieta equilibrada e nutritiva\",\n        health_checkups: \"Check-ups de saúde regulares\",\n        overall_wellness: \"Focar no bem-estar geral e composição corporal\",\n      },\n      overweight: {\n        gradual_weight_loss: \"Visar perda de peso gradual (0,5-1 kg por semana)\",\n        increase_activity: \"Aumentar atividade física para 150+ minutos por semana\",\n        portion_control: \"Focar no controlo de porções e nutrição equilibrada\",\n        healthcare_provider: \"Considerar consultar profissional de saúde\",\n        lifestyle_goals: \"Estabelecer objetivos de estilo de vida realistas e sustentáveis\",\n      },\n      obese_class_1: {\n        healthcare_provider: \"Consultar profissional de saúde para plano de gestão de peso\",\n        weight_loss_target: \"Visar perda de peso de 5-10% inicialmente\",\n        diet_exercise: \"Combinar intervenções de dieta e exercício\",\n        nutritional_counseling: \"Considerar aconselhamento nutricional profissional\",\n        screen_conditions: \"Rastreio de condições de saúde relacionadas com o peso\",\n      },\n      obese_class_2: {\n        medical_supervision: \"Procurar supervisão médica para gestão de peso\",\n        lifestyle_programs: \"Considerar programas abrangentes de intervenção de estilo de vida\",\n        evaluate_conditions: \"Avaliar condições de saúde relacionadas com o peso\",\n        medical_treatments: \"Pode beneficiar de tratamentos médicos para perda de peso\",\n        bariatric_surgery: \"Considerar avaliação de cirurgia bariátrica se apropriado\",\n      },\n      obese_class_3: {\n        medical_consultation: \"Consulta médica imediata recomendada\",\n        bariatric_surgery: \"Considerar avaliação de cirurgia bariátrica\",\n        weight_management: \"Programa médico abrangente de gestão de peso\",\n        health_complications: \"Abordar complicações de saúde relacionadas com o peso\",\n        multidisciplinary: \"Abordagem multidisciplinar com equipa médica\",\n      },\n    },\n  },\n  levels: {\n    BEGINNER: \"Iniciante\",\n    INTERMEDIATE: \"Intermédio\",\n    ADVANCED: \"Avançado\",\n  },\n  email_sent: \"Email enviado\",\n  cant_send_email: \"Não foi possível enviar o email\",\n  logout: \"Terminar sessão\",\n  verify_email: \"Verifique o seu email\",\n  verify_email_subtitle: \"Por favor, verifique o seu email para continuar.\",\n  resend_email: \"Reenviar email\",\n  resend_email_countdown: \"Reenviar email em {seconds} segundos\",\n  signin_error_subtitle: \"Por favor, verifique as suas credenciais e tente novamente.\",\n  register_title: \"Criar conta\",\n  register_description: \"Insira os seus dados abaixo para criar uma conta\",\n  register_terms: \"Ao registar-se, concorda com os nossos\",\n  register_privacy: \"Termos e Condições\",\n  register_privacy_link: \"e com a nossa\",\n  register_privacy_link_2: \"Política de Privacidade\",\n  password_forgot_title: \"Esqueceu-se da palavra-passe?\",\n  password_forgot_subtitle: \"Insira o seu email para redefinir a palavra-passe\",\n  new_password: \"Nova palavra-passe\",\n  new_password_placeholder: \"Insira a nova palavra-passe\",\n  current_password: \"Palavra-passe atual\",\n  current_password_placeholder: \"Insira a palavra-passe atual\",\n  confirm_password: \"Confirmar palavra-passe\",\n  confirm_password_placeholder: \"Confirme a palavra-passe\",\n\n  success: {\n    feedback_sent: \"Feedback enviado\",\n    password_forgot_success: \"Email enviado\",\n    reset_password_success: \"Palavra-passe redefinida com sucesso\",\n    password_updated_successfully: \"Palavra-passe atualizada com sucesso\",\n  },\n\n  error: {\n    invalid_credentials: \"Credenciais inválidas ou conta inexistente\",\n    upload_failed: \"Falha ao enviar ficheiro\",\n    generic_error: \"Erro durante a operação\",\n    sending_email: \"Erro ao enviar email\",\n  },\n\n  backend_errors: {\n    EMAIL_ALREADY_EXISTS: \"Este email já está registado\",\n    INVALID_FILE_TYPE: \"Tipo de ficheiro inválido\",\n    FILE_TOO_LARGE: \"Ficheiro demasiado grande\",\n    NO_FILE_UPLOADED: \"Nenhum ficheiro enviado\",\n    IMAGE_PROCESSING_ERROR: \"Erro no processamento da imagem\",\n    upload_failed: \"Falha no envio\",\n  },\n\n  profile: {\n    new_workout: \"Novo treino\",\n    alert: {\n      title: \"O seu progresso está guardado no navegador.\",\n      create_account: \"Crie uma conta\",\n      log_in: \"Inicie sessão\",\n      to_ensure_it_is_not_getting_lost: \"para garantir que não se perde.\",\n    },\n  },\n\n  // Release Notes\n  release_notes: {\n    title: \"Novidades\",\n    release_notes: \"Notas de Lançamento\",\n    notes: {\n      note_2025_10_29: {\n        title: \"🍑 Novo Programa Booty Lançado!\",\n        content:\n          \"<li>Um novo <a href='/programs/booty-pump' class='text-blue-500 hover:underline'>programa Booty</a> já está disponível!</li><li>Trabalhe e fortaleça os seus glúteos com treinos especializados</li><li>Desenhado para resultados máximos e crescimento muscular</li><li>Junte-se ao programa hoje! 💪</li>\",\n      },\n      note_2025_08_18: {\n        title: \"🏆 Nova Funcionalidade de Classificação!\",\n        content:\n          \"<li>Nova <strong>classificação</strong> para competir com outros campeões de treino</li><li>Ver rankings por períodos <strong>global, mensal e semanal</strong></li><li>Acompanhe a sua posição entre os melhores performers</li><li>Motive-se para subir na classificação! 🚀</li>\",\n      },\n      note_2025_07_09: {\n        title: \"🎯 Seleção de Exercícios, Favoritos e Novas Ferramentas\",\n        content:\n          \"<li>Nova <strong>seleção de exercícios</strong> durante a criação de treinos (passo 3)</li><li>Sistema de <strong>exercícios favoritos</strong> para marcar os seus movimentos preferidos</li><li>Novas <em>ferramentas de fitness</em>: calculadora de IMC e zonas de frequência cardíaca</li><li>Cartões de programas melhorados</li><li>Novos colaboradores juntam-se ao projeto! 🚀</li>\",\n      },\n      note_2025_07_02: {\n        title: \"🛠️ Auto-hospedagem, Russo e Novas Ferramentas\",\n        content:\n          \"Melhoria da <strong>auto-hospedagem</strong>, adicionado suporte para <strong>russo</strong>, e introdução de novas <em>ferramentas de fitness</em> incluindo uma calculadora de calorias. 🚀\",\n      },\n      note_2025_06_23: {\n        title: \"🇵🇹 Suporte Português & Banner de Doação\",\n        content:\n          \"A app agora suporta <strong>português</strong>! Também adicionámos um <em>banner de doação</em> para ajudar a suportar os custos do projeto via <a href='https://github.com/sponsors/snouzy' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>GitHub Sponsors</a> ou <a href='https://ko-fi.com/workoutcool' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Ko-fi</a>. 🙏\",\n      },\n      note_2025_06_22: {\n        title: \"🌍 Novos idiomas e melhorias de desempenho!\",\n        content:\n          \"A aplicação agora está disponível em chinês e russo! Também melhoramos o desempenho do arrastar e largar para uma experiência mais fluida. ⚡\",\n      },\n      note_2025_06_19: {\n        title: \"📱 Agora disponível como PWA!\",\n        content:\n          \"O Workout.cool v1.2 já é uma Progressive Web App! Instale-a no seu telemóvel para uma experiência de aplicação nativa com acesso offline. 🚀\",\n      },\n      note_2025_06_18: {\n        title:\n          \"🚀 Nº 1 em destaque no <a href='https://news.ycombinator.com/item?id=44309320' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Hacker News</a>!\",\n        content:\n          \"O Workout.cool chegou ao primeiro lugar no Hacker News! Obrigado a todos pelo apoio incrível e bem-vindos todos os novos utilizadores! 💪\",\n      },\n      note_2025_06_01: {\n        title: \"🎉 Novo: Dialogo de Notas de Lançamento\",\n        content: \"Agora pode ver as novidades diretamente no cabeçalho! Fique atento a mais atualizações.\",\n      },\n      note_2025_05_20: {\n        title: \"Melhorias na UI\",\n        content: \"Responsividade móvel aprimorada e efeitos subtis ao passar o cursor sobre botões.\",\n      },\n    },\n  },\n\n  // Premium Upsell Alert\n  donation_alert: {\n    title: \"Desbloqueie funcionalidades avançadas com Workout.cool Premium\",\n    or: \"ou\",\n  },\n\n  // Donation Modal\n  donation_modal: {\n    support_via: \"Apoiar via...\",\n    title: \"Apoiar o projeto\",\n    congrats: \"Parabéns pelo seu treino! 🎉\",\n    subtitle: \"Esta app ajuda-o gratuitamente, mas tem um custo real para mim...\",\n    costs_title: \"A realidade dos custos\",\n    costs_description:\n      \"Actualmente, as doações nem sequer cobrem os custos básicos: servidores, autenticação, infraestrutura, base de dados, etc.\",\n    open_source_title: \"100% Open Source\",\n    open_source_description:\n      \"Esta app é completamente gratuita e de open source. Não há lucro - é um projeto de paixão para ajudar a comunidade e ajudar as pessoas a fazer exercício.\",\n    no_ads: \"Sem publicidade\",\n    no_tracking: \"Sem rastreamento\",\n    impact_title: \"O seu impacto\",\n    impact_3_euros: \"• Mesmo 3€ cobrem 1 semana de servidor\",\n    impact_support: \"• O seu apoio mantém a app gratuita para todos\",\n    impact_footer: \"Cada doação, mesmo pequena, faz uma diferença real! 🙏\",\n    later_button: \"Mais tarde\",\n    support_button: \"Apoiar o projeto\",\n  },\n\n  // Contact Support\n  contact_support: \"Contactar Suporte\",\n  contact_support_subtitle: \"Descreva o seu problema e iremos ajudá-lo o mais rápido possível. Também pode escrever-nos diretamente para\",\n\n  // Social Platforms\n  social_platforms: {\n    x: \"X (Twitter)\",\n    facebook: \"Facebook\",\n    email: \"Email\",\n    whatsapp: \"WhatsApp\",\n    website: \"Website\",\n    phone: \"Telefone\",\n    youtube: \"YouTube\",\n    linkedin: \"LinkedIn\",\n    snapchat: \"Snapchat\",\n    instagram: \"Instagram\",\n    tiktok: \"TikTok\",\n    threads: \"Threads\",\n  },\n\n  // Workout Builder\n  workout_builder: {\n    confirm_delete: \"Tem a certeza de que pretende eliminar esta sessão de treino?\",\n    steps: {\n      equipment: {\n        title: \"Equipamento\",\n        description: \"Selecione o seu equipamento\",\n      },\n      muscles: {\n        title: \"Músculos\",\n        description: \"Escolha a sua zona de treino\",\n      },\n      exercises: {\n        title: \"Exercícios\",\n        description: \"Personalize o seu treino\",\n      },\n    },\n    muscles: {\n      back: \"Costas\",\n      abdominals: \"Abdominais\",\n      adductors: \"Adutores\",\n      abductors: \"Abdutores\",\n      biceps: \"Bíceps\",\n      triceps: \"Tríceps\",\n      chest: \"Peito\",\n      shoulders: \"Ombros\",\n      quadriceps: \"Quadríceps\",\n      hamstrings: \"Isquiotibiais\",\n      glutes: \"Glúteos\",\n      calves: \"Panturrilhas\",\n      forearms: \"Antebraços\",\n      traps: \"Trapézio\",\n      obliques: \"Oblíquos\",\n    },\n    exercise: {\n      watch_video: \"Ver vídeo\",\n      shuffle: \"Aleatorizar\",\n      pick: \"Escolher\",\n      remove: \"Remover\",\n      no_video_available: \"Vídeo indisponível.\",\n    },\n    loading: {\n      exercises: \"A carregar exercícios...\",\n    },\n    error: {\n      loading_exercises: \"Erro ao carregar exercícios\",\n    },\n    no_exercises_found: \"Nenhum exercício encontrado. Experimente mudar a seleção de equipamento ou músculos.\",\n    addExercise: \"Adicionar exercício\",\n    exerciseAdded: \"{name} adicionado ao treino\",\n    exercises: \"exercícios\",\n    equipment: {\n      bodyweight: {\n        label: \"Peso corporal\",\n        description: \"Exercícios apenas com o peso do corpo\",\n      },\n      dumbbell: {\n        label: \"Halteres\",\n        description: \"Exercícios com halteres\",\n      },\n      barbell: {\n        label: \"Barra\",\n        description: \"Movimentos compostos com barra\",\n      },\n      kettlebell: {\n        label: \"Kettlebell\",\n        description: \"Exercícios dinâmicos com kettlebell\",\n      },\n      band: {\n        label: \"Elástico\",\n        description: \"Exercícios com banda de resistência\",\n      },\n      plate: {\n        label: \"Placa\",\n        description: \"Exercícios usando discos de peso\",\n      },\n      pullup_bar: {\n        label: \"Barra de barra fixa\",\n        description: \"Exercícios para a parte superior do corpo com barra fixa\",\n      },\n      bench: {\n        label: \"Banco\",\n        description: \"Exercícios de banco e apoio\",\n      },\n    },\n    navigation: {\n      previous: \"Anterior\",\n      continue: \"Continuar\",\n      complete: \"Concluir\",\n    },\n    stats: {\n      \"muscle_selected#zero\": \"0 músculos selecionados\",\n      \"muscle_selected#one\": \"1 músculo selecionado\",\n      \"muscle_selected#other\": \"{count} músculos selecionados\",\n      \"equipment_selected#zero\": \"0 equipamentos selecionados\",\n      \"equipment_selected#one\": \"1 equipamento selecionado\",\n      \"equipment_selected#other\": \"{count} equipamentos selecionados\",\n      selected: \"Selecionado\",\n      total: \"Total\",\n      equipment_ready: \"equipamento pronto\",\n      equipment_ready_plural: \"equipamentos prontos\",\n    },\n    selection: {\n      choose_your_arsenal: \"Escolha o seu arsenal\",\n      select_equipment_description: \"Selecione equipamento para desbloquear treinos personalizados\",\n      clear_all: \"Limpar tudo\",\n      muscle_selection_coming_soon: \"Seleção de músculos (Em breve)\",\n      muscle_selection_description: \"Selecione o(s) músculo(s) que deseja treinar clicando neles.\",\n      exercise_selection_coming_soon: \"Seleção de exercícios (Em breve)\",\n      exercise_selection_description: \"Nesta etapa verá recomendações de exercícios personalizadas.\",\n    },\n    session: {\n      back_to_workout: \"Voltar ao treino\",\n      congrats: \"Parabéns, treino concluído! 🎉\",\n      congrats_subtitle: \"Conseguiu!\",\n      see_instructions: \"Ver instruções\",\n      finish_set: \"Concluir série\",\n      finish_session: \"Terminar sessão\",\n      bodyweight: \"Peso corporal\",\n      weight: \"Peso\",\n      reps: \"Reps\",\n      time: \"Tempo\",\n      next_exercise: \"Próximo exercício\",\n      add_set: \"Adicionar série\",\n      add_column: \"Adicionar coluna\",\n      add_row: \"Adicionar linha\",\n      remove_column: \"Remover coluna\",\n      set_number: \"Série {number}\",\n      set_number_plural: \"Séries {number}\",\n      set_number_singular: \"Série {number}\",\n      set_number_plural_singular: \"Séries {number}\",\n      workout_in_progress: \"Treino em curso\",\n      started_at: \"Iniciado às\",\n      quit_workout: \"Abandonar treino\",\n      elapsed_time: \"Tempo decorrido\",\n      chronometer: \"Cronómetro\",\n      exercise_progress: \"Progresso do exercício\",\n      total_volume: \"Volume total\",\n      current_exercise: \"Exercício atual\",\n      complete: \"Concluído\",\n      active: \"Ativo\",\n      already_have_a_active_session: \"Já tem uma sessão ativa. Não é possível repetir sem terminar ou abandonar o treino.\",\n      no_exercise_selected: \"Nenhum exercício selecionado\",\n      quit_workout_title: \"Abandonar treino?\",\n      progress: \"Progresso\",\n      quit_warning: \"Tem a certeza de que pretende abandonar? Pode guardar o progresso ou perdê-lo completamente.\",\n      save_and_quit: \"Guardar e sair\",\n      quit_without_save: \"Sair sem guardar\",\n      continue_workout: \"Continuar treino\",\n      history: \"Histórico de treinos [{count}]\",\n      no_workout_yet: \"Ainda sem treinos.\",\n      start: \"iniciar\",\n      end: \"terminar\",\n      exercise: \"EXERCÍCIO\",\n      repeat: \"Repetir\",\n      delete: \"Eliminar\",\n    },\n    attribute_value: {\n      bodyweight: \"Peso corporal\",\n      strength: \"Força\",\n      powerlifting: \"Powerlifting\",\n      calisthenic: \"Calistenia\",\n      plyometrics: \"Pliometria\",\n      stretching: \"Alongamento\",\n      strongman: \"Strongman\",\n      cardio: \"Cardio\",\n      stabilization: \"Estabilização\",\n      power: \"Potência\",\n      resistance: \"Resistência\",\n      crossfit: \"CrossFit\",\n      weightlifting: \"Levantamento de peso\",\n      neck: \"Pescoço\",\n      lats: \"Dorsais\",\n      adductors: \"Adutores\",\n      abductors: \"Abdutores\",\n      groin: \"Virilha\",\n      full_body: \"Corpo inteiro\",\n      rotator_cuff: \"Manguito rotador\",\n      hip_flexor: \"Flexor da anca\",\n      achilles_tendon: \"Tendão de Aquiles\",\n      fingers: \"Dedos\",\n      smith_machine: \"Máquina Smith\",\n      other: \"Outro\",\n      ez_bar: \"Barra EZ\",\n      machine: \"Máquina\",\n      desk: \"Secretária\",\n      none: \"Nenhum\",\n      cable: \"Cabo\",\n      medicine_ball: \"Medicine ball\",\n      swiss_ball: \"Swiss ball\",\n      foam_roll: \"Rolo de espuma\",\n      trx: \"TRX\",\n      box: \"Caixa\",\n      ropes: \"Corda\",\n      spin_bike: \"Bicicleta de spinning\",\n      step: \"Step\",\n      bosu: \"BOSU\",\n      tyre: \"Pneu\",\n      sandbag: \"Saco de areia\",\n      pole: \"Barra vertical\",\n      wall: \"Parede\",\n      bar: \"Barra\",\n      rack: \"Rack\",\n      car: \"Carro\",\n      sled: \"Sledge\",\n      chain: \"Corrente\",\n      skierg: \"SkiErg\",\n      rope: \"Corda\",\n      na: \"N/D\",\n      isolation: \"Isolamento\",\n      compound: \"Composto\",\n    },\n  },\n\n  commons: {\n    upgrade_to_premium: \"Torne-se Premium\",\n    last_activity: \"Última atividade\",\n    registered_on: \"Inscrito em\",\n    just_now: \"agora mesmo\",\n    signup_with: \"Inscrever com {provider}\",\n    signin_with: \"Entrar com {provider}\",\n    signup: \"Inscrever-se\",\n    login: \"Iniciar sessão\",\n    connecting: \"A ligar...\",\n    login_to_your_account_title: \"Inicie sessão na sua conta\",\n    login_to_your_account_subtitle: \"Insira as suas credenciais abaixo para entrar\",\n    password_forgot: \"Esqueceu-se da palavra-passe?\",\n    password_reset_success: \"Palavra-passe redefinida com sucesso\",\n    dont_have_account: \"Ainda não tem conta?\",\n    already_have_account: \"Já tem uma conta?\",\n    or: \"Ou\",\n    add: \"Adicionar\",\n    your_feminine: \"a sua\",\n    password: \"Palavra-passe\",\n    email: \"Email\",\n    logout: \"Terminar sessão\",\n    first_name: \"Nome\",\n    last_name: \"Apelido\",\n    verify_password: \"Verificar palavra-passe\",\n    submit: \"Enviar\",\n    upload: \"Carregar\",\n    cancel: \"Cancelar\",\n    save_changes: \"Guardar alterações\",\n    change: \"Alterar\",\n    subject: \"Assunto\",\n    message: \"Mensagem\",\n    saving: \"A guardar...\",\n    edit: \"Editar\",\n    more_options: \"Mais opções\",\n    open_link: \"Abrir ligação\",\n    hide: \"Ocultar\",\n    make_visible: \"Tornar visível\",\n    delete: \"Eliminar\",\n    share: \"Partilhar\",\n    title: \"Título\",\n    subtitle: \"Subtítulo\",\n    content: \"Conteúdo\",\n    save: \"Guardar\",\n    button: \"Botão\",\n    card: \"Cartão\",\n    go_back: \"Voltar atrás\",\n    next: \"Seguinte\",\n    choose_image: \"Escolher imagem\",\n    soon: \"Em breve\",\n    coming_soon_with_emoji: \"Em breve 🤫\",\n    no_image: \"Sem imagem\",\n    description: \"Descrição\",\n    price: \"Preço\",\n    duration: \"Duração\",\n    location: \"Localização\",\n    schedule: \"Agenda\",\n    participants_info: \"Informação dos participantes\",\n    description_placeholder: \"Insira a descrição\",\n    title_placeholder: \"Insira o título\",\n    changes_saved: \"Alterações guardadas\",\n    replace: \"Substituir\",\n    loading: \"A carregar...\",\n    image_deleted: \"A imagem foi eliminada\",\n    discover_workoutcool: \"Descubra o Workout Cool\",\n    received_just_now: \"Recebido agora mesmo\",\n    copied: \"Copiado\",\n    url_copied: \"A URL foi copiada\",\n    copy_failed: \"Falha ao copiar\",\n    accordion: \"Acordeão\",\n    image: \"Imagem\",\n    other: \"Outro\",\n    register: \"Registar\",\n    instantly: \"instantaneamente\",\n    immediately: \"imediatamente\",\n    link: \"Ligação\",\n    accept: \"Aceitar\",\n    deny: \"Negar\",\n    invalid_input: \"Entrada inválida. Por favor, verifique os erros.\",\n    copy_url: \"Copiar URL\",\n    page_url: \"URL da página\",\n    saving_short: \"A guardar...\",\n    saved_short: \"OK\",\n    looks_like_you_are_lost: \"Parece que está perdido\",\n    the_page_you_are_looking_for_is_not_available: \"A página que procura não está disponível\",\n    go_to_home: \"Ir para o início\",\n    go_to_profile: \"Ir para o perfil\",\n    terms: \"Termos de Serviço\",\n    privacy: \"Política de Privacidade\",\n    sales_terms: \"Termos de Venda\",\n    consent_banner: \"Utilizamos cookies para melhorar a sua experiência. Ao clicar em Aceitar, concorda com a nossa utilização de cookies.\",\n    about: \"Sobre nós\",\n    profile: \"Perfil\",\n    donate: \"Doar\",\n    my_account: \"A minha conta\",\n    dashboard: \"Dashboard\",\n    home: \"Início\",\n    changelog: \"Histórico de alterações\",\n    stop_impersonation_button: \"Parar personificação\",\n    impersonating_user_label: \"Personificando utilizador\",\n    re_hello: \"Re Olá\",\n    back_to_login: \"Voltar para Login\",\n    sending: \"A enviar...\",\n    send_me_link: \"Enviar-me ligação\",\n    extremely_dissatisfied: \"Extremamente insatisfeito\",\n    somewhat_dissatisfied: \"Ligeiramente insatisfeito\",\n    neutral: \"Neutro\",\n    satisfied: \"Satisfeito\",\n    support: \"Suporte\",\n    change_language: \"Alterar idioma\",\n    in_progress: \"Em progresso\",\n    close: \"Fechar\",\n    premium: \"Premium\",\n    subscription: \"Abonamento\",\n    manage_subscription: \"Gerir abonamento\",\n    become_premium: \"Torne-se Premium\",\n    remove_ads: \"Remover anúncios\",\n    coming_soon: \"Em breve\",\n    free: \"Gratuito\",\n    new: \"Novo\",\n    monday: \"Segunda-feira\",\n    tuesday: \"Terça-feira\",\n    wednesday: \"Quarta-feira\",\n    thursday: \"Quinta-feira\",\n    friday: \"Sexta-feira\",\n    saturday: \"Sábado\",\n    sunday: \"Domingo\",\n    added_to_favorites: \"Adicionado aos favoritos\",\n    add_to_favorites: \"Adicionar aos favoritos\",\n    remove_from_favorites: \"Remover dos favoritos\",\n    favorites: \"Favoritos\",\n  },\n  statistics: {\n    title: \"Estatísticas\",\n    page_subtitle: \"Acompanhe sua jornada fitness com análises avançadas e insights personalizados.\",\n    select_exercise: \"Selecionar Exercício\",\n    active_daily_users: \"Usuários Ativos Diários\",\n    success_rate: \"Taxa de Sucesso\",\n    user_rating: \"Avaliação do Usuário\",\n\n    // Tabs\n    tabs: {\n      video: \"Vídeo\",\n      statistics: \"Estatísticas\",\n    },\n\n    // Chart titles and labels\n    weight: \"Peso\",\n    volume: \"Volume\",\n    weight_progression: \"Progressão de Peso\",\n    weight_progression_chart: \"Gráfico de progressão de peso\",\n    weekly_volume: \"Volume Semanal\",\n    volume_chart: \"Gráfico de volume\",\n    estimated_1rm: \"1 Rep Máx Estimado (1RM)\",\n    one_rep_max_chart: \"Gráfico de repetição máxima\",\n    performance_over_time: \"Desempenho ao Longo do Tempo\",\n\n    // Form and controls\n    timeframe: \"Período de Tempo\",\n    timeframe_selector: \"Seletor de período de tempo\",\n\n    // Timeframes\n    timeframes: {\n      \"4weeks\": \"4 Semanas\",\n      \"8weeks\": \"8 Semanas\",\n      \"12weeks\": \"12 Semanas\",\n      \"1year\": \"1 Ano\",\n    },\n\n    // Error messages\n    error_loading_data: \"Erro ao carregar dados\",\n    error_loading_weight_progression: \"Erro ao carregar a progressão de peso\",\n    error_loading_1rm: \"Erro ao carregar dados de 1RM\",\n    error_loading_volume: \"Erro ao carregar dados de volume\",\n\n    // Empty states\n    no_data_yet: \"Ainda sem dados\",\n    start_tracking: \"Comece a rastrear para ver seu progresso\",\n    no_1rm_data: \"Sem dados de 1RM disponíveis\",\n    complete_sets_with_weight: \"Complete séries com peso para ver seu 1 Rep Máx (1RM)\",\n    no_volume_data: \"Sem dados de volume disponíveis\",\n    complete_workouts: \"Complete treinos para ver seu volume\",\n\n    // Info and tooltips\n    \"1rm_formula_info\": \"Informações da fórmula 1RM\",\n    volume_calculation: \"Volume = Peso × Reps × Séries\",\n    last_updated: \"Última atualização: {date}\",\n\n    // Premium\n    premium_required: \"Premium necessário para acessar estatísticas\",\n\n    // StatisticsPreviewOverlay\n    premium_statistics: \"Estatísticas Premium\",\n    premium_statistics_description: \"Obtenha insights detalhados sobre sua jornada fitness com análises avançadas para cada exercício.\",\n    total_volume: \"Volume Total\",\n    pr_increase: \"Aumento de PR\",\n    weight_progress: \"Progresso de Peso\",\n    upgrade_now: \"Atualizar Agora\",\n    rating: \"Avaliação 4.8/5\",\n    no_ads: \"Sem anúncios\",\n    cancel_anytime: \"Cancelar a qualquer momento\",\n    preview_notice: \"Isto é apenas uma pré-visualização! 👀\",\n    preview_description: \"Desbloqueie o acesso completo a análises detalhadas, rastreamento de progresso e insights personalizados.\",\n    get_premium_access: \"Obter Acesso Premium\",\n\n    // ExercisesBrowser\n    all_equipment: \"Todo o Equipamento\",\n    all_muscles: \"Todos os Músculos\",\n    search_exercises: \"Pesquisar Exercícios\",\n    error_loading_exercises: \"Erro ao carregar exercícios\",\n    no_exercises_found: \"Nenhum exercício encontrado\",\n    equipment_label: \"Equipamento:\",\n    primary_muscle_label: \"Músculo Principal:\",\n    unknown: \"Desconhecido\",\n    no_image_available: \"Nenhuma imagem disponível\",\n  },\n  heatmap: {\n    week_days_short: {\n      sunday: \"D\",\n      monday: \"S\",\n      tuesday: \"T\",\n      wednesday: \"Q\",\n      thursday: \"Q\",\n      friday: \"S\",\n      saturday: \"S\",\n    },\n    month_names_short: {\n      january: \"Jan\",\n      february: \"Fev\",\n      march: \"Mar\",\n      april: \"Abr\",\n      may: \"Mai\",\n      june: \"Jun\",\n      july: \"Jul\",\n      august: \"Ago\",\n      september: \"Set\",\n      october: \"Out\",\n      november: \"Nov\",\n      december: \"Dez\",\n    },\n    \"workout#one\": \"treino\",\n    \"workout#other\": \"treinos\",\n  },\n} as const;\n"
  },
  {
    "path": "locales/ru.ts",
    "content": "export default {\n  leaderboard: {\n    title: \"Рейтинг\",\n    description: \"Чемпионы тренировок\",\n    champion_badge: \"🏆 Чемпион\",\n    runner_up_badge: \"🥈 Второе место\",\n    third_place_badge: \"🥉 Третье место\",\n    second_place: \"2 место\",\n    third_place: \"3 место\",\n    workouts: \"тренировок\",\n    unable_to_load: \"Не удалось загрузить рейтинг\",\n    try_again_later: \"Попробуйте еще раз позже\",\n    no_champions_yet: \"Пока нет чемпионов\",\n    complete_first_workout: \"Завершите первую тренировку, чтобы занять трон!\",\n    member_since: \"Мембер с\",\n    workouts_per_week: \"тренировок/неделя\",\n    last_workout: \"Последняя тренировка\",\n    page_title: \"Рейтинг чемпионов\",\n    page_subtitle: \"Поднимитесь на вершину и станьте легендой Workout.cool\",\n    period_all_time: \"За все время\",\n    period_monthly: \"За месяц\",\n    period_weekly: \"За неделю\",\n    no_sessions_this_week: \"Нет тренировок на этой неделе\",\n    no_sessions_this_month: \"Нет тренировок в этом месяце\",\n    registered_members_only: \"Только зарегистрированные пользователи\",\n    registered_members_description: \"Создайте учетную запись, чтобы появиться в рейтинге\",\n    reset_timezone: \"Сброс Европа/Париж\",\n    reset_timezone_description: \"Еженедельные и месячные рейтинги сбрасываются в полночь по парижскому времени\",\n  },\n  programs: {\n    available_programs: \"Доступные программы\",\n    exercises_in_session: \"Упражнение в сессии\",\n    start_session: \"Начать сессию\",\n    starting_session: \"Запуск...\",\n    more_than: \"больше\",\n    my_progress: \"Мой прогресс\",\n    session: \"сессия\",\n    completed_feminine: \"завершенных\",\n    completed_sets: \"завершенных сессий\",\n    \"set#zero\": \"подход\",\n    \"set#one\": \"подход\",\n    \"set#other\": \"подходов\",\n    error_starting_session: \"Ошибка при запуске сессии\",\n    premium_session: \"Премиум сессия\",\n    premium_session_description:\n      \"Эта сессия является частью премиум-контента. Вы можете посмотреть детали, но не можете выполнить тренировку.\",\n    premium_session_exercises: \"Включенные упражнения\",\n    workout_description: \"Описание сессии\",\n    connect_to_access: \"Подключитесь для доступа\",\n    become_premium: \"Стань Премиум\",\n    back_to_program: \"Вернуться к программе\",\n    no_equipment: \"Без оборудования\",\n    workout_programs_title: \"Программы тренировок (+ в процессе создания)\",\n    workout_programs: \"Программы тренировок\",\n    workout_programs_description: \"Выберите свой вызов и станьте сильнее! 💪\",\n    no_programs_available: \"Программы не найдены\",\n    no_programs_available_description: \"Программы будут доступны в ближайшее время!\",\n    auth_required: \"Требуется аутентификация\",\n    auth_required_description: \"Вы должны войти в систему, чтобы получить доступ к этой тренировке.\",\n    login_to_continue: \"Войти для продолжения\",\n    signup_to_continue: \"Зарегистрироваться для продолжения\",\n    premium_required: \"Премиум требуется\",\n    premium_required_description: \"Эта сессия является премиум-контентом. Обновитесь, чтобы получить доступ ко всему премиум-контенту.\",\n    upgrade_to_premium: \"Обновить до Премиум\",\n    completed: \"Завершено\",\n    about: \"О программе\",\n    program: \"Программа\",\n    not_found: \"Программа не найдена\",\n    characteristics: \"Характеристики\",\n    weeks: \"недель\",\n    sessions_per_week: \"сессий/неделя\",\n    session_duration: \"мин/сессия\",\n    \"your_coach#zero\": \"Ваш крутой тренер\",\n    \"your_coach#one\": \"Ваш крутой тренер\",\n    \"your_coach#other\": \"Ваши крутые тренеры\",\n    community: \"Активная сообщество\",\n    community_count: \"coolbuilders уже присоединились\",\n    week_short: \"Нед.\",\n    week: \"Неделя\",\n    exercises: \"упражнений\",\n    min_short: \"мин\",\n    premium: \"Премиум\",\n    free: \"Бесплатно\",\n    join_cta: \"Присоединиться к программе\",\n    continue: \"Продолжить\",\n    sessions: \"Сессии\",\n    check_out_program: \"Откройте эту программу тренировки!\",\n    share_success: \"Поделиться успешно!\",\n    program_completed: \"Программа завершена\",\n    copied_to_clipboard: \"Ссылка скопирована!\",\n    share_failed: \"Ошибка при делении\",\n    important_info: \"Важная информация\",\n    donation_teaser:\n      \"Вначале мы работали на пожертвованиях. Но, как вы понимаете, пожертвований было недостаточно для покрытия расходов на разработку и поддержку. Поэтому мы создали пакет, который поможет нам держать свет включенным и разблокировать несколько суперспособностей по пути.\",\n    new: \"НОВЫЙ\",\n    more_programs_coming_title: \"Больше программ скоро!\",\n    more_programs_coming_description:\n      \"Мы работаем над созданием новых программ. Перейдя на Премиум сейчас, вы получите их все автоматически. Спасибо за вашу поддержку. 🚀\",\n    coming_strength: \"Сила & Мышцы\",\n    coming_cardio: \"Кардио HIIT\",\n    coming_yoga: \"Йога & Мобильность\",\n    sessions_coming_soon: \"Секции скоро!\",\n    sessions_in_creation: \"Наша команда работает над созданием качественных секций для этой недели. Вернитесь очень скоро! 🚀\",\n    welcome_modal: {\n      welcome_title: \"Добро пожаловать в {programTitle}!\",\n      subtitle: \"Приготовьтесь преодолевать свои пределы! 💪\",\n      level_label: \"Уровень\",\n      duration_label: \"Продолжительность\",\n      frequency_label: \"Частота\",\n      later_button: \"Позже\",\n      start_button: \"Поехали!\",\n    },\n  },\n  premium: {\n    checkout_error: \"Ошибка при оплате\",\n    premium_required_title: \"Требуется Премиум\",\n    premium_required_subtitle: \"Это премиум-доступ. Обновитесь до Премиум для доступа ко всему премиум-контенту.\",\n    premium_required_button: \"Обновить до Премиум\",\n    already_premium: \"Вы наслаждаетесь Workout.cool Premium\",\n    no_ads: \"Без рекламы\",\n    upgrade: \"Обновить\",\n\n    // Hero Section\n    pricing: {\n      month: \"месяц\",\n      year: \"год\",\n      monthly: \"Ежемесячно\",\n      yearly: \"Ежегодно\",\n      discount: \"-48%\",\n    },\n\n    hero: {\n      badge: \"Open-Source & Self-hosting ВСЕГДА бесплатны\",\n      title: \"Тренируйтесь свободно, поддержите миссию\",\n      subtitle: \"Для тех, кто верит в проект и хочет (ре)убедиться в себе с power boosters !\",\n      stats: {\n        athletes: {\n          count: \"12.4K+\",\n          label: \"Активные атлеты\",\n        },\n        series: {\n          count: \"1.2M+\",\n          label: \"Записанные подходы\",\n        },\n        rating: {\n          count: \"4.9/5\",\n          label: \"Рейтинг сообщества\",\n        },\n        progression: {\n          count: \"+23%\",\n          label: \"Средний прогресс\",\n        },\n      },\n\n      // Health Risks\n      health_risks: {\n        overweight: {\n          high_blood_pressure: \"Высокое кровяное давление\",\n          ldl_cholesterol: \"Повышенные уровни холестерина ЛПНП (плохой холестерин)\",\n          hdl_cholesterol: \"Пониженные уровни холестерина ЛПВП (хороший холестерин)\",\n          triglycerides: \"Высокие уровни триглицеридов\",\n          type_2_diabetes: \"Диабет II типа\",\n          coronary_heart_disease: \"Ишемическая болезнь сердца\",\n          stroke: \"Инсульт\",\n          gallbladder_disease: \"Заболевание желчного пузыря\",\n          osteoarthritis: \"Остеоартрит\",\n          sleep_apnea: \"Апноэ сна и проблемы с дыханием\",\n          certain_cancers: \"Определенные виды рака (эндометрия, молочной железы, толстой кишки, почек, желчного пузыря, печени)\",\n          low_quality_life: \"Низкое качество жизни\",\n          mental_illnesses: \"Психические заболевания, такие как клиническая депрессия и тревожность\",\n          body_pains: \"Боли в теле и трудности с физическими функциями\",\n          increased_mortality: \"Общий повышенный риск смертности\",\n        },\n        underweight: {\n          malnutrition: \"Недоедание и дефицит витаминов\",\n          anemia: \"Анемия (сниженная способность переносить кислород в крови)\",\n          osteoporosis: \"Остеопороз (повышенный риск переломов костей)\",\n          immune_function: \"Сниженная иммунная функция\",\n          growth_development: \"Проблемы роста и развития (особенно у детей)\",\n          reproductive_issues: \"Репродуктивные проблемы у женщин из-за гормональных дисбалансов\",\n          miscarriage_risk: \"Повышенная вероятность выкидыша в первом триместре\",\n          surgery_complications: \"Потенциальные осложнения во время операций\",\n          increased_mortality: \"Общий повышенный риск смертности\",\n          underlying_conditions: \"Может указывать на основные медицинские состояния\",\n        },\n      },\n    },\n\n    // Mission Banner\n    mission: {\n      supporters_count: \"234\",\n      supporters_text: \"поддерживающих миссию\",\n      limited: \"Ограниченный\",\n      early_access: \"ранний доступ\",\n    },\n\n    // Plans\n    plans: {\n      monthly: \"Ежемесячно\",\n      yearly: \"Ежегодно\",\n      yearly_discount: \"-48%\",\n      per_month: \"/месяц\",\n      per_year: \"/год\",\n\n      free: {\n        name: \"БЕСПЛАТНО\",\n        price: \"€0\",\n        period: \"/вечно\",\n        price_label: \"€0/год\",\n        badge: \"Open-Source • Всегда бесплатно\",\n        description: \"Все основные функции для тренировки\",\n        features: [\n          \"Генератор упражнений с видео\",\n          \"История тренировок типа GitHub (6 месяцев)\",\n          \"Поделиться и повторить сессии (скоро)\",\n          \"Авто-хостинг возможен\",\n          \"Доступ к исходному коду\",\n        ],\n        button: \"Ваш фактический план\",\n        footer_note: \"Без регистрации • Постоянный доступ\",\n      },\n\n      premium: {\n        name: \"PREMIUM ⭐\",\n        price_label: \"€7.90/месяц или €49/год\",\n        badge: \"МОДНЫЙ • Для энтузиастов\",\n        description: \"Все функции + ранний доступ\",\n        footer_monthly: \"Присоединяйтесь к страстному сообществу! 🔥\",\n        footer_yearly: \"Спасибо за годовую поддержку! 🙏\",\n        yearly_price_note: \"/month\",\n        features: [\n          \"...все из бесплатного плана\",\n          \"Без рекламы\",\n          \"Неограниченная история (вместо 6 месяцев бесплатно)\",\n          \"Отслеживание прогресса с расширенными статистическими данными (объем, прогресс, PR)\",\n          \"Предназначенные программы тренировок\",\n          \"Приватный чат с тренером 1:1\",\n          \"Ранний доступ к новым функциям\",\n        ],\n      },\n    },\n\n    // Buttons and Actions\n    actions: {\n      processing: \"Обработка...\",\n      go_premium: \"Перейти к Премиум\",\n      sign_in_continue: \"Перейти к Премиум\",\n      upgrade_now: \"Обновить сейчас\",\n      current_plan: \"Ваш фактический план\",\n    },\n\n    // Trust Elements\n    trust: {\n      gdpr_compliant: \"100% соответствует GDPR\",\n      money_back: \"30-дневный гарантийный срок\",\n      cancel_anytime: \"1 клик для отмены, без обязательств\",\n      secure_payment: \"100% безопасный платеж через Stripe\",\n    },\n\n    // Feature Comparison\n    comparison: {\n      title: \"Подробное сравнение функций\",\n      subtitle: \"Все, что вам нужно знать о том, что включено в каждый план\",\n      features_label: \"Функции\",\n      headers: {\n        features: \"Функции\",\n        free: \"Бесплатно\",\n        premium: \"Премиум\",\n      },\n      categories: {\n        equipment: \"Оборудование & Упражнения\",\n        tracking: \"Отслеживание & Аналитика\",\n        programs: \"Программы & ИИ\",\n        community: \"Сообщество & Общение\",\n        support: \"Поддержка & Проект\",\n      },\n      features: {\n        exercise_library: \"Библиотека упражнений\",\n        custom_exercise: \"Пользовательское упражнение\",\n        video_tutorials: \"Видеоуроки\",\n        workout_history: \"История тренировок\",\n        progress_statistics: \"Статистика прогресса\",\n        personal_records: \"Отслеживание личных рекордов\",\n        volume_analytics: \"Анализ объема & прогресса\",\n        predesigned_programs: \"Предназначенные программы\",\n        personalized_recommendations: \"Персонализированные рекомендации\",\n        pro_templates: \"Профессиональные шаблоны (пауэрлифтинг, бодибилдинг и т.д.)\",\n        community_access: \"Доступ к сообществу\",\n        discord_community: \"Сообщество Discord\",\n        private_chat: \"Приватный чат 1:1 с тренером\",\n        community_support: \"Поддержка сообщества\",\n        priority_support: \"Приоритетная поддержка\",\n        early_access: \"Ранний доступ к функциям\",\n        beta_testing: \"Тестирование бета-версии\",\n      },\n      values: {\n        basic: \"Базовый\",\n        complete: \"Полный\",\n        unlimited: \"Неограниченный\",\n        professional: \"Профессиональный\",\n        six_months: \"6 месяцев\",\n        limited: \"Ограниченный\",\n        all_programs: \"Все программы\",\n        public: \"Публичный\",\n        vip_access: \"VIP доступ\",\n        private_channels: \"Приватные каналы\",\n        soon: \"Скоро\",\n        hd_slowmo: \"4K + Slow-mo (медленное движение)\",\n        early_access: \"Ранний доступ\",\n      },\n    },\n\n    // FAQ\n    faq: {\n      title: \"Часто задаваемые вопросы\",\n      subtitle: \"Все, что вам нужно знать о Workout.cool и нашей миссии\",\n      items: [\n        {\n          question: \"Почему платить, если это open-source?\",\n          answer:\n            \"Отличный вопрос! Код всегда будет бесплатным, но поддержание серверов, базы данных и инфраструктуры стоит денег. Ваш вклад помогает нам держать инструмент бесплатным для всех. Это выигрышная модель: вы получаете премиум-функции, сообщество сохраняет бесплатный доступ!\",\n        },\n        {\n          question: \"Могу ли я самохостить Workout.cool?\",\n          answer:\n            \"Абсолютно! Весь исходный код доступен на GitHub под лицензией MIT. Вы можете развернуть его на своих собственных серверах, настроить его по своему усмотрению и использовать его бесплатно. Самохостинг дает вам полный контроль над вашими данными и конфиденциальностью тренировок.\",\n        },\n        {\n          question: \"Безопасны ли мои данные тренировок?\",\n          answer:\n            \"Да! Мы соответствуем GDPR, используем зашифрованные соединения и храним ваши данные в безопасности. Кроме того, поскольку мы являемся open-source, вы можете проверить наши практики безопасности. Вы также можете экспортировать свои данные в любое время или самохостить для полного контроля.\",\n        },\n        {\n          question: \"Могу ли я отменить подписку в любое время?\",\n          answer:\n            \"Конечно! Без контрактов, без обязательств. Отмените с одним кликом в любое время. Вы будете иметь доступ до окончания текущего периода оплаты, и вы всегда можете начать снова позже. Ваши данные тренировок остаются доступными, даже если вы перейдете на бесплатный план.\",\n        },\n        {\n          question: \"Есть ли упражнения для начинающих?\",\n          answer:\n            \"Конечно! Наша библиотека упражнений охватывает все уровни фитнеса от полных начинающих до опытных спортсменов. Видео и инструкции помогают начинающим найти подходящие упражнения, а наши видеоуроки показывают правильную форму.\",\n        },\n        {\n          question: \"Как работает отслеживание прогресса?\",\n          answer:\n            \"Каждый подход, повторение, вес и время автоматически регистрируются. Вы получаете историю тренировок в стиле GitHub, показывающую вашу последовательность, а также подробный анализ объема, прогресса и личных рекордов. Премиум-пользователи получают расширенные диаграммы и аналитику.\",\n        },\n        {\n          question: \"Могу ли я импортировать данные из других приложений?\",\n          answer:\n            \"Скоро. Мы будем поддерживать импорт данных в формате CSV для основных данных (повторения и вес). Если вы переходите с другого фитнес-приложения, наша команда поддержки может помочь перенести вашу историю тренировок.\",\n        },\n        {\n          question: \"Работает ли приложение без подключения к интернету?\",\n          answer:\n            \"Основное отслеживание тренировок работает без подключения к интернету. Вы можете регистрировать подходы и повторения без подключения к интернету для 10 тренировок. Видеоуроки и синхронизация в облаке требуют подключения к интернету. Все ваши данные без подключения к интернету синхронизируются автоматически, когда вы снова подключитесь к интернету.\",\n        },\n        {\n          question: \"Есть ли программы для женщин?\",\n          answer:\n            \"Конечно! И в будущем будет больше программ. Мы работаем над этим. Планы Supporter и Premium будут включать все будущие специализированные программы для разных целей: сила, тонус, пауэрлифтинг, бодибилдинг и многое другое!\",\n        },\n        {\n          question: \"Могу ли я создавать свои собственные программы?\",\n          answer: \"К сожалению, нет. Мы работаем над этим!\",\n        },\n      ],\n      additional_support: {\n        title: \"У вас все еще есть вопросы?\",\n        description: \"Наша сообщество фитнеса здесь, чтобы помочь вам достичь успеха\",\n        community: \"Поддержка сообщества (дискорд или hello@workout.cool)\",\n        discussions: \"Открытые обсуждения (github/discord)\",\n        roadmap: \"Прозрачный план (github)\",\n      },\n    },\n\n    // Final CTA\n    final_cta: {\n      motivation: \"Продолжайте двигаться вперед! 💪\",\n      title: \"Готовы поддержать миссию?\",\n      subtitle: \"Присоединяйтесь к тысячам фитнес-энтузиастов, которые верят в свободу тренировок с открытым исходным кодом\",\n      values: [\n        {\n          title: \"Сообщество в первую очередь\",\n          description: \"Создано и для сообщества фитнеса\",\n        },\n        {\n          title: \"Всегда прозрачно\",\n          description: \"Открытый исходный код, прозрачная финансирование\",\n        },\n        {\n          title: \"Дело любви\",\n          description: \"15 лет страсти!\",\n        },\n      ],\n      quote: {\n        text: \"Мы верим, что фитнес-инструменты должны быть доступны всем. Ваша поддержка помогает нам поддерживать это видение, продолжая инновации.\",\n        author: \"— Команда Workout.cool\",\n      },\n    },\n\n    // Premium Active State\n    premium_active: {\n      title: \"Премиум активен! 💪\",\n      supporting: \"Поддержка миссии\",\n    },\n\n    // Legacy translations (keeping for compatibility)\n    premium_active_title: \"Премиум Активен\",\n    premium_active_subtitle: \"Все функции разблокированы\",\n    free_intro_title: \"Вы уже получаете много бесплатно...\",\n    free_intro_text:\n      \"Workout.cool — это бесплатное приложение для фитнеса с открытым исходным кодом, которым ежедневно пользуются более 60 000 пользователей. Оно создано с любовью (а не на деньги венчурных фондов ^^) и требует реальных затрат времени и денег для поддержки.\",\n    donation_story_text:\n      \"Сначала мы работали на пожертвованиях. Но, как вы понимаете, пожертвований было недостаточно для покрытия расходов на разработку и поддержку. Поэтому мы создали пакет, который поможет нам держать свет включенным и разблокировать несколько суперспособностей по пути.\",\n    health_upgrade_text: \"Если Workout.cool помогает вам улучшить здоровье, пожалуйста, рассмотрите возможность перехода на Премиум :D !\",\n    unlock_features_text: \"Разблокируйте расширенные функции и поддержите фитнес с открытым кодом.\",\n    invest_yourself_quote: \"Никогда не экономьте на фитнесе и книгах — инвестируйте в себя!\",\n    support_mission: \"Поддержать миссию\",\n    best_value_badge: \"ЛУЧШАЯ ЦЕНА\",\n    annual_plan: \"Годовой\",\n    monthly_plan: \"Месячный\",\n    discount_badge: \"Скидка 40%\",\n    per_month: \"/месяц\",\n    feature_all_programs: \"Все программы тренировок\",\n    feature_progress_tracking: \"Отслеживание прогресса\",\n    coming_soon: \"(скоро)\",\n    feature_future_updates: \"Все будущие программы и обновления\",\n    feature_priority_support: \"Приоритетная поддержка\",\n    save_yearly: \"Экономьте 40% в год\",\n    processing: \"Обработка...\",\n    cta_annual: \"Хочу поддержать + сэкономить 40%\",\n    cta_monthly: \"Разблокировать полный план\",\n    thank_supporting: \"Спасибо за вашу поддержку.\",\n    no_pressure: \"Без давления. Вы можете обновиться в любое время.\",\n    keep_pushing: \"продолжайте двигаться вперед! huhu\",\n    still_unsure: \"Все еще не уверены? Не волнуйтесь. Workout.cool всегда останется бесплатным и с открытым исходным кодом.\",\n    support_helps: \"Но если вы верите в то, что мы создаем, и можете себе это позволить, ваша поддержка поможет 💚\",\n    self_hosting: \"Самостоятельный хостинг\",\n    community: \"Сообщество\",\n    mit_license: \"Лицензия MIT\",\n    pricing_year: \"год\",\n    pricing_month: \"месяц\",\n    conversion_flow_title: \"Перенаправление...\",\n    conversion_flow_message: \"Успешный вход! Перенаправление на оформление заказа...\",\n    redirecting_to_checkout: \"Перенаправление на оформление заказа\",\n  },\n  bottom_navigation: {\n    statistics: \"Статистика\",\n    statistics_tooltip: \"Просмотр статистики\",\n    programs: \"Программы\",\n    programs_tooltip: \"Просмотр программ\",\n    workouts: \"Тренировки\",\n    workouts_tooltip: \"Создать свою тренировку\",\n    premium: \"Премиум\",\n    premium_tooltip: \"Стань Премиум\",\n    tools: \"Инструменты\",\n    tools_tooltip: \"Просмотр инструментов\",\n    profile: \"Профиль\",\n    profile_tooltip: \"Просмотр профиля\",\n    leaderboard: \"Рейтинг\",\n    leaderboard_tooltip: \"Просмотр рейтинга\",\n  },\n  tools: {\n    try_now: \"Попробовать сейчас\",\n    title: \"Фитнес-инструменты\",\n    subtitle: \"Основные калькуляторы для оптимизации тренировок и питания\",\n    moreComingSoon: \"Больше инструментов скоро\",\n    meta: {\n      title: \"Фитнес-инструменты - Калькуляторы для тренировок и питания\",\n      description:\n        \"Бесплатные фитнес-калькуляторы: TDEE, макросы, ИМТ, зоны пульса, 1ПМ и многое другое. Оптимизируйте тренировки и питание с нашими инструментами.\",\n      keywords:\n        \"калькулятор фитнеса, калькулятор калорий, калькулятор макросов, калькулятор ИМТ, калькулятор TDEE, зоны пульса, максимальный повтор, фитнес-инструменты\",\n    },\n    \"calorie-calculator\": {\n      title: \"Калькулятор калорий\",\n      description: \"Рассчитайте дневную потребность в калориях (TDEE) на основе уровня активности и целей\",\n      meta: {\n        title: \"Калькулятор калорий - TDEE и дневная потребность в калориях\",\n        description:\n          \"Рассчитайте общий дневной расход энергии (TDEE) и дневную потребность в калориях. Получите персонализированные рекомендации для похудения, поддержания веса или набора мышечной массы.\",\n        keywords:\n          \"калькулятор калорий, калькулятор TDEE, дневные калории, калькулятор для похудения, потребность в калориях, калькулятор BMR, калькулятор метаболизма\",\n      },\n      subtitle: \"Рассчитайте дневную потребность в калориях на основе уравнения Миффлина-Сан Жеора\",\n      how_it_works: \"Как работает этот калькулятор?\",\n      how_it_works_description:\n        \"Этот калькулятор использует научно обоснованные формулы для оценки дневной потребности в калориях на основе ваших личных характеристик и образа жизни.\",\n      how_it_works_step1: \"Мы рассчитываем базовый метаболизм (калории, сжигаемые в покое)\",\n      how_it_works_step2: \"Мы корректируем на основе уровня активности\",\n      how_it_works_step3: \"Мы персонализируем согласно вашей цели (похудеть, поддержать или набрать вес)\",\n      calculate: \"Рассчитать\",\n      calculating: \"Расчет...\",\n      tap_info_icons: \"Нажмите на иконки ℹ️ для получения дополнительной информации\",\n      gender: \"Пол\",\n      male: \"Мужской\",\n      female: \"Женский\",\n      units: \"Единицы\",\n      metric: \"Метрическая\",\n      imperial: \"Имперская\",\n      age: \"Возраст\",\n      age_placeholder: \"Введите ваш возраст\",\n      years: \"лет\",\n      height: \"Рост\",\n      height_placeholder: \"Введите ваш рост\",\n      weight: \"Вес\",\n      weight_placeholder: \"Введите ваш вес\",\n      cm: \"см\",\n      kg: \"кг\",\n      lbs: \"фунты\",\n      feet: \"футы\",\n      inches: \"дюймы\",\n      activity_level: \"Уровень активности\",\n      activity: {\n        sedentary: \"Малоподвижный\",\n        sedentary_desc: \"Мало или нет упражнений, сидячая работа, минимальная ходьба\",\n        light: \"Легко активный\",\n        light_desc: \"Легкие упражнения 1-3 дня/неделю или ежедневная ходьба\",\n        moderate: \"Умеренно активный\",\n        moderate_desc: \"Умеренные упражнения 3-5 дней/неделю, активный образ жизни\",\n        active: \"Очень активный\",\n        active_desc: \"Интенсивные упражнения 6-7 дней/неделю, очень активная работа\",\n        very_active: \"Чрезвычайно активный\",\n        very_active_desc: \"Спортсмен, физическая работа + ежедневные тренировки\",\n      },\n      goal: \"Цель\",\n      goals: {\n        lose_fast: \"Быстро похудеть\",\n        lose_fast_desc: \"Потерять 2 фунта (1 кг) в неделю - Агрессивно, но эффективно\",\n        lose_slow: \"Похудеть\",\n        lose_slow_desc: \"Потерять 1 фунт (0,5 кг) в неделю - Устойчиво и здорово\",\n        maintain: \"Поддерживать вес\",\n        maintain_desc: \"Оставаться в текущем весе - Идеально для поддержания формы\",\n        gain_slow: \"Набрать вес\",\n        gain_slow_desc: \"Набрать 1 фунт (0,5 кг) в неделю - Чистый набор мышечной массы\",\n        gain_fast: \"Быстро набрать вес\",\n        gain_fast_desc: \"Набрать 2 фунта (1 кг) в неделю - Максимальный рост мышц\",\n      },\n      results: {\n        overview: \"Обзор\",\n        title: \"Ваши результаты\",\n        bmr: \"BMR\",\n        bmr_explanation:\n          \"Базальная скорость метаболизма (BMR) - это количество калорий, которое ваше тело сжигает в полном покое, просто для поддержания основных функций, таких как дыхание, кровообращение и производство клеток. Это минимальная энергия, необходимая вашему телу для выживания.\",\n        tdee: \"TDEE\",\n        tdee_explanation:\n          \"Общий дневной расход энергии (TDEE) - это ваш BMR плюс калории, сжигаемые через ежедневные активности и упражнения. Это общее количество калорий, которое вы сжигаете за день на основе уровня активности.\",\n        target: \"Целевые калории\",\n        macros: \"Рекомендуемые макросы\",\n        macros_explanation:\n          \"Макронутриенты (макросы) - это три основные группы питательных веществ, необходимых вашему организму: Белки (для построения и восстановления мышц), Углеводы (для энергии) и Жиры (для гормонов и усвоения витаминов). Показанные проценты представляют сбалансированное распределение, подходящее для большинства фитнес-целей.\",\n        protein: \"Белки\",\n        carbs: \"Углеводы\",\n        fat: \"Жиры\",\n        disclaimer:\n          \"Эти расчеты являются оценками на основе средних формул. Фактическая потребность в калориях может варьироваться в зависимости от индивидуальных факторов. Проконсультируйтесь с медицинским работником или дипломированным диетологом для получения персонализированного совета.\",\n      },\n      faq: {\n        title: \"Часто задаваемые вопросы\",\n        q1: \"Почему моя цель по калориям отличается от других калькуляторов?\",\n        a1: \"Разные калькуляторы могут использовать разные формулы или множители активности. Мы используем уравнение Миффлина-Сан Жеора, которое считается одним из самых точных для большинства людей. Однако индивидуальный метаболизм может варьироваться на 10-20% от этих оценок.\",\n        q2: \"Должен ли я есть именно столько калорий каждый день?\",\n        a2: \"Это средние дневные цели. Нормально есть немного больше в некоторые дни и меньше в другие. Сосредоточьтесь на вашем недельном среднем, а не на точности каждый день. Слушайте сигналы голода и сытости вашего тела.\",\n        q3: \"Что если я не вижу результатов после следования этим рекомендациям?\",\n        a3: \"Если вы не видите результатов после 2-3 недель, возможно, нужно скорректировать. Ваш фактический метаболизм может быть выше или ниже расчетного. Попробуйте скорректировать на 100-200 калорий и отслеживайте еще 2 недели. Также убедитесь, что точно отслеживаете свою еду.\",\n        q4: \"Подходят ли рекомендации по макросам всем?\",\n        a4: \"Соотношение 30/40/30 (белки/углеводы/жиры) - это сбалансированный подход, подходящий для большинства людей. Однако спортсменам, людям с медицинскими состояниями или тем, кто следует специальным диетам (кето, веган и т.д.), могут потребоваться другие соотношения. Обратитесь к диетологу за персонализированными рекомендациями.\",\n      },\n    },\n    \"macro-calculator\": {\n      title: \"Калькулятор макросов\",\n      description: \"Найдите оптимальное распределение белков, углеводов и жиров для ваших фитнес-целей\",\n    },\n    \"bmi-calculator\": {\n      title: \"Калькулятор ИМТ\",\n      description: \"Рассчитайте индекс массы тела и поймите свою весовую категорию\",\n    },\n    \"heart-rate-calculator\": {\n      title: \"Зоны пульса\",\n      description: \"Откройте оптимальные тренировочные зоны для сжигания жира и производительности\",\n    },\n    \"heart-rate-zones\": {\n      title: \"Калькулятор зон частоты сердечных сокращений\",\n      description: \"Рассчитайте оптимальные зоны тренировки по пульсу для максимальной производительности и сжигания жира\",\n      page_title: \"Калькулятор зон частоты сердечных сокращений\",\n      page_description:\n        \"Рассчитайте персональные зоны пульса с помощью научно обоснованных формул. Оптимизируйте кардиотренировки для сжигания жира, выносливости и результатов.\",\n      meta: {\n        title: \"Калькулятор зон пульса – Целевой пульс и зоны тренировки\",\n        description:\n          \"Рассчитайте вашу максимальную ЧСС и персональные зоны тренировки. Используйте базовые формулы или формулу Карвонена для зон VO2 Max, анаэробной, аэробной, жиросжигающей и разминки.\",\n        keywords:\n          \"калькулятор зон пульса, целевой пульс, максимальный пульс, зоны тренировки, зона VO2 Max, анаэробная зона, аэробная зона, зона сжигания жира, формула Карвонена, тренировка по пульсу\",\n      },\n      calculate: \"Рассчитать зоны\",\n      calculating: \"Расчет...\",\n      method: \"Метод расчета\",\n      method_info: \"Выберите формулу, подходящую для вашего уровня подготовки и доступных данных\",\n      methods: {\n        basic: \"Простая по возрасту\",\n        basic_desc: \"Простая формула, использует только возраст – подходит новичкам\",\n        karvonen_age: \"Карвонен по возрасту и ЧСС₀\",\n        karvonen_age_desc: \"Более точная, учитывает возраст и пульс в покое\",\n        karvonen_custom: \"Карвонен по ЧССₘₐₓ и ЧСС₀\",\n        karvonen_custom_desc: \"Самая точная, использует измеренные максимальный и покоящийся пульс\",\n      },\n      age: \"Возраст\",\n      age_placeholder: \"Введите ваш возраст\",\n      resting_heart_rate: \"Пульс в покое (ЧСС₀)\",\n      resting_heart_rate_placeholder: \"Введите ЧСС₀\",\n      resting_heart_rate_info: \"Измерьте пульс сразу после пробуждения, до подъёма с кровати. Норма: 60–100 уд/мин.\",\n      max_heart_rate: \"Максимальная ЧСС (ЧССₘₐₓ)\",\n      max_heart_rate_placeholder: \"Введите ЧССₘₐₓ\",\n      max_heart_rate_info: \"Ваш максимальный пульс из теста или интенсивной тренировки. Точнее, чем оценка по возрасту.\",\n\n      results: {\n        title: \"Ваши зоны пульса\",\n        max_heart_rate: \"Максимальная ЧСС\",\n        heart_rate_reserve: \"Резерв пульса\",\n        target_zones: \"Целевые зоны тренировки\",\n        zone: \"Зона\",\n        intensity: \"Интенсивность\",\n        heart_rate_range: \"Пульс (уд/мин)\",\n        benefits: \"Польза\",\n        duration: \"Типичная длительность\",\n      },\n      zones: {\n        warm_up: {\n          name: \"Зона разминки\",\n          intensity: \"50–60%\",\n          benefits: \"🧘 Идеальная разминка\",\n          example: \"Легкая прогулка\",\n          duration: \"5–10 мин\",\n          description: \"Очень легкая нагрузка для разминки и восстановления\",\n        },\n        fat_burn: {\n          name: \"Зона жиросжигания\",\n          intensity: \"60–70%\",\n          benefits: \"🔥 Сжигает жир\",\n          example: \"Легкий бег\",\n          duration: \"20–40 мин\",\n          description: \"Легкая интенсивность для длительных тренировок\",\n        },\n        aerobic: {\n          name: \"Аэробная зона\",\n          intensity: \"70–80%\",\n          benefits: \"💪 Улучшает выносливость\",\n          example: \"Умеренный бег\",\n          duration: \"10–40 мин\",\n          description: \"Средняя интенсивность, выдерживается длительное время\",\n        },\n        anaerobic: {\n          name: \"Анаэробная зона\",\n          intensity: \"80–90%\",\n          benefits: \"⚡ Увеличивает скорость\",\n          example: \"Короткий спринт\",\n          duration: \"2–10 мин\",\n          description: \"Высокая нагрузка, держится короткое время\",\n        },\n        vo2_max: {\n          name: \"Зона VO2 Max\",\n          intensity: \"90–100%\",\n          benefits: \"🏆 Максимальная мощность\",\n          example: \"Интенсивный спринт\",\n          duration: \"30 с–2 мин\",\n          description: \"Максимальная нагрузка, выдерживается очень кратко\",\n        },\n      },\n      formulas: {\n        basic_formula: \"Базовая формула\",\n        basic_explanation: \"ЦП = ЧССₘₐₓ × %Интенсивности\",\n        karvonen_formula: \"Формула Карвонена\",\n        karvonen_explanation: \"ЦП = [(ЧССₘₐₓ – ЧСС₀) × %Интенсивности] + ЧСС₀\",\n        mhr_calculation: \"ЧССₘₐₓ = 220 – возраст\",\n      },\n      abbreviations: {\n        thr: \"ЦП = целевой пульс\",\n        mhr: \"ЧССₘₐₓ = максимальная ЧСС\",\n        rhr: \"ЧСС₀ = пульс в покое\",\n        hrr: \"РП = резерв пульса\",\n        bpm: \"уд/мин = удары в минуту\",\n      },\n      tips: {\n        title: \"Советы по тренировкам\",\n        tip1: \"Начинайте с низкой интенсивности, если вы новичок\",\n        tip2: \"Комбинируйте разные зоны в недельном плане для лучшего результата\",\n        tip3: \"Используйте пульсометр для точного контроля\",\n        tip4: \"Зоны могут меняться с улучшением формы – пересчитывайте регулярно\",\n      },\n      faq: {\n        title: \"Часто задаваемые вопросы\",\n        q1: \"Какой метод расчета выбрать?\",\n        a1: \"Если вы новичок, используйте базовый. Зная пульс в покое, берите Карвонен по возрасту. Для максимальной точности – Карвонен по измеренным ЧССₘₐₓ и ЧСС₀.\",\n        q2: \"Как измерить пульс в покое?\",\n        a2: \"Измерьте пульс 60 с сразу после пробуждения, до подъёма. Повторите 3–5 дней и возьмите среднее. Норма: 60–100 уд/мин.\",\n        q3: \"В какой зоне тренироваться для похудения?\",\n        a3: \"Зона жиросжигания (60–70%) оптимальна для сжигания жира. Зоны выше интенсивности сжигают больше калорий. Сочетайте обе.\",\n        q4: \"Насколько точна формула 220 – возраст?\",\n        a4: \"Это общая оценка, может отклоняться на ±10–15 уд/мин. Для точности – тест под контролем или формула Карвонена.\",\n        q5: \"Можно ли каждый день тренироваться в зоне VO2 Max?\",\n        a5: \"Нет, слишком высоко. 1–2 раза в неделю короткими интервалами. Основной объем в аэробной и жиросжигающей зонах.\",\n      },\n      guide: {\n        title: \"Полный гид по зонам пульса\",\n        text1:\n          \"Зоны пульса – научный инструмент для оптимизации тренировок и достижения целей. Хотите похудеть, повысить выносливость или улучшить результаты – правильные зоны изменят ваш подход.\",\n        text2:\n          \"Этот калькулятор использует проверенные формулы для персональных зон на основе возраста и (опционально) пульса в покое. Каждая зона имеет свои характеристики и пользу для сердца.\",\n      },\n      table: {\n        title: \"Таблица ЧСС по возрасту\",\n        col1: \"Возраст\",\n        col2: \"ЧССₘₐₓ\",\n        col3: \"50% интенсивности\",\n        col4: \"85% интенсивности\",\n        avertiser: \"* Значения средние. Ваша ЧССₘₐₓ может варьироваться на ±10–15 уд/мин.\",\n      },\n      details: {\n        title: \"Подробно о 5 зонах тренировки\",\n        benefits: \"Польза\",\n        zone1_title: \"Зона 1: Разминка (50–60% ЧССₘₐₓ)\",\n        zone1_content: \"Идеальна для начала или восстановления. Можно говорить без одышки.\",\n        zone1_details_1: \"Улучшает кровообращение\",\n        zone1_details_2: \"Готовит мышцы и суставы\",\n        zone1_details_3: \"Снижает риск травм\",\n        zone1_details_4: \"Содействует активному восстановлению\",\n        zone1_duration: \"Рекомендуемая длительность\",\n        zone1_duration_value: \"5–10 мин в начале/конце\",\n        zone1_duration_value_2: \"20–30 мин для восстановления\",\n        zone2_title: \"Зона 2: Жиросжигание (60–70% ЧССₘₐₓ)\",\n        zone2_content: \"Основная энергия – жир. Развивает базовую выносливость и метаболизм.\",\n        zone2_details_1: \"Максимальное сжигание жира\",\n        zone2_details_2: \"Развитие аэробной выносливости\",\n        zone2_details_3: \"Улучшение работы сердца\",\n        zone2_details_4: \"Укрепление иммунитета\",\n        zone2_duration: \"Рекомендуемая длительность\",\n        zone2_duration_value: \"30–90 мин для выносливости\",\n        zone2_duration_value_2: \"45–60 мин для похудения\",\n        zone3_title: \"Зона 3: Аэробная (70–80% ЧССₘₐₓ)\",\n        zone3_content: \"Усиленная нагрузка, но можно говорить короткими фразами. Основная зона для спортсменов.\",\n        zone3_details_1: \"Увеличивает ёмкость лёгких\",\n        zone3_details_2: \"Улучшает сердечно-сосудистую выносливость\",\n        zone3_details_3: \"Укрепляет сердце\",\n        zone3_details_4: \"Оптимизирует использование кислорода\",\n        zone3_duration: \"Рекомендуемая длительность\",\n        zone3_duration_value: \"20–60 мин непрерывно\",\n        zone3_duration_value_2: \"Интервалы 5–15 мин\",\n        zone4_title: \"Зона 4: Анаэробная (80–90% ЧССₘₐₓ)\",\n        zone4_content: \"Производится лактат быстрее, чем выводится. Развивает скорость и мощность.\",\n        zone4_details_1: \"Рост мышечной силы\",\n        zone4_details_2: \"Улучшение толерантности к лактату\",\n        zone4_details_3: \"Развитие скорости\",\n        zone4_details_4: \"Укрепление воли\",\n        zone4_duration: \"Рекомендуемая длительность\",\n        zone4_duration_value: \"Интервалы 2–8 мин\",\n        zone4_duration_value_2: \"Время восстановления равно или вдвое больше\",\n        zone5_title: \"Зона 5: VO2 Max (90–100% ЧССₘₐₓ)\",\n        zone5_content: \"Максимальная нагрузка. Можно говорить лишь пару слов. Только для опытных.\",\n        zone5_details_1: \"Максимизация аэробной ёмкости\",\n        zone5_details_2: \"Улучшение экономики бега\",\n        zone5_details_3: \"Развитие максимальной мощности\",\n        zone5_details_4: \"Преодоление ментальных границ\",\n        zone5_duration: \"Рекомендуемая длительность\",\n        zone5_duration_value: \"Интервалы 30 с–2 мин\",\n        zone5_duration_value_2: \"Макс. 1–2 раза в неделю\",\n      },\n      educational: {\n        title: \"Понимание тренировок по пульсу\",\n        description: \"Легко визуализируйте каждую зону тренировки\",\n        what_are_zones: {\n          title: \"Что такое зоны пульса?\",\n          content: \"Это диапазоны ударов в минуту, соответствующие разной интенсивности. Помогают эффективно достигать целей.\",\n        },\n        why_use_zones: {\n          title: \"Зачем использовать зоны пульса?\",\n          content:\n            \"Гарантируют нужную интенсивность, предотвращают переутомление, максимизируют результаты и делают тренировки эффективнее.\",\n        },\n        zone_distribution: {\n          title: \"Рекомендуемое недельное распределение\",\n          content: \"80% в зонах 1–3 (аэробика), 15% в зоне 4 (порог), 5% в зоне 5 (VO2 Max). Настраивайте под свои цели и уровень.\",\n        },\n        monitoring: {\n          title: \"Как мониторить пульс\",\n          content:\n            \"Используйте нагрудный сенсор для точности или наручный – для удобства. Регулярно проверяйте пульс и корректируйте нагрузку.\",\n        },\n      },\n      training_tips: {\n        title: \"Советы экспертов\",\n        tip1: {\n          title: \"Постепенная разминка\",\n          description: \"Начинайте с 5–10 мин в зоне 1 (50–60%) для подготовки сердца.\",\n        },\n        tip2: {\n          title: \"Правило 80/20\",\n          description: \"80% тренировок в зонах 1–3, 20% в зонах 4–5 для оптимального прогресса.\",\n        },\n        tip3: {\n          title: \"Активное восстановление\",\n          description: \"После интенсивной нагрузки опускайтесь в зоны 1–2 на 5–10 мин.\",\n        },\n        tip4: {\n          title: \"Постоянная гидратация\",\n          description: \"Пейте до, во время и после тренировки. Обезвоживание повышает пульс.\",\n        },\n        tip5: {\n          title: \"Качественный сон\",\n          description: \"7–9 часов сна улучшают восстановление и снижают пульс в покое.\",\n        },\n        tip6: {\n          title: \"Постепенное увеличение\",\n          description: \"Увеличивайте нагрузку не более чем на 10% в неделю, чтобы избежать перетренированности.\",\n        },\n      },\n      training_tips_2: {\n        title: \"Практические советы\",\n        title1: \"Найдите свою зону\",\n        description1: \"Каждая зона – своя цель. Выбирайте согласно задаче!\",\n        title2: \"Рекомендуемая длительность\",\n        description2: \"Чем выше интенсивность, тем короче должна быть тренировка.\",\n        title3: \"Прогрессия\",\n        description3: \"Начинайте медленно и постепенно увеличивайте нагрузку.\",\n        title4: \"Слушайте тело\",\n        description4: \"Если плохо – снижайте интенсивность сразу.\",\n      },\n      quick_facts: {\n        title: \"Знали ли вы?\",\n        fact1: \"220 – ваш возраст = приблизительная максимальная ЧСС\",\n        fact2: \"Измерьте пульс сразу после пробуждения для ЧСС₀\",\n        fact3: \"Смарт-часы могут отслеживать пульс в реальном времени\",\n        fact4: \"80% тренировок должно быть в зонах 1–3\",\n      },\n      weekly_plan: {\n        title: \"Пример недельного плана\",\n        description: \"Балансированная неделя тренировок\",\n        monday: {\n          title: \"Зоны 1–2\",\n          description: \"30–45 мин\",\n        },\n        tuesday: {\n          title: \"Зоны 2–3\",\n          description: \"45–60 мин\",\n        },\n        wednesday: {\n          title: \"Отдых\",\n          description: \"Восстановление\",\n        },\n        thursday: {\n          title: \"Зоны 3–4\",\n          description: \"30–40 мин\",\n        },\n        friday: {\n          title: \"Зоны 1–2\",\n          description: \"30 мин\",\n        },\n        saturday: {\n          title: \"Зоны 4–5\",\n          description: \"20–30 мин\",\n        },\n        tips: \"💡 Корректируйте план под уровень и цели!\",\n        cta: \"⬆️ Рассчитать зоны сейчас\",\n      },\n      seo_faq_title: \"Часто спрашивают о зонах пульса\",\n      seo_faq_q1_question: \"Что такое максимальная ЧСС (ЧССₘₐₓ)?\",\n      seo_faq_q1_answer:\n        \"Максимальная ЧСС – наибольшее число ударов в минуту при интенсивной нагрузке. Обычно 220 – возраст. Может отклоняться ±10–15 уд/мин.\",\n      seo_faq_q2_question: \"Как измерить пульс в покое?\",\n      seo_faq_q2_answer:\n        \"Измерьте 60 с сразу после пробуждения до подъёма. Или 15 с ×4. Повторите 3–5 дней и возьмите среднее. Норма 60–100 уд/мин.\",\n      seo_faq_q3_question: \"Какая зона лучше для похудения?\",\n      seo_faq_q3_answer: \"Жиросжигающая (60–70%) – лучшая для жира. Но более интенсивные зоны сжигают больше калорий. Чередуйте.\",\n      seo_faq_q4_question: \"Можно ли каждый день тренироваться в зоне VO2 Max?\",\n      seo_faq_q4_answer:\n        \"Нет, зона VO2 Max (90–100%) очень интенсивна. 1–2 раза в неделю короткими интервалами (30 с–2 мин). Основной объём в аэробике.\",\n      seo_faq_q5_question: \"Точная ли формула 220 – возраст?\",\n      seo_faq_q5_answer: \"Оценка средняя, может колебаться ±10–15 уд/мин. Для точности – формула Карвонена или тест под наблюдением.\",\n      seo_faq_q6_question: \"Как понять, что в нужной зоне?\",\n      seo_faq_q6_answer:\n        \"Используйте пульсометр. Без него – тест речи: легкая зона = свободный разговор, средняя = короткие фразы, интенсивная = отдельные слова.\",\n      seo_faq_q7_question: \"Меняются ли зоны с улучшением формы?\",\n      seo_faq_q7_answer: \"Да, пульс в покое снижается, эффективность сердца растёт. Пересчитывайте зоны каждые 2–3 месяца.\",\n      seo_faq_q8_question: \"В чем разница между Basic и Karvonen?\",\n      seo_faq_q8_answer:\n        \"Basic учитывает только возраст (ЦП = ЧССₘₐₓ × %). Karvonen точнее, учитывает пульс в покое: ЦП = [(ЧССₘₐₓ – ЧСС₀) × %] + ЧСС₀.\",\n      intern_links_title: \"Готовы оптимизировать тренировки?\",\n      intern_links_subtitle: \"Используйте калькулятор для своих персональных зон и улучшите фитнес\",\n      intern_links_button: \"Рассчитать мои зоны\",\n      intern_links_bmi_title: \"Калькулятор ИМТ\",\n      intern_links_bmi_description: \"Оцените индекс массы тела\",\n      intern_links_calorie_title: \"Калькулятор калорий\",\n      intern_links_calorie_description: \"Определите суточные потребности\",\n      intern_links_macro_title: \"Калькулятор макроэлементов\",\n      intern_links_macro_description: \"Оптимизируйте распределение питательных веществ\",\n      cta: {\n        title: \"Готовы оптимизировать тренировки?\",\n        subtitle: \"Используйте калькулятор для своих персональных зон и улучшите фитнес\",\n        button: \"Рассчитать мои зоны\",\n        bmi_title: \"Калькулятор ИМТ\",\n        bmi_description: \"Оцените индекс массы тела\",\n        calorie_title: \"Калькулятор калорий\",\n        calorie_description: \"Определите суточные потребности\",\n        macro_title: \"Калькулятор макроэлементов\",\n        macro_description: \"Оптимизируйте распределение питательных веществ\",\n      },\n      medical_warning_title: \"Важное медицинское предупреждение\",\n      medical_warning_content:\n        \"Этот калькулятор даёт оценки по общим формулам. Результаты могут меняться в зависимости от состояния здоровья, препаратов и уровня подготовки. Перед началом новой программы проконсультируйтесь с врачом, особенно при мед. противопоказаниях или необычных симптомах во время тренировки.\",\n    },\n    \"one-rep-max\": {\n      title: \"Калькулятор 1ПМ\",\n      description: \"Оцените свой максимальный повтор и планируйте проценты силовых тренировок\",\n    },\n    back_to_calculators: \"Назад к калькуляторам\",\n    body_fat_percentage: \"Процент жира в теле\",\n    body_fat_info_title: \"Что такое процент жира в теле?\",\n    body_fat_info_content:\n      \"Процент жира в теле важен для формул Катча-МакАрдла и Каннингема, поскольку они рассчитывают на основе тощей массы тела. Если вы не знаете точный процент жира в теле, используйте онлайн визуальные руководства или DEXA-сканирование для точности.\",\n    \"calorie-calculator-hub\": {\n      title: \"Формулы калькулятора калорий\",\n      subtitle: \"Выберите лучшую формулу для ваших потребностей и получите точные расчеты калорий\",\n      meta: {\n        title: \"Формулы калькулятора калорий - Калькуляторы BMR и TDEE\",\n        description:\n          \"Сравните различные формулы BMR: Миффлина-Сан Жеора, Харриса-Бенедикта, Катча-МакАрдла, Каннингема и Оксфорда. Выберите лучший калькулятор калорий для ваших потребностей.\",\n        keywords:\n          \"формулы BMR, сравнение калькуляторов калорий, Миффлин-Сан Жеор, Харрис-Бенедикт, Катч-МакАрдл, Каннингем, Оксфорд, калькулятор TDEE\",\n      },\n      which_formula: \"Какую формулу выбрать?\",\n      formula_explanation: \"Разные формулы работают лучше для разных людей. Вот краткое руководство, которое поможет вам выбрать:\",\n      recommendation_general: \"Лучшая общая формула, наиболее точная для общей популяции\",\n      recommendation_traditional: \"Классическая формула, широко используемая, но немного менее точная\",\n      recommendation_bodyfat: \"Наиболее точная, если вы знаете процент жира в теле\",\n      since: \"С\",\n      all_formulas: \"Все формулы\",\n      popularity: \"Популярность\",\n      accuracy: \"Точность\",\n      accuracy_high: \"Высокая\",\n      accuracy_good: \"Хорошая\",\n      accuracy_medium: \"Средняя\",\n      best_for: \"Лучше для\",\n      best_for_general: \"Общего использования\",\n      best_for_traditional: \"Традиционного\",\n      best_for_athletes: \"Спортсменов\",\n      best_for_bodybuilders: \"Бодибилдеров\",\n      best_for_european: \"Европейского населения\",\n      best_for_comparison: \"Сравнения всех\",\n      \"mifflin-st-jeor\": {\n        title: \"Миффлин-Сан Жеор (Рекомендуется)\",\n        description:\n          \"Наиболее точная формула для общей популяции, разработанная в 1990 году. В настоящее время золотой стандарт для расчетов BMR.\",\n      },\n      \"harris-benedict\": {\n        title: \"Харрис-Бенедикт (Классическая)\",\n        description:\n          \"Пересмотренная версия 1984 года классической формулы. Широко используется, но имеет тенденцию переоценивать калории для некоторых людей.\",\n      },\n      \"katch-mcardle\": {\n        title: \"Катч-МакАрдл (Спортсмены)\",\n        description: \"Основана на тощей массе тела. Наиболее точная для людей, которые знают процент жира в теле и физически активны.\",\n      },\n      cunningham: {\n        title: \"Каннингем (Бодибилдеры)\",\n        description: \"Разработана для очень худых спортсменов и бодибилдеров с низким процентом жира. Использует расчет тощей массы тела.\",\n      },\n      oxford: {\n        title: \"Оксфорд (Европейская)\",\n        description: \"Более современная формула (2005), основанная на европейских популяциях. Учитывает возрастные группы.\",\n      },\n      comparison: {\n        title: \"Сравнить все формулы\",\n        description: \"Сравните результаты всех формул бок о бок, чтобы увидеть различия и выбрать то, что работает лучше всего для вас.\",\n      },\n    },\n    \"mifflin-st-jeor\": {\n      title: \"Калькулятор Миффлина-Сан Жеора\",\n      subtitle: \"Золотой стандарт для расчета BMR - наиболее точный для общей популяции\",\n      meta: {\n        title: \"Калькулятор Миффлина-Сан Жеора - Наиболее точный BMR и TDEE\",\n        description:\n          \"Рассчитайте BMR и TDEE, используя уравнение Миффлина-Сан Жеора - наиболее точную формулу для общей популяции. Получите персонализированные рекомендации по калориям.\",\n        keywords:\n          \"калькулятор Миффлина-Сан Жеора, калькулятор BMR, калькулятор TDEE, наиболее точный калькулятор калорий, калькулятор метаболизма\",\n      },\n      how_it_works: \"Как работает формула Миффлина-Сан Жеора\",\n      how_it_works_description:\n        \"Разработанная в 1990 году, эта формула считается наиболее точной для расчета базальной скорости метаболизма (BMR) у здоровых взрослых. Она более точная, чем уравнение Харриса-Бенедикта, и широко рекомендуется диетологами и фитнес-профессионалами.\",\n    },\n    \"harris-benedict\": {\n      title: \"Калькулятор Харриса-Бенедикта\",\n      subtitle: \"Классическая формула BMR - традиционный подход к расчету калорий\",\n      meta: {\n        title: \"Калькулятор Харриса-Бенедикта - Классическая формула BMR и TDEE\",\n        description:\n          \"Рассчитайте BMR и TDEE, используя пересмотренное уравнение Харриса-Бенедикта (1984). Классическая формула, которая начала современные расчеты калорий.\",\n        keywords:\n          \"калькулятор Харриса-Бенедикта, классический калькулятор BMR, традиционный калькулятор TDEE, пересмотренная формула Харриса-Бенедикта\",\n      },\n      how_it_works: \"Как работает формула Харриса-Бенедикта\",\n      how_it_works_description:\n        \"Первоначально разработанная в 1919 году и пересмотренная в 1984 году, уравнение Харриса-Бенедикта было одной из первых формул для расчета BMR. Хотя она немного менее точная, чем новые формулы, она остается широко используемой и дает хорошие оценки для большинства людей.\",\n    },\n    \"katch-mcardle\": {\n      title: \"Калькулятор Катча-МакАрдла\",\n      subtitle: \"Точный расчет BMR на основе тощей массы тела - идеально для спортсменов\",\n      meta: {\n        title: \"Калькулятор Катча-МакАрдла - BMR и TDEE на основе тощей массы тела\",\n        description:\n          \"Рассчитайте BMR и TDEE, используя формулу Катча-МакАрдла на основе тощей массы тела. Наиболее точная для людей, которые знают процент жира в теле.\",\n        keywords:\n          \"калькулятор Катча-МакАрдла, BMR тощей массы тела, калькулятор процента жира в теле, калькулятор BMR спортсмена, точный TDEE\",\n      },\n      how_it_works: \"Как работает формула Катча-МакАрдла\",\n      how_it_works_description:\n        \"Эта формула рассчитывает BMR на основе тощей массы тела, а не общего веса тела, что делает ее более точной для людей, которые знают процент жира в теле. Она особенно полезна для спортсменов и физически активных людей.\",\n    },\n    cunningham: {\n      title: \"Калькулятор Каннингема\",\n      subtitle: \"Формула BMR, разработанная для очень худых спортсменов и бодибилдеров\",\n      meta: {\n        title: \"Калькулятор Каннингема - BMR для худых спортсменов и бодибилдеров\",\n        description:\n          \"Рассчитайте BMR и TDEE, используя формулу Каннингема, специально разработанную для очень худых спортсменов и бодибилдеров с низким процентом жира.\",\n        keywords:\n          \"калькулятор Каннингема, калькулятор BMR бодибилдера, BMR худого спортсмена, калькулятор BMR низкого жира, калькулятор подготовки к соревнованиям\",\n      },\n      how_it_works: \"Как работает формула Каннингема\",\n      how_it_works_description:\n        \"Разработанная специально для очень худых людей с низким процентом жира в теле, эта формула дает более высокие оценки BMR, чем другие уравнения. Она наиболее точна для соревнующихся спортсменов и бодибилдеров в подготовке к соревнованиям.\",\n    },\n    oxford: {\n      title: \"Калькулятор Оксфорда\",\n      subtitle: \"Современная формула BMR на основе европейских популяций с учетом возраста\",\n      meta: {\n        title: \"Калькулятор Оксфорда - Современная формула BMR и TDEE\",\n        description:\n          \"Рассчитайте BMR и TDEE, используя уравнение Оксфорда (2005), современную формулу на основе европейских популяций с возрастными расчетами.\",\n        keywords:\n          \"калькулятор Оксфорда, современный калькулятор BMR, европейская формула BMR, возрастной калькулятор BMR, уравнение BMR 2005\",\n      },\n      how_it_works: \"Как работает формула Оксфорда\",\n      how_it_works_description:\n        \"Опубликованная в 2005 году, это одна из более современных формул BMR. Она была разработана с использованием данных европейских популяций и учитывает возрастные группы, предоставляя разные уравнения для людей младше и старше 30 лет.\",\n    },\n    \"calorie-calculator-comparison\": {\n      title: \"Сравнить все формулы BMR\",\n      subtitle: \"Посмотрите, как разные формулы BMR рассчитывают ваши потребности в калориях бок о бок\",\n      meta: {\n        title: \"Сравнение формул BMR - Сравнить все калькуляторы калорий\",\n        description:\n          \"Сравните формулы Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham и Oxford BMR бок о бок. Посмотрите, какая формула работает лучше всего для вас.\",\n        keywords:\n          \"сравнение формул BMR, сравнение калькулятора калорий, Mifflin против Harris-Benedict, лучший калькулятор BMR, сравнить формулы калорий\",\n      },\n      how_it_works: \"Как работает это сравнение\",\n      how_it_works_description:\n        \"Введите свои данные один раз и посмотрите, как все основные формулы BMR рассчитывают ваши ежедневные потребности в калориях. Это поможет вам понять различия и выбрать наиболее подходящую формулу для ваших целей.\",\n      input_details: \"Ваши данные\",\n      compare: \"Сравнить\",\n      results_comparison: \"Результаты сравнения формул\",\n      vs_mifflin: \"против Mifflin-St Jeor\",\n      summary: \"Резюме и рекомендации\",\n      summary_explanation:\n        \"Различные формулы могут давать разные результаты. Обычно различия в ±100-200 калорий являются нормальными и ожидаемыми.\",\n      recommendation:\n        \"Для большинства людей Mifflin-St Jeor обеспечивает наиболее точную базу. Спортсмены должны рассмотреть Katch-McArdle, если они знают свой процент жира в теле.\",\n    },\n    \"bmi-calculator-hub\": {\n      title: \"Инструменты Калькулятора ИМТ\",\n      subtitle: \"Рассчитайте свой Индекс Массы Тела различными методами и получите персонализированную информацию о здоровье\",\n      meta: {\n        title: \"Калькулятор ИМТ - Инструменты Индекса Массы Тела и Оценка Здоровья\",\n        description:\n          \"Рассчитайте свой ИМТ с помощью наших комплексных инструментов. Стандартный ИМТ, скорректированный для спортсменов, детский ИМТ, и инструменты сравнения. Получите информацию о здоровье и рекомендации.\",\n        keywords: \"калькулятор ИМТ, индекс массы тела, оценка здоровья, статус веса, инструменты ИМТ, детский ИМТ, ИМТ спортсмена\",\n      },\n      understanding_bmi: \"Понимание ИМТ\",\n      bmi_explanation:\n        \"ИМТ - это инструмент скрининга, который помогает оценить, имеете ли вы здоровый вес для своего роста. Выберите подходящий калькулятор для ваших потребностей:\",\n      recommendation_standard: \"Лучше всего для общего населения и первичного скрининга здоровья\",\n      recommendation_adjusted: \"Более точный для спортсменов и мускулистых людей\",\n      recommendation_pediatric: \"Специализированный для детей и подростков с возрастными процентилями\",\n      popularity: \"Популярность\",\n      accuracy: \"Точность\",\n      accuracy_high: \"Высокая\",\n      accuracy_good: \"Хорошая\",\n      accuracy_medium: \"Средняя\",\n      best_for: \"Лучше для\",\n      best_for_general: \"Общее использование\",\n      best_for_athletes: \"Спортсмены\",\n      best_for_children: \"Дети\",\n      best_for_comparison: \"Сравнить все\",\n      category_standard: \"Стандартный\",\n      category_advanced: \"Продвинутый\",\n      category_specialized: \"Специализированный\",\n      standard: {\n        title: \"Стандартный Калькулятор ИМТ\",\n        description: \"Классический расчет ИМТ с использованием стандартной формулы ВОЗ. Быстрая и простая оценка для общего населения.\",\n        page_title: \"Стандартный Калькулятор ИМТ\",\n        page_description:\n          \"Рассчитайте свой Индекс Массы Тела, используя стандартную формулу ВОЗ. Получите мгновенные результаты с категорией здоровья и персонализированными рекомендациями.\",\n      },\n      adjusted: {\n        title: \"Скорректированный Калькулятор ИМТ\",\n        description:\n          \"Улучшенный расчет ИМТ, который учитывает мышечную массу и состав тела для более точных результатов у спортивных людей.\",\n      },\n      pediatric: {\n        title: \"Детский Калькулятор ИМТ\",\n        description:\n          \"Специализированный калькулятор ИМТ для детей и подростков, использующий возрастные и половые процентили и графики роста.\",\n      },\n      comparison: {\n        title: \"Инструмент Сравнения ИМТ\",\n        description: \"Сравните различные методы расчета ИМТ бок о бок, чтобы понять, как различные факторы влияют на ваши результаты.\",\n      },\n    },\n  },\n  \"bmi-calculator\": {\n    educational: {\n      introduction_title: \"Введение в ИМТ\",\n      introduction_text:\n        \"ИМТ - это измерение худобы или полноты человека на основе его роста и веса, предназначенное для количественной оценки тканевой массы. Он широко используется как общий показатель того, имеет ли человек здоровый вес тела для своего роста.\",\n      introduction_usage:\n        \"В частности, значение, полученное при расчете ИМТ, используется для категоризации того, имеет ли человек недостаточный вес, нормальный вес, избыточный вес или ожирение в зависимости от диапазона, в который попадает значение. Эти диапазоны ИМТ варьируются в зависимости от таких факторов, как регион и возраст, и иногда дополнительно подразделяются на подкategории, такие как сильная недостаточность веса или очень сильное ожирение.\",\n\n      adult_table_title: \"Таблица ИМТ для Взрослых\",\n      adult_table_description:\n        \"Это рекомендация Всемирной организации здравоохранения (ВОЗ) по массе тела на основе значений ИМТ для взрослых. Используется как для мужчин, так и для женщин в возрасте 20 лет и старше.\",\n\n      children_table_title: \"Таблица ИМТ для Детей и Подростков, Возраст 2-20\",\n      children_table_description:\n        \"Центры по контролю и профилактике заболеваний (CDC) рекомендуют категоризацию ИМТ для детей и подростков в возрасте от 2 до 20 лет.\",\n\n      classification: \"Классификация\",\n      bmi_range: \"Диапазон ИМТ - кг/м²\",\n      category: \"Категория\",\n      percentile_range: \"Диапазон Процентилей\",\n      underweight: \"Недостаточный вес\",\n      healthy_weight: \"Здоровый Вес\",\n      at_risk_overweight: \"Риск Избыточного Веса\",\n      overweight: \"Избыточный Вес\",\n\n      overweight_risks_title: \"Риски, Связанные с Избыточным Весом\",\n      overweight_risks_intro:\n        \"Избыточный вес увеличивает риск ряда серьезных заболеваний и состояний здоровья. Ниже приведен список таких рисков согласно Центрам по контролю и профилактике заболеваний (CDC):\",\n\n      cardiovascular_risks: \"Сердечно-сосудистые Риски\",\n      high_blood_pressure: \"Высокое кровяное давление\",\n      cholesterol_issues: \"Более высокие уровни холестерина ЛПНП, более низкие уровни холестерина ЛПВП и высокие уровни триглицеридов\",\n      coronary_heart_disease: \"Ишемическая болезнь сердца\",\n      stroke: \"Инсульт\",\n\n      metabolic_risks: \"Метаболические Риски\",\n      type_2_diabetes: \"Диабет II типа\",\n      gallbladder_disease: \"Заболевание желчного пузыря\",\n      sleep_apnea: \"Апноэ сна и проблемы с дыханием\",\n      osteoarthritis: \"Остеоартрит, тип заболевания суставов, вызванный разрушением суставного хряща\",\n\n      other_risks: \"Другие Риски для Здоровья\",\n      certain_cancers: \"Определенные виды рака (эндометрия, молочной железы, толстой кишки, почек, желчного пузыря, печени)\",\n      mental_health_issues: \"Психические заболевания, такие как клиническая депрессия, тревожность и другие\",\n      reduced_quality_life: \"Низкое качество жизни и боли в теле\",\n      increased_mortality: \"В целом, повышенный риск смертности по сравнению с теми, у кого здоровый ИМТ\",\n\n      underweight_risks_title: \"Риски, Связанные с Недостаточным Весом\",\n      underweight_risks_intro: \"Недостаточный вес имеет свои собственные связанные риски, перечисленные ниже:\",\n      malnutrition: \"Недоедание, дефицит витаминов, анемия (сниженная способность переносить кислород в крови)\",\n      osteoporosis: \"Остеопороз, заболевание, которое вызывает слабость костей, увеличивая риск перелома костей\",\n      immune_function_decrease: \"Снижение иммунной функции\",\n      growth_development_issues: \"Проблемы роста и развития, особенно у детей и подростков\",\n      reproductive_issues: \"Возможные репродуктивные проблемы у женщин из-за гормональных дисбалансов\",\n      surgery_complications: \"Потенциальные осложнения в результате хирургического вмешательства\",\n      increased_mortality_underweight: \"В целом, повышенный риск смертности по сравнению с теми, у кого здоровый ИМТ\",\n\n      adults_limitations: \"У Взрослых\",\n      older_adults_fat: \"Пожилые люди, как правило, имеют больше жира в организме, чем молодые взрослые с тем же ИМТ\",\n      women_fat_difference: \"Женщины, как правило, имеют больше жира в организме, чем мужчины при эквивалентном ИМТ\",\n      athletes_muscle_mass: \"Мускулистые люди и высокотренированные спортсмены могут иметь более высокий ИМТ из-за большой мышечной массы\",\n\n      children_limitations: \"У Детей и Подростков\",\n      height_maturation_influence: \"Рост и уровень полового созревания могут влиять на ИМТ и жировые отложения у детей\",\n      fat_free_mass_difference: \"ИМТ может быть результатом повышенных уровней жира или безжировой массы\",\n      population_accuracy: \"ИМТ довольно показателен для жировых отложений у 90-95% населения\",\n\n      formulas_title: \"Формула ИМТ\",\n      metric_formula: \"Метрическая Формула\",\n      imperial_formula: \"Имперская Формула\",\n      example: \"Пример\",\n\n      bmi_prime_formula: \"Формула ИМТ Прайм\",\n      bmi_prime_description: \"Отношение вашего ИМТ к верхнему пределу нормального ИМТ (25)\",\n\n      ponderal_index_title: \"Пондеральный Индекс\",\n      ponderal_index_explanation:\n        \"Пондеральный индекс (ПИ) похож на ИМТ тем, что он измеряет худобу или полноту человека на основе его роста и веса. Основное различие между ПИ и ИМТ заключается в возведении в куб, а не в квадрат роста в формуле. Хотя ИМТ может быть полезным инструментом при рассмотрении больших популяций, он не надежен для определения худобы или полноты у отдельных людей.\",\n      ponderal_index_metric_description: \"Пондеральный индекс с использованием метрических единиц\",\n      ponderal_index_imperial_description: \"Пондеральный индекс с использованием имперских единиц\",\n\n      medical_disclaimer_title: \"Медицинский Отказ от Ответственности\",\n    },\n    height: \"Рост\",\n    weight: \"Вес\",\n    feet: \"фут\",\n    inches: \"дюйм\",\n    cm: \"см\",\n    kg: \"кг\",\n    lbs: \"фунт\",\n    height_placeholder: \"Введите рост\",\n    weight_placeholder: \"Введите вес\",\n    calculate: \"Рассчитать ИМТ\",\n    your_bmi: \"Ваш ИМТ\",\n    bmi_prime: \"ИМТ Прайм\",\n    ponderal_index: \"Пондеральный Индекс\",\n    bmi_category: \"Категория ИМТ\",\n    health_risk: \"Риск для Здоровья\",\n    recommendations_label: \"Рекомендации\",\n    units: \"Единицы\",\n    metric: \"Метрическая (кг/см)\",\n    imperial: \"Имперская (фунт/фут)\",\n\n    // Detailed BMI Categories (WHO)\n    category_severe_thinness: \"Выраженный Дефицит Массы\",\n    category_moderate_thinness: \"Умеренный Дефицит Массы\",\n    category_mild_thinness: \"Легкий Дефицит Массы\",\n    category_normal: \"Нормальная Масса\",\n    category_overweight: \"Избыточная Масса\",\n    category_obese_class_1: \"Ожирение I Степени\",\n    category_obese_class_2: \"Ожирение II Степени\",\n    category_obese_class_3: \"Ожирение III Степени\",\n\n    // Health Risks\n    risk_low: \"Низкий\",\n    risk_normal: \"Нормальный\",\n    risk_increased: \"Повышенный\",\n    risk_high: \"Высокий\",\n    risk_very_high: \"Очень Высокий\",\n    risk_extremely_high: \"Крайне Высокий\",\n\n    // Additional Information\n    bmi_range: \"Диапазон ИМТ\",\n    ideal_weight: \"Идеальный Диапазон Веса\",\n    weight_to_lose: \"Вес для Снижения\",\n    weight_to_gain: \"Вес для Набора\",\n    normal_range: \"Нормальный диапазон ИМТ: 18,5 - 24,9\",\n\n    // BMI Prime\n    about_bmi_prime: \"О ИМТ Прайм\",\n    bmi_prime_explanation:\n      \"ИМТ Прайм - это отношение вашего ИМТ к верхней границе нормального ИМТ (25). Значение 1,0 означает, что вы находитесь на верхней границе нормального веса.\",\n    underweight: \"Недостаточный вес\",\n    normal: \"Нормальный\",\n    overweight: \"Избыточный вес\",\n    obese: \"Ожирение\",\n\n    // Limitations\n    limitations_title: \"Ограничения ИМТ\",\n    limitations_text:\n      \"ИМТ не различает мышечную и жировую массу. Спортсмены и очень мускулистые люди могут иметь высокий ИМТ, оставаясь здоровыми. Возраст, пол, этническая принадлежность и состав тела также влияют на интерпретацию.\",\n\n    disclaimer:\n      \"ИМТ является инструментом скрининга и может не отражать состав тела. Обратитесь к медицинским специалистам за персонализированными советами.\",\n\n    // Recommendations\n    recommendations: {\n      severe_thinness: {\n        medical_consultation: \"Настоятельно рекомендуется немедленная медицинская консультация\",\n        nutritional_assessment: \"Необходима комплексная оценка питания\",\n        weight_gain_program: \"Может потребоваться контролируемая программа набора веса\",\n        screen_conditions: \"Обследование на наличие основных заболеваний\",\n        psychological_evaluation: \"Рассмотреть психологическую оценку при подозрении на расстройство пищевого поведения\",\n      },\n      moderate_thinness: {\n        healthcare_provider: \"Обратиться к врачу для обследования\",\n        nutrient_dense_foods: \"Сосредоточиться на питательных, калорийных продуктах\",\n        registered_dietitian: \"Рассмотреть работу с дипломированным диетологом\",\n        monitor_malnutrition: \"Следить за признаками недоедания\",\n        gradual_weight_gain: \"Рекомендуется постепенный, здоровый набор веса\",\n      },\n      mild_thinness: {\n        consider_healthcare: \"Рассмотреть консультацию с врачом\",\n        nutrient_dense_foods: \"Сосредоточиться на питательных продуктах для здорового набора веса\",\n        strength_training: \"Включить силовые тренировки для наращивания мышечной массы\",\n        monitor_health: \"Регулярно следить за своим здоровьем\",\n        gradual_weight_gain: \"Стремиться к постепенному набору веса (0,5-1 кг в неделю)\",\n      },\n      normal: {\n        maintain_weight: \"Поддерживать текущий здоровый вес\",\n        physical_activity: \"Продолжать регулярную физическую активность (150+ минут в неделю)\",\n        balanced_diet: \"Придерживаться сбалансированного, питательного рациона\",\n        health_checkups: \"Регулярные медицинские осмотры\",\n        overall_wellness: \"Сосредоточиться на общем благополучии и составе тела\",\n      },\n      overweight: {\n        gradual_weight_loss: \"Стремиться к постепенному снижению веса (0,5-1 кг в неделю)\",\n        increase_activity: \"Увеличить физическую активность до 150+ минут в неделю\",\n        portion_control: \"Сосредоточиться на контроле порций и сбалансированном питании\",\n        healthcare_provider: \"Рассмотреть консультацию с врачом\",\n        lifestyle_goals: \"Поставить реалистичные, устойчивые цели образа жизни\",\n      },\n      obese_class_1: {\n        healthcare_provider: \"Обратиться к врачу за планом управления весом\",\n        weight_loss_target: \"Стремиться к снижению веса на 5-10% изначально\",\n        diet_exercise: \"Сочетать диетические и физические вмешательства\",\n        nutritional_counseling: \"Рассмотреть профессиональное консультирование по питанию\",\n        screen_conditions: \"Обследование на заболевания, связанные с весом\",\n      },\n      obese_class_2: {\n        medical_supervision: \"Обратиться за медицинским наблюдением для управления весом\",\n        lifestyle_programs: \"Рассмотреть комплексные программы изменения образа жизни\",\n        evaluate_conditions: \"Оценить заболевания, связанные с весом\",\n        medical_treatments: \"Может принести пользу медицинские методы снижения веса\",\n        bariatric_surgery: \"Рассмотреть оценку бариатрической хирургии при необходимости\",\n      },\n      obese_class_3: {\n        medical_consultation: \"Рекомендуется немедленная медицинская консультация\",\n        bariatric_surgery: \"Рассмотреть оценку бариатрической хирургии\",\n        weight_management: \"Комплексная медицинская программа управления весом\",\n        health_complications: \"Устранить осложнения здоровья, связанные с весом\",\n        multidisciplinary: \"Мультидисциплинарный подход с медицинской командой\",\n      },\n    },\n  },\n  levels: {\n    BEGINNER: \"Начальный\",\n    INTERMEDIATE: \"Средний\",\n    ADVANCED: \"Профессиональный\",\n  },\n  email_sent: \"Письмо отправлено\",\n  cant_send_email: \"Не удается отправить письмо\",\n  logout: \"Выйти\",\n  verify_email: \"Подтвердите вашу электронную почту. ⚠️ Не забудьте проверить папку спам.\",\n  verify_email_subtitle: \"Пожалуйста, подтвердите вашу электронную почту, чтобы продолжить.\",\n  resend_email: \"Отправить письмо повторно\",\n  resend_email_countdown: \"Отправить письмо повторно через {seconds} секунд\",\n  signin_error_subtitle: \"Пожалуйста, проверьте ваши учетные данные и попробуйте снова.\",\n  register_title: \"Создать аккаунт\",\n  register_description: \"Введите ваши данные ниже, чтобы создать аккаунт\",\n  register_terms: \"Регистрируясь, вы соглашаетесь с нашими\",\n  register_privacy: \"Политикой конфиденциальности\",\n  register_privacy_link: \"и нашей\",\n  register_privacy_link_2: \"Политикой конфиденциальности\",\n  password_forgot_title: \"Забыли пароль?\",\n  password_forgot_subtitle: \"Введите ваш email для сброса пароля\",\n  new_password: \"Новый пароль\",\n  new_password_placeholder: \"Введите ваш новый пароль\",\n  current_password: \"Текущий пароль\",\n  current_password_placeholder: \"Введите ваш текущий пароль\",\n  confirm_password: \"Подтвердить пароль\",\n  confirm_password_placeholder: \"Подтвердите ваш пароль\",\n\n  success: {\n    feedback_sent: \"Отзыв отправлен\",\n    password_forgot_success: \"Письмо отправлено\",\n    reset_password_success: \"Пароль успешно сброшен\",\n    password_updated_successfully: \"Пароль успешно обновлен\",\n  },\n\n  error: {\n    invalid_credentials: \"Неверные учетные данные или аккаунт не существует\",\n    upload_failed: \"Загрузка не удалась\",\n    generic_error: \"Ошибка во время операции\",\n    sending_email: \"Ошибка отправки письма\",\n  },\n\n  backend_errors: {\n    EMAIL_ALREADY_EXISTS: \"Email уже существует\",\n    INVALID_FILE_TYPE: \"Недопустимый тип файла\",\n    FILE_TOO_LARGE: \"Файл слишком большой\",\n    NO_FILE_UPLOADED: \"Файл не загружен\",\n    IMAGE_PROCESSING_ERROR: \"Ошибка обработки изображения\",\n    upload_failed: \"Загрузка не удалась\",\n  },\n\n  profile: {\n    new_workout: \"Новая тренировка\",\n    alert: {\n      title: \"Ваш прогресс сохраняется в браузере.\",\n      create_account: \"Создать аккаунт\",\n      log_in: \"Войти\",\n      to_ensure_it_is_not_getting_lost: \"чтобы не потерять его.\",\n    },\n  },\n\n  // Release Notes\n  release_notes: {\n    title: \"Что нового\",\n    release_notes: \"Заметки о выпуске\",\n    notes: {\n      note_2025_10_29: {\n        title: \"🍑 Новая Программа Booty Выпущена!\",\n        content:\n          \"<li>Новая <a href='/programs/booty-pump' class='text-blue-500 hover:underline'>программа Booty</a> теперь доступна!</li><li>Целенаправленно тренируйте и укрепляйте ягодичные мышцы со специализированными тренировками</li><li>Разработана для максимальных результатов и роста мышц</li><li>Присоединяйтесь к программе сегодня! 💪</li>\",\n      },\n      note_2025_08_18: {\n        title: \"🏆 Новая Функция Таблицы Лидеров!\",\n        content:\n          \"<li>Новая <strong>таблица лидеров</strong> для соревнования с другими чемпионами тренировок</li><li>Просмотр рейтингов по периодам <strong>все время, месячный и недельный</strong></li><li>Отслеживайте свою позицию среди лучших исполнителей</li><li>Мотивируйте себя подняться в рейтинге! 🚀</li>\",\n      },\n      note_2025_07_09: {\n        title: \"🎯 Выбор Упражнений, Избранное и Новые Инструменты\",\n        content:\n          \"<li>Новый <strong>выбор упражнений</strong> при создании тренировок (шаг 3)</li><li>Система <strong>избранных упражнений</strong> для отметки предпочитаемых движений</li><li>Новые <em>фитнес-инструменты</em>: калькулятор ИМТ и зоны частоты сердечных сокращений</li><li>Улучшенные карточки программ</li><li>Новые участники присоединяются к проекту! 🚀</li>\",\n      },\n      note_2025_07_02: {\n        title: \"🛠️ Самохостинг, Русский язык и Новые Инструменты\",\n        content:\n          \"Улучшен <strong>самохостинг</strong>, добавлена поддержка <strong>русского языка</strong>, и введены новые <em>фитнес-инструменты</em>, включая калькулятор калорий. 🚀\",\n      },\n      note_2025_06_23: {\n        title: \"🇵🇹 Поддержка Португальского & Баннер Пожертвований\",\n        content:\n          \"Приложение теперь поддерживает <strong>португальский язык</strong>! Мы также добавили <em>баннер пожертвований</em>, чтобы помочь покрыть расходы проекта через <a href='https://github.com/sponsors/snouzy' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>GitHub Sponsors</a> или <a href='https://ko-fi.com/workoutcool' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Ko-fi</a>. 🙏\",\n      },\n      note_2025_06_22: {\n        title: \"🌍 Новые языки и улучшение производительности!\",\n        content:\n          \"Приложение теперь доступно на китайском и русском языках! Мы также улучшили производительность перетаскивания для более плавного опыта. ⚡\",\n      },\n      note_2025_06_19: {\n        title: \"📱 Теперь доступно как PWA!\",\n        content:\n          \"Workout.cool v1.2 теперь является прогрессивным веб-приложением! Установите его на ваш телефон для получения нативного опыта приложения с офлайн доступом. 🚀\",\n      },\n      note_2025_06_18: {\n        title:\n          \"🚀 #1 на <a href='https://news.ycombinator.com/item?id=44309320' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Hacker News</a>!\",\n        content:\n          \"Workout.cool достиг первого места на Hacker News! Спасибо всем за потрясающую поддержку и добро пожаловать всем новым пользователям! 💪\",\n      },\n      note_2025_06_01: {\n        title: \"🎉 Новое: Диалог заметок о выпуске\",\n        content: \"Теперь вы можете просматривать новости прямо из заголовка! Следите за обновлениями.\",\n      },\n      note_2025_05_20: {\n        title: \"Улучшения интерфейса\",\n        content: \"Улучшена отзывчивость на мобильных устройствах и добавлены тонкие эффекты наведения для кнопок.\",\n      },\n    },\n  },\n\n  // Premium Upsell Alert\n  donation_alert: {\n    title: \"Разблокируйте расширенные функции с Workout.cool Premium\",\n    or: \"или\",\n  },\n\n  // Donation Modal\n  donation_modal: {\n    support_via: \"Поддержать через...\",\n    title: \"Поддержите проект\",\n    congrats: \"Поздравляем с завершением тренировки! 🎉\",\n    subtitle: \"Это приложение помогает вам бесплатно, но у меня есть реальные расходы...\",\n    costs_title: \"Реальность расходов\",\n    costs_description:\n      \"В настоящее время пожертвования даже не покрывают базовые расходы: серверы, аутентификация, инфраструктура, база данных и т.д.\",\n    open_source_title: \"100% с открытым исходным кодом\",\n    open_source_description:\n      \"Это приложение полностью бесплатное и с открытым исходным кодом. Никакой прибыли не получается - это проект из страсти, чтобы помочь сообществу и помочь людям заниматься спортом.\",\n    no_ads: \"Без рекламы\",\n    no_tracking: \"Без отслеживания\",\n    impact_title: \"Ваше влияние\",\n    impact_3_euros: \"• Даже €3 покрывают 1 неделю сервера\",\n    impact_support: \"• Ваша поддержка делает приложение бесплатным для всех\",\n    impact_footer: \"Каждое пожертвование, даже маленькое, имеет реальное значение! 🙏\",\n    later_button: \"Позже\",\n    support_button: \"Поддержать проект\",\n  },\n\n  // Contact Support\n  contact_support: \"Связаться с поддержкой\",\n  contact_support_subtitle: \"Опишите вашу проблему, и мы поможем вам как можно скорее. Вы также можете написать нам напрямую на\",\n\n  // Social Platforms\n  social_platforms: {\n    x: \"X (Twitter)\",\n    facebook: \"Facebook\",\n    email: \"Email\",\n    whatsapp: \"WhatsApp\",\n    website: \"Веб-сайт\",\n    phone: \"Телефон\",\n    youtube: \"YouTube\",\n    linkedin: \"LinkedIn\",\n    snapchat: \"Snapchat\",\n    instagram: \"Instagram\",\n    tiktok: \"TikTok\",\n    threads: \"Threads\",\n  },\n\n  breadcrumbs: {\n    home: \"Главная\",\n  },\n\n  // Workout Builder\n  workout_builder: {\n    confirm_delete: \"Вы уверены, что хотите удалить эту тренировочную сессию?\",\n    steps: {\n      equipment: {\n        title: \"Оборудование\",\n        description: \"Выберите ваше оборудование\",\n      },\n      muscles: {\n        title: \"Мышцы\",\n        description: \"Выберите вашу тренировку\",\n      },\n      exercises: {\n        title: \"Упражнения\",\n        description: \"Настройте вашу тренировку\",\n      },\n    },\n    muscles: {\n      back: \"Спина\",\n      abdominals: \"Пресс\",\n      adductors: \"Приводящие\",\n      abductors: \"Отводящие\",\n      biceps: \"Бицепс\",\n      triceps: \"Трицепс\",\n      chest: \"Грудь\",\n      shoulders: \"Плечи\",\n      quadriceps: \"Квадрицепс\",\n      hamstrings: \"Задняя поверхность бедра\",\n      glutes: \"Ягодицы\",\n      calves: \"Икры\",\n      forearms: \"Предплечья\",\n      traps: \"Трапеции\",\n      obliques: \"Косые мышцы\",\n    },\n    exercise: {\n      watch_video: \"Смотреть видео\",\n      shuffle: \"Перемешать\",\n      pick: \"Выбрать\",\n      remove: \"Удалить\",\n      no_video_available: \"Видео недоступно.\",\n    },\n    loading: {\n      exercises: \"Загрузка упражнений...\",\n    },\n    error: {\n      loading_exercises: \"Ошибка загрузки упражнений\",\n    },\n    no_exercises_found: \"Упражнения не найдены. Попробуйте изменить выбор оборудования или мышц.\",\n    addExercise: \"Добавить упражнение\",\n    exerciseAdded: \"{name} добавлено в тренировку\",\n    exercises: \"упражнения\",\n    equipment: {\n      bodyweight: {\n        label: \"Собственный вес\",\n        description: \"Упражнения с использованием только веса тела\",\n      },\n      dumbbell: {\n        label: \"Гантели\",\n        description: \"Упражнения со свободными весами с гантелями\",\n      },\n      barbell: {\n        label: \"Штанга\",\n        description: \"Комплексные движения со штангой\",\n      },\n      kettlebell: {\n        label: \"Гиря\",\n        description: \"Динамичные упражнения с гирями\",\n      },\n      band: {\n        label: \"Резинка\",\n        description: \"Упражнения с эластичными лентами\",\n      },\n      plate: {\n        label: \"Диски\",\n        description: \"Упражнения с использованием дисков\",\n      },\n      pullup_bar: {\n        label: \"Турник\",\n        description: \"Упражнения для верхней части тела с турником\",\n      },\n      bench: {\n        label: \"Скамья\",\n        description: \"Упражнения на скамье и поддержка\",\n      },\n    },\n    navigation: {\n      home: \"Главная\",\n      previous: \"Назад\",\n      continue: \"Продолжить\",\n      complete: \"Завершить\",\n    },\n    stats: {\n      \"muscle_selected#zero\": \"0 мышц выбрано\",\n      \"muscle_selected#one\": \"1 мышца выбрана\",\n      \"muscle_selected#other\": \"{count} мышц выбрано\",\n      \"equipment_selected#zero\": \"0 оборудования выбрано\",\n      \"equipment_selected#one\": \"1 оборудование выбрано\",\n      \"equipment_selected#other\": \"{count} оборудования выбрано\",\n      selected: \"Выбрано\",\n      total: \"Всего\",\n      equipment_ready: \"оборудование готово\",\n      equipment_ready_plural: \"оборудования готово\",\n    },\n    selection: {\n      choose_your_arsenal: \"Выберите ваш арсенал\",\n      select_equipment_description: \"Выберите оборудование для разблокировки персонализированных тренировок\",\n      clear_all: \"Очистить все\",\n      muscle_selection_coming_soon: \"Выбор мышц (Скоро)\",\n      muscle_selection_description: \"Выберите мышцы, которые вы хотите тренировать, нажав на них.\",\n      exercise_selection_coming_soon: \"Выбор упражнений (Скоро)\",\n      exercise_selection_description: \"Этот шаг покажет вам персонализированные рекомендации упражнений.\",\n    },\n    session: {\n      back_to_workout: \"Вернуться к тренировке\",\n      congrats: \"Поздравляем, тренировка завершена! 🎉\",\n      congrats_subtitle: \"Вы справились!\",\n      see_instructions: \"Смотреть инструкции\",\n      finish_set: \"Завершить подход\",\n      finish_session: \"Завершить сессию\",\n      bodyweight: \"Собственный вес\",\n      weight: \"Вес\",\n      reps: \"Повторения\",\n      time: \"Время\",\n      next_exercise: \"Следующее упражнение\",\n      add_set: \"Добавить подход\",\n      add_column: \"Добавить колонку\",\n      add_row: \"Добавить строку\",\n      remove_column: \"Удалить колонку\",\n      set_number: \"Подход {number}\",\n      set_number_plural: \"Подходы {number}\",\n      set_number_singular: \"Подход {number}\",\n      set_number_plural_singular: \"Подходы {number}\",\n      workout_in_progress: \"Тренировка в процессе\",\n      started_at: \"Начато в\",\n      quit_workout: \"Завершить тренировку\",\n      elapsed_time: \"Прошедшее время\",\n      chronometer: \"Хронометр\",\n      exercise_progress: \"Прогресс упражнений\",\n      total_volume: \"Общий объем\",\n      current_exercise: \"Текущее упражнение\",\n      complete: \"Завершено\",\n      active: \"Активно\",\n      already_have_a_active_session: \"У вас уже есть активная сессия. Невозможно повторить без завершения или выхода из тренировки.\",\n      no_exercise_selected: \"Упражнение не выбрано\",\n      quit_workout_title: \"Завершить тренировку?\",\n      progress: \"Прогресс\",\n      quit_warning: \"Вы уверены, что хотите выйти? Вы можете сохранить прогресс или потерять его полностью.\",\n      save_and_quit: \"Сохранить и выйти\",\n      quit_without_save: \"Выйти без сохранения\",\n      continue_workout: \"Продолжить тренировку\",\n      history: \"История тренировок [{count}]\",\n      no_workout_yet: \"Пока нет тренировок.\",\n      start: \"начало\",\n      end: \"конец\",\n      exercise: \"УПРАЖНЕНИЕ\",\n      repeat: \"Повторить\",\n      delete: \"Удалить\",\n    },\n    attribute_value: {\n      bodyweight: \"Собственный вес\",\n      strength: \"Сила\",\n      powerlifting: \"Пауэрлифтинг\",\n      calisthenic: \"Калистеника\",\n      plyometrics: \"Плиометрика\",\n      stretching: \"Растяжка\",\n      strongman: \"Стронгмен\",\n      cardio: \"Кардио\",\n      stabilization: \"Стабилизация\",\n      power: \"Мощность\",\n      resistance: \"Сопротивление\",\n      crossfit: \"CrossFit\",\n      weightlifting: \"Тяжелая атлетика\",\n      neck: \"Шея\",\n      lats: \"Широчайшие\",\n      adductors: \"Приводящие\",\n      abductors: \"Отводящие\",\n      groin: \"Пах\",\n      full_body: \"Все тело\",\n      rotator_cuff: \"Вращательная манжета\",\n      hip_flexor: \"Сгибатель бедра\",\n      achilles_tendon: \"Ахиллово сухожилие\",\n      fingers: \"Пальцы\",\n      smith_machine: \"Машина Смита\",\n      other: \"Другое\",\n      ez_bar: \"EZ штанга\",\n      machine: \"Тренажер\",\n      desk: \"Стол\",\n      none: \"Нет\",\n      cable: \"Трос\",\n      medicine_ball: \"Медицинский мяч\",\n      swiss_ball: \"Швейцарский мяч\",\n      foam_roll: \"Пенный валик\",\n      trx: \"TRX\",\n      box: \"Бокс\",\n      ropes: \"Канаты\",\n      spin_bike: \"Спин-байк\",\n      step: \"Степ\",\n      bosu: \"BOSU\",\n      tyre: \"Шина\",\n      sandbag: \"Мешок с песком\",\n      pole: \"Шест\",\n      wall: \"Стена\",\n      bar: \"Перекладина\",\n      rack: \"Стойка\",\n      car: \"Машина\",\n      sled: \"Сани\",\n      chain: \"Цепь\",\n      skierg: \"SkiErg\",\n      rope: \"Канат\",\n      na: \"Н/Д\",\n      isolation: \"Изоляция\",\n      compound: \"Комплексное\",\n    },\n  },\n  commons: {\n    upgrade_to_premium: \"Стань Премиум\",\n    last_activity: \"Последняя активность\",\n    registered_on: \"Зарегистрирован\",\n    just_now: \"только что\",\n    signup_with: \"Регистрация через {provider}\",\n    signin_with: \"Вход через {provider}\",\n    signup: \"Регистрация\",\n    login: \"Вход\",\n    connecting: \"Подключение...\",\n    login_to_your_account_title: \"Войти в ваш аккаунт\",\n    login_to_your_account_subtitle: \"Введите ваши данные ниже для входа\",\n    password_forgot: \"Забыли пароль?\",\n    password_reset_success: \"Пароль успешно сброшен\",\n    dont_have_account: \"Нет аккаунта?\",\n    already_have_account: \"Уже есть аккаунт?\",\n    or: \"Или\",\n    add: \"Добавить\",\n    your_feminine: \"ваша\",\n    password: \"Пароль\",\n    email: \"Email\",\n    logout: \"Выйти\",\n    first_name: \"Имя\",\n    last_name: \"Фамилия\",\n    verify_password: \"Подтвердить пароль\",\n    submit: \"Отправить\",\n    upload: \"Загрузить\",\n    cancel: \"Отмена\",\n    save_changes: \"Сохранить изменения\",\n    change: \"Изменить\",\n    subject: \"Тема\",\n    message: \"Сообщение\",\n    saving: \"Сохранение...\",\n    edit: \"Редактировать\",\n    more_options: \"Больше опций\",\n    open_link: \"Открыть ссылку\",\n    hide: \"Скрыть\",\n    make_visible: \"Сделать видимым\",\n    delete: \"Удалить\",\n    share: \"Поделиться\",\n    title: \"Заголовок\",\n    subtitle: \"Подзаголовок\",\n    content: \"Контент\",\n    save: \"Сохранить\",\n    button: \"Кнопка\",\n    card: \"Карточка\",\n    go_back: \"Назад\",\n    next: \"Далее\",\n    choose_image: \"Выбрать изображение\",\n    soon: \"Скоро\",\n    coming_soon_with_emoji: \"Скоро 🤫\",\n    no_image: \"Нет изображения\",\n    description: \"Описание\",\n    price: \"Цена\",\n    duration: \"Продолжительность\",\n    location: \"Местоположение\",\n    schedule: \"Расписание\",\n    participants_info: \"Информация об участниках\",\n    description_placeholder: \"Введите описание\",\n    title_placeholder: \"Введите заголовок\",\n    changes_saved: \"Изменения сохранены\",\n    replace: \"Замените\",\n    loading: \"Загрузка...\",\n    image_deleted: \"Изображение удалено\",\n    discover_workoutcool: \"Откройте Workout Cool\",\n    received_just_now: \"Получено только что\",\n    copied: \"Скопировано\",\n    url_copied: \"URL скопирован\",\n    copy_failed: \"Копирование не удалось\",\n    accordion: \"Аккордеон\",\n    image: \"Изображение\",\n    other: \"Другое\",\n    register: \"Регистрация\",\n    instantly: \"мгновенно\",\n    immediately: \"немедленно\",\n    link: \"Ссылка\",\n    accept: \"Принять\",\n    deny: \"Отклонить\",\n    invalid_input: \"Недопустимый ввод. Пожалуйста, проверьте ошибки.\",\n    copy_url: \"Скопировать URL\",\n    page_url: \"URL страницы\",\n    saving_short: \"Сохранение...\",\n    saved_short: \"ОК\",\n    looks_like_you_are_lost: \"Похоже, вы заблудились\",\n    the_page_you_are_looking_for_is_not_available: \"Страница, которую вы ищете, недоступна\",\n    go_to_home: \"На главную\",\n    go_to_profile: \"В профиль\",\n    terms: \"Условия обслуживания\",\n    privacy: \"Политика конфиденциальности\",\n    sales_terms: \"Условия продажи\",\n    consent_banner: \"Мы используем cookie для улучшения вашего опыта. Нажимая «Принять», вы соглашаетесь на использование cookie.\",\n    about: \"О нас\",\n    profile: \"Профиль\",\n    donate: \"Пожертвовать\",\n    my_account: \"Мой аккаунт\",\n    dashboard: \"Панель управления\",\n    home: \"Главная\",\n    changelog: \"Журнал изменений\",\n    stop_impersonation_button: \"Остановить имитацию\",\n    impersonating_user_label: \"Имитация пользователя\",\n    re_hello: \"Привет снова\",\n    back_to_login: \"Вернуться ко входу\",\n    sending: \"Отправка...\",\n    send_me_link: \"Отправить мне ссылку\",\n    extremely_dissatisfied: \"Крайне неудовлетворен\",\n    somewhat_dissatisfied: \"Несколько неудовлетворен\",\n    neutral: \"Нейтрально\",\n    satisfied: \"Удовлетворен\",\n    support: \"Поддержка\",\n    change_language: \"Изменить язык\",\n    in_progress: \"В процессе\",\n    close: \"Закрыть\",\n    subscription: \"Подписка\",\n    manage_subscription: \"Управление подпиской\",\n    become_premium: \"Стань Премиум\",\n    remove_ads: \"Убрать рекламу\",\n    coming_soon: \"Скоро\",\n    premium: \"Премиум\",\n    free: \"Бесплатно\",\n    new: \"Новый\",\n    monday: \"Понедельник\",\n    tuesday: \"Вторник\",\n    wednesday: \"Среда\",\n    thursday: \"Четверг\",\n    friday: \"Пятница\",\n    saturday: \"Суббота\",\n    sunday: \"Воскресенье\",\n    added_to_favorites: \"Добавлено в избранное\",\n    add_to_favorites: \"Добавить в избранное\",\n    remove_from_favorites: \"Удалить из избранного\",\n    favorites: \"Избранное\",\n  },\n  statistics: {\n    title: \"Статистика\",\n    page_subtitle: \"Отслеживайте свой фитнес-путь с помощью расширенной аналитики и персонализированных рекомендаций.\",\n    select_exercise: \"Выбрать Упражнение\",\n    active_daily_users: \"Активные Ежедневные Пользователи\",\n    success_rate: \"Показатель Успеха\",\n    user_rating: \"Рейтинг Пользователей\",\n\n    // Tabs\n    tabs: {\n      video: \"Видео\",\n      statistics: \"Статистика\",\n    },\n\n    // Chart titles and labels\n    weight: \"Вес\",\n    volume: \"Объем\",\n    weight_progression: \"Прогресс Веса\",\n    weight_progression_chart: \"График прогресса веса\",\n    weekly_volume: \"Недельный Объем\",\n    volume_chart: \"График объема\",\n    estimated_1rm: \"Расчетный 1 Повтор Макс (1RM)\",\n    one_rep_max_chart: \"График максимального повтора\",\n    performance_over_time: \"Производительность с Течением Времени\",\n\n    // Form and controls\n    timeframe: \"Период Времени\",\n    timeframe_selector: \"Выбор периода времени\",\n\n    // Timeframes\n    timeframes: {\n      \"4weeks\": \"4 Недели\",\n      \"8weeks\": \"8 Недель\",\n      \"12weeks\": \"12 Недель\",\n      \"1year\": \"1 Год\",\n    },\n\n    // Error messages\n    error_loading_data: \"Ошибка загрузки данных\",\n    error_loading_weight_progression: \"Ошибка загрузки прогресса веса\",\n    error_loading_1rm: \"Ошибка загрузки данных 1RM\",\n    error_loading_volume: \"Ошибка загрузки данных объема\",\n\n    // Empty states\n    no_data_yet: \"Пока нет данных\",\n    start_tracking: \"Начните отслеживать, чтобы увидеть свой прогресс\",\n    no_1rm_data: \"Нет доступных данных 1RM\",\n    complete_sets_with_weight: \"Выполните подходы с весом, чтобы увидеть ваш 1 Повтор Макс (1RM)\",\n    no_volume_data: \"Нет доступных данных об объеме\",\n    complete_workouts: \"Завершите тренировки, чтобы увидеть ваш объем\",\n\n    // Info and tooltips\n    \"1rm_formula_info\": \"Информация о формуле 1RM\",\n    volume_calculation: \"Объем = Вес × Повторы × Подходы\",\n    last_updated: \"Последнее обновление: {date}\",\n\n    // Premium\n    premium_required: \"Требуется Premium для доступа к статистике\",\n\n    // StatisticsPreviewOverlay\n    premium_statistics: \"Премиум Статистика\",\n    premium_statistics_description: \"Получите подробную информацию о вашем фитнес-пути с расширенной аналитикой для каждого упражнения.\",\n    total_volume: \"Общий Объем\",\n    pr_increase: \"Увеличение ПР\",\n    weight_progress: \"Прогресс Веса\",\n    upgrade_now: \"Обновить Сейчас\",\n    rating: \"Рейтинг 4.8/5\",\n    no_ads: \"Без рекламы\",\n    cancel_anytime: \"Отменить в любое время\",\n    preview_notice: \"Это всего лишь предварительный просмотр! 👀\",\n    preview_description: \"Разблокируйте полный доступ к подробной аналитике, отслеживанию прогресса и персонализированным рекомендациям.\",\n    get_premium_access: \"Получить Премиум Доступ\",\n\n    // ExercisesBrowser\n    all_equipment: \"Все Оборудование\",\n    all_muscles: \"Все Мышцы\",\n    search_exercises: \"Поиск Упражнений\",\n    error_loading_exercises: \"Ошибка загрузки упражнений\",\n    no_exercises_found: \"Упражнения не найдены\",\n    equipment_label: \"Оборудование:\",\n    primary_muscle_label: \"Основная Мышца:\",\n    unknown: \"Неизвестно\",\n    no_image_available: \"Изображение недоступно\",\n  },\n  heatmap: {\n    week_days_short: {\n      sunday: \"Вс\",\n      monday: \"Пн\",\n      tuesday: \"Вт\",\n      wednesday: \"Ср\",\n      thursday: \"Чт\",\n      friday: \"Пт\",\n      saturday: \"Сб\",\n    },\n    month_names_short: {\n      january: \"Янв\",\n      february: \"Фев\",\n      march: \"Мар\",\n      april: \"Апр\",\n      may: \"Май\",\n      june: \"Июнь\",\n      july: \"Июль\",\n      august: \"Авг\",\n      september: \"Сен\",\n      october: \"Окт\",\n      november: \"Ноя\",\n      december: \"Дек\",\n    },\n    \"workout#one\": \"тренировка\",\n    \"workout#other\": \"тренировок\",\n  },\n} as const;\n"
  },
  {
    "path": "locales/server.ts",
    "content": "import { createI18nServer } from \"next-international/server\";\n\nexport const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({\n  en: () => import(\"./en\"),\n  fr: () => import(\"./fr\"),\n  es: () => import(\"./es\"),\n  \"zh-CN\": () => import(\"./zh-CN\"),\n  ru: () => import(\"./ru\"),\n  pt: () => import(\"./pt\")\n});"
  },
  {
    "path": "locales/types.ts",
    "content": "export const locales = [\"en\", \"fr\", \"es\", \"zh-CN\", \"ru\", \"pt\"] as const;\nexport type Locale = (typeof locales)[number];\n"
  },
  {
    "path": "locales/zh-CN.ts",
    "content": "export default {\n  leaderboard: {\n    title: \"排行榜\",\n    description: \"锻炼冠军\",\n    champion_badge: \"🏆 冠军\",\n    runner_up_badge: \"🥈 亚军\",\n    third_place_badge: \"🥉 第三名\",\n    second_place: \"第二名\",\n    third_place: \"第三名\",\n    workouts: \"次锻炼\",\n    unable_to_load: \"无法加载排行榜\",\n    try_again_later: \"请稍后再试\",\n    no_champions_yet: \"还没有冠军\",\n    complete_first_workout: \"完成您的第一次锻炼来夺取王座！\",\n    member_since: \"会员自\",\n    workouts_per_week: \"次/周\",\n    last_workout: \"上次锻炼\",\n    page_title: \"冠军排行榜\",\n    page_subtitle: \"登上顶峰，成为 Workout.cool 传奇\",\n    period_all_time: \"所有时间\",\n    period_monthly: \"月度\",\n    period_weekly: \"每周\",\n    no_sessions_this_week: \"本周没有训练\",\n    no_sessions_this_month: \"本月没有训练\",\n    registered_members_only: \"仅限注册会员\",\n    registered_members_description: \"创建账户以出现在排行榜中\",\n    reset_timezone: \"欧洲/巴黎重置\",\n    reset_timezone_description: \"每周和每月排行榜在巴黎时间午夜重置\",\n  },\n  programs: {\n    available_programs: \"可用的课程\",\n    workout_programs: \"锻炼程序\",\n    exercises_in_session: \"课程中的练习\",\n    start_session: \"开始课程\",\n    starting_session: \"启动中...\",\n    more_than: \"超过\",\n    my_progress: \"我的进度\",\n    session: \"课程\",\n    completed_feminine: \"已完成的\",\n    completed_sets: \"已完成的课程\",\n    \"set#zero\": \"组\",\n    \"set#one\": \"组\",\n    \"set#other\": \"组\",\n    error_starting_session: \"启动课程时出错\",\n    premium_session: \"高级课程\",\n    premium_session_description: \"这个课程是高级课程的一部分。您可以查看细节，但不能进行锻炼。\",\n    premium_session_exercises: \"包含的练习\",\n    workout_description: \"课程描述\",\n    connect_to_access: \"连接以访问\",\n    become_premium: \"成为高级\",\n    back_to_program: \"返回程序\",\n    no_equipment: \"没有设备\",\n    workout_programs_title: \"锻炼程序 (+ 正在创建中)\",\n    workout_programs_description: \"选择您的挑战并变得更强大！💪\",\n    no_programs_available: \"没有可用的程序\",\n    no_programs_available_description: \"程序将在不久后可用！\",\n    program_completed: \"程序已完成\",\n    auth_required: \"需要认证\",\n    auth_required_description: \"您需要登录以访问此课程。\",\n    login_to_continue: \"登录以继续\",\n    signup_to_continue: \"注册以继续\",\n    premium_required: \"高级要求\",\n    premium_required_description: \"这是一个高级课程。升级以访问所有高级内容。\",\n    upgrade_to_premium: \"Upgrade to Premium\",\n    completed: \"已完成\",\n    about: \"关于\",\n    program: \"程序\",\n    not_found: \"程序未找到\",\n    characteristics: \"特征\",\n    weeks: \"周\",\n    sessions_per_week: \"次/周\",\n    session_duration: \"分钟/次\",\n    \"your_coach#zero\": \"您的教练\",\n    \"your_coach#one\": \"您的教练\",\n    \"your_coach#other\": \"您的教练\",\n    community: \"活跃社区\",\n    community_count: \"coolbuilders 已加入\",\n    week_short: \"周\",\n    week: \"周\",\n    exercises: \"练习\",\n    min_short: \"分钟\",\n    premium: \"高级\",\n    free: \"免费\",\n    join_cta: \"加入挑战\",\n    continue: \"继续\",\n    sessions: \"课程\",\n    check_out_program: \"查看这个训练程序！\",\n    share_success: \"分享成功！\",\n    copied_to_clipboard: \"链接已复制！\",\n    share_failed: \"分享失败\",\n    important_info: \"重要信息\",\n    donation_teaser:\n      \"起初，我们依靠捐赠运营。但正如您所想，捐赠不足以支付开发和运营成本。因此，我们为您制作了一个套餐，这将帮助我们维持运营 — 并在此过程中解锁一些超能力。\",\n    new: \"新\",\n    more_programs_coming_title: \"更多课程即将推出！\",\n    more_programs_coming_description: \"我们正在努力创建新的课程。通过现在升级到高级版，您将自动获得所有课程。感谢您的支持。🚀\",\n    coming_strength: \"力量 & 肌肉\",\n    coming_cardio: \"有氧 HIIT\",\n    coming_yoga: \"瑜伽 & 移动性\",\n    sessions_coming_soon: \"课程即将推出！\",\n    sessions_in_creation: \"我们的团队正在努力为本周创建高质量的课程。请稍后再来！🚀\",\n    welcome_modal: {\n      welcome_title: \"欢迎来到 {programTitle}！\",\n      subtitle: \"准备挑战你的极限！💪\",\n      level_label: \"级别\",\n      duration_label: \"持续时间\",\n      frequency_label: \"频率\",\n      later_button: \"稍后\",\n      start_button: \"开始吧！\",\n    },\n  },\n\n  premium: {\n    checkout_error: \"结账时出错\",\n    premium_required_title: \"需要高级\",\n    premium_required_subtitle: \"这是一个高级访问。升级以访问所有高级内容。\",\n    premium_required_button: \"升级到高级\",\n    already_premium: \"您正在享受 Workout.cool Premium\",\n    no_ads: \"无广告\",\n    upgrade: \"升级\",\n\n    // Checkout\n    checkout: {\n      processing: \"处理中...\",\n    },\n\n    // Pricing\n    pricing: {\n      month: \"月\",\n      year: \"年\",\n      monthly: \"每月\",\n      yearly: \"每年\",\n      discount: \"-48%\",\n    },\n\n    // Hero Section\n    hero: {\n      badge: \"开源 & 自托管 始终免费\",\n      title: \"自由训练，支持使命\",\n      subtitle: \"对于那些相信这个项目并希望通过力量提升器来（重新）相信自己的人！\",\n      stats: {\n        athletes: {\n          count: \"12.4K+\",\n          label: \"活跃运动员\",\n        },\n        series: {\n          count: \"1.2M+\",\n          label: \"系列记录\",\n        },\n        rating: {\n          count: \"4.9/5\",\n          label: \"社区评分\",\n        },\n        progression: {\n          count: \"+23%\",\n          label: \"平均进展\",\n        },\n      },\n\n      // Health Risks\n      health_risks: {\n        overweight: {\n          high_blood_pressure: \"高血压\",\n          ldl_cholesterol: \"低密度脂蛋白胆固醇（坏胆固醇）水平升高\",\n          hdl_cholesterol: \"高密度脂蛋白胆固醇（好胆固醇）水平降低\",\n          triglycerides: \"甘油三酯水平升高\",\n          type_2_diabetes: \"2型糖尿病\",\n          coronary_heart_disease: \"冠心病\",\n          stroke: \"中风\",\n          gallbladder_disease: \"胆囊疾病\",\n          osteoarthritis: \"骨关节炎\",\n          sleep_apnea: \"睡眠呼吸暂停和呼吸问题\",\n          certain_cancers: \"某些癌症（子宫内膜癌、乳腺癌、结肠癌、肾癌、胆囊癌、肝癌）\",\n          low_quality_life: \"生活质量低下\",\n          mental_illnesses: \"精神疾病，如临床抑郁症和焦虑症\",\n          body_pains: \"身体疼痛和身体功能困难\",\n          increased_mortality: \"总体死亡风险增加\",\n        },\n        underweight: {\n          malnutrition: \"营养不良和维生素缺乏\",\n          anemia: \"贫血（血液携氧能力降低）\",\n          osteoporosis: \"骨质疏松症（骨折风险增加）\",\n          immune_function: \"免疫功能下降\",\n          growth_development: \"生长发育问题（特别是儿童）\",\n          reproductive_issues: \"女性因荷尔蒙失衡导致的生殖问题\",\n          miscarriage_risk: \"妊娠早期流产风险较高\",\n          surgery_complications: \"手术期间潜在并发症\",\n          increased_mortality: \"总体死亡风险增加\",\n          underlying_conditions: \"可能表明潜在的医疗状况\",\n        },\n      },\n    },\n\n    // Educational Content\n    // Mission Banner\n    mission: {\n      supporters_count: \"234\",\n      supporters_text: \"支持者帮助使命\",\n      limited: \"有限\",\n      early_access: \"早期访问\",\n    },\n\n    // Plans\n    plans: {\n      monthly: \"每月\",\n      yearly: \"每年\",\n      yearly_discount: \"-48%\",\n      per_month: \"/月\",\n      per_year: \"/year\",\n\n      free: {\n        name: \"免费\",\n        price: \"€0\",\n        period: \"/forever\",\n        price_label: \"€0/forever 永久\",\n        badge: \"开源 • 始终免费\",\n        description: \"所有基本功能\",\n        features: [\"生成带有视频的练习\", \"GitHub 风格的训练历史（6 个月）\", \"分享和重复训练（即将推出）\", \"自托管可能\", \"代码源可用\"],\n        button: \"您的实际计划\",\n        footer_note: \"无需注册 • 永久访问\",\n      },\n\n      premium: {\n        name: \"PREMIUM ⭐\",\n        price_label: \"€7.90/月或€49/年\",\n        badge: \"最受欢迎 • 给爱好者\",\n        description: \"所有功能 + 早期访问\",\n        footer_monthly: \"加入热情的社区！🔥\",\n        footer_yearly: \"感谢您的年度支持！🙏\",\n        yearly_price_note: \"/月\",\n        features: [\n          \"...所有免费计划\",\n          \"无广告\",\n          \"无限历史（与 6 个月免费相比）\",\n          \"跟踪进度，包括高级统计（体积、进展、PR）\",\n          \"预设计训练程序\",\n          \"私人 1:1 聊天与教练\",\n          \"早期访问新功能\",\n        ],\n      },\n    },\n\n    // Buttons and Actions\n    actions: {\n      processing: \"处理中...\",\n      go_premium: \"升级到高级\",\n      sign_in_continue: \"升级到高级\",\n      upgrade_now: \"立即升级\",\n      current_plan: \"您的实际计划\",\n    },\n\n    // Trust Elements\n    trust: {\n      gdpr_compliant: \"100% GDPR 合规\",\n      money_back: \"30天退款保证\",\n      cancel_anytime: \"1 点击取消，无承诺\",\n      secure_payment: \"100% 安全支付 via Stripe\",\n    },\n\n    // Feature Comparison\n    comparison: {\n      title: \"详细功能比较\",\n      subtitle: \"了解每个计划中包含的内容\",\n      features_label: \"功能\",\n      headers: {\n        features: \"功能\",\n        free: \"免费\",\n        premium: \"高级\",\n      },\n      categories: {\n        equipment: \"设备 & 练习\",\n        tracking: \"跟踪 & 分析\",\n        programs: \"程序 & AI\",\n        community: \"社区 & 分享\",\n        support: \"支持 & 项目\",\n      },\n      features: {\n        exercise_library: \"练习库\",\n        custom_exercise: \"自定义练习\",\n        video_tutorials: \"视频教程\",\n        workout_history: \"锻炼历史\",\n        progress_statistics: \"进展统计\",\n        personal_records: \"个人记录跟踪\",\n        volume_analytics: \"体积 & 进展分析\",\n        predesigned_programs: \"预设计程序\",\n        personalized_recommendations: \"个性化推荐\",\n        pro_templates: \"专业模板（举重、健美、等）\",\n        community_access: \"社区访问\",\n        discord_community: \"Discord 社区\",\n        private_chat: \"私人 1:1 聊天与教练\",\n        community_support: \"社区支持\",\n        priority_support: \"优先支持\",\n        early_access: \"早期访问功能\",\n        beta_testing: \"Beta 测试访问\",\n      },\n      values: {\n        basic: \"基本\",\n        complete: \"完整\",\n        unlimited: \"无限\",\n        professional: \"专业\",\n        six_months: \"6 个月\",\n        limited: \"有限\",\n        all_programs: \"所有程序\",\n        public: \"公开\",\n        vip_access: \"VIP 访问\",\n        private_channels: \"私人频道\",\n        soon: \"即将推出\",\n        hd_slowmo: \"4K + 慢动作\",\n        early_access: \"早期访问\",\n      },\n    },\n\n    // FAQ\n    faq: {\n      title: \"常见问题\",\n      subtitle: \"关于 Workout.cool 和我们的使命\",\n      items: [\n        {\n          question: \"为什么开源还要付费？\",\n          answer:\n            \"这是一个很好的问题！代码将始终保持免费，但维护服务器、数据库和基础设施需要成本。您的贡献帮助我们让工具免费提供给每个人。这是一个双赢的模式：您获得高级功能，社区保持免费访问！\",\n        },\n        {\n          question: \"我可以自托管 Workout.cool 吗？\",\n          answer:\n            \"当然可以！整个代码库在 MIT 许可证下可在 GitHub 上获得。您可以在自己的服务器上部署它，根据需要进行定制，并完全免费使用。自托管为您提供对数据和锻炼隐私的完全控制。\",\n        },\n        {\n          question: \"我的锻炼数据安全吗？\",\n          answer:\n            \"是的！我们符合 GDPR，使用加密连接，并安全存储您的数据。此外，由于我们是开源的，您可以审计我们的安全实践。您还可以随时导出数据或自托管以完全控制。\",\n        },\n        {\n          question: \"我可以随时取消订阅吗？\",\n          answer:\n            \"当然可以！没有合同，没有承诺。随时点击取消。您将保持访问权限，直到当前计费期结束，并且您可以随时稍后重新开始。即使您降级到免费，您的锻炼数据仍可访问。\",\n        },\n        {\n          question: \"有针对初学者的练习吗？\",\n          answer:\n            \"当然有！我们的练习库涵盖从完全初学者到高级运动员的所有健身水平。视频和说明帮助初学者找到合适的练习，我们的视频教程展示正确的形式。\",\n        },\n        {\n          question: \"进展跟踪如何工作？\",\n          answer:\n            \"每个组、重复、重量和时间都会自动记录。您会得到一个 GitHub 风格的锻炼历史，显示您的连贯性，以及详细的分析，包括体积、进展和个人记录。高级用户会得到高级图表和见解。\",\n        },\n        {\n          question: \"我可以从其他应用程序导入数据吗？\",\n          answer:\n            \"很快。我们将支持 CSV 导入基本数据（重复和重量）。如果您从另一个健身应用程序切换，我们的支持团队可以帮助迁移您的锻炼历史。\",\n        },\n        {\n          question: \"应用程序可以在离线状态下工作吗？\",\n          answer:\n            \"核心锻炼跟踪可以在离线状态下工作。您可以在没有互联网连接的情况下记录 10 次锻炼的组和重复。练习视频和云同步需要互联网连接。所有离线数据都会自动同步，当您再次在线时。\",\n        },\n        {\n          question: \"有针对女性的程序吗？\",\n          answer:\n            \"当然有！而且将来会有更多程序。我们正在努力。支持者和高级计划将包括所有未来的专门程序，用于不同的目标：力量、塑形、举重、健美等！\",\n        },\n        {\n          question: \"我可以创建自己的程序吗？\",\n          answer: \"不幸的是，不能。我们正在努力！\",\n        },\n      ],\n      additional_support: {\n        title: \"还有问题吗？\",\n        description: \"我们的健身社区在这里帮助您成功\",\n        community: \"社区支持（Discord 或 hello@workout.cool）\",\n        discussions: \"开放讨论（github/discord）\",\n        roadmap: \"透明路线图（github）\",\n      },\n    },\n\n    // Final CTA\n    final_cta: {\n      motivation: \"继续努力！💪\",\n      title: \"准备好支持使命吗？\",\n      subtitle: \"加入数千名相信开源训练自由的健身爱好者\",\n      values: [\n        {\n          title: \"社区优先\",\n          description: \"由健身社区构建和为健身社区构建\",\n        },\n        {\n          title: \"始终透明\",\n          description: \"开源代码，透明资金\",\n        },\n        {\n          title: \"爱的劳动\",\n          description: \"15 年的激情！\",\n        },\n      ],\n      quote: {\n        text: \"我们相信健身工具应该对每个人都是可访问的。您的支持帮助我们继续创新，同时保持这一愿景。\",\n        author: \"— Workout.cool 团队\",\n      },\n    },\n\n    // Premium Active State\n    premium_active: {\n      title: \"高级活跃！💪\",\n      supporting: \"支持使命\",\n    },\n\n    // Legacy translations (keeping for compatibility)\n    premium_active_title: \"高级活跃\",\n    premium_active_subtitle: \"所有功能解锁\",\n    free_intro_title: \"您已经免费获得很多了...\",\n    free_intro_text:\n      \"Workout.cool 是一个免费、开源的健身应用，每天有 60,000+ 用户使用。它是由爱（不是 VC 资金 ^^）构建的，并且为我们保持运行需要真实的时间和金钱。\",\n    donation_story_text:\n      \"起初，我们依靠捐赠运行。但正如您所想，捐赠不足以支付开发和运行成本。所以我们为您制作了一个包，将帮助我们保持灯光，并在路上解锁一些超级能力。\",\n    health_upgrade_text: \"如果 Workout.cool 帮助您提升健康，请考虑升级到高级：D ！\",\n    unlock_features_text: \"解锁高级功能 & 支持开源健身。\",\n    invest_yourself_quote: \"不要在健身和书籍上吝啬 — 投资于自己！\",\n    support_mission: \"支持使命\",\n    best_value_badge: \"最佳价值\",\n    annual_plan: \"年度\",\n    monthly_plan: \"每月\",\n    discount_badge: \"40% 折扣\",\n    per_month: \"/月\",\n    feature_all_programs: \"所有锻炼程序\",\n    feature_progress_tracking: \"进展跟踪\",\n    coming_soon: \"(即将推出)\",\n    feature_future_updates: \"所有未来程序 & 更新\",\n    feature_priority_support: \"优先支持\",\n    save_yearly: \"每年节省 40%\",\n    processing: \"处理中...\",\n    cta_annual: \"我想支持 + 节省 40%\",\n    cta_monthly: \"让我们解锁我的完整计划\",\n    thank_supporting: \"感谢您的支持。\",\n    no_pressure: \"没有压力。您可以随时升级。\",\n    keep_pushing: \"继续努力！huhu\",\n    still_unsure: \"还不确定？没关系。Workout.cool 将始终保持免费和开源。\",\n    support_helps: \"但如果你相信我们在构建的东西，并且你负担得起，你的支持将帮助 💚\",\n    self_hosting: \"自托管\",\n    community: \"社区\",\n    mit_license: \"MIT 许可证\",\n    pricing_year: \"年\",\n    pricing_month: \"月\",\n    conversion_flow_title: \"重定向...\",\n    conversion_flow_message: \"成功登录！重定向到结账...\",\n    redirecting_to_checkout: \"重定向到结账\",\n  },\n  breadcrumbs: {\n    home: \"首页\",\n  },\n  bottom_navigation: {\n    statistics: \"统计\",\n    statistics_tooltip: \"查看您的统计\",\n    programs: \"课程\",\n    programs_tooltip: \"浏览课程\",\n    workouts: \"锻炼\",\n    workouts_tooltip: \"创建您的锻炼\",\n    premium: \"高级\",\n    premium_tooltip: \"成为高级\",\n    tools: \"工具\",\n    tools_tooltip: \"浏览工具\",\n    profile: \"个人资料\",\n    profile_tooltip: \"查看您的个人资料\",\n    leaderboard: \"排行榜\",\n    leaderboard_tooltip: \"查看排行榜\",\n  },\n  levels: {\n    BEGINNER: \"初学者\",\n    INTERMEDIATE: \"中级\",\n    ADVANCED: \"高级\",\n  },\n  email_sent: \"邮件已发送\",\n  cant_send_email: \"无法发送邮件\",\n  logout: \"登出\",\n  verify_email: \"验证您的电子邮件。⚠️ 请检查您的垃圾邮件文件夹。\",\n  verify_email_subtitle: \"请验证您的电子邮件以继续。\",\n  resend_email: \"重新发送邮件\",\n  resend_email_countdown: \"在 {seconds} 秒后重新发送邮件\",\n  signin_error_subtitle: \"请检查您的凭据并重试。\",\n  register_title: \"创建账户\",\n  register_description: \"在下方输入您的信息以创建您的账户\",\n  register_terms: \"注册即表示您同意我们的\",\n  register_privacy: \"隐私政策\",\n  register_privacy_link: \"以及我们的\",\n  register_privacy_link_2: \"隐私政策\",\n  password_forgot_title: \"忘记密码？\",\n  password_forgot_subtitle: \"输入您的电子邮件以重设密码\",\n  new_password: \"新密码\",\n  new_password_placeholder: \"输入您的新密码\",\n  current_password: \"当前密码\",\n  current_password_placeholder: \"输入您当前的密码\",\n  confirm_password: \"确认密码\",\n  confirm_password_placeholder: \"确认您的密码\",\n\n  success: {\n    feedback_sent: \"反馈已发送\",\n    password_forgot_success: \"邮件已发送\",\n    reset_password_success: \"密码重置成功\",\n    password_updated_successfully: \"密码更新成功\",\n  },\n\n  error: {\n    invalid_credentials: \"凭据无效或账户不存在\",\n    upload_failed: \"上传失败\",\n    generic_error: \"操作过程中出错\",\n    sending_email: \"发送邮件时出错\",\n  },\n\n  backend_errors: {\n    EMAIL_ALREADY_EXISTS: \"电子邮件已存在\",\n    INVALID_FILE_TYPE: \"无效的文件类型\",\n    FILE_TOO_LARGE: \"文件过大\",\n    NO_FILE_UPLOADED: \"未上传文件\",\n    IMAGE_PROCESSING_ERROR: \"图像处理错误\",\n    upload_failed: \"上传失败\",\n  },\n\n  profile: {\n    new_workout: \"新的锻炼\",\n    alert: {\n      title: \"您的进度存储在浏览器中。\",\n      create_account: \"创建账户\",\n      log_in: \"登录\",\n      to_ensure_it_is_not_getting_lost: \"以确保不会丢失。\",\n    },\n  },\n\n  // Release Notes\n  release_notes: {\n    title: \"新功能\",\n    release_notes: \"更新日志\",\n    notes: {\n      note_2025_10_29: {\n        title: \"🍑 新的臀部训练计划发布！\",\n        content:\n          \"<li>全新的<a href='/programs/booty-pump' class='text-blue-500 hover:underline'>臀部训练计划</a>现已推出！</li><li>通过专门的训练来锻炼和强化您的臀部肌肉</li><li>专为获得最佳效果和肌肉增长而设计</li><li>立即加入计划！💪</li>\",\n      },\n      note_2025_08_18: {\n        title: \"🏆 新排行榜功能！\",\n        content:\n          \"<li>新的<strong>排行榜</strong>功能，与其他训练冠军竞争</li><li>按<strong>历史总榜、月榜和周榜</strong>查看排名</li><li>跟踪您在顶级表现者中的位置</li><li>激励自己攀登排行榜！🚀</li>\",\n      },\n      note_2025_07_09: {\n        title: \"🎯 运动选择、收藏和新工具\",\n        content:\n          \"<li>新的<strong>运动选择</strong>功能在创建训练时（第3步）</li><li><strong>收藏运动</strong>系统，标记您喜欢的动作</li><li>新的<em>健身工具</em>：BMI计算器和心率区间</li><li>改进的程序卡片</li><li>新的贡献者加入项目！🚀</li>\",\n      },\n      note_2025_07_02: {\n        title: \"🛠️ 自托管、俄语和新工具\",\n        content: \"改进了<strong>自托管</strong>功能，添加了<strong>俄语</strong>支持，并引入了新的<em>健身工具</em>，包括卡路里计算器。🚀\",\n      },\n      note_2025_06_23: {\n        title: \"🇵🇹 葡萄牙语支持和捐赠横幅\",\n        content:\n          \"应用现已支持<strong>葡萄牙语</strong>！我们还添加了<em>捐赠横幅</em>以帮助通过<a href='https://github.com/sponsors/snouzy' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>GitHub Sponsors</a>或<a href='https://ko-fi.com/workoutcool' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Ko-fi</a>支持项目的持续成本。🙏\",\n      },\n      note_2025_06_22: {\n        title: \"🌍 新语言支持和性能提升！\",\n        content: \"应用现已支持中文和俄语！我们还改进了拖放功能的性能，提供更流畅的体验。⚡\",\n      },\n      note_2025_06_19: {\n        title: \"📱 现已作为 PWA 提供！\",\n        content: \"Workout.cool v1.2 现在是一个渐进式网络应用！将其安装在您的手机上，即可享受原生应用体验和离线访问。🚀\",\n      },\n      note_2025_06_18: {\n        title:\n          \"🚀 在 <a href='https://news.ycombinator.com/item?id=44309320' target='_blank' rel='noopener' class='text-blue-500 hover:underline'>Hacker News</a> 上排名第一！\",\n        content: \"Workout.cool 在 Hacker News 上登顶！感谢大家的大力支持，欢迎所有新用户！💪\",\n      },\n      note_2025_06_01: {\n        title: \"🎉 新功能：版本说明对话框\",\n        content: \"您现在可以直接从标题栏查看新增功能！敬请期待更多更新。\",\n      },\n      note_2025_05_20: {\n        title: \"UI 改进\",\n        content: \"改进了移动设备响应能力，并为按钮添加了微妙的悬停效果。\",\n      },\n    },\n  },\n\n  // Premium Upsell Alert\n  donation_alert: {\n    title: \"使用 Workout.cool Premium 解锁高级功能\",\n    or: \"或\",\n  },\n\n  // Donation Modal\n  donation_modal: {\n    support_via: \"支持方式...\",\n    title: \"支持项目\",\n    congrats: \"恭喜完成锻炼！🎉\",\n    subtitle: \"这个应用免费帮助您，但对我来说有真正的成本...\",\n    costs_title: \"成本现实\",\n    costs_description: \"目前，捐赠甚至无法覆盖基本成本：服务器、身份验证、基础设施、数据库等。\",\n    open_source_title: \"100% 开源\",\n    open_source_description: \"这个应用完全免费且开源。不产生任何利润 - 这是一个激情项目，帮助社区和帮助人们锻炼。\",\n    no_ads: \"无广告\",\n    no_tracking: \"无追踪\",\n    impact_title: \"您的影响\",\n    impact_3_euros: \"• 即使 €3 也能覆盖 1 周的服务器费用\",\n    impact_support: \"• 您的支持让应用对所有人保持免费\",\n    impact_footer: \"每一笔捐赠，即使很小，都会产生真正的影响！🙏\",\n    later_button: \"稍后\",\n    support_button: \"支持项目\",\n  },\n\n  // Contact Support\n  contact_support: \"联系支持\",\n  contact_support_subtitle: \"描述您的问题，我们将尽快帮助您。您也可以直接写信给我们：\",\n\n  // Social Platforms\n  social_platforms: {\n    x: \"X (Twitter)\",\n    facebook: \"Facebook\",\n    email: \"电子邮件\",\n    whatsapp: \"WhatsApp\",\n    website: \"网站\",\n    phone: \"电话\",\n    youtube: \"YouTube\",\n    linkedin: \"LinkedIn\",\n    snapchat: \"Snapchat\",\n    instagram: \"Instagram\",\n    tiktok: \"TikTok\",\n    threads: \"Threads\",\n  },\n\n  // Workout Builder\n  workout_builder: {\n    confirm_delete: \"您确定要删除此锻炼回合吗？\",\n    steps: {\n      equipment: {\n        title: \"设备\",\n        description: \"选择您的设备\",\n      },\n      muscles: {\n        title: \"肌肉\",\n        description: \"选择您的训练\",\n      },\n      exercises: {\n        title: \"练习\",\n        description: \"自定义您的锻炼\",\n      },\n    },\n    muscles: {\n      back: \"背部\",\n      abdominals: \"腹肌\",\n      adductors: \"内收肌\",\n      abductors: \"外展肌\",\n      biceps: \"肱二头肌\",\n      triceps: \"肱三头肌\",\n      chest: \"胸部\",\n      shoulders: \"肩部\",\n      quadriceps: \"股四头肌\",\n      hamstrings: \"腿筋\",\n      glutes: \"臀部\",\n      calves: \"小腿\",\n      forearms: \"前臂\",\n      traps: \"斜方肌\",\n      obliques: \"腹斜肌\",\n    },\n    exercise: {\n      watch_video: \"观看视频\",\n      shuffle: \"随机\",\n      pick: \"选择\",\n      remove: \"移除\",\n      no_video_available: \"无可用视频。\",\n    },\n    loading: {\n      exercises: \"正在加载练习...\",\n    },\n    error: {\n      loading_exercises: \"加载练习时出错\",\n    },\n    no_exercises_found: \"未找到练习。请尝试更改您的设备或肌肉选择。\",\n    addExercise: \"添加练习\",\n    exerciseAdded: \"{name} 已添加到锻炼\",\n    exercises: \"练习\",\n    equipment: {\n      bodyweight: {\n        label: \"自重\",\n        description: \"仅使用自身体重的练习\",\n      },\n      dumbbell: {\n        label: \"哑铃\",\n        description: \"使用哑铃的自由重量练习\",\n      },\n      barbell: {\n        label: \"杠铃\",\n        description: \"使用杠铃的复合动作\",\n      },\n      kettlebell: {\n        label: \"壶铃\",\n        description: \"使用壶铃的动态练习\",\n      },\n      band: {\n        label: \"弹力带\",\n        description: \"使用阻力带的练习\",\n      },\n      plate: {\n        label: \"配重片\",\n        description: \"使用配重片的练习\",\n      },\n      pullup_bar: {\n        label: \"引体向上杆\",\n        description: \"使用引体向上杆的上半身练习\",\n      },\n      bench: {\n        label: \"长凳\",\n        description: \"长凳练习和支撑\",\n      },\n    },\n    navigation: {\n      previous: \"上一步\",\n      continue: \"继续\",\n      complete: \"完成\",\n    },\n    stats: {\n      \"muscle_selected#zero\": \"已选择 0 块肌肉\",\n      \"muscle_selected#one\": \"已选择 1 块肌肉\",\n      \"muscle_selected#other\": \"已选择 {count} 块肌肉\",\n      \"equipment_selected#zero\": \"已选择 0 件设备\",\n      \"equipment_selected#one\": \"已选择 1 件设备\",\n      \"equipment_selected#other\": \"已选择 {count} 件设备\",\n      selected: \"已选择\",\n      total: \"总计\",\n      equipment_ready: \"设备准备就绪\",\n      equipment_ready_plural: \"设备准备就绪\",\n    },\n    selection: {\n      choose_your_arsenal: \"选择您的“武器”\",\n      select_equipment_description: \"选择设备以解锁个性化锻炼\",\n      clear_all: \"全部清除\",\n      muscle_selection_coming_soon: \"肌肉选择（即将推出）\",\n      muscle_selection_description: \"点击选择您想训练的肌肉。\",\n      exercise_selection_coming_soon: \"练习选择（即将推出）\",\n      exercise_selection_description: \"此步骤将向您显示个性化的练习建议。\",\n    },\n    session: {\n      back_to_workout: \"返回锻炼\",\n      congrats: \"恭喜，锻炼完成！🎉\",\n      congrats_subtitle: \"您做到了！\",\n      see_instructions: \"查看说明\",\n      finish_set: \"完成组\",\n      finish_session: \"完成回合\",\n      bodyweight: \"自重\",\n      weight: \"重量\",\n      reps: \"次数\",\n      time: \"时间\",\n      next_exercise: \"下一个练习\",\n      add_set: \"添加组\",\n      add_column: \"添加列\",\n      add_row: \"添加行\",\n      remove_column: \"移除列\",\n      set_number: \"第 {number} 组\",\n      set_number_plural: \"第 {number} 组\",\n      set_number_singular: \"第 {number} 组\",\n      set_number_plural_singular: \"第 {number} 组\",\n      workout_in_progress: \"锻炼进行中\",\n      started_at: \"开始于\",\n      quit_workout: \"退出锻炼\",\n      elapsed_time: \"已用时间\",\n      chronometer: \"计时器\",\n      exercise_progress: \"练习进度\",\n      total_volume: \"总训练量\",\n      current_exercise: \"当前练习\",\n      complete: \"完成\",\n      active: \"进行中\",\n      already_have_a_active_session: \"您已有一个进行中的回合。在完成或退出锻炼前无法重复。\",\n      no_exercise_selected: \"未选择练习\",\n      quit_workout_title: \"退出锻炼？\",\n      progress: \"进度\",\n      quit_warning: \"您确定要退出吗？您可以保存进度或完全丢失它。\",\n      save_and_quit: \"保存并退出\",\n      quit_without_save: \"不保存退出\",\n      continue_workout: \"继续锻炼\",\n      history: \"锻炼历史 [{count}]\",\n      no_workout_yet: \"尚无锻炼记录。\",\n      start: \"开始\",\n      end: \"结束\",\n      exercise: \"练习\",\n      repeat: \"重复\",\n      delete: \"删除\",\n    },\n    attribute_value: {\n      bodyweight: \"自重\",\n      strength: \"力量\",\n      powerlifting: \"力量举\",\n      calisthenic: \"健美操\",\n      plyometrics: \"增强式训练\",\n      stretching: \"拉伸\",\n      strongman: \"大力士\",\n      cardio: \"有氧运动\",\n      stabilization: \"稳定\",\n      power: \"爆发力\",\n      resistance: \"阻力\",\n      crossfit: \"CrossFit\",\n      weightlifting: \"举重\",\n      neck: \"颈部\",\n      lats: \"背阔肌\",\n      adductors: \"内收肌\",\n      abductors: \"外展肌\",\n      groin: \"腹股沟\",\n      full_body: \"全身\",\n      rotator_cuff: \"肩袖\",\n      hip_flexor: \"髋屈肌\",\n      achilles_tendon: \"跟腱\",\n      fingers: \"手指\",\n      smith_machine: \"史密斯机\",\n      other: \"其他\",\n      ez_bar: \"EZ 曲杆\",\n      machine: \"器械\",\n      desk: \"桌子\",\n      none: \"无\",\n      cable: \"绳索\",\n      medicine_ball: \"药球\",\n      swiss_ball: \"瑞士球\",\n      foam_roll: \"泡沫轴\",\n      trx: \"TRX\",\n      box: \"箱子\",\n      ropes: \"绳索\",\n      spin_bike: \"动感单车\",\n      step: \"踏板\",\n      bosu: \"BOSU 球\",\n      tyre: \"轮胎\",\n      sandbag: \"沙袋\",\n      pole: \"杆\",\n      wall: \"墙\",\n      bar: \"杠\",\n      rack: \"架子\",\n      car: \"汽车\",\n      sled: \"雪橇\",\n      chain: \"链条\",\n      skierg: \"滑雪机\",\n      rope: \"绳子\",\n      na: \"不适用\",\n      isolation: \"孤立\",\n      compound: \"复合\",\n    },\n  },\n  commons: {\n    upgrade_to_premium: \"成为高级\",\n    last_activity: \"最近活动\",\n    registered_on: \"注册于\",\n    just_now: \"刚刚\",\n    signup_with: \"使用 {provider} 注册\",\n    signin_with: \"使用 {provider} 登录\",\n    signup: \"注册\",\n    login: \"登录\",\n    connecting: \"连接中...\",\n    login_to_your_account_title: \"登录您的账户\",\n    login_to_your_account_subtitle: \"在下方输入您的凭据以登录\",\n    password_forgot: \"忘记密码？\",\n    password_reset_success: \"密码重置成功\",\n    dont_have_account: \"没有账户？\",\n    already_have_account: \"已经有账户了？\",\n    or: \"或\",\n    add: \"添加\",\n    your_feminine: \"您的\",\n    password: \"密码\",\n    email: \"电子邮件\",\n    logout: \"登出\",\n    first_name: \"名\",\n    last_name: \"姓\",\n    verify_password: \"验证密码\",\n    submit: \"提交\",\n    upload: \"上传\",\n    cancel: \"取消\",\n    save_changes: \"保存更改\",\n    change: \"更改\",\n    subject: \"主题\",\n    message: \"消息\",\n    saving: \"保存中...\",\n    edit: \"编辑\",\n    more_options: \"更多选项\",\n    open_link: \"打开链接\",\n    hide: \"隐藏\",\n    make_visible: \"设为可见\",\n    delete: \"删除\",\n    share: \"分享\",\n    title: \"标题\",\n    subtitle: \"副标题\",\n    content: \"内容\",\n    save: \"保存\",\n    button: \"按钮\",\n    card: \"卡片\",\n    go_back: \"返回\",\n    next: \"下一步\",\n    choose_image: \"选择图片\",\n    soon: \"即将推出\",\n    coming_soon_with_emoji: \"即将推出 🤫\",\n    no_image: \"无图片\",\n    description: \"描述\",\n    price: \"价格\",\n    duration: \"时长\",\n    location: \"地点\",\n    schedule: \"时间表\",\n    participants_info: \"参与者信息\",\n    description_placeholder: \"输入描述\",\n    title_placeholder: \"输入标题\",\n    changes_saved: \"更改已保存\",\n    replace: \"替换\",\n    loading: \"加载中...\",\n    image_deleted: \"图片已删除\",\n    discover_workoutcool: \"发现 Workout Cool\",\n    received_just_now: \"刚刚收到\",\n    copied: \"已复制\",\n    url_copied: \"URL 已复制\",\n    copy_failed: \"复制失败\",\n    accordion: \"手风琴\",\n    image: \"图片\",\n    other: \"其他\",\n    register: \"注册\",\n    instantly: \"立即\",\n    immediately: \"立即\",\n    link: \"链接\",\n    accept: \"接受\",\n    deny: \"拒绝\",\n    invalid_input: \"输入无效。请检查错误。\",\n    copy_url: \"复制 URL\",\n    page_url: \"页面 URL\",\n    saving_short: \"保存中...\",\n    saved_short: \"好的\",\n    looks_like_you_are_lost: \"您似乎迷路了\",\n    the_page_you_are_looking_for_is_not_available: \"您正在查找的页面不可用\",\n    go_to_home: \"返回首页\",\n    go_to_profile: \"转到个人资料\",\n    terms: \"服务条款\",\n    privacy: \"隐私政策\",\n    sales_terms: \"销售条款\",\n    consent_banner: \"我们使用 cookie 来改善您的体验。点击“接受”，即表示您同意我们使用 cookie。\",\n    about: \"关于我们\",\n    profile: \"个人资料\",\n    donate: \"捐赠\",\n    my_account: \"我的账户\",\n    dashboard: \"仪表盘\",\n    home: \"首页\",\n    changelog: \"更新日志\",\n    stop_impersonation_button: \"停止模拟\",\n    impersonating_user_label: \"正在模拟用户\",\n    re_hello: \"再次问好\",\n    back_to_login: \"返回登录\",\n    sending: \"发送中...\",\n    send_me_link: \"给我发送链接\",\n    extremely_dissatisfied: \"非常不满意\",\n    somewhat_dissatisfied: \"有些不满意\",\n    neutral: \"中立\",\n    satisfied: \"满意\",\n    support: \"支持\",\n    change_language: \"更改语言\",\n    subscription: \"订阅\",\n    manage_subscription: \"管理订阅\",\n    become_premium: \"成为高级\",\n    remove_ads: \"移除广告\",\n    coming_soon: \"即将推出\",\n    in_progress: \"进行中\",\n    close: \"关闭\",\n    premium: \"高级\",\n    free: \"免费\",\n    new: \"新\",\n    monday: \"周一\",\n    tuesday: \"周二\",\n    wednesday: \"周三\",\n    thursday: \"周四\",\n    friday: \"周五\",\n    saturday: \"周六\",\n    sunday: \"周日\",\n    added_to_favorites: \"已添加到收藏夹\",\n    add_to_favorites: \"添加到收藏夹\",\n    remove_from_favorites: \"从收藏夹中删除\",\n    favorites: \"收藏夹\",\n  },\n  tools: {\n    try_now: \"立即试用\",\n    title: \"健身工具\",\n    subtitle: \"优化训练和营养的必备计算器\",\n    moreComingSoon: \"更多工具即将推出\",\n    meta: {\n      title: \"健身工具 - 训练与营养计算器\",\n      description: \"免费健身计算器：TDEE、宏量营养素、BMI、心率区间、1RM等。使用我们的必备工具优化您的训练和营养。\",\n      keywords: \"健身计算器, 卡路里计算器, 宏量营养素计算器, BMI计算器, TDEE计算器, 心率区间, 单次最大重量, 健身工具\",\n    },\n    \"calorie-calculator\": {\n      title: \"卡路里计算器\",\n      description: \"根据您的活动水平和目标计算每日卡路里需求 (TDEE)\",\n      meta: {\n        title: \"卡路里计算器 - TDEE与每日卡路里需求\",\n        description: \"计算您的总每日能量消耗 (TDEE) 和每日卡路里需求。获得减重、维持或增肌的个性化建议。\",\n        keywords: \"卡路里计算器, TDEE计算器, 每日卡路里, 减重计算器, 卡路里需求, BMR计算器, 新陈代谢计算器\",\n      },\n      subtitle: \"基于Mifflin-St Jeor方程计算您的每日卡路里需求\",\n      how_it_works: \"这个计算器如何工作？\",\n      how_it_works_description: \"该计算器使用科学验证的公式，根据您的个人特征和生活方式估算您的每日卡路里需求。\",\n      how_it_works_step1: \"我们计算您的基础代谢率（静息时燃烧的卡路里）\",\n      how_it_works_step2: \"我们根据您的活动水平进行调整\",\n      how_it_works_step3: \"我们根据您的目标（减重、维持或增重）进行个性化调整\",\n      calculate: \"计算\",\n      calculating: \"计算中...\",\n      tap_info_icons: \"点击 ℹ️ 图标获取更多信息\",\n      gender: \"性别\",\n      male: \"男性\",\n      female: \"女性\",\n      units: \"单位\",\n      metric: \"公制\",\n      imperial: \"英制\",\n      age: \"年龄\",\n      age_placeholder: \"输入您的年龄\",\n      years: \"岁\",\n      height: \"身高\",\n      height_placeholder: \"输入您的身高\",\n      weight: \"体重\",\n      weight_placeholder: \"输入您的体重\",\n      cm: \"厘米\",\n      kg: \"公斤\",\n      lbs: \"磅\",\n      feet: \"英尺\",\n      inches: \"英寸\",\n      activity_level: \"活动水平\",\n      activity: {\n        sedentary: \"久坐\",\n        sedentary_desc: \"很少或不运动，办公室工作，很少步行\",\n        light: \"轻度活跃\",\n        light_desc: \"每周轻度运动1-3天，或每日步行\",\n        moderate: \"中度活跃\",\n        moderate_desc: \"每周中度运动3-5天，活跃的生活方式\",\n        active: \"非常活跃\",\n        active_desc: \"每周高强度运动6-7天，非常活跃的工作\",\n        very_active: \"极其活跃\",\n        very_active_desc: \"运动员，体力工作+每日训练\",\n      },\n      goal: \"目标\",\n      goals: {\n        lose_fast: \"快速减重\",\n        lose_fast_desc: \"每周减2磅（1公斤） - 激进但有效\",\n        lose_slow: \"减重\",\n        lose_slow_desc: \"每周减1磅（0.5公斤） - 可持续且健康\",\n        maintain: \"维持体重\",\n        maintain_desc: \"保持当前体重 - 完美维持体型\",\n        gain_slow: \"增重\",\n        gain_slow_desc: \"每周增1磅（0.5公斤） - 清洁肌肉增长\",\n        gain_fast: \"快速增重\",\n        gain_fast_desc: \"每周增2磅（1公斤） - 最大肌肉增长\",\n      },\n      results: {\n        overview: \"概览\",\n        title: \"您的结果\",\n        bmr: \"基础代谢率\",\n        bmr_explanation:\n          \"基础代谢率 (BMR) 是您的身体在完全静息状态下燃烧的卡路里数，仅用于维持呼吸、循环和细胞产生等基本功能。这是您的身体生存所需的最低能量。\",\n        tdee: \"总每日能量消耗\",\n        tdee_explanation: \"总每日能量消耗 (TDEE) 是您的BMR加上通过日常活动和运动燃烧的卡路里。这是您根据活动水平一天燃烧的总卡路里数。\",\n        target: \"目标卡路里\",\n        macros: \"推荐宏量营养素\",\n        macros_explanation:\n          \"宏量营养素（宏量）是您身体需要的三大营养组：蛋白质（用于肌肉建造和修复）、碳水化合物（用于能量）和脂肪（用于激素和维生素吸收）。显示的百分比是适合大多数健身目标的平衡分配。\",\n        protein: \"蛋白质\",\n        carbs: \"碳水化合物\",\n        fat: \"脂肪\",\n        disclaimer: \"这些计算是基于平均公式的估算。实际卡路里需求可能因个体因素而异。请咨询医疗专业人员或注册营养师获取个性化建议。\",\n      },\n      faq: {\n        title: \"常见问题\",\n        q1: \"为什么我的卡路里目标与其他计算器不同？\",\n        a1: \"不同的计算器可能使用不同的公式或活动倍数。我们使用Mifflin-St Jeor方程，被认为是对大多数人最准确的。然而，个体新陈代谢可能与这些估算相差10-20%。\",\n        q2: \"我应该每天都严格按照这个卡路里数进食吗？\",\n        a2: \"这些是每日平均目标。某些天吃得稍多或稍少是正常的。专注于您的每周平均值，而不是每天都要精确。倾听您身体的饥饿和饱腹感信号。\",\n        q3: \"如果按照这些建议没有看到效果怎么办？\",\n        a3: \"如果您按照建议2-3周后没有看到效果，您可能需要调整。您的实际新陈代谢可能比计算的更高或更低。尝试调整100-200卡路里，再监测2周。同时确保您准确记录食物摄入。\",\n        q4: \"宏量营养素推荐是否适合每个人？\",\n        a4: \"30/40/30比例（蛋白质/碳水化合物/脂肪）对大多数人来说是一个平衡的方法。然而，运动员、有医疗条件的人或遵循特定饮食（生酮、素食等）的人可能需要不同的比例。请咨询营养师获取个性化建议。\",\n      },\n    },\n    \"macro-calculator\": {\n      title: \"宏量营养素计算器\",\n      description: \"为您的健身目标找到最佳的蛋白质、碳水化合物和脂肪分配\",\n    },\n    \"bmi-calculator\": {\n      title: \"BMI计算器\",\n      description: \"计算您的身体质量指数并了解您的体重类别\",\n    },\n    \"heart-rate-calculator\": {\n      title: \"心率区间\",\n      description: \"发现您的最佳训练区间，用于燃脂和提升表现\",\n    },\n    \"heart-rate-zones\": {\n      title: \"心率区间计算器\",\n      description: \"计算您的最佳心率训练区间，以实现最佳表现和脂肪燃烧\",\n      page_title: \"心率区间计算器\",\n      page_description: \"使用科学验证的公式计算个性化心率训练区间。优化您的有氧训练以燃烧脂肪、提升耐力和表现。\",\n      meta: {\n        title: \"心率区间计算器 – 目标心率与训练区间\",\n        description:\n          \"计算您的最大心率和个性化训练区间。使用基础公式或卡尔沃能公式获得VO2 Max区间、无氧区间、有氧区间、燃脂区间和热身区间。\",\n        keywords: \"心率区间计算器, 目标心率, 最大心率, 训练区间, VO2 Max区间, 无氧区间, 有氧区间, 燃脂区间, 卡尔沃能公式, 心率训练\",\n      },\n      calculate: \"计算区间\",\n      calculating: \"计算中…\",\n      method: \"计算方法\",\n      method_info: \"选择最适合您体能水平和可用数据的公式\",\n      methods: {\n        basic: \"基础按年龄\",\n        basic_desc: \"仅使用年龄的简单公式 – 适合初学者\",\n        karvonen_age: \"卡尔沃能按年龄与静息心率\",\n        karvonen_age_desc: \"使用年龄与静息心率，精确度更高\",\n        karvonen_custom: \"卡尔沃能按最大心率与静息心率\",\n        karvonen_custom_desc: \"使用测量的最大心率与静息心率，最精确\",\n      },\n      age: \"年龄\",\n      age_placeholder: \"请输入您的年龄\",\n      resting_heart_rate: \"静息心率 (RHR)\",\n      resting_heart_rate_placeholder: \"请输入静息心率\",\n      resting_heart_rate_info: \"醒来后未起床前测量静息心率。正常范围 60-100 bpm。\",\n      max_heart_rate: \"最大心率 (MHR)\",\n      max_heart_rate_placeholder: \"请输入最大心率\",\n      max_heart_rate_info: \"通过负荷测试或最大努力训练测得的真实最大心率，比基于年龄的估算更精确。\",\n\n      results: {\n        title: \"您的心率区间\",\n        max_heart_rate: \"最大心率\",\n        heart_rate_reserve: \"心率储备\",\n        target_zones: \"目标训练区间\",\n        zone: \"区间\",\n        intensity: \"强度\",\n        heart_rate_range: \"心率范围 (bpm)\",\n        benefits: \"益处\",\n        duration: \"典型时长\",\n      },\n      zones: {\n        warm_up: {\n          name: \"热身区间\",\n          intensity: \"50-60%\",\n          benefits: \"🧘 理想热身\",\n          example: \"轻松散步\",\n          duration: \"5-10 分钟\",\n          description: \"非常轻松的强度，适合热身和恢复\",\n        },\n        fat_burn: {\n          name: \"燃脂区间\",\n          intensity: \"60-70%\",\n          benefits: \"🔥 燃烧脂肪\",\n          example: \"轻度慢跑\",\n          duration: \"20-40 分钟\",\n          description: \"轻度强度，舒适节奏适合较长训练\",\n        },\n        aerobic: {\n          name: \"有氧区间\",\n          intensity: \"70-80%\",\n          benefits: \"💪 提升耐力\",\n          example: \"中等强度跑步\",\n          duration: \"10-40 分钟\",\n          description: \"中等强度，可长时间维持\",\n        },\n        anaerobic: {\n          name: \"无氧区间\",\n          intensity: \"80-90%\",\n          benefits: \"⚡ 提高速度\",\n          example: \"短距离冲刺\",\n          duration: \"2-10 分钟\",\n          description: \"高强度，但仅可维持短时间\",\n        },\n        vo2_max: {\n          name: \"VO2 Max 区间\",\n          intensity: \"90-100%\",\n          benefits: \"🏆 极限表现\",\n          example: \"高强度冲刺\",\n          duration: \"30 秒 - 2 分钟\",\n          description: \"最大强度，仅可维持极短时间\",\n        },\n      },\n      formulas: {\n        basic_formula: \"基础公式\",\n        basic_explanation: \"THR = MHR × 强度百分比\",\n        karvonen_formula: \"卡尔沃能公式\",\n        karvonen_explanation: \"THR = [(MHR – RHR) × 强度百分比] + RHR\",\n        mhr_calculation: \"MHR = 220 – 年龄\",\n      },\n      abbreviations: {\n        thr: \"THR = 目标心率\",\n        mhr: \"MHR = 最大心率\",\n        rhr: \"RHR = 静息心率\",\n        hrr: \"HRR = 心率储备\",\n        bpm: \"bpm = 每分钟心跳次数\",\n      },\n      tips: {\n        title: \"训练建议\",\n        tip1: \"如果是初学者，请从低强度区间开始\",\n        tip2: \"每周训练中混合使用不同区间以获得最佳效果\",\n        tip3: \"使用心率监测器以获得精确反馈\",\n        tip4: \"随着体能提升，您的区间会变化 – 请定期重新计算\",\n      },\n      faq: {\n        title: \"常见问题\",\n        q1: \"我应该选择哪种计算方法？\",\n        a1: \"初学者可使用基础方法；若知静息心率，可用卡尔沃能按年龄公式；测得最大心率与静息心率后，卡尔沃能自定义公式最为精确。\",\n        q2: \"如何测量静息心率？\",\n        a2: \"醒来后躺床上60秒测量，连续3-5天取平均。正常范围60-100 bpm，更低表示更佳心肺健康。\",\n        q3: \"想减脂应在哪个区间训练？\",\n        a3: \"燃脂区间 (60-70%) 最适合脂肪燃烧，但更高强度区间燃烧总热量更多。混合训练效果更佳。\",\n        q4: \"“220-年龄”公式准确吗？\",\n        a4: \"该公式为一般估算，实际可偏差±10-15 bpm。若需更精确，请进行专业最大心率测试或使用卡尔沃能公式。\",\n        q5: \"能天天在VO2 Max区间训练吗？\",\n        a5: \"不建议。VO2 Max区间强度极高，每周仅1-2次短时间隔训练；大部分训练应集中在有氧和燃脂区间以促进恢复。\",\n      },\n      guide: {\n        title: \"心率训练区间全面指南\",\n        text1: \"心率区间是优化训练、实现健身目标的科学工具。无论减脂、提升耐力还是增强表现，掌握并应用心率区间将颠覆您的训练方式。\",\n        text2: \"本计算器基于科学验证的公式，根据您的年龄及（可选）静息心率计算个性化区间。每个区间对应特定强度，带来独特的心血管健康益处。\",\n      },\n      table: {\n        title: \"按年龄划分的参考心率表\",\n        col1: \"年龄\",\n        col2: \"最大心率\",\n        col3: \"50% 强度\",\n        col4: \"85% 强度\",\n        avertiser: \"* 此表为平均值，实际最大心率可偏差±10-15 bpm。\",\n      },\n      details: {\n        title: \"五大训练区间详解\",\n        benefits: \"益处\",\n        zone1_title: \"区间1：热身 (50-60% MHR)\",\n        zone1_content: \"适合开始训练、间歇恢复或结束训练，可轻松对话无气促。\",\n        zone1_details_1: \"促进血液循环\",\n        zone1_details_2: \"激活肌肉与关节\",\n        zone1_details_3: \"降低受伤风险\",\n        zone1_details_4: \"帮助主动恢复\",\n        zone1_duration: \"建议时长\",\n        zone1_duration_value: \"训练前后5-10分钟\",\n        zone1_duration_value_2: \"恢复训练20-30分钟\",\n        zone2_title: \"区间2：燃脂 (60-70% MHR)\",\n        zone2_content: \"主要消耗脂肪能源，可提升基础耐力和代谢效率。\",\n        zone2_details_1: \"最大化脂肪利用\",\n        zone2_details_2: \"提高有氧耐力\",\n        zone2_details_3: \"增强心脏效率\",\n        zone2_details_4: \"强化免疫系统\",\n        zone2_duration: \"建议时长\",\n        zone2_duration_value: \"耐力训练30-90分钟\",\n        zone2_duration_value_2: \"减脂训练45-60分钟\",\n        zone3_title: \"区间3：有氧 (70-80% MHR)\",\n        zone3_content: \"显著提升心肺功能，呼吸加快但可短句交流，适合多数运动员主体训练。\",\n        zone3_details_1: \"增加肺活量\",\n        zone3_details_2: \"改善心血管耐力\",\n        zone3_details_3: \"强化心肌\",\n        zone3_details_4: \"优化氧气利用\",\n        zone3_duration: \"建议时长\",\n        zone3_duration_value: \"持续训练20-60分钟\",\n        zone3_duration_value_2: \"间歇5-15分钟\",\n        zone4_title: \"区间4：无氧 (80-90% MHR)\",\n        zone4_content: \"乳酸生成快于清除，提升速度与爆发力，但仅可短时维持。\",\n        zone4_details_1: \"增强肌肉力量\",\n        zone4_details_2: \"提高乳酸耐受\",\n        zone4_details_3: \"发展速度\",\n        zone4_details_4: \"锻炼意志力\",\n        zone4_duration: \"建议时长\",\n        zone4_duration_value: \"间歇2-10分钟\",\n        zone4_duration_value_2: \"恢复时间相等或双倍\",\n        zone5_title: \"区间5：VO2 Max (90-100% MHR)\",\n        zone5_content: \"最高强度，仅能维持极短时间，仅适合经验丰富运动员。\",\n        zone5_details_1: \"最大化有氧能力\",\n        zone5_details_2: \"提升跑步经济性\",\n        zone5_details_3: \"发展最大爆发力\",\n        zone5_details_4: \"突破心理极限\",\n        zone5_duration: \"建议时长\",\n        zone5_duration_value: \"间歇30秒-2分钟\",\n        zone5_duration_value_2: \"每周最多1-2次\",\n      },\n      educational: {\n        title: \"了解心率区间训练\",\n        description: \"轻松可视化每个训练区间\",\n        what_are_zones: {\n          title: \"什么是心率区间？\",\n          content: \"心率区间是不同强度运动对应的心跳范围，有助于更有效地实现健身目标。\",\n        },\n        why_use_zones: {\n          title: \"为什么使用心率区间？\",\n          content: \"确保训练强度符合目标，防止过度训练，最大化效果，提高训练效率。\",\n        },\n        zone_distribution: {\n          title: \"推荐的周训练区间分配\",\n          content: \"80% 在区间1-3（有氧基础），15% 在区间4（阈值），5% 在区间5（VO2 Max）。根据目标与水平调整。\",\n        },\n        monitoring: {\n          title: \"如何监测心率？\",\n          content: \"使用胸带更精准，使用腕表更方便。训练中定期检查心率并调整强度以保持目标区间。\",\n        },\n      },\n      training_tips: {\n        title: \"专家训练建议\",\n        tip1: {\n          title: \"渐进热身\",\n          description: \"始终先进行5-10分钟区间1 (50-60%) 热身\",\n        },\n        tip2: {\n          title: \"80/20 原则\",\n          description: \"80% 训练在区间1-3，有氧基础；20% 在区间4-5，无氧强度\",\n        },\n        tip3: {\n          title: \"主动恢复\",\n          description: \"高强度后逐渐降低至区间1-2，持续5-10分钟\",\n        },\n        tip4: {\n          title: \"持续补水\",\n          description: \"训练前、中、后均需补水，防止脱水导致心率升高\",\n        },\n        tip5: {\n          title: \"充足睡眠\",\n          description: \"7-9小时睡眠有助恢复，降低静息心率\",\n        },\n        tip6: {\n          title: \"渐进增长\",\n          description: \"每周增加训练量或强度不超过10%，避免过度训练\",\n        },\n      },\n      training_tips_2: {\n        title: \"实用小贴士\",\n        title1: \"找到您的区间\",\n        description1: \"每个区间目标不同，根据目标选择！\",\n        title2: \"建议时长\",\n        description2: \"强度越高，持续时间越短。\",\n        title3: \"逐步进阶\",\n        description3: \"先慢后快，循序渐进。\",\n        title4: \"倾听身体\",\n        description4: \"如感觉不适，请立即减速。\",\n      },\n      quick_facts: {\n        title: \"您知道吗？\",\n        fact1: \"220 - 您的年龄 = 估算最大心率\",\n        fact2: \"醒来后测量脉搏可得静息心率\",\n        fact3: \"智能手表可实时监测心率\",\n        fact4: \"80% 训练应在区间1-3\",\n      },\n      weekly_plan: {\n        title: \"示例周训练计划\",\n        description: \"平衡的一周训练示例\",\n        monday: {\n          title: \"区间1-2\",\n          description: \"30-45分钟\",\n        },\n        tuesday: {\n          title: \"区间2-3\",\n          description: \"45-60分钟\",\n        },\n        wednesday: {\n          title: \"休息\",\n          description: \"恢复\",\n        },\n        thursday: {\n          title: \"区间3-4\",\n          description: \"30-40分钟\",\n        },\n        friday: {\n          title: \"区间1-2\",\n          description: \"30分钟\",\n        },\n        saturday: {\n          title: \"区间4-5\",\n          description: \"20-30分钟\",\n        },\n        tips: \"💡 根据您的水平和目标调整此计划！\",\n        cta: \"⬆️ 立即计算我的区间\",\n      },\n      seo_faq_title: \"关于心率区间的常见问题\",\n      seo_faq_q1_question: \"什么是最大心率 (MHR)?\",\n      seo_faq_q1_answer: \"最大心率是在高强度运动中心脏每分钟跳动的最大次数，通常计算公式为 220 - 年龄，可有 ±10-15 bpm 偏差。\",\n      seo_faq_q2_question: \"如何测量静息心率？\",\n      seo_faq_q2_answer: \"醒来后未起床前测量60秒，或测15秒乘4。连续3-5天取平均。静息心率正常范围60-100 bpm。\",\n      seo_faq_q3_question: \"哪个区间最适合减脂？\",\n      seo_faq_q3_answer: \"燃脂区间 (60-70%) 最适合脂肪燃烧，但更高强度区间燃烧更多卡路里，应交替训练。\",\n      seo_faq_q4_question: \"能每天进行VO2 Max训练吗？\",\n      seo_faq_q4_answer: \"不建议。VO2 Max区间 (90-100%) 强度极高，仅限每周1-2次短间隔训练；其余时间应在有氧区间。\",\n      seo_faq_q5_question: \"“220-年龄”公式准确吗？\",\n      seo_faq_q5_answer: \"这是一般估算，可能偏差 ±10-15 bpm。更精确请使用卡尔沃能公式或进行专业测试。\",\n      seo_faq_q6_question: \"如何判断自己处于正确区间？\",\n      seo_faq_q6_answer: \"使用心率监测器最为准确；无设备可通过对话测试：轻松区间可正常对话，中度区间只能短句，对话区间只能说单词。\",\n      seo_faq_q7_question: \"随着体能提升，区间会变化吗？\",\n      seo_faq_q7_answer: \"会。静息心率下降，心脏效率提升。建议每2-3个月重新计算区间。\",\n      seo_faq_q8_question: \"基础公式与卡尔沃能公式有何区别？\",\n      seo_faq_q8_answer: \"基础公式仅用年龄 (THR = MHR × 强度%)；卡尔沃能公式更精确，考虑静息心率 (THR = [(MHR – RHR) × 强度%] + RHR)。\",\n      intern_links_title: \"准备优化您的训练？\",\n      intern_links_subtitle: \"使用我们的计算器发现个性化区间，改变您的健身之旅\",\n      intern_links_button: \"计算我的区间\",\n      intern_links_bmi_title: \"BMI 计算器\",\n      intern_links_bmi_description: \"评估您的体质指数\",\n      intern_links_calorie_title: \"卡路里计算器\",\n      intern_links_calorie_description: \"确定每日热量需求\",\n      intern_links_macro_title: \"宏量营养素计算器\",\n      intern_links_macro_description: \"优化您的营养配比\",\n      cta: {\n        title: \"准备优化您的训练？\",\n        subtitle: \"使用我们的计算器发现个性化区间，改变您的健身之旅\",\n        button: \"计算我的区间\",\n        bmi_title: \"BMI 计算器\",\n        bmi_description: \"评估您的体质指数\",\n        calorie_title: \"卡路里计算器\",\n        calorie_description: \"确定每日热量需求\",\n        macro_title: \"宏量营养素计算器\",\n        macro_description: \"优化您的营养配比\",\n      },\n      medical_warning_title: \"重要医学警告\",\n      medical_warning_content:\n        \"此计算器基于通用公式提供估算。结果可能因个人健康状况、药物及体能水平而异。如有既往病史或锻炼中出现异常，请在开始新训练计划前咨询专业医疗人员。\",\n    },\n    \"one-rep-max\": {\n      title: \"1RM计算器\",\n      description: \"估算您的单次最大重量并规划力量训练百分比\",\n    },\n    back_to_calculators: \"返回计算器\",\n    body_fat_percentage: \"体脂百分比\",\n    body_fat_info_title: \"什么是体脂百分比？\",\n    body_fat_info_content:\n      \"体脂百分比对Katch-McArdle和Cunningham公式至关重要，因为它们基于瘦体重进行计算。如果您不知道确切的体脂百分比，请使用在线视觉指南或DEXA扫描以获得准确性。\",\n    \"calorie-calculator-hub\": {\n      title: \"卡路里计算器公式\",\n      subtitle: \"选择最适合您需求的公式，获得准确的卡路里计算\",\n      meta: {\n        title: \"卡路里计算器公式 - BMR与TDEE计算器\",\n        description:\n          \"比较不同的BMR公式：Mifflin-St Jeor、Harris-Benedict、Katch-McArdle、Cunningham和Oxford。选择最适合您需求的卡路里计算器。\",\n        keywords: \"BMR公式, 卡路里计算器比较, Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham, Oxford, TDEE计算器\",\n      },\n      which_formula: \"我应该选择哪个公式？\",\n      formula_explanation: \"不同的公式适合不同的人群。以下是帮助您选择的快速指南：\",\n      recommendation_general: \"最佳整体公式，对一般人群最准确\",\n      recommendation_traditional: \"经典公式，广泛使用但准确性稍低\",\n      recommendation_bodyfat: \"如果您知道体脂百分比，最准确\",\n      since: \"自\",\n      all_formulas: \"所有公式\",\n      popularity: \"流行度\",\n      accuracy: \"准确性\",\n      accuracy_high: \"高\",\n      accuracy_good: \"良好\",\n      accuracy_medium: \"中等\",\n      best_for: \"最适合\",\n      best_for_general: \"一般使用\",\n      best_for_traditional: \"传统\",\n      best_for_athletes: \"运动员\",\n      best_for_bodybuilders: \"健美运动员\",\n      best_for_european: \"欧洲人群\",\n      best_for_comparison: \"比较所有\",\n      \"mifflin-st-jeor\": {\n        title: \"Mifflin-St Jeor（推荐）\",\n        description: \"对一般人群最准确的公式，1990年开发。目前BMR计算的黄金标准。\",\n      },\n      \"harris-benedict\": {\n        title: \"Harris-Benedict（经典）\",\n        description: \"经典公式的1984年修订版。广泛使用但对某些人往往高估卡路里。\",\n      },\n      \"katch-mcardle\": {\n        title: \"Katch-McArdle（运动员）\",\n        description: \"基于瘦体重。对于知道体脂百分比且身体活跃的人最准确。\",\n      },\n      cunningham: {\n        title: \"Cunningham（健美运动员）\",\n        description: \"专为低体脂的精瘦运动员和健美运动员设计。使用瘦体重计算。\",\n      },\n      oxford: {\n        title: \"Oxford（欧洲）\",\n        description: \"较新的公式（2005年），基于欧洲人群。考虑年龄段。\",\n      },\n      comparison: {\n        title: \"比较所有公式\",\n        description: \"并排比较所有公式的结果，看看差异并选择最适合您的。\",\n      },\n    },\n    \"mifflin-st-jeor\": {\n      title: \"Mifflin-St Jeor计算器\",\n      subtitle: \"BMR计算的黄金标准 - 对一般人群最准确\",\n      meta: {\n        title: \"Mifflin-St Jeor计算器 - 最准确的BMR与TDEE\",\n        description: \"使用Mifflin-St Jeor方程计算您的BMR和TDEE - 对一般人群最准确的公式。获得个性化卡路里建议。\",\n        keywords: \"Mifflin-St Jeor计算器, BMR计算器, TDEE计算器, 最准确的卡路里计算器, 新陈代谢计算器\",\n      },\n      how_it_works: \"Mifflin-St Jeor公式如何工作\",\n      how_it_works_description:\n        \"1990年开发，该公式被认为是计算健康成人基础代谢率 (BMR) 最准确的。它比Harris-Benedict方程更精确，被营养师和健身专业人员广泛推荐。\",\n    },\n    \"harris-benedict\": {\n      title: \"Harris-Benedict计算器\",\n      subtitle: \"经典BMR公式 - 卡路里计算的传统方法\",\n      meta: {\n        title: \"Harris-Benedict计算器 - 经典BMR与TDEE公式\",\n        description: \"使用修订的Harris-Benedict方程（1984年）计算您的BMR和TDEE。开创现代卡路里计算的经典公式。\",\n        keywords: \"Harris-Benedict计算器, 经典BMR计算器, 传统TDEE计算器, 修订Harris-Benedict公式\",\n      },\n      how_it_works: \"Harris-Benedict公式如何工作\",\n      how_it_works_description:\n        \"最初于1919年开发，1984年修订，Harris-Benedict方程是第一批计算BMR的公式之一。虽然比新公式准确性稍低，但仍被广泛使用，对大多数人提供良好的估算。\",\n    },\n    \"katch-mcardle\": {\n      title: \"Katch-McArdle计算器\",\n      subtitle: \"基于瘦体重的精确BMR计算 - 适合运动员\",\n      meta: {\n        title: \"Katch-McArdle计算器 - 瘦体重BMR与TDEE\",\n        description: \"使用基于瘦体重的Katch-McArdle公式计算您的BMR和TDEE。对于知道体脂百分比的人最准确。\",\n        keywords: \"Katch-McArdle计算器, 瘦体重BMR, 体脂百分比计算器, 运动员BMR计算器, 精确TDEE\",\n      },\n      how_it_works: \"Katch-McArdle公式如何工作\",\n      how_it_works_description: \"该公式基于瘦体重而非总体重计算BMR，对于知道体脂百分比的人更准确。对运动员和身体活跃的个体特别有用。\",\n    },\n    cunningham: {\n      title: \"Cunningham计算器\",\n      subtitle: \"专为精瘦运动员和健美运动员设计的BMR公式\",\n      meta: {\n        title: \"Cunningham计算器 - 精瘦运动员与健美运动员BMR\",\n        description: \"使用Cunningham公式计算您的BMR和TDEE，专为低体脂的精瘦运动员和健美运动员设计。\",\n        keywords: \"Cunningham计算器, 健美运动员BMR计算器, 精瘦运动员BMR, 低体脂BMR计算器, 比赛准备计算器\",\n      },\n      how_it_works: \"Cunningham公式如何工作\",\n      how_it_works_description:\n        \"专为低体脂百分比的精瘦个体开发，该公式提供比其他方程更高的BMR估算。对于竞技运动员和备赛期健美运动员最准确。\",\n    },\n    oxford: {\n      title: \"Oxford计算器\",\n      subtitle: \"基于欧洲人群的现代BMR公式，考虑年龄因素\",\n      meta: {\n        title: \"Oxford计算器 - 现代BMR与TDEE公式\",\n        description: \"使用Oxford方程（2005年）计算您的BMR和TDEE，基于欧洲人群的现代公式，采用年龄特定计算。\",\n        keywords: \"Oxford计算器, 现代BMR计算器, 欧洲BMR公式, 年龄特定BMR计算器, 2005年BMR方程\",\n      },\n      how_it_works: \"Oxford公式如何工作\",\n      how_it_works_description:\n        \"2005年发布，这是较新的BMR公式之一。它基于欧洲人群数据开发，考虑年龄段，为30岁以下和30岁以上的人提供不同的方程。\",\n    },\n    \"calorie-calculator-comparison\": {\n      title: \"比较所有BMR公式\",\n      subtitle: \"查看不同BMR公式如何并排计算您的卡路里需求\",\n      meta: {\n        title: \"BMR公式比较 - 比较所有卡路里计算器\",\n        description: \"并排比较Mifflin-St Jeor、Harris-Benedict、Katch-McArdle、Cunningham和Oxford BMR公式。看看哪个公式最适合您。\",\n        keywords: \"BMR公式比较, 卡路里计算器比较, Mifflin vs Harris-Benedict, 最佳BMR计算器, 比较卡路里公式\",\n      },\n      how_it_works: \"此比较如何工作\",\n      how_it_works_description:\n        \"输入您的详细信息一次，即可查看所有主要BMR公式如何计算您的每日卡路里需求。这有助于您了解差异并选择最适合您目标的公式。\",\n      input_details: \"您的详细信息\",\n      compare: \"比较\",\n      results_comparison: \"公式比较结果\",\n      vs_mifflin: \"vs Mifflin-St Jeor\",\n      summary: \"总结与建议\",\n      summary_explanation: \"不同的公式可能给出不同的结果。通常，±100-200卡路里的差异是正常和预期的。\",\n      recommendation: \"对于大多数人来说，Mifflin-St Jeor提供最准确的基准。如果运动员知道他们的体脂百分比，应该考虑Katch-McArdle。\",\n    },\n    \"bmi-calculator-hub\": {\n      title: \"BMI计算器工具\",\n      subtitle: \"使用不同方法计算您的身体质量指数，获得个性化健康见解\",\n      meta: {\n        title: \"BMI计算器 - 身体质量指数工具和健康评估\",\n        description: \"使用我们的综合工具计算您的BMI。标准BMI、运动员调整版、儿童BMI和比较工具。获得健康见解和建议。\",\n        keywords: \"BMI计算器, 身体质量指数, 健康评估, 体重状态, BMI工具, 儿童BMI, 运动员BMI\",\n      },\n      understanding_bmi: \"了解BMI\",\n      bmi_explanation: \"BMI是一个筛查工具，帮助评估您的体重相对于身高是否健康。选择适合您需求的计算器：\",\n      recommendation_standard: \"最适合一般人群和初步健康筛查\",\n      recommendation_adjusted: \"对运动员和肌肉发达的人更准确\",\n      recommendation_pediatric: \"专为儿童和青少年设计，使用年龄特定百分位数\",\n      popularity: \"受欢迎程度\",\n      accuracy: \"准确性\",\n      accuracy_high: \"高\",\n      accuracy_good: \"良好\",\n      accuracy_medium: \"中等\",\n      best_for: \"最适合\",\n      best_for_general: \"一般使用\",\n      best_for_athletes: \"运动员\",\n      best_for_children: \"儿童\",\n      best_for_comparison: \"比较所有\",\n      category_standard: \"标准\",\n      category_advanced: \"高级\",\n      category_specialized: \"专业\",\n      standard: {\n        title: \"标准BMI计算器\",\n        description: \"使用WHO标准公式的经典BMI计算。为一般人群提供快速简便的评估。\",\n        page_title: \"标准BMI计算器\",\n        page_description: \"使用WHO标准公式计算您的身体质量指数。获得即时结果，包含健康类别和个性化建议。\",\n      },\n      adjusted: {\n        title: \"调整版BMI计算器\",\n        description: \"增强的BMI计算，考虑肌肉质量和身体成分，为运动人群提供更准确的结果。\",\n      },\n      pediatric: {\n        title: \"儿童BMI计算器\",\n        description: \"专为儿童和青少年设计的BMI计算器，使用年龄和性别特定百分位数和生长图表。\",\n      },\n      comparison: {\n        title: \"BMI比较工具\",\n        description: \"并排比较不同的BMI计算方法，了解各种因素如何影响您的结果。\",\n      },\n    },\n  },\n  \"bmi-calculator\": {\n    educational: {\n      introduction_title: \"BMI介绍\",\n      introduction_text: \"BMI是基于身高和体重测量人的瘦弱或肥胖程度，旨在量化组织质量。它被广泛用作判断一个人的体重是否健康的一般指标。\",\n      introduction_usage:\n        \"具体来说，BMI计算得出的数值用于根据数值所在的范围来分类一个人是体重不足、正常体重、超重还是肥胖。这些BMI范围因地区和年龄等因素而异，有时进一步细分为严重体重不足或极度肥胖等子类别。\",\n\n      adult_table_title: \"成人BMI表\",\n      adult_table_description: \"这是世界卫生组织（WHO）基于成人BMI值推荐的体重标准。适用于20岁或以上的男性和女性。\",\n\n      children_table_title: \"儿童和青少年BMI表，年龄2-20岁\",\n      children_table_description: \"美国疾病控制与预防中心（CDC）建议对2至20岁的儿童和青少年进行BMI分类。\",\n\n      classification: \"分类\",\n      bmi_range: \"BMI范围 - kg/m²\",\n      category: \"类别\",\n      percentile_range: \"百分位数范围\",\n      underweight: \"体重不足\",\n      healthy_weight: \"健康体重\",\n      at_risk_overweight: \"超重风险\",\n      overweight: \"超重\",\n\n      overweight_risks_title: \"超重相关风险\",\n      overweight_risks_intro: \"超重会增加多种严重疾病和健康状况的风险。根据美国疾病控制与预防中心（CDC），以下是这些风险的列表：\",\n\n      cardiovascular_risks: \"心血管风险\",\n      high_blood_pressure: \"高血压\",\n      cholesterol_issues: \"低密度脂蛋白胆固醇水平较高，高密度脂蛋白胆固醇水平较低，甘油三酯水平较高\",\n      coronary_heart_disease: \"冠心病\",\n      stroke: \"中风\",\n\n      metabolic_risks: \"代谢风险\",\n      type_2_diabetes: \"2型糖尿病\",\n      gallbladder_disease: \"胆囊疾病\",\n      sleep_apnea: \"睡眠呼吸暂停和呼吸问题\",\n      osteoarthritis: \"骨关节炎，一种由关节软骨破坏引起的关节疾病\",\n\n      other_risks: \"其他健康风险\",\n      certain_cancers: \"某些癌症（子宫内膜癌、乳腺癌、结肠癌、肾癌、胆囊癌、肝癌）\",\n      mental_health_issues: \"精神疾病，如临床抑郁症、焦虑症等\",\n      reduced_quality_life: \"生活质量低下和身体疼痛\",\n      increased_mortality: \"总体而言，与BMI健康的人相比，死亡风险增加\",\n\n      underweight_risks_title: \"体重不足相关风险\",\n      underweight_risks_intro: \"体重不足有其自身的相关风险，列举如下：\",\n      malnutrition: \"营养不良、维生素缺乏、贫血（血液携氧能力降低）\",\n      osteoporosis: \"骨质疏松症，一种导致骨骼脆弱、增加骨折风险的疾病\",\n      immune_function_decrease: \"免疫功能下降\",\n      growth_development_issues: \"生长发育问题，特别是在儿童和青少年中\",\n      reproductive_issues: \"女性因荷尔蒙失衡可能出现的生殖问题\",\n      surgery_complications: \"手术可能出现的并发症\",\n      increased_mortality_underweight: \"总体而言，与BMI健康的人相比，死亡风险增加\",\n\n      adults_limitations: \"成人方面\",\n      older_adults_fat: \"老年人往往比相同BMI的年轻成人有更多体脂\",\n      women_fat_difference: \"在相同BMI下，女性往往比男性有更多体脂\",\n      athletes_muscle_mass: \"肌肉发达的个体和高度训练的运动员可能因肌肉量大而BMI较高\",\n\n      children_limitations: \"儿童和青少年方面\",\n      height_maturation_influence: \"身高和性成熟水平可能影响儿童的BMI和体脂\",\n      fat_free_mass_difference: \"BMI可能是脂肪或无脂质量增加的结果\",\n      population_accuracy: \"BMI对90-95%的人群来说是体脂的相当好的指标\",\n\n      formulas_title: \"BMI公式\",\n      metric_formula: \"公制公式\",\n      imperial_formula: \"英制公式\",\n      example: \"示例\",\n\n      bmi_prime_formula: \"BMI Prime公式\",\n      bmi_prime_description: \"您的BMI与正常BMI上限（25）的比值\",\n\n      ponderal_index_title: \"体重指数\",\n      ponderal_index_explanation:\n        \"体重指数（PI）与BMI相似，都是基于身高和体重测量人的瘦弱或肥胖程度。PI和BMI的主要区别在于公式中身高是立方而不是平方。虽然BMI在考虑大人群时可能是有用的工具，但对于确定个体的瘦弱或肥胖程度并不可靠。\",\n      ponderal_index_metric_description: \"使用公制单位的体重指数\",\n      ponderal_index_imperial_description: \"使用英制单位的体重指数\",\n\n      medical_disclaimer_title: \"医疗免责声明\",\n    },\n\n    height: \"身高\",\n    weight: \"体重\",\n    feet: \"英尺\",\n    inches: \"英寸\",\n    cm: \"厘米\",\n    kg: \"公斤\",\n    lbs: \"磅\",\n    height_placeholder: \"输入身高\",\n    weight_placeholder: \"输入体重\",\n    calculate: \"计算BMI\",\n    your_bmi: \"您的BMI\",\n    bmi_prime: \"BMI Prime\",\n    ponderal_index: \"体重指数\",\n    bmi_category: \"BMI类别\",\n    health_risk: \"健康风险\",\n    recommendations_label: \"建议\",\n    units: \"单位\",\n    metric: \"公制 (公斤/厘米)\",\n    imperial: \"英制 (磅/英尺)\",\n\n    // Detailed BMI Categories (WHO)\n    category_severe_thinness: \"重度消瘦\",\n    category_moderate_thinness: \"中度消瘦\",\n    category_mild_thinness: \"轻度消瘦\",\n    category_normal: \"正常体重\",\n    category_overweight: \"超重\",\n    category_obese_class_1: \"肥胖I级\",\n    category_obese_class_2: \"肥胖II级\",\n    category_obese_class_3: \"肥胖III级\",\n\n    // Health Risks\n    risk_low: \"低\",\n    risk_normal: \"正常\",\n    risk_increased: \"增加\",\n    risk_high: \"高\",\n    risk_very_high: \"很高\",\n    risk_extremely_high: \"极高\",\n\n    // Additional Information\n    bmi_range: \"BMI范围\",\n    ideal_weight: \"理想体重范围\",\n    weight_to_lose: \"需要减重\",\n    weight_to_gain: \"需要增重\",\n    normal_range: \"正常BMI范围：18.5 - 24.9\",\n\n    // BMI Prime\n    about_bmi_prime: \"关于BMI Prime\",\n    bmi_prime_explanation: \"BMI Prime是您的BMI与正常BMI上限(25)的比值。值为1.0表示您处于正常体重的上限。\",\n    underweight: \"体重不足\",\n    normal: \"正常\",\n    overweight: \"超重\",\n    obese: \"肥胖\",\n\n    // Limitations\n    limitations_title: \"BMI的局限性\",\n    limitations_text: \"BMI无法区分肌肉和脂肪质量。运动员和肌肉发达的人可能BMI较高但仍然健康。年龄、性别、种族和身体成分也会影响解释。\",\n\n    disclaimer: \"BMI是筛查工具，可能无法反映身体成分。请咨询医疗专业人员获得个性化建议。\",\n\n    // Recommendations\n    recommendations: {\n      severe_thinness: {\n        medical_consultation: \"强烈建议立即就医咨询\",\n        nutritional_assessment: \"需要全面的营养评估\",\n        weight_gain_program: \"可能需要监督下的增重计划\",\n        screen_conditions: \"筛查潜在疾病\",\n        psychological_evaluation: \"如怀疑饮食失调，考虑心理评估\",\n      },\n      moderate_thinness: {\n        healthcare_provider: \"咨询医疗专业人员进行评估\",\n        nutrient_dense_foods: \"专注于营养丰富、高热量的食物\",\n        registered_dietitian: \"考虑与注册营养师合作\",\n        monitor_malnutrition: \"监测营养不良迹象\",\n        gradual_weight_gain: \"建议逐步健康增重\",\n      },\n      mild_thinness: {\n        consider_healthcare: \"考虑咨询医疗专业人员\",\n        nutrient_dense_foods: \"专注于营养丰富的食物以健康增重\",\n        strength_training: \"包括力量训练以增加肌肉量\",\n        monitor_health: \"定期监测健康状况\",\n        gradual_weight_gain: \"目标是逐步增重（每周0.5-1公斤）\",\n      },\n      normal: {\n        maintain_weight: \"保持当前健康体重\",\n        physical_activity: \"继续定期体育活动（每周150+分钟）\",\n        balanced_diet: \"保持均衡营养的饮食\",\n        health_checkups: \"定期健康检查\",\n        overall_wellness: \"专注于整体健康和身体成分\",\n      },\n      overweight: {\n        gradual_weight_loss: \"目标是逐步减重（每周0.5-1公斤）\",\n        increase_activity: \"增加体育活动至每周150+分钟\",\n        portion_control: \"专注于控制分量和均衡营养\",\n        healthcare_provider: \"考虑咨询医疗专业人员\",\n        lifestyle_goals: \"设定现实可持续的生活方式目标\",\n      },\n      obese_class_1: {\n        healthcare_provider: \"咨询医疗专业人员制定体重管理计划\",\n        weight_loss_target: \"初期目标减重5-10%\",\n        diet_exercise: \"结合饮食和运动干预\",\n        nutritional_counseling: \"考虑专业营养咨询\",\n        screen_conditions: \"筛查与体重相关的健康状况\",\n      },\n      obese_class_2: {\n        medical_supervision: \"寻求医疗监督进行体重管理\",\n        lifestyle_programs: \"考虑综合生活方式干预计划\",\n        evaluate_conditions: \"评估与体重相关的健康状况\",\n        medical_treatments: \"可能受益于医疗减重治疗\",\n        bariatric_surgery: \"如适当，考虑减重手术评估\",\n      },\n      obese_class_3: {\n        medical_consultation: \"建议立即就医咨询\",\n        bariatric_surgery: \"考虑减重手术评估\",\n        weight_management: \"综合医疗体重管理计划\",\n        health_complications: \"处理与体重相关的健康并发症\",\n        multidisciplinary: \"与医疗团队采用多学科方法\",\n      },\n    },\n  },\n  statistics: {\n    title: \"统计数据\",\n    page_subtitle: \"通过高级分析和个性化洞察，追踪您的健身之旅。\",\n    select_exercise: \"选择练习\",\n    active_daily_users: \"每日活跃用户\",\n    success_rate: \"成功率\",\n    user_rating: \"用户评分\",\n\n    // Tabs\n    tabs: {\n      video: \"视频\",\n      statistics: \"统计数据\",\n    },\n\n    // Chart titles and labels\n    weight: \"重量\",\n    volume: \"训练量\",\n    weight_progression: \"重量进展\",\n    weight_progression_chart: \"重量进展图表\",\n    weekly_volume: \"每周训练量\",\n    volume_chart: \"训练量图表\",\n    estimated_1rm: \"估计最大单次重量 (1RM)\",\n    one_rep_max_chart: \"最大单次重量图表\",\n    performance_over_time: \"随时间的表现\",\n\n    // Form and controls\n    timeframe: \"时间范围\",\n    timeframe_selector: \"时间范围选择器\",\n\n    // Timeframes\n    timeframes: {\n      \"4weeks\": \"4周\",\n      \"8weeks\": \"8周\",\n      \"12weeks\": \"12周\",\n      \"1year\": \"1年\",\n    },\n\n    // Error messages\n    error_loading_data: \"加载数据时出错\",\n    error_loading_weight_progression: \"加载重量进展时出错\",\n    error_loading_1rm: \"加载1RM数据时出错\",\n    error_loading_volume: \"加载训练量数据时出错\",\n\n    // Empty states\n    no_data_yet: \"暂无数据\",\n    start_tracking: \"开始跟踪以查看您的进展\",\n    no_1rm_data: \"没有可用的1RM数据\",\n    complete_sets_with_weight: \"完成带重量的组数以查看您的最大单次重量 (1RM)\",\n    no_volume_data: \"没有可用的训练量数据\",\n    complete_workouts: \"完成训练以查看您的训练量\",\n\n    // Info and tooltips\n    \"1rm_formula_info\": \"1RM公式信息\",\n    volume_calculation: \"训练量 = 重量 × 次数 × 组数\",\n    last_updated: \"最后更新：{date}\",\n\n    // Premium\n    premium_required: \"需要高级版才能访问统计数据\",\n\n    // StatisticsPreviewOverlay\n    premium_statistics: \"高级统计\",\n    premium_statistics_description: \"通过每个练习的高级分析，获得关于您健身之旅的详细见解。\",\n    total_volume: \"总训练量\",\n    pr_increase: \"PR 增加\",\n    weight_progress: \"重量进展\",\n    upgrade_now: \"立即升级\",\n    rating: \"4.8/5 评分\",\n    no_ads: \"无广告\",\n    cancel_anytime: \"随时取消\",\n    preview_notice: \"这只是预览！👀\",\n    preview_description: \"解锁完整访问权限，获得详细分析、进度跟踪和个性化见解。\",\n    get_premium_access: \"获得高级访问权限\",\n\n    // ExercisesBrowser\n    all_equipment: \"所有器械\",\n    all_muscles: \"所有肌肉\",\n    search_exercises: \"搜索练习\",\n    error_loading_exercises: \"加载练习错误\",\n    no_exercises_found: \"未找到练习\",\n    equipment_label: \"器械：\",\n    primary_muscle_label: \"主要肌肉：\",\n    unknown: \"未知\",\n    no_image_available: \"无可用图像\",\n  },\n  heatmap: {\n    week_days_short: {\n      sunday: \"日\",\n      monday: \"一\",\n      tuesday: \"二\",\n      wednesday: \"三\",\n      thursday: \"四\",\n      friday: \"五\",\n      saturday: \"六\",\n    },\n    month_names_short: {\n      january: \"一月\",\n      february: \"二月\",\n      march: \"三月\",\n      april: \"四月\",\n      may: \"五月\",\n      june: \"六月\",\n      july: \"七月\",\n      august: \"八月\",\n      september: \"九月\",\n      october: \"十月\",\n      november: \"十一月\",\n      december: \"十二月\",\n    },\n    \"workout#one\": \"次训练\",\n    \"workout#other\": \"次训练\",\n  },\n} as const;\n"
  },
  {
    "path": "middleware.ts",
    "content": "// middleware.ts\nimport { createI18nMiddleware } from \"next-international/middleware\";\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { getSessionCookie } from \"better-auth/cookies\";\n\nfunction detectUserLocale(request: NextRequest): string {\n  const acceptLanguage = request.headers.get(\"accept-language\");\n\n  if (!acceptLanguage) return \"en\";\n\n  // Parse Accept-Language header\n  const languages = acceptLanguage\n    .split(\",\")\n    .map((lang) => {\n      const [locale, quality = \"1\"] = lang.trim().split(\";q=\");\n      return { locale: locale.toLowerCase(), quality: parseFloat(quality) };\n    })\n    .sort((a, b) => b.quality - a.quality);\n\n  // Map browser locales to supported locales\n  const supportedLocales = [\"en\", \"fr\", \"es\", \"zh-cn\", \"ru\", \"pt\"];\n\n  for (const { locale } of languages) {\n    // Exact match\n    if (supportedLocales.includes(locale)) {\n      return locale === \"zh-cn\" ? \"zh-CN\" : locale;\n    }\n\n    // Language match (fr-FR -> fr)\n    const lang = locale.split(\"-\")[0];\n    if (supportedLocales.includes(lang)) {\n      return lang;\n    }\n\n    // Chinese variants\n    if (locale.startsWith(\"zh\")) {\n      return \"zh-CN\";\n    }\n  }\n\n  return \"en\"; // fallback\n}\n\nconst I18nMiddleware = createI18nMiddleware({\n  locales: [\"en\", \"fr\", \"es\", \"zh-CN\", \"ru\", \"pt\"],\n  defaultLocale: \"en\",\n  urlMappingStrategy: \"rewrite\",\n});\n\nexport async function middleware(request: NextRequest) {\n  const pathname = request.nextUrl.pathname;\n  const detectedLocale = detectUserLocale(request);\n\n  // If on root path and no locale detected yet, redirect to detected locale\n  if (pathname === \"/\" && !request.cookies.get(\"detected-locale\")) {\n    const url = new URL(`/${detectedLocale}`, request.url);\n    const response = NextResponse.redirect(url);\n\n    response.cookies.set(\"detected-locale\", detectedLocale, {\n      maxAge: 60 * 60 * 24 * 365, // 1 year\n      httpOnly: false,\n      secure: process.env.NODE_ENV === \"production\",\n      sameSite: \"lax\",\n    });\n\n    return response;\n  }\n\n  // Normal i18n middleware processing\n  const response = I18nMiddleware(request);\n\n  // Extract current locale from pathname\n  const localeMatch = pathname.match(/^\\/([a-z]{2}(?:-[A-Z]{2})?)/);\n  const currentLocale = localeMatch ? localeMatch[1] : detectedLocale;\n\n  // Set Content-Language header for SEO\n  response.headers.set(\"Content-Language\", currentLocale);\n\n  // Store detected locale in cookie for future visits\n  if (!request.cookies.get(\"detected-locale\")) {\n    response.cookies.set(\"detected-locale\", detectedLocale, {\n      maxAge: 60 * 60 * 24 * 365, // 1 year\n      httpOnly: false,\n      secure: process.env.NODE_ENV === \"production\",\n      sameSite: \"lax\",\n    });\n  }\n\n  const searchParams = request.nextUrl.searchParams.toString();\n  response.headers.set(\"searchParams\", searchParams);\n\n  if (request.nextUrl.pathname.includes(\"/dashboard\")) {\n    const session = getSessionCookie(request);\n\n    if (!session) {\n      return NextResponse.redirect(new URL(\"/\", request.url));\n    }\n  }\n\n  return response;\n}\n\nexport const config = {\n  matcher: [\n    \"/((?!api|static|_next|manifest.json|favicon.ico|robots.txt|sw.js|apple-touch-icon.png|android-chrome-.*\\\\.png|images|logo|icons|sitemap.xml|ads.txt).*)\",\n  ],\n};\n"
  },
  {
    "path": "next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  reactStrictMode: true,\n  images: {\n    unoptimized: true,\n    domains: [\"lh3.googleusercontent.com\", \"192.168.1.12\", \"localhost\", \"www.facebook.com\", \"api.dicebear.com\"],\n    remotePatterns: [\n      {\n        protocol: \"https\",\n        hostname: \"**.vercel.app\",\n      },\n      {\n        protocol: \"https\",\n        hostname: \"api.dicebear.com\",\n      },\n    ],\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "nextauth.d.ts",
    "content": "/* eslint-disable @typescript-eslint/consistent-type-definitions */\nimport type { DefaultSession } from \"next-auth\";\n\ndeclare module \"next-auth\" {\n  interface Session {\n    user: DefaultSession[\"user\"] & {\n      id: string;\n      email: string;\n      name?: string;\n      image?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"workoutcool\",\n  \"version\": \"1.3.2\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev --turbopack\",\n    \"build\": \"next build\",\n    \"email\": \"email dev\",\n    \"start\": \"next start\",\n    \"stripe-webhooks\": \"stripe listen --forward-to localhost:3000/api/webhooks/stripe\",\n    \"vercel-build\": \"next build\",\n    \"old-vercel-build\": \"prisma generate && prisma migrate deploy && next build\",\n    \"postinstall\": \"prisma generate\",\n    \"lint\": \"next lint\",\n    \"import:exercises-full\": \"tsx scripts/import-exercises-with-attributes.ts\",\n    \"db:seed\": \"pnpm run import:exercises-full ./data/sample-exercises.csv\",\n    \"db:seed-plans\": \"tsx scripts/seed-subscription-plans-simple.ts\",\n    \"db:seed-workout-advanced\": \"tsx scripts/seed-workout-data-advanced.ts\",\n    \"lint:fix\": \"next lint --fix\",\n    \"db:seed-leaderboard\": \"tsx scripts/seed-leaderboard-data.ts\",\n    \"migrate:prod\": \"env-cmd -f .env.production prisma migrate deploy\"\n  },\n  \"resolutions\": {\n    \"prettier\": \"^3.4.2\"\n  },\n  \"dependencies\": {\n    \"@auth/prisma-adapter\": \"^2.8.0\",\n    \"@better-auth/expo\": \"^1.2.12\",\n    \"@dnd-kit/core\": \"^6.3.1\",\n    \"@dnd-kit/modifiers\": \"^9.0.0\",\n    \"@dnd-kit/sortable\": \"^10.0.0\",\n    \"@dnd-kit/utilities\": \"^3.2.2\",\n    \"@hookform/resolvers\": \"^5.0.1\",\n    \"@openpanel/nextjs\": \"^1.0.8\",\n    \"@prisma/client\": \"^6.5.0\",\n    \"@radix-ui/react-accordion\": \"^1.2.3\",\n    \"@radix-ui/react-alert-dialog\": \"^1.1.6\",\n    \"@radix-ui/react-aspect-ratio\": \"^1.1.2\",\n    \"@radix-ui/react-avatar\": \"^1.1.3\",\n    \"@radix-ui/react-collapsible\": \"^1.1.11\",\n    \"@radix-ui/react-dialog\": \"^1.1.6\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.6\",\n    \"@radix-ui/react-hover-card\": \"^1.1.7\",\n    \"@radix-ui/react-icons\": \"^1.3.2\",\n    \"@radix-ui/react-label\": \"^2.1.2\",\n    \"@radix-ui/react-navigation-menu\": \"^1.2.6\",\n    \"@radix-ui/react-popover\": \"^1.1.6\",\n    \"@radix-ui/react-portal\": \"^1.1.5\",\n    \"@radix-ui/react-radio-group\": \"^1.3.3\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.6\",\n    \"@radix-ui/react-select\": \"^2.1.7\",\n    \"@radix-ui/react-separator\": \"^1.1.2\",\n    \"@radix-ui/react-slider\": \"^1.3.2\",\n    \"@radix-ui/react-slot\": \"^1.2.0\",\n    \"@radix-ui/react-switch\": \"^1.2.2\",\n    \"@radix-ui/react-tabs\": \"^1.1.3\",\n    \"@radix-ui/react-toast\": \"^1.2.7\",\n    \"@radix-ui/react-tooltip\": \"^1.1.8\",\n    \"@react-email/components\": \"^0.0.35\",\n    \"@react-email/html\": \"^0.0.11\",\n    \"@react-email/render\": \"^1.1.2\",\n    \"@react-email/tailwind\": \"^1.0.4\",\n    \"@t3-oss/env-nextjs\": \"^0.12.0\",\n    \"@tailwindcss/typography\": \"^0.5.16\",\n    \"@tanstack/react-query\": \"^5.74.3\",\n    \"@tanstack/react-query-devtools\": \"^5.74.4\",\n    \"@vercel/functions\": \"^2.0.3\",\n    \"better-auth\": \"^1.2.7\",\n    \"canvas-confetti\": \"^1.9.3\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"crypto-js\": \"^4.2.0\",\n    \"csv-parser\": \"^3.2.0\",\n    \"daisyui\": \"^5.0.43\",\n    \"dayjs\": \"^1.11.13\",\n    \"eslint-config-prettier\": \"^10.1.1\",\n    \"framer-motion\": \"^12.7.2\",\n    \"geist\": \"^1.3.1\",\n    \"lodash.debounce\": \"^4.0.8\",\n    \"lucide-react\": \"^0.487.0\",\n    \"mathjax-react\": \"^2.0.1\",\n    \"next\": \"15.2.6\",\n    \"next-international\": \"^1.3.1\",\n    \"next-mdx-remote\": \"^5.0.0\",\n    \"next-safe-action\": \"^7.10.4\",\n    \"next-themes\": \"^0.4.6\",\n    \"nodemailer\": \"^6.10.0\",\n    \"npm\": \"^11.3.0\",\n    \"nprogress\": \"^0.2.0\",\n    \"nuqs\": \"^2.4.3\",\n    \"papaparse\": \"^5.5.3\",\n    \"pg\": \"^8.14.1\",\n    \"pinyin-pro\": \"^3.26.0\",\n    \"prisma\": \"^6.5.0\",\n    \"react\": \"^19.2.1\",\n    \"react-dom\": \"^19.2.1\",\n    \"react-hook-form\": \"^7.55.0\",\n    \"react-icons\": \"^5.5.0\",\n    \"recharts\": \"^3.1.0\",\n    \"slugify\": \"^1.6.6\",\n    \"sonner\": \"^2.0.3\",\n    \"stripe\": \"18.2.1\",\n    \"usehooks-ts\": \"^3.1.1\",\n    \"vaul\": \"^1.1.2\",\n    \"zod\": \"^3.24.2\",\n    \"zustand\": \"^5.0.3\"\n  },\n  \"devDependencies\": {\n    \"@eslint/compat\": \"^1.2.7\",\n    \"@eslint/eslintrc\": \"^3.3.1\",\n    \"@eslint/js\": \"^9.28.0\",\n    \"@next/bundle-analyzer\": \"^15.3.4\",\n    \"@next/eslint-plugin-next\": \"^15.2.4\",\n    \"@types/canvas-confetti\": \"^1.9.0\",\n    \"@types/crypto-js\": \"^4.2.2\",\n    \"@types/lodash.debounce\": \"^4.0.9\",\n    \"@types/node\": \"^20\",\n    \"@types/nodemailer\": \"^6.4.17\",\n    \"@types/nprogress\": \"^0.2.3\",\n    \"@types/papaparse\": \"^5.3.16\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.29.0\",\n    \"@typescript-eslint/parser\": \"^8.29.0\",\n    \"autoprefixer\": \"^10.4.21\",\n    \"env-cmd\": \"^10.1.0\",\n    \"eslint\": \"^9.23.0\",\n    \"eslint-config-next\": \"15.2.3\",\n    \"eslint-plugin-import\": \"^2.31.0\",\n    \"eslint-plugin-prettier\": \"^5.2.5\",\n    \"eslint-plugin-react\": \"^7.37.4\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"eslint-plugin-unused-imports\": \"^4.1.4\",\n    \"globals\": \"^16.0.0\",\n    \"postcss\": \"^8.5.3\",\n    \"prettier\": \"^3.4.2\",\n    \"prettier-plugin-sort-json\": \"^4.1.1\",\n    \"tailwind-merge\": \"^2.3.0\",\n    \"tailwindcss\": \"^3.4.13\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"tslog\": \"^4.9.3\",\n    \"tsx\": \"^4.19.4\",\n    \"typescript\": \"^5\",\n    \"typescript-eslint\": \"^8.29.0\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "prisma/migrations/0_init/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"UserRole\" AS ENUM ('admin', 'user');\n\n-- CreateEnum\nCREATE TYPE \"ExercisePrivacy\" AS ENUM ('PUBLIC', 'PRIVATE');\n\n-- CreateEnum\nCREATE TYPE \"ExerciseAttributeNameEnum\" AS ENUM ('TYPE', 'PRIMARY_MUSCLE', 'SECONDARY_MUSCLE', 'EQUIPMENT', 'MECHANICS_TYPE');\n\n-- CreateEnum\nCREATE TYPE \"ExerciseAttributeValueEnum\" AS ENUM ('BODYWEIGHT', 'STRENGTH', 'POWERLIFTING', 'CALISTHENIC', 'PLYOMETRICS', 'STRETCHING', 'STRONGMAN', 'CARDIO', 'STABILIZATION', 'POWER', 'RESISTANCE', 'CROSSFIT', 'WEIGHTLIFTING', 'BICEPS', 'SHOULDERS', 'CHEST', 'BACK', 'GLUTES', 'TRICEPS', 'HAMSTRINGS', 'QUADRICEPS', 'FOREARMS', 'CALVES', 'TRAPS', 'ABDOMINALS', 'NECK', 'LATS', 'ADDUCTORS', 'ABDUCTORS', 'OBLIQUES', 'GROIN', 'FULL_BODY', 'ROTATOR_CUFF', 'HIP_FLEXOR', 'ACHILLES_TENDON', 'FINGERS', 'DUMBBELL', 'KETTLEBELLS', 'BARBELL', 'SMITH_MACHINE', 'BODY_ONLY', 'OTHER', 'BANDS', 'EZ_BAR', 'MACHINE', 'DESK', 'PULLUP_BAR', 'NONE', 'CABLE', 'MEDICINE_BALL', 'SWISS_BALL', 'FOAM_ROLL', 'WEIGHT_PLATE', 'TRX', 'BOX', 'ROPES', 'SPIN_BIKE', 'STEP', 'BOSU', 'TYRE', 'SANDBAG', 'POLE', 'BENCH', 'WALL', 'BAR', 'RACK', 'CAR', 'SLED', 'CHAIN', 'SKIERG', 'ROPE', 'NA', 'ISOLATION', 'COMPOUND');\n\n-- CreateEnum\nCREATE TYPE \"WorkoutSetType\" AS ENUM ('TIME', 'WEIGHT', 'REPS', 'BODYWEIGHT', 'NA');\n\n-- CreateEnum\nCREATE TYPE \"WorkoutSetUnit\" AS ENUM ('kg', 'lbs');\n\n-- CreateEnum\nCREATE TYPE \"SubscriptionStatus\" AS ENUM ('ACTIVE', 'TRIAL', 'CANCELLED', 'EXPIRED', 'PAUSED');\n\n-- CreateEnum\nCREATE TYPE \"Platform\" AS ENUM ('WEB', 'IOS', 'ANDROID');\n\n-- CreateEnum\nCREATE TYPE \"PaymentProcessor\" AS ENUM ('STRIPE', 'PAYPAL', 'LEMONSQUEEZY', 'PADDLE', 'APPLE_PAY', 'GOOGLE_PAY', 'REVENUECAT', 'NONE', 'OTHER');\n\n-- CreateEnum\nCREATE TYPE \"ProgramLevel\" AS ENUM ('BEGINNER', 'INTERMEDIATE', 'ADVANCED', 'EXPERT');\n\n-- CreateEnum\nCREATE TYPE \"ProgramVisibility\" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');\n\n-- CreateTable\nCREATE TABLE \"user\" (\n    \"id\" TEXT NOT NULL,\n    \"firstName\" TEXT NOT NULL DEFAULT '',\n    \"lastName\" TEXT NOT NULL DEFAULT '',\n    \"name\" TEXT NOT NULL,\n    \"email\" TEXT NOT NULL,\n    \"emailVerified\" BOOLEAN NOT NULL,\n    \"image\" TEXT,\n    \"locale\" TEXT DEFAULT 'fr',\n    \"createdAt\" TIMESTAMP(3) NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"role\" \"UserRole\" DEFAULT 'user',\n    \"banned\" BOOLEAN DEFAULT false,\n    \"banReason\" TEXT,\n    \"banExpires\" TIMESTAMP(3),\n    \"isPremium\" BOOLEAN DEFAULT false,\n\n    CONSTRAINT \"user_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"user_favorite_exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"user_favorite_exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"session\" (\n    \"id\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"token\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"ipAddress\" TEXT,\n    \"userAgent\" TEXT,\n    \"userId\" TEXT NOT NULL,\n    \"impersonatedBy\" TEXT,\n\n    CONSTRAINT \"session_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"account\" (\n    \"id\" TEXT NOT NULL,\n    \"accountId\" TEXT NOT NULL,\n    \"providerId\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"accessToken\" TEXT,\n    \"refreshToken\" TEXT,\n    \"idToken\" TEXT,\n    \"accessTokenExpiresAt\" TIMESTAMP(3),\n    \"refreshTokenExpiresAt\" TIMESTAMP(3),\n    \"scope\" TEXT,\n    \"password\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"account_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"verification\" (\n    \"id\" TEXT NOT NULL,\n    \"identifier\" TEXT NOT NULL,\n    \"value\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"createdAt\" TIMESTAMP(3),\n    \"updatedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"verification_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"feedbacks\" (\n    \"id\" TEXT NOT NULL,\n    \"review\" INTEGER NOT NULL,\n    \"message\" TEXT NOT NULL,\n    \"email\" TEXT,\n    \"userId\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"feedbacks_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"nameEn\" TEXT,\n    \"description\" TEXT,\n    \"descriptionEn\" TEXT,\n    \"fullVideoUrl\" TEXT,\n    \"fullVideoImageUrl\" TEXT,\n    \"introduction\" TEXT,\n    \"introductionEn\" TEXT,\n    \"slug\" TEXT,\n    \"slugEn\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercise_attribute_names\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" \"ExerciseAttributeNameEnum\" NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"exercise_attribute_names_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercise_attribute_values\" (\n    \"id\" TEXT NOT NULL,\n    \"attributeNameId\" TEXT NOT NULL,\n    \"value\" \"ExerciseAttributeValueEnum\" NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"exercise_attribute_values_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercise_attributes\" (\n    \"id\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"attributeNameId\" TEXT NOT NULL,\n    \"attributeValueId\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"exercise_attributes_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"workout_sessions\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"startedAt\" TIMESTAMP(3) NOT NULL,\n    \"endedAt\" TIMESTAMP(3),\n    \"duration\" INTEGER,\n    \"muscles\" \"ExerciseAttributeValueEnum\"[] DEFAULT ARRAY[]::\"ExerciseAttributeValueEnum\"[],\n    \"rating\" INTEGER,\n    \"ratingComment\" TEXT,\n\n    CONSTRAINT \"workout_sessions_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"workout_session_exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"workoutSessionId\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL,\n\n    CONSTRAINT \"workout_session_exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"workout_sets\" (\n    \"id\" TEXT NOT NULL,\n    \"workoutSessionExerciseId\" TEXT NOT NULL,\n    \"setIndex\" INTEGER NOT NULL,\n    \"type\" \"WorkoutSetType\" NOT NULL,\n    \"types\" \"WorkoutSetType\"[] DEFAULT ARRAY[]::\"WorkoutSetType\"[],\n    \"valuesInt\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"valuesSec\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"units\" \"WorkoutSetUnit\"[] DEFAULT ARRAY[]::\"WorkoutSetUnit\"[],\n    \"completed\" BOOLEAN NOT NULL DEFAULT false,\n\n    CONSTRAINT \"workout_sets_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"subscription_plans\" (\n    \"id\" TEXT NOT NULL,\n    \"priceMonthly\" DECIMAL(10,2),\n    \"priceYearly\" DECIMAL(10,2),\n    \"currency\" TEXT NOT NULL DEFAULT 'EUR',\n    \"interval\" TEXT NOT NULL DEFAULT 'month',\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"availableRegions\" TEXT[] DEFAULT ARRAY[]::TEXT[],\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"subscription_plans_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"plan_provider_mappings\" (\n    \"id\" TEXT NOT NULL,\n    \"planId\" TEXT NOT NULL,\n    \"provider\" \"PaymentProcessor\" NOT NULL,\n    \"externalId\" TEXT NOT NULL,\n    \"region\" TEXT,\n    \"metadata\" JSONB,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"plan_provider_mappings_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"subscriptions\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"planId\" TEXT NOT NULL,\n    \"revenueCatUserId\" TEXT,\n    \"status\" \"SubscriptionStatus\" NOT NULL,\n    \"startedAt\" TIMESTAMP(3) NOT NULL,\n    \"currentPeriodEnd\" TIMESTAMP(3),\n    \"cancelledAt\" TIMESTAMP(3),\n    \"platform\" \"Platform\",\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"subscriptions_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"licenses\" (\n    \"id\" TEXT NOT NULL,\n    \"key\" TEXT NOT NULL,\n    \"userId\" TEXT,\n    \"validFrom\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"validUntil\" TIMESTAMP(3),\n    \"maxUsers\" INTEGER DEFAULT 1,\n    \"features\" JSONB,\n    \"activatedAt\" TIMESTAMP(3),\n    \"lastCheckedAt\" TIMESTAMP(3),\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"licenses_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"revenuecat_webhook_events\" (\n    \"id\" TEXT NOT NULL,\n    \"eventType\" TEXT NOT NULL,\n    \"eventTimestamp\" TIMESTAMP(3) NOT NULL,\n    \"appUserId\" TEXT NOT NULL,\n    \"environment\" TEXT NOT NULL,\n    \"productId\" TEXT,\n    \"transactionId\" TEXT,\n    \"originalTransactionId\" TEXT,\n    \"entitlementIds\" TEXT,\n    \"processed\" BOOLEAN NOT NULL DEFAULT false,\n    \"processingError\" TEXT,\n    \"retryCount\" INTEGER NOT NULL DEFAULT 0,\n    \"rawEventData\" JSONB NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"revenuecat_webhook_events_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"programs\" (\n    \"id\" TEXT NOT NULL,\n    \"slug\" TEXT NOT NULL,\n    \"slugEn\" TEXT NOT NULL,\n    \"slugEs\" TEXT NOT NULL,\n    \"slugPt\" TEXT NOT NULL,\n    \"slugRu\" TEXT NOT NULL,\n    \"slugZhCn\" TEXT NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"titleEn\" TEXT NOT NULL,\n    \"titleEs\" TEXT NOT NULL,\n    \"titlePt\" TEXT NOT NULL,\n    \"titleRu\" TEXT NOT NULL,\n    \"titleZhCn\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"descriptionEn\" TEXT NOT NULL,\n    \"descriptionEs\" TEXT NOT NULL,\n    \"descriptionPt\" TEXT NOT NULL,\n    \"descriptionRu\" TEXT NOT NULL,\n    \"descriptionZhCn\" TEXT NOT NULL,\n    \"category\" TEXT NOT NULL,\n    \"image\" TEXT NOT NULL,\n    \"level\" \"ProgramLevel\" NOT NULL,\n    \"type\" \"ExerciseAttributeValueEnum\" NOT NULL,\n    \"durationWeeks\" INTEGER NOT NULL DEFAULT 4,\n    \"sessionsPerWeek\" INTEGER NOT NULL DEFAULT 3,\n    \"sessionDurationMin\" INTEGER NOT NULL DEFAULT 30,\n    \"equipment\" \"ExerciseAttributeValueEnum\"[] DEFAULT ARRAY[]::\"ExerciseAttributeValueEnum\"[],\n    \"isPremium\" BOOLEAN NOT NULL DEFAULT true,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"visibility\" \"ProgramVisibility\" NOT NULL DEFAULT 'DRAFT',\n    \"participantCount\" INTEGER NOT NULL DEFAULT 0,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"programs_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_coaches\" (\n    \"id\" TEXT NOT NULL,\n    \"programId\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"image\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL DEFAULT 0,\n\n    CONSTRAINT \"program_coaches_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_weeks\" (\n    \"id\" TEXT NOT NULL,\n    \"programId\" TEXT NOT NULL,\n    \"weekNumber\" INTEGER NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"titleEn\" TEXT NOT NULL,\n    \"titleEs\" TEXT NOT NULL,\n    \"titlePt\" TEXT NOT NULL,\n    \"titleRu\" TEXT NOT NULL,\n    \"titleZhCn\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"descriptionEn\" TEXT NOT NULL,\n    \"descriptionEs\" TEXT NOT NULL,\n    \"descriptionPt\" TEXT NOT NULL,\n    \"descriptionRu\" TEXT NOT NULL,\n    \"descriptionZhCn\" TEXT NOT NULL,\n\n    CONSTRAINT \"program_weeks_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_sessions\" (\n    \"id\" TEXT NOT NULL,\n    \"weekId\" TEXT NOT NULL,\n    \"sessionNumber\" INTEGER NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"titleEn\" TEXT NOT NULL,\n    \"titleEs\" TEXT NOT NULL,\n    \"titlePt\" TEXT NOT NULL,\n    \"titleRu\" TEXT NOT NULL,\n    \"titleZhCn\" TEXT NOT NULL,\n    \"slug\" TEXT NOT NULL,\n    \"slugEn\" TEXT NOT NULL,\n    \"slugEs\" TEXT NOT NULL,\n    \"slugPt\" TEXT NOT NULL,\n    \"slugRu\" TEXT NOT NULL,\n    \"slugZhCn\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"descriptionEn\" TEXT NOT NULL,\n    \"descriptionEs\" TEXT NOT NULL,\n    \"descriptionPt\" TEXT NOT NULL,\n    \"descriptionRu\" TEXT NOT NULL,\n    \"descriptionZhCn\" TEXT NOT NULL,\n    \"equipment\" \"ExerciseAttributeValueEnum\"[] DEFAULT ARRAY[]::\"ExerciseAttributeValueEnum\"[],\n    \"estimatedMinutes\" INTEGER NOT NULL,\n    \"isPremium\" BOOLEAN NOT NULL DEFAULT true,\n\n    CONSTRAINT \"program_sessions_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_session_exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"sessionId\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL,\n    \"instructions\" TEXT NOT NULL,\n    \"instructionsEn\" TEXT NOT NULL,\n    \"instructionsEs\" TEXT NOT NULL,\n    \"instructionsPt\" TEXT NOT NULL,\n    \"instructionsRu\" TEXT NOT NULL,\n    \"instructionsZhCn\" TEXT NOT NULL,\n\n    CONSTRAINT \"program_session_exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_suggested_sets\" (\n    \"id\" TEXT NOT NULL,\n    \"programSessionExerciseId\" TEXT NOT NULL,\n    \"setIndex\" INTEGER NOT NULL,\n    \"types\" \"WorkoutSetType\"[] DEFAULT ARRAY[]::\"WorkoutSetType\"[],\n    \"valuesInt\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"valuesSec\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"units\" \"WorkoutSetUnit\"[] DEFAULT ARRAY[]::\"WorkoutSetUnit\"[],\n\n    CONSTRAINT \"program_suggested_sets_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"user_program_enrollments\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"programId\" TEXT NOT NULL,\n    \"enrolledAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"currentWeek\" INTEGER NOT NULL DEFAULT 1,\n    \"currentSession\" INTEGER NOT NULL DEFAULT 1,\n    \"completedSessions\" INTEGER NOT NULL DEFAULT 0,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"completedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"user_program_enrollments_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"user_session_progress\" (\n    \"id\" TEXT NOT NULL,\n    \"enrollmentId\" TEXT NOT NULL,\n    \"sessionId\" TEXT NOT NULL,\n    \"startedAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"completedAt\" TIMESTAMP(3),\n    \"workoutSessionId\" TEXT,\n\n    CONSTRAINT \"user_session_progress_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_email_key\" ON \"user\"(\"email\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_favorite_exercises_userId_exerciseId_key\" ON \"user_favorite_exercises\"(\"userId\", \"exerciseId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"session_token_key\" ON \"session\"(\"token\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercises_slug_key\" ON \"exercises\"(\"slug\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercises_slugEn_key\" ON \"exercises\"(\"slugEn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercise_attribute_names_name_key\" ON \"exercise_attribute_names\"(\"name\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercise_attribute_values_attributeNameId_value_key\" ON \"exercise_attribute_values\"(\"attributeNameId\", \"value\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercise_attributes_exerciseId_attributeNameId_attributeVal_key\" ON \"exercise_attributes\"(\"exerciseId\", \"attributeNameId\", \"attributeValueId\");\n\n-- CreateIndex\nCREATE INDEX \"plan_provider_mappings_provider_externalId_idx\" ON \"plan_provider_mappings\"(\"provider\", \"externalId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"plan_provider_mappings_planId_provider_region_key\" ON \"plan_provider_mappings\"(\"planId\", \"provider\", \"region\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"subscriptions_userId_platform_key\" ON \"subscriptions\"(\"userId\", \"platform\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"licenses_key_key\" ON \"licenses\"(\"key\");\n\n-- CreateIndex\nCREATE INDEX \"revenuecat_webhook_events_appUserId_idx\" ON \"revenuecat_webhook_events\"(\"appUserId\");\n\n-- CreateIndex\nCREATE INDEX \"revenuecat_webhook_events_eventType_idx\" ON \"revenuecat_webhook_events\"(\"eventType\");\n\n-- CreateIndex\nCREATE INDEX \"revenuecat_webhook_events_processed_idx\" ON \"revenuecat_webhook_events\"(\"processed\");\n\n-- CreateIndex\nCREATE INDEX \"revenuecat_webhook_events_eventTimestamp_idx\" ON \"revenuecat_webhook_events\"(\"eventTimestamp\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slug_key\" ON \"programs\"(\"slug\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugEn_key\" ON \"programs\"(\"slugEn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugEs_key\" ON \"programs\"(\"slugEs\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugPt_key\" ON \"programs\"(\"slugPt\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugRu_key\" ON \"programs\"(\"slugRu\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugZhCn_key\" ON \"programs\"(\"slugZhCn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_weeks_programId_weekNumber_key\" ON \"program_weeks\"(\"programId\", \"weekNumber\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_sessionNumber_key\" ON \"program_sessions\"(\"weekId\", \"sessionNumber\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slug_key\" ON \"program_sessions\"(\"weekId\", \"slug\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugEn_key\" ON \"program_sessions\"(\"weekId\", \"slugEn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugEs_key\" ON \"program_sessions\"(\"weekId\", \"slugEs\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugPt_key\" ON \"program_sessions\"(\"weekId\", \"slugPt\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugRu_key\" ON \"program_sessions\"(\"weekId\", \"slugRu\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugZhCn_key\" ON \"program_sessions\"(\"weekId\", \"slugZhCn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_session_exercises_sessionId_order_key\" ON \"program_session_exercises\"(\"sessionId\", \"order\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_suggested_sets_programSessionExerciseId_setIndex_key\" ON \"program_suggested_sets\"(\"programSessionExerciseId\", \"setIndex\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_program_enrollments_userId_programId_key\" ON \"user_program_enrollments\"(\"userId\", \"programId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_session_progress_workoutSessionId_key\" ON \"user_session_progress\"(\"workoutSessionId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_session_progress_enrollmentId_sessionId_key\" ON \"user_session_progress\"(\"enrollmentId\", \"sessionId\");\n\n-- AddForeignKey\nALTER TABLE \"user_favorite_exercises\" ADD CONSTRAINT \"user_favorite_exercises_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_favorite_exercises\" ADD CONSTRAINT \"user_favorite_exercises_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"session\" ADD CONSTRAINT \"session_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"account\" ADD CONSTRAINT \"account_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"feedbacks\" ADD CONSTRAINT \"feedbacks_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attribute_values\" ADD CONSTRAINT \"exercise_attribute_values_attributeNameId_fkey\" FOREIGN KEY (\"attributeNameId\") REFERENCES \"exercise_attribute_names\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attributes\" ADD CONSTRAINT \"exercise_attributes_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attributes\" ADD CONSTRAINT \"exercise_attributes_attributeNameId_fkey\" FOREIGN KEY (\"attributeNameId\") REFERENCES \"exercise_attribute_names\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attributes\" ADD CONSTRAINT \"exercise_attributes_attributeValueId_fkey\" FOREIGN KEY (\"attributeValueId\") REFERENCES \"exercise_attribute_values\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_sessions\" ADD CONSTRAINT \"workout_sessions_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_session_exercises\" ADD CONSTRAINT \"workout_session_exercises_workoutSessionId_fkey\" FOREIGN KEY (\"workoutSessionId\") REFERENCES \"workout_sessions\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_session_exercises\" ADD CONSTRAINT \"workout_session_exercises_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_sets\" ADD CONSTRAINT \"workout_sets_workoutSessionExerciseId_fkey\" FOREIGN KEY (\"workoutSessionExerciseId\") REFERENCES \"workout_session_exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"plan_provider_mappings\" ADD CONSTRAINT \"plan_provider_mappings_planId_fkey\" FOREIGN KEY (\"planId\") REFERENCES \"subscription_plans\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"subscriptions\" ADD CONSTRAINT \"subscriptions_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"subscriptions\" ADD CONSTRAINT \"subscriptions_planId_fkey\" FOREIGN KEY (\"planId\") REFERENCES \"subscription_plans\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"licenses\" ADD CONSTRAINT \"licenses_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_coaches\" ADD CONSTRAINT \"program_coaches_programId_fkey\" FOREIGN KEY (\"programId\") REFERENCES \"programs\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_weeks\" ADD CONSTRAINT \"program_weeks_programId_fkey\" FOREIGN KEY (\"programId\") REFERENCES \"programs\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_sessions\" ADD CONSTRAINT \"program_sessions_weekId_fkey\" FOREIGN KEY (\"weekId\") REFERENCES \"program_weeks\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_session_exercises\" ADD CONSTRAINT \"program_session_exercises_sessionId_fkey\" FOREIGN KEY (\"sessionId\") REFERENCES \"program_sessions\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_session_exercises\" ADD CONSTRAINT \"program_session_exercises_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_suggested_sets\" ADD CONSTRAINT \"program_suggested_sets_programSessionExerciseId_fkey\" FOREIGN KEY (\"programSessionExerciseId\") REFERENCES \"program_session_exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_program_enrollments\" ADD CONSTRAINT \"user_program_enrollments_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_program_enrollments\" ADD CONSTRAINT \"user_program_enrollments_programId_fkey\" FOREIGN KEY (\"programId\") REFERENCES \"programs\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_session_progress\" ADD CONSTRAINT \"user_session_progress_enrollmentId_fkey\" FOREIGN KEY (\"enrollmentId\") REFERENCES \"user_program_enrollments\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_session_progress\" ADD CONSTRAINT \"user_session_progress_sessionId_fkey\" FOREIGN KEY (\"sessionId\") REFERENCES \"program_sessions\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_session_progress\" ADD CONSTRAINT \"user_session_progress_workoutSessionId_fkey\" FOREIGN KEY (\"workoutSessionId\") REFERENCES \"workout_sessions\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n"
  },
  {
    "path": "prisma/migrations_backup/20240726_simplify_subscription_model/migration.sql",
    "content": "-- Simplify Subscription model by removing unnecessary RevenueCat fields\nALTER TABLE \"subscriptions\" \nDROP COLUMN IF EXISTS \"revenueCatTransactionId\",\nDROP COLUMN IF EXISTS \"revenueCatProductId\",\nDROP COLUMN IF EXISTS \"revenueCatEntitlement\";"
  },
  {
    "path": "prisma/migrations_backup/20250101000000_baseline/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"UserRole\" AS ENUM ('admin', 'user');\n\n-- CreateEnum\nCREATE TYPE \"ExercisePrivacy\" AS ENUM ('PUBLIC', 'PRIVATE');\n\n-- CreateEnum\nCREATE TYPE \"ExerciseAttributeNameEnum\" AS ENUM ('TYPE', 'PRIMARY_MUSCLE', 'SECONDARY_MUSCLE', 'EQUIPMENT', 'MECHANICS_TYPE');\n\n-- CreateEnum\nCREATE TYPE \"ExerciseAttributeValueEnum\" AS ENUM ('BODYWEIGHT', 'STRENGTH', 'POWERLIFTING', 'CALISTHENIC', 'PLYOMETRICS', 'STRETCHING', 'STRONGMAN', 'CARDIO', 'STABILIZATION', 'POWER', 'RESISTANCE', 'CROSSFIT', 'WEIGHTLIFTING', 'BICEPS', 'SHOULDERS', 'CHEST', 'BACK', 'GLUTES', 'TRICEPS', 'HAMSTRINGS', 'QUADRICEPS', 'FOREARMS', 'CALVES', 'TRAPS', 'ABDOMINALS', 'NECK', 'LATS', 'ADDUCTORS', 'ABDUCTORS', 'OBLIQUES', 'GROIN', 'FULL_BODY', 'ROTATOR_CUFF', 'HIP_FLEXOR', 'ACHILLES_TENDON', 'FINGERS', 'DUMBBELL', 'KETTLEBELLS', 'BARBELL', 'SMITH_MACHINE', 'BODY_ONLY', 'OTHER', 'BANDS', 'EZ_BAR', 'MACHINE', 'DESK', 'PULLUP_BAR', 'NONE', 'CABLE', 'MEDICINE_BALL', 'SWISS_BALL', 'FOAM_ROLL', 'WEIGHT_PLATE', 'TRX', 'BOX', 'ROPES', 'SPIN_BIKE', 'STEP', 'BOSU', 'TYRE', 'SANDBAG', 'POLE', 'BENCH', 'WALL', 'BAR', 'RACK', 'CAR', 'SLED', 'CHAIN', 'SKIERG', 'ROPE', 'NA', 'ISOLATION', 'COMPOUND');\n\n-- CreateEnum\nCREATE TYPE \"WorkoutSetType\" AS ENUM ('TIME', 'WEIGHT', 'REPS', 'BODYWEIGHT', 'NA');\n\n-- CreateEnum\nCREATE TYPE \"WorkoutSetUnit\" AS ENUM ('kg', 'lbs');\n\n-- CreateEnum\nCREATE TYPE \"SubscriptionStatus\" AS ENUM ('ACTIVE', 'TRIAL', 'CANCELLED', 'EXPIRED', 'PAUSED');\n\n-- CreateEnum\nCREATE TYPE \"Platform\" AS ENUM ('WEB', 'IOS', 'ANDROID');\n\n-- CreateEnum\nCREATE TYPE \"PaymentProcessor\" AS ENUM ('STRIPE', 'PAYPAL', 'LEMONSQUEEZY', 'PADDLE', 'APPLE_PAY', 'GOOGLE_PAY', 'REVENUECAT', 'NONE', 'OTHER');\n\n-- CreateEnum\nCREATE TYPE \"ProgramLevel\" AS ENUM ('BEGINNER', 'INTERMEDIATE', 'ADVANCED', 'EXPERT');\n\n-- CreateEnum\nCREATE TYPE \"ProgramVisibility\" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');\n\n-- CreateTable\nCREATE TABLE \"user\" (\n    \"id\" TEXT NOT NULL,\n    \"firstName\" TEXT NOT NULL DEFAULT '',\n    \"lastName\" TEXT NOT NULL DEFAULT '',\n    \"name\" TEXT NOT NULL,\n    \"email\" TEXT NOT NULL,\n    \"emailVerified\" BOOLEAN NOT NULL,\n    \"image\" TEXT,\n    \"locale\" TEXT DEFAULT 'fr',\n    \"createdAt\" TIMESTAMP(3) NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"role\" \"UserRole\" DEFAULT 'user',\n    \"banned\" BOOLEAN DEFAULT false,\n    \"banReason\" TEXT,\n    \"banExpires\" TIMESTAMP(3),\n    \"isPremium\" BOOLEAN DEFAULT false,\n\n    CONSTRAINT \"user_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"user_favorite_exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"user_favorite_exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"session\" (\n    \"id\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"token\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"ipAddress\" TEXT,\n    \"userAgent\" TEXT,\n    \"userId\" TEXT NOT NULL,\n    \"impersonatedBy\" TEXT,\n\n    CONSTRAINT \"session_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"account\" (\n    \"id\" TEXT NOT NULL,\n    \"accountId\" TEXT NOT NULL,\n    \"providerId\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"accessToken\" TEXT,\n    \"refreshToken\" TEXT,\n    \"idToken\" TEXT,\n    \"accessTokenExpiresAt\" TIMESTAMP(3),\n    \"refreshTokenExpiresAt\" TIMESTAMP(3),\n    \"scope\" TEXT,\n    \"password\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"account_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"verification\" (\n    \"id\" TEXT NOT NULL,\n    \"identifier\" TEXT NOT NULL,\n    \"value\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"createdAt\" TIMESTAMP(3),\n    \"updatedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"verification_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"feedbacks\" (\n    \"id\" TEXT NOT NULL,\n    \"review\" INTEGER NOT NULL,\n    \"message\" TEXT NOT NULL,\n    \"email\" TEXT,\n    \"userId\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"feedbacks_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"nameEn\" TEXT,\n    \"description\" TEXT,\n    \"descriptionEn\" TEXT,\n    \"fullVideoUrl\" TEXT,\n    \"fullVideoImageUrl\" TEXT,\n    \"introduction\" TEXT,\n    \"introductionEn\" TEXT,\n    \"slug\" TEXT,\n    \"slugEn\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercise_attribute_names\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" \"ExerciseAttributeNameEnum\" NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"exercise_attribute_names_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercise_attribute_values\" (\n    \"id\" TEXT NOT NULL,\n    \"attributeNameId\" TEXT NOT NULL,\n    \"value\" \"ExerciseAttributeValueEnum\" NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"exercise_attribute_values_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercise_attributes\" (\n    \"id\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"attributeNameId\" TEXT NOT NULL,\n    \"attributeValueId\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"exercise_attributes_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"workout_sessions\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"startedAt\" TIMESTAMP(3) NOT NULL,\n    \"endedAt\" TIMESTAMP(3),\n    \"duration\" INTEGER,\n    \"muscles\" \"ExerciseAttributeValueEnum\"[] DEFAULT ARRAY[]::\"ExerciseAttributeValueEnum\"[],\n    \"rating\" INTEGER,\n    \"ratingComment\" TEXT,\n\n    CONSTRAINT \"workout_sessions_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"workout_session_exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"workoutSessionId\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL,\n\n    CONSTRAINT \"workout_session_exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"workout_sets\" (\n    \"id\" TEXT NOT NULL,\n    \"workoutSessionExerciseId\" TEXT NOT NULL,\n    \"setIndex\" INTEGER NOT NULL,\n    \"type\" \"WorkoutSetType\" NOT NULL,\n    \"types\" \"WorkoutSetType\"[] DEFAULT ARRAY[]::\"WorkoutSetType\"[],\n    \"valuesInt\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"valuesSec\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"units\" \"WorkoutSetUnit\"[] DEFAULT ARRAY[]::\"WorkoutSetUnit\"[],\n    \"completed\" BOOLEAN NOT NULL DEFAULT false,\n\n    CONSTRAINT \"workout_sets_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"subscription_plans\" (\n    \"id\" TEXT NOT NULL,\n    \"priceMonthly\" DECIMAL(10,2),\n    \"priceYearly\" DECIMAL(10,2),\n    \"currency\" TEXT NOT NULL DEFAULT 'EUR',\n    \"interval\" TEXT NOT NULL DEFAULT 'month',\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"availableRegions\" TEXT[] DEFAULT ARRAY[]::TEXT[],\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"subscription_plans_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"plan_provider_mappings\" (\n    \"id\" TEXT NOT NULL,\n    \"planId\" TEXT NOT NULL,\n    \"provider\" \"PaymentProcessor\" NOT NULL,\n    \"externalId\" TEXT NOT NULL,\n    \"region\" TEXT,\n    \"metadata\" JSONB,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"plan_provider_mappings_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"subscriptions\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"planId\" TEXT NOT NULL,\n    \"revenueCatUserId\" TEXT,\n    \"status\" \"SubscriptionStatus\" NOT NULL,\n    \"startedAt\" TIMESTAMP(3) NOT NULL,\n    \"currentPeriodEnd\" TIMESTAMP(3),\n    \"cancelledAt\" TIMESTAMP(3),\n    \"platform\" \"Platform\",\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"subscriptions_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"licenses\" (\n    \"id\" TEXT NOT NULL,\n    \"key\" TEXT NOT NULL,\n    \"userId\" TEXT,\n    \"validFrom\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"validUntil\" TIMESTAMP(3),\n    \"maxUsers\" INTEGER DEFAULT 1,\n    \"features\" JSONB,\n    \"activatedAt\" TIMESTAMP(3),\n    \"lastCheckedAt\" TIMESTAMP(3),\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"licenses_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"revenuecat_webhook_events\" (\n    \"id\" TEXT NOT NULL,\n    \"eventType\" TEXT NOT NULL,\n    \"eventTimestamp\" TIMESTAMP(3) NOT NULL,\n    \"appUserId\" TEXT NOT NULL,\n    \"environment\" TEXT NOT NULL,\n    \"productId\" TEXT,\n    \"transactionId\" TEXT,\n    \"originalTransactionId\" TEXT,\n    \"entitlementIds\" TEXT,\n    \"processed\" BOOLEAN NOT NULL DEFAULT false,\n    \"processingError\" TEXT,\n    \"retryCount\" INTEGER NOT NULL DEFAULT 0,\n    \"rawEventData\" JSONB NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"revenuecat_webhook_events_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"programs\" (\n    \"id\" TEXT NOT NULL,\n    \"slug\" TEXT NOT NULL,\n    \"slugEn\" TEXT NOT NULL,\n    \"slugEs\" TEXT NOT NULL,\n    \"slugPt\" TEXT NOT NULL,\n    \"slugRu\" TEXT NOT NULL,\n    \"slugZhCn\" TEXT NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"titleEn\" TEXT NOT NULL,\n    \"titleEs\" TEXT NOT NULL,\n    \"titlePt\" TEXT NOT NULL,\n    \"titleRu\" TEXT NOT NULL,\n    \"titleZhCn\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"descriptionEn\" TEXT NOT NULL,\n    \"descriptionEs\" TEXT NOT NULL,\n    \"descriptionPt\" TEXT NOT NULL,\n    \"descriptionRu\" TEXT NOT NULL,\n    \"descriptionZhCn\" TEXT NOT NULL,\n    \"category\" TEXT NOT NULL,\n    \"image\" TEXT NOT NULL,\n    \"level\" \"ProgramLevel\" NOT NULL,\n    \"type\" \"ExerciseAttributeValueEnum\" NOT NULL,\n    \"durationWeeks\" INTEGER NOT NULL DEFAULT 4,\n    \"sessionsPerWeek\" INTEGER NOT NULL DEFAULT 3,\n    \"sessionDurationMin\" INTEGER NOT NULL DEFAULT 30,\n    \"equipment\" \"ExerciseAttributeValueEnum\"[] DEFAULT ARRAY[]::\"ExerciseAttributeValueEnum\"[],\n    \"isPremium\" BOOLEAN NOT NULL DEFAULT true,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"visibility\" \"ProgramVisibility\" NOT NULL DEFAULT 'DRAFT',\n    \"participantCount\" INTEGER NOT NULL DEFAULT 0,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"programs_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_coaches\" (\n    \"id\" TEXT NOT NULL,\n    \"programId\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"image\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL DEFAULT 0,\n\n    CONSTRAINT \"program_coaches_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_weeks\" (\n    \"id\" TEXT NOT NULL,\n    \"programId\" TEXT NOT NULL,\n    \"weekNumber\" INTEGER NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"titleEn\" TEXT NOT NULL,\n    \"titleEs\" TEXT NOT NULL,\n    \"titlePt\" TEXT NOT NULL,\n    \"titleRu\" TEXT NOT NULL,\n    \"titleZhCn\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"descriptionEn\" TEXT NOT NULL,\n    \"descriptionEs\" TEXT NOT NULL,\n    \"descriptionPt\" TEXT NOT NULL,\n    \"descriptionRu\" TEXT NOT NULL,\n    \"descriptionZhCn\" TEXT NOT NULL,\n\n    CONSTRAINT \"program_weeks_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_sessions\" (\n    \"id\" TEXT NOT NULL,\n    \"weekId\" TEXT NOT NULL,\n    \"sessionNumber\" INTEGER NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"titleEn\" TEXT NOT NULL,\n    \"titleEs\" TEXT NOT NULL,\n    \"titlePt\" TEXT NOT NULL,\n    \"titleRu\" TEXT NOT NULL,\n    \"titleZhCn\" TEXT NOT NULL,\n    \"slug\" TEXT NOT NULL,\n    \"slugEn\" TEXT NOT NULL,\n    \"slugEs\" TEXT NOT NULL,\n    \"slugPt\" TEXT NOT NULL,\n    \"slugRu\" TEXT NOT NULL,\n    \"slugZhCn\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"descriptionEn\" TEXT NOT NULL,\n    \"descriptionEs\" TEXT NOT NULL,\n    \"descriptionPt\" TEXT NOT NULL,\n    \"descriptionRu\" TEXT NOT NULL,\n    \"descriptionZhCn\" TEXT NOT NULL,\n    \"equipment\" \"ExerciseAttributeValueEnum\"[] DEFAULT ARRAY[]::\"ExerciseAttributeValueEnum\"[],\n    \"estimatedMinutes\" INTEGER NOT NULL,\n    \"isPremium\" BOOLEAN NOT NULL DEFAULT true,\n\n    CONSTRAINT \"program_sessions_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_session_exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"sessionId\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL,\n    \"instructions\" TEXT NOT NULL,\n    \"instructionsEn\" TEXT NOT NULL,\n    \"instructionsEs\" TEXT NOT NULL,\n    \"instructionsPt\" TEXT NOT NULL,\n    \"instructionsRu\" TEXT NOT NULL,\n    \"instructionsZhCn\" TEXT NOT NULL,\n\n    CONSTRAINT \"program_session_exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_suggested_sets\" (\n    \"id\" TEXT NOT NULL,\n    \"programSessionExerciseId\" TEXT NOT NULL,\n    \"setIndex\" INTEGER NOT NULL,\n    \"types\" \"WorkoutSetType\"[] DEFAULT ARRAY[]::\"WorkoutSetType\"[],\n    \"valuesInt\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"valuesSec\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"units\" \"WorkoutSetUnit\"[] DEFAULT ARRAY[]::\"WorkoutSetUnit\"[],\n\n    CONSTRAINT \"program_suggested_sets_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"user_program_enrollments\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"programId\" TEXT NOT NULL,\n    \"enrolledAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"currentWeek\" INTEGER NOT NULL DEFAULT 1,\n    \"currentSession\" INTEGER NOT NULL DEFAULT 1,\n    \"completedSessions\" INTEGER NOT NULL DEFAULT 0,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"completedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"user_program_enrollments_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"user_session_progress\" (\n    \"id\" TEXT NOT NULL,\n    \"enrollmentId\" TEXT NOT NULL,\n    \"sessionId\" TEXT NOT NULL,\n    \"startedAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"completedAt\" TIMESTAMP(3),\n    \"workoutSessionId\" TEXT,\n\n    CONSTRAINT \"user_session_progress_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_email_key\" ON \"user\"(\"email\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_favorite_exercises_userId_exerciseId_key\" ON \"user_favorite_exercises\"(\"userId\", \"exerciseId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"session_token_key\" ON \"session\"(\"token\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercises_slug_key\" ON \"exercises\"(\"slug\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercises_slugEn_key\" ON \"exercises\"(\"slugEn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercise_attribute_names_name_key\" ON \"exercise_attribute_names\"(\"name\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercise_attribute_values_attributeNameId_value_key\" ON \"exercise_attribute_values\"(\"attributeNameId\", \"value\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercise_attributes_exerciseId_attributeNameId_attributeVal_key\" ON \"exercise_attributes\"(\"exerciseId\", \"attributeNameId\", \"attributeValueId\");\n\n-- CreateIndex\nCREATE INDEX \"plan_provider_mappings_provider_externalId_idx\" ON \"plan_provider_mappings\"(\"provider\", \"externalId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"plan_provider_mappings_planId_provider_region_key\" ON \"plan_provider_mappings\"(\"planId\", \"provider\", \"region\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"subscriptions_userId_platform_key\" ON \"subscriptions\"(\"userId\", \"platform\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"licenses_key_key\" ON \"licenses\"(\"key\");\n\n-- CreateIndex\nCREATE INDEX \"revenuecat_webhook_events_appUserId_idx\" ON \"revenuecat_webhook_events\"(\"appUserId\");\n\n-- CreateIndex\nCREATE INDEX \"revenuecat_webhook_events_eventType_idx\" ON \"revenuecat_webhook_events\"(\"eventType\");\n\n-- CreateIndex\nCREATE INDEX \"revenuecat_webhook_events_processed_idx\" ON \"revenuecat_webhook_events\"(\"processed\");\n\n-- CreateIndex\nCREATE INDEX \"revenuecat_webhook_events_eventTimestamp_idx\" ON \"revenuecat_webhook_events\"(\"eventTimestamp\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slug_key\" ON \"programs\"(\"slug\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugEn_key\" ON \"programs\"(\"slugEn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugEs_key\" ON \"programs\"(\"slugEs\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugPt_key\" ON \"programs\"(\"slugPt\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugRu_key\" ON \"programs\"(\"slugRu\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugZhCn_key\" ON \"programs\"(\"slugZhCn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_weeks_programId_weekNumber_key\" ON \"program_weeks\"(\"programId\", \"weekNumber\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_sessionNumber_key\" ON \"program_sessions\"(\"weekId\", \"sessionNumber\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slug_key\" ON \"program_sessions\"(\"weekId\", \"slug\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugEn_key\" ON \"program_sessions\"(\"weekId\", \"slugEn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugEs_key\" ON \"program_sessions\"(\"weekId\", \"slugEs\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugPt_key\" ON \"program_sessions\"(\"weekId\", \"slugPt\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugRu_key\" ON \"program_sessions\"(\"weekId\", \"slugRu\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugZhCn_key\" ON \"program_sessions\"(\"weekId\", \"slugZhCn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_session_exercises_sessionId_order_key\" ON \"program_session_exercises\"(\"sessionId\", \"order\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_suggested_sets_programSessionExerciseId_setIndex_key\" ON \"program_suggested_sets\"(\"programSessionExerciseId\", \"setIndex\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_program_enrollments_userId_programId_key\" ON \"user_program_enrollments\"(\"userId\", \"programId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_session_progress_workoutSessionId_key\" ON \"user_session_progress\"(\"workoutSessionId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_session_progress_enrollmentId_sessionId_key\" ON \"user_session_progress\"(\"enrollmentId\", \"sessionId\");\n\n-- AddForeignKey\nALTER TABLE \"user_favorite_exercises\" ADD CONSTRAINT \"user_favorite_exercises_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_favorite_exercises\" ADD CONSTRAINT \"user_favorite_exercises_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"session\" ADD CONSTRAINT \"session_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"account\" ADD CONSTRAINT \"account_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"feedbacks\" ADD CONSTRAINT \"feedbacks_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attribute_values\" ADD CONSTRAINT \"exercise_attribute_values_attributeNameId_fkey\" FOREIGN KEY (\"attributeNameId\") REFERENCES \"exercise_attribute_names\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attributes\" ADD CONSTRAINT \"exercise_attributes_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attributes\" ADD CONSTRAINT \"exercise_attributes_attributeNameId_fkey\" FOREIGN KEY (\"attributeNameId\") REFERENCES \"exercise_attribute_names\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attributes\" ADD CONSTRAINT \"exercise_attributes_attributeValueId_fkey\" FOREIGN KEY (\"attributeValueId\") REFERENCES \"exercise_attribute_values\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_sessions\" ADD CONSTRAINT \"workout_sessions_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_session_exercises\" ADD CONSTRAINT \"workout_session_exercises_workoutSessionId_fkey\" FOREIGN KEY (\"workoutSessionId\") REFERENCES \"workout_sessions\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_session_exercises\" ADD CONSTRAINT \"workout_session_exercises_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_sets\" ADD CONSTRAINT \"workout_sets_workoutSessionExerciseId_fkey\" FOREIGN KEY (\"workoutSessionExerciseId\") REFERENCES \"workout_session_exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"plan_provider_mappings\" ADD CONSTRAINT \"plan_provider_mappings_planId_fkey\" FOREIGN KEY (\"planId\") REFERENCES \"subscription_plans\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"subscriptions\" ADD CONSTRAINT \"subscriptions_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"subscriptions\" ADD CONSTRAINT \"subscriptions_planId_fkey\" FOREIGN KEY (\"planId\") REFERENCES \"subscription_plans\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"licenses\" ADD CONSTRAINT \"licenses_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_coaches\" ADD CONSTRAINT \"program_coaches_programId_fkey\" FOREIGN KEY (\"programId\") REFERENCES \"programs\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_weeks\" ADD CONSTRAINT \"program_weeks_programId_fkey\" FOREIGN KEY (\"programId\") REFERENCES \"programs\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_sessions\" ADD CONSTRAINT \"program_sessions_weekId_fkey\" FOREIGN KEY (\"weekId\") REFERENCES \"program_weeks\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_session_exercises\" ADD CONSTRAINT \"program_session_exercises_sessionId_fkey\" FOREIGN KEY (\"sessionId\") REFERENCES \"program_sessions\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_session_exercises\" ADD CONSTRAINT \"program_session_exercises_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_suggested_sets\" ADD CONSTRAINT \"program_suggested_sets_programSessionExerciseId_fkey\" FOREIGN KEY (\"programSessionExerciseId\") REFERENCES \"program_session_exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_program_enrollments\" ADD CONSTRAINT \"user_program_enrollments_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_program_enrollments\" ADD CONSTRAINT \"user_program_enrollments_programId_fkey\" FOREIGN KEY (\"programId\") REFERENCES \"programs\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_session_progress\" ADD CONSTRAINT \"user_session_progress_enrollmentId_fkey\" FOREIGN KEY (\"enrollmentId\") REFERENCES \"user_program_enrollments\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_session_progress\" ADD CONSTRAINT \"user_session_progress_sessionId_fkey\" FOREIGN KEY (\"sessionId\") REFERENCES \"program_sessions\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_session_progress\" ADD CONSTRAINT \"user_session_progress_workoutSessionId_fkey\" FOREIGN KEY (\"workoutSessionId\") REFERENCES \"workout_sessions\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n"
  },
  {
    "path": "prisma/migrations_backup/20250117000000_add_statistics_indexes/migration.sql",
    "content": "-- CreateIndex for exercise statistics queries\n-- Index for workout_session_exercises by exerciseId and session date\nCREATE INDEX \"workout_session_exercises_exerciseId_idx\" ON \"workout_session_exercises\"(\"exerciseId\");\n\n-- Index for workout_sessions by userId and createdAt\nCREATE INDEX \"workout_sessions_userId_createdAt_idx\" ON \"workout_sessions\"(\"userId\", \"createdAt\");\n\n-- Index for workout_sets by completed status and types array\nCREATE INDEX \"workout_sets_completed_idx\" ON \"workout_sets\"(\"completed\");\n\n-- Composite index for efficient statistics queries\nCREATE INDEX \"workout_sessions_userId_createdAt_desc_idx\" ON \"workout_sessions\"(\"userId\", \"createdAt\" DESC);\n\n-- Index for workoutSessionExerciseId lookups\nCREATE INDEX \"workout_sets_workoutSessionExerciseId_idx\" ON \"workout_sets\"(\"workoutSessionExerciseId\");"
  },
  {
    "path": "prisma/migrations_backup/20250414120436_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"user\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"email\" TEXT NOT NULL,\n    \"emailVerified\" BOOLEAN NOT NULL,\n    \"image\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"user_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"session\" (\n    \"id\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"token\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"ipAddress\" TEXT,\n    \"userAgent\" TEXT,\n    \"userId\" TEXT NOT NULL,\n\n    CONSTRAINT \"session_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"account\" (\n    \"id\" TEXT NOT NULL,\n    \"accountId\" TEXT NOT NULL,\n    \"providerId\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"accessToken\" TEXT,\n    \"refreshToken\" TEXT,\n    \"idToken\" TEXT,\n    \"accessTokenExpiresAt\" TIMESTAMP(3),\n    \"refreshTokenExpiresAt\" TIMESTAMP(3),\n    \"scope\" TEXT,\n    \"password\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"account_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"verification\" (\n    \"id\" TEXT NOT NULL,\n    \"identifier\" TEXT NOT NULL,\n    \"value\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"createdAt\" TIMESTAMP(3),\n    \"updatedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"verification_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_email_key\" ON \"user\"(\"email\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"session_token_key\" ON \"session\"(\"token\");\n\n-- AddForeignKey\nALTER TABLE \"session\" ADD CONSTRAINT \"session_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"account\" ADD CONSTRAINT \"account_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250414170807_add_feedbacks/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"Feedback\" (\n    \"id\" TEXT NOT NULL,\n    \"review\" INTEGER NOT NULL,\n    \"message\" TEXT NOT NULL,\n    \"email\" TEXT,\n    \"userId\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Feedback_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"Feedback\" ADD CONSTRAINT \"Feedback_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250414174246_rename_feedbacks/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the `Feedback` table. If the table is not empty, all the data it contains will be lost.\n\n*/\n-- DropForeignKey\nALTER TABLE \"Feedback\" DROP CONSTRAINT \"Feedback_userId_fkey\";\n\n-- DropTable\nDROP TABLE \"Feedback\";\n\n-- CreateTable\nCREATE TABLE \"feedbacks\" (\n    \"id\" TEXT NOT NULL,\n    \"review\" INTEGER NOT NULL,\n    \"message\" TEXT NOT NULL,\n    \"email\" TEXT,\n    \"userId\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"feedbacks_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"feedbacks\" ADD CONSTRAINT \"feedbacks_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250414232816_add_first_name_and_last_name/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"user\" ADD COLUMN     \"firstName\" TEXT NOT NULL DEFAULT '',\nADD COLUMN     \"lastName\" TEXT NOT NULL DEFAULT '';\n"
  },
  {
    "path": "prisma/migrations_backup/20250416160303_add_plans/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"Plan\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"description\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"Plan_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"PlanVariant\" (\n    \"id\" TEXT NOT NULL,\n    \"label\" TEXT NOT NULL,\n    \"planId\" TEXT NOT NULL,\n    \"stripePriceId\" TEXT NOT NULL,\n    \"description\" TEXT,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"PlanVariant_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Subscription\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"planVariantId\" TEXT NOT NULL,\n    \"stripeCustomerId\" TEXT NOT NULL,\n    \"stripeSubId\" TEXT NOT NULL,\n    \"status\" TEXT NOT NULL,\n    \"currentPeriodEnd\" TIMESTAMP(3) NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Subscription_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Subscription_userId_key\" ON \"Subscription\"(\"userId\");\n\n-- AddForeignKey\nALTER TABLE \"PlanVariant\" ADD CONSTRAINT \"PlanVariant_planId_fkey\" FOREIGN KEY (\"planId\") REFERENCES \"Plan\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Subscription\" ADD CONSTRAINT \"Subscription_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Subscription\" ADD CONSTRAINT \"Subscription_planVariantId_fkey\" FOREIGN KEY (\"planVariantId\") REFERENCES \"PlanVariant\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250416160502_map/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the `Plan` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `PlanVariant` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `Subscription` table. If the table is not empty, all the data it contains will be lost.\n\n*/\n-- DropForeignKey\nALTER TABLE \"PlanVariant\" DROP CONSTRAINT \"PlanVariant_planId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"Subscription\" DROP CONSTRAINT \"Subscription_planVariantId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"Subscription\" DROP CONSTRAINT \"Subscription_userId_fkey\";\n\n-- DropTable\nDROP TABLE \"Plan\";\n\n-- DropTable\nDROP TABLE \"PlanVariant\";\n\n-- DropTable\nDROP TABLE \"Subscription\";\n\n-- CreateTable\nCREATE TABLE \"plan\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"description\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"plan_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"plan_variant\" (\n    \"id\" TEXT NOT NULL,\n    \"label\" TEXT NOT NULL,\n    \"planId\" TEXT NOT NULL,\n    \"stripePriceId\" TEXT NOT NULL,\n    \"description\" TEXT,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"plan_variant_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"subscription\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"planVariantId\" TEXT NOT NULL,\n    \"stripeCustomerId\" TEXT NOT NULL,\n    \"stripeSubId\" TEXT NOT NULL,\n    \"status\" TEXT NOT NULL,\n    \"currentPeriodEnd\" TIMESTAMP(3) NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"subscription_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"subscription_userId_key\" ON \"subscription\"(\"userId\");\n\n-- AddForeignKey\nALTER TABLE \"plan_variant\" ADD CONSTRAINT \"plan_variant_planId_fkey\" FOREIGN KEY (\"planId\") REFERENCES \"plan\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"subscription\" ADD CONSTRAINT \"subscription_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"subscription\" ADD CONSTRAINT \"subscription_planVariantId_fkey\" FOREIGN KEY (\"planVariantId\") REFERENCES \"plan_variant\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250505114841_add_user_role/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"UserRole\" AS ENUM ('ADMIN', 'USER');\n\n-- AlterTable\nALTER TABLE \"session\" ADD COLUMN     \"impersonatedBy\" TEXT;\n\n-- AlterTable\nALTER TABLE \"user\" ADD COLUMN     \"banExpires\" TIMESTAMP(3),\nADD COLUMN     \"banReason\" TEXT,\nADD COLUMN     \"banned\" BOOLEAN DEFAULT false,\nADD COLUMN     \"role\" \"UserRole\" DEFAULT 'USER';\n"
  },
  {
    "path": "prisma/migrations_backup/20250505191954_admin_and_user_lowercase/migration.sql",
    "content": "/*\n  Warnings:\n\n  - The values [ADMIN,USER] on the enum `UserRole` will be removed. If these variants are still used in the database, this will fail.\n\n*/\n-- AlterEnum\nBEGIN;\nCREATE TYPE \"UserRole_new\" AS ENUM ('admin', 'user');\nALTER TABLE \"user\" ALTER COLUMN \"role\" DROP DEFAULT;\nALTER TABLE \"user\" ALTER COLUMN \"role\" TYPE \"UserRole_new\" USING (\"role\"::text::\"UserRole_new\");\nALTER TYPE \"UserRole\" RENAME TO \"UserRole_old\";\nALTER TYPE \"UserRole_new\" RENAME TO \"UserRole\";\nDROP TYPE \"UserRole_old\";\nALTER TABLE \"user\" ALTER COLUMN \"role\" SET DEFAULT 'user';\nCOMMIT;\n\n-- AlterTable\nALTER TABLE \"user\" ALTER COLUMN \"role\" SET DEFAULT 'user';\n"
  },
  {
    "path": "prisma/migrations_backup/20250610182024_add_exercises_and_attributes/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the `plan` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `plan_variant` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `subscription` table. If the table is not empty, all the data it contains will be lost.\n\n*/\n-- CreateEnum\nCREATE TYPE \"ExercisePrivacy\" AS ENUM ('PUBLIC', 'PRIVATE');\n\n-- CreateEnum\nCREATE TYPE \"ExerciseAttributeNameEnum\" AS ENUM ('MUSCLE_GROUP', 'EQUIPMENT', 'DIFFICULTY', 'MOVEMENT_TYPE');\n\n-- CreateEnum\nCREATE TYPE \"ExerciseAttributeValueEnum\" AS ENUM ('CHEST', 'BACK', 'SHOULDERS', 'ARMS', 'LEGS', 'CORE', 'BARBELL', 'DUMBBELL', 'BODYWEIGHT', 'MACHINE', 'BEGINNER', 'INTERMEDIATE', 'ADVANCED', 'PUSH', 'PULL', 'SQUAT', 'HINGE');\n\n-- DropForeignKey\nALTER TABLE \"plan_variant\" DROP CONSTRAINT \"plan_variant_planId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"subscription\" DROP CONSTRAINT \"subscription_planVariantId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"subscription\" DROP CONSTRAINT \"subscription_userId_fkey\";\n\n-- AlterTable\nALTER TABLE \"user\" ADD COLUMN     \"locale\" TEXT DEFAULT 'fr';\n\n-- DropTable\nDROP TABLE \"plan\";\n\n-- DropTable\nDROP TABLE \"plan_variant\";\n\n-- DropTable\nDROP TABLE \"subscription\";\n\n-- CreateTable\nCREATE TABLE \"exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"coachId\" TEXT,\n    \"privacy\" \"ExercisePrivacy\" NOT NULL DEFAULT 'PUBLIC',\n    \"name\" TEXT NOT NULL,\n    \"nameEn\" TEXT,\n    \"introduction\" TEXT,\n    \"introductionEn\" TEXT,\n    \"description\" TEXT,\n    \"descriptionEn\" TEXT,\n    \"fullVideoUrl\" TEXT,\n    \"fullVideoImageUrl\" TEXT,\n    \"isArchived\" BOOLEAN NOT NULL DEFAULT false,\n    \"slug\" TEXT,\n    \"slugEn\" TEXT,\n    \"metaTitle\" TEXT,\n    \"metaTitleEn\" TEXT,\n    \"metaDescription\" TEXT,\n    \"metaDescriptionEn\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"deletedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercise_attribute_names\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" \"ExerciseAttributeNameEnum\" NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"deletedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"exercise_attribute_names_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercise_attribute_values\" (\n    \"id\" TEXT NOT NULL,\n    \"attributeNameId\" TEXT NOT NULL,\n    \"value\" \"ExerciseAttributeValueEnum\" NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"deletedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"exercise_attribute_values_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"exercise_attributes\" (\n    \"id\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"attributeNameId\" TEXT NOT NULL,\n    \"attributeValueId\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"deletedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"exercise_attributes_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercises_slug_key\" ON \"exercises\"(\"slug\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercises_slugEn_key\" ON \"exercises\"(\"slugEn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercise_attributes_exerciseId_attributeNameId_attributeVal_key\" ON \"exercise_attributes\"(\"exerciseId\", \"attributeNameId\", \"attributeValueId\");\n\n-- AddForeignKey\nALTER TABLE \"exercise_attribute_values\" ADD CONSTRAINT \"exercise_attribute_values_attributeNameId_fkey\" FOREIGN KEY (\"attributeNameId\") REFERENCES \"exercise_attribute_names\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attributes\" ADD CONSTRAINT \"exercise_attributes_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attributes\" ADD CONSTRAINT \"exercise_attributes_attributeNameId_fkey\" FOREIGN KEY (\"attributeNameId\") REFERENCES \"exercise_attribute_names\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"exercise_attributes\" ADD CONSTRAINT \"exercise_attributes_attributeValueId_fkey\" FOREIGN KEY (\"attributeValueId\") REFERENCES \"exercise_attribute_values\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250610182815_add_exercise_enums/migration.sql",
    "content": "/*\n  Warnings:\n\n  - The values [MUSCLE_GROUP,DIFFICULTY,MOVEMENT_TYPE] on the enum `ExerciseAttributeNameEnum` will be removed. If these variants are still used in the database, this will fail.\n  - The values [CHEST,BACK,ARMS,LEGS,CORE,DUMBBELL,BODYWEIGHT,MACHINE,BEGINNER,INTERMEDIATE,ADVANCED,PUSH,PULL,SQUAT,HINGE] on the enum `ExerciseAttributeValueEnum` will be removed. If these variants are still used in the database, this will fail.\n  - A unique constraint covering the columns `[attributeNameId,value]` on the table `exercise_attribute_values` will be added. If there are existing duplicate values, this will fail.\n\n*/\n-- AlterEnum\nBEGIN;\nCREATE TYPE \"ExerciseAttributeNameEnum_new\" AS ENUM ('TYPE', 'PRIMARY_MUSCLE', 'SECONDARY_MUSCLE', 'EQUIPMENT', 'MECHANICS_TYPE');\nALTER TABLE \"exercise_attribute_names\" ALTER COLUMN \"name\" TYPE \"ExerciseAttributeNameEnum_new\" USING (\"name\"::text::\"ExerciseAttributeNameEnum_new\");\nALTER TYPE \"ExerciseAttributeNameEnum\" RENAME TO \"ExerciseAttributeNameEnum_old\";\nALTER TYPE \"ExerciseAttributeNameEnum_new\" RENAME TO \"ExerciseAttributeNameEnum\";\nDROP TYPE \"ExerciseAttributeNameEnum_old\";\nCOMMIT;\n\n-- AlterEnum\nBEGIN;\nCREATE TYPE \"ExerciseAttributeValueEnum_new\" AS ENUM ('STRENGTH', 'PLYOMETRICS', 'CROSSFIT', 'CARDIO', 'QUADRICEPS', 'SHOULDERS', 'FULL_BODY', 'GLUTES', 'HAMSTRINGS', 'FOREARMS', 'BARBELL', 'BAR', 'CABLE', 'ROPE', 'BENCH', 'COMPOUND', 'ISOLATION');\nALTER TABLE \"exercise_attribute_values\" ALTER COLUMN \"value\" TYPE \"ExerciseAttributeValueEnum_new\" USING (\"value\"::text::\"ExerciseAttributeValueEnum_new\");\nALTER TYPE \"ExerciseAttributeValueEnum\" RENAME TO \"ExerciseAttributeValueEnum_old\";\nALTER TYPE \"ExerciseAttributeValueEnum_new\" RENAME TO \"ExerciseAttributeValueEnum\";\nDROP TYPE \"ExerciseAttributeValueEnum_old\";\nCOMMIT;\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercise_attribute_values_attributeNameId_value_key\" ON \"exercise_attribute_values\"(\"attributeNameId\", \"value\");\n"
  },
  {
    "path": "prisma/migrations_backup/20250610184725_simplified_exercises/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `deletedAt` on the `exercise_attribute_names` table. All the data in the column will be lost.\n  - You are about to drop the column `deletedAt` on the `exercise_attribute_values` table. All the data in the column will be lost.\n  - You are about to drop the column `deletedAt` on the `exercise_attributes` table. All the data in the column will be lost.\n  - You are about to drop the column `coachId` on the `exercises` table. All the data in the column will be lost.\n  - You are about to drop the column `deletedAt` on the `exercises` table. All the data in the column will be lost.\n  - You are about to drop the column `isArchived` on the `exercises` table. All the data in the column will be lost.\n  - You are about to drop the column `metaDescription` on the `exercises` table. All the data in the column will be lost.\n  - You are about to drop the column `metaDescriptionEn` on the `exercises` table. All the data in the column will be lost.\n  - You are about to drop the column `metaTitle` on the `exercises` table. All the data in the column will be lost.\n  - You are about to drop the column `metaTitleEn` on the `exercises` table. All the data in the column will be lost.\n  - You are about to drop the column `privacy` on the `exercises` table. All the data in the column will be lost.\n  - A unique constraint covering the columns `[name]` on the table `exercise_attribute_names` will be added. If there are existing duplicate values, this will fail.\n  - Changed the type of `name` on the `exercise_attribute_names` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.\n  - Changed the type of `value` on the `exercise_attribute_values` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.\n\n*/\n-- AlterTable\nALTER TABLE \"exercise_attribute_names\" DROP COLUMN \"deletedAt\",\nDROP COLUMN \"name\",\nADD COLUMN     \"name\" TEXT NOT NULL;\n\n-- AlterTable\nALTER TABLE \"exercise_attribute_values\" DROP COLUMN \"deletedAt\",\nDROP COLUMN \"value\",\nADD COLUMN     \"value\" TEXT NOT NULL;\n\n-- AlterTable\nALTER TABLE \"exercise_attributes\" DROP COLUMN \"deletedAt\";\n\n-- AlterTable\nALTER TABLE \"exercises\" DROP COLUMN \"coachId\",\nDROP COLUMN \"deletedAt\",\nDROP COLUMN \"isArchived\",\nDROP COLUMN \"metaDescription\",\nDROP COLUMN \"metaDescriptionEn\",\nDROP COLUMN \"metaTitle\",\nDROP COLUMN \"metaTitleEn\",\nDROP COLUMN \"privacy\";\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercise_attribute_names_name_key\" ON \"exercise_attribute_names\"(\"name\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"exercise_attribute_values_attributeNameId_value_key\" ON \"exercise_attribute_values\"(\"attributeNameId\", \"value\");\n"
  },
  {
    "path": "prisma/migrations_backup/20250611190228_convert_text_to_enums/migration.sql",
    "content": "/*\n  Warnings:\n\n  - Changed the type of `name` on the `exercise_attribute_names` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.\n  - Changed the type of `value` on the `exercise_attribute_values` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.\n\n*/\n\n-- Safe migration for exercise_attribute_names\n-- 1. Add temporary column with enum type\nALTER TABLE \"exercise_attribute_names\" ADD COLUMN \"name_temp\" \"ExerciseAttributeNameEnum\";\n\n-- 2. Migrate data from text to enum (cast text to enum)\nUPDATE \"exercise_attribute_names\" SET \"name_temp\" = \"name\"::\"ExerciseAttributeNameEnum\";\n\n-- 3. Drop old column and rename temp column\nALTER TABLE \"exercise_attribute_names\" DROP COLUMN \"name\";\nALTER TABLE \"exercise_attribute_names\" RENAME COLUMN \"name_temp\" TO \"name\";\n\n-- 4. Set NOT NULL constraint\nALTER TABLE \"exercise_attribute_names\" ALTER COLUMN \"name\" SET NOT NULL;\n\n-- Safe migration for exercise_attribute_values  \n-- 1. Add temporary column with enum type\nALTER TABLE \"exercise_attribute_values\" ADD COLUMN \"value_temp\" \"ExerciseAttributeValueEnum\";\n\n-- 2. Migrate data from text to enum (cast text to enum)\nUPDATE \"exercise_attribute_values\" SET \"value_temp\" = \"value\"::\"ExerciseAttributeValueEnum\";\n\n-- 3. Drop old column and rename temp column\nALTER TABLE \"exercise_attribute_values\" DROP COLUMN \"value\";\nALTER TABLE \"exercise_attribute_values\" RENAME COLUMN \"value_temp\" TO \"value\";\n\n-- 4. Set NOT NULL constraint\nALTER TABLE \"exercise_attribute_values\" ALTER COLUMN \"value\" SET NOT NULL;\n\n-- Recreate indexes\nCREATE UNIQUE INDEX \"exercise_attribute_names_name_key\" ON \"exercise_attribute_names\"(\"name\");\nCREATE UNIQUE INDEX \"exercise_attribute_values_attributeNameId_value_key\" ON \"exercise_attribute_values\"(\"attributeNameId\", \"value\");\n"
  },
  {
    "path": "prisma/migrations_backup/20250611210106_add_enum_values/migration.sql",
    "content": "/*\n  Add missing values to enums to prepare for schema alignment\n*/\n\n-- Add missing values to ExerciseAttributeValueEnum\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'BODYWEIGHT';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'POWERLIFTING';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'CALISTHENIC';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'STRETCHING';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'STRONGMAN';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'STABILIZATION';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'POWER';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'RESISTANCE';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'WEIGHTLIFTING';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'BICEPS';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'CHEST';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'BACK';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'TRICEPS';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'CALVES';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'TRAPS';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'ABDOMINALS';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'NECK';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'LATS';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'ADDUCTORS';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'ABDUCTORS';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'OBLIQUES';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'GROIN';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'ROTATOR_CUFF';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'HIP_FLEXOR';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'ACHILLES_TENDON';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'FINGERS';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'DUMBBELL';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'KETTLEBELLS';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'SMITH_MACHINE';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'BODY_ONLY';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'OTHER';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'BANDS';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'EZ_BAR';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'MACHINE';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'DESK';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'PULLUP_BAR';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'NONE';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'MEDICINE_BALL';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'SWISS_BALL';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'FOAM_ROLL';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'WEIGHT_PLATE';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'TRX';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'BOX';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'ROPES';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'SPIN_BIKE';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'STEP';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'BOSU';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'TYRE';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'SANDBAG';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'POLE';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'WALL';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'RACK';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'CAR';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'SLED';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'CHAIN';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'SKIERG';\nALTER TYPE \"ExerciseAttributeValueEnum\" ADD VALUE IF NOT EXISTS 'NA';\n\n-- Add missing values to ExerciseAttributeNameEnum (if needed)\nALTER TYPE \"ExerciseAttributeNameEnum\" ADD VALUE IF NOT EXISTS 'TYPE';\nALTER TYPE \"ExerciseAttributeNameEnum\" ADD VALUE IF NOT EXISTS 'PRIMARY_MUSCLE';\nALTER TYPE \"ExerciseAttributeNameEnum\" ADD VALUE IF NOT EXISTS 'SECONDARY_MUSCLE';\nALTER TYPE \"ExerciseAttributeNameEnum\" ADD VALUE IF NOT EXISTS 'EQUIPMENT';\nALTER TYPE \"ExerciseAttributeNameEnum\" ADD VALUE IF NOT EXISTS 'MECHANICS_TYPE'; "
  },
  {
    "path": "prisma/migrations_backup/20250612213546_workout_session_sets/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"WorkoutSetType\" AS ENUM ('TIME', 'WEIGHT', 'REPS', 'BODYWEIGHT', 'NA');\n\n-- CreateEnum\nCREATE TYPE \"WorkoutSetUnit\" AS ENUM ('kg', 'lbs');\n\n-- CreateTable\nCREATE TABLE \"WorkoutSession\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"startedAt\" TIMESTAMP(3) NOT NULL,\n    \"endedAt\" TIMESTAMP(3),\n    \"duration\" INTEGER,\n\n    CONSTRAINT \"WorkoutSession_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"WorkoutSessionExercise\" (\n    \"id\" TEXT NOT NULL,\n    \"workoutSessionId\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL,\n\n    CONSTRAINT \"WorkoutSessionExercise_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"WorkoutSet\" (\n    \"id\" TEXT NOT NULL,\n    \"workoutSessionExerciseId\" TEXT NOT NULL,\n    \"setIndex\" INTEGER NOT NULL,\n    \"type\" \"WorkoutSetType\" NOT NULL,\n    \"valueInt\" INTEGER,\n    \"valueSec\" INTEGER,\n    \"unit\" \"WorkoutSetUnit\",\n    \"completed\" BOOLEAN NOT NULL DEFAULT false,\n\n    CONSTRAINT \"WorkoutSet_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"WorkoutSession\" ADD CONSTRAINT \"WorkoutSession_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"WorkoutSessionExercise\" ADD CONSTRAINT \"WorkoutSessionExercise_workoutSessionId_fkey\" FOREIGN KEY (\"workoutSessionId\") REFERENCES \"WorkoutSession\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"WorkoutSessionExercise\" ADD CONSTRAINT \"WorkoutSessionExercise_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"WorkoutSet\" ADD CONSTRAINT \"WorkoutSet_workoutSessionExerciseId_fkey\" FOREIGN KEY (\"workoutSessionExerciseId\") REFERENCES \"WorkoutSessionExercise\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250613095031_add_multi_column_support/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"WorkoutSet\" ADD COLUMN     \"types\" \"WorkoutSetType\"[] DEFAULT ARRAY[]::\"WorkoutSetType\"[],\nADD COLUMN     \"units\" \"WorkoutSetUnit\"[] DEFAULT ARRAY[]::\"WorkoutSetUnit\"[],\nADD COLUMN     \"valuesInt\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\nADD COLUMN     \"valuesSec\" INTEGER[] DEFAULT ARRAY[]::INTEGER[];\n"
  },
  {
    "path": "prisma/migrations_backup/20250614125347_add_table_maps/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the `WorkoutSession` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `WorkoutSessionExercise` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `WorkoutSet` table. If the table is not empty, all the data it contains will be lost.\n\n*/\n-- DropForeignKey\nALTER TABLE \"WorkoutSession\" DROP CONSTRAINT \"WorkoutSession_userId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"WorkoutSessionExercise\" DROP CONSTRAINT \"WorkoutSessionExercise_exerciseId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"WorkoutSessionExercise\" DROP CONSTRAINT \"WorkoutSessionExercise_workoutSessionId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"WorkoutSet\" DROP CONSTRAINT \"WorkoutSet_workoutSessionExerciseId_fkey\";\n\n-- DropTable\nDROP TABLE \"WorkoutSession\";\n\n-- DropTable\nDROP TABLE \"WorkoutSessionExercise\";\n\n-- DropTable\nDROP TABLE \"WorkoutSet\";\n\n-- CreateTable\nCREATE TABLE \"workout_sessions\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"startedAt\" TIMESTAMP(3) NOT NULL,\n    \"endedAt\" TIMESTAMP(3),\n    \"duration\" INTEGER,\n\n    CONSTRAINT \"workout_sessions_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"workout_session_exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"workoutSessionId\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL,\n\n    CONSTRAINT \"workout_session_exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"workout_sets\" (\n    \"id\" TEXT NOT NULL,\n    \"workoutSessionExerciseId\" TEXT NOT NULL,\n    \"setIndex\" INTEGER NOT NULL,\n    \"type\" \"WorkoutSetType\" NOT NULL,\n    \"types\" \"WorkoutSetType\"[] DEFAULT ARRAY[]::\"WorkoutSetType\"[],\n    \"valueInt\" INTEGER,\n    \"valuesInt\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"valueSec\" INTEGER,\n    \"valuesSec\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"unit\" \"WorkoutSetUnit\",\n    \"units\" \"WorkoutSetUnit\"[] DEFAULT ARRAY[]::\"WorkoutSetUnit\"[],\n    \"completed\" BOOLEAN NOT NULL DEFAULT false,\n\n    CONSTRAINT \"workout_sets_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"workout_sessions\" ADD CONSTRAINT \"workout_sessions_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_session_exercises\" ADD CONSTRAINT \"workout_session_exercises_workoutSessionId_fkey\" FOREIGN KEY (\"workoutSessionId\") REFERENCES \"workout_sessions\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_session_exercises\" ADD CONSTRAINT \"workout_session_exercises_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_sets\" ADD CONSTRAINT \"workout_sets_workoutSessionExerciseId_fkey\" FOREIGN KEY (\"workoutSessionExerciseId\") REFERENCES \"workout_session_exercises\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250614153656_remove_value_int_value_sec_unit_from_workoutset/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `unit` on the `workout_sets` table. All the data in the column will be lost.\n  - You are about to drop the column `valueInt` on the `workout_sets` table. All the data in the column will be lost.\n  - You are about to drop the column `valueSec` on the `workout_sets` table. All the data in the column will be lost.\n\n*/\n-- AlterTable\nALTER TABLE \"workout_sets\" DROP COLUMN \"unit\",\nDROP COLUMN \"valueInt\",\nDROP COLUMN \"valueSec\";\n"
  },
  {
    "path": "prisma/migrations_backup/20250615160343_add_muscle_to_a_workout_session/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workout_sessions\" ADD COLUMN     \"muscles\" \"ExerciseAttributeValueEnum\"[] DEFAULT ARRAY[]::\"ExerciseAttributeValueEnum\"[];\n"
  },
  {
    "path": "prisma/migrations_backup/20250615170916_add_cascade_delete_workout_sessions/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"workout_session_exercises\" DROP CONSTRAINT \"workout_session_exercises_workoutSessionId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"workout_sets\" DROP CONSTRAINT \"workout_sets_workoutSessionExerciseId_fkey\";\n\n-- AddForeignKey\nALTER TABLE \"workout_session_exercises\" ADD CONSTRAINT \"workout_session_exercises_workoutSessionId_fkey\" FOREIGN KEY (\"workoutSessionId\") REFERENCES \"workout_sessions\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"workout_sets\" ADD CONSTRAINT \"workout_sets_workoutSessionExerciseId_fkey\" FOREIGN KEY (\"workoutSessionExerciseId\") REFERENCES \"workout_session_exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250623142458_add_billing_and_subscriptions/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"BillingMode\" AS ENUM ('DISABLED', 'LICENSE_KEY', 'SUBSCRIPTION', 'FREEMIUM');\n\n-- CreateEnum\nCREATE TYPE \"SubscriptionStatus\" AS ENUM ('ACTIVE', 'TRIAL', 'CANCELLED', 'EXPIRED', 'PAUSED');\n\n-- CreateEnum\nCREATE TYPE \"Platform\" AS ENUM ('WEB', 'IOS', 'ANDROID');\n\n-- CreateEnum\nCREATE TYPE \"PaymentProcessor\" AS ENUM ('STRIPE', 'PAYPAL', 'LEMONSQUEEZY', 'PADDLE', 'APPLE_PAY', 'GOOGLE_PAY', 'REVENUECAT', 'NONE', 'OTHER');\n\n-- CreateEnum\nCREATE TYPE \"PaymentStatus\" AS ENUM ('PENDING', 'COMPLETED', 'FAILED', 'REFUNDED');\n\n-- CreateEnum\nCREATE TYPE \"WebhookSource\" AS ENUM ('REVENUECAT', 'STRIPE', 'PAYPAL', 'LEMONSQUEEZY', 'OTHER');\n\n-- AlterTable\nALTER TABLE \"user\" ADD COLUMN     \"isPremium\" BOOLEAN DEFAULT false,\nADD COLUMN     \"premiumUntil\" TIMESTAMP(3);\n\n-- CreateTable\nCREATE TABLE \"app_configuration\" (\n    \"id\" TEXT NOT NULL DEFAULT 'default',\n    \"billingMode\" \"BillingMode\" NOT NULL DEFAULT 'DISABLED',\n    \"freeUserLimits\" JSONB,\n    \"activeProcessor\" \"PaymentProcessor\",\n    \"processorConfig\" JSONB,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"app_configuration_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"subscription_plans\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"description\" TEXT,\n    \"priceMonthly\" DECIMAL(10,2),\n    \"priceYearly\" DECIMAL(10,2),\n    \"currency\" TEXT DEFAULT 'EUR',\n    \"revenueCatProductId\" TEXT,\n    \"externalProductId\" TEXT,\n    \"features\" JSONB,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"isVisibleInSelfHosted\" BOOLEAN NOT NULL DEFAULT false,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"subscription_plans_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"subscriptions\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"planId\" TEXT NOT NULL,\n    \"revenueCatUserId\" TEXT,\n    \"revenueCatEntitlement\" TEXT,\n    \"status\" \"SubscriptionStatus\" NOT NULL,\n    \"startedAt\" TIMESTAMP(3) NOT NULL,\n    \"currentPeriodEnd\" TIMESTAMP(3),\n    \"cancelledAt\" TIMESTAMP(3),\n    \"platform\" \"Platform\",\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"subscriptions_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"payments\" (\n    \"id\" TEXT NOT NULL,\n    \"subscriptionId\" TEXT NOT NULL,\n    \"amount\" DECIMAL(10,2) NOT NULL,\n    \"currency\" TEXT NOT NULL,\n    \"processor\" \"PaymentProcessor\" NOT NULL,\n    \"processorPaymentId\" TEXT,\n    \"revenueCatTransactionId\" TEXT,\n    \"status\" \"PaymentStatus\" NOT NULL,\n    \"paidAt\" TIMESTAMP(3),\n    \"failedAt\" TIMESTAMP(3),\n    \"metadata\" JSONB,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"payments_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"licenses\" (\n    \"id\" TEXT NOT NULL,\n    \"key\" TEXT NOT NULL,\n    \"userId\" TEXT,\n    \"validFrom\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"validUntil\" TIMESTAMP(3),\n    \"maxUsers\" INTEGER DEFAULT 1,\n    \"features\" JSONB,\n    \"activatedAt\" TIMESTAMP(3),\n    \"lastCheckedAt\" TIMESTAMP(3),\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"licenses_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"webhook_events\" (\n    \"id\" TEXT NOT NULL,\n    \"source\" \"WebhookSource\" NOT NULL,\n    \"eventType\" TEXT NOT NULL,\n    \"payload\" JSONB NOT NULL,\n    \"processed\" BOOLEAN NOT NULL DEFAULT false,\n    \"processedAt\" TIMESTAMP(3),\n    \"error\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"webhook_events_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"subscription_plans_revenueCatProductId_key\" ON \"subscription_plans\"(\"revenueCatProductId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"subscriptions_userId_platform_key\" ON \"subscriptions\"(\"userId\", \"platform\");\n\n-- CreateIndex\nCREATE INDEX \"payments_processorPaymentId_idx\" ON \"payments\"(\"processorPaymentId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"licenses_key_key\" ON \"licenses\"(\"key\");\n\n-- AddForeignKey\nALTER TABLE \"subscriptions\" ADD CONSTRAINT \"subscriptions_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"subscriptions\" ADD CONSTRAINT \"subscriptions_planId_fkey\" FOREIGN KEY (\"planId\") REFERENCES \"subscription_plans\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"payments\" ADD CONSTRAINT \"payments_subscriptionId_fkey\" FOREIGN KEY (\"subscriptionId\") REFERENCES \"subscriptions\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"licenses\" ADD CONSTRAINT \"licenses_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250623143952_remove_webhook_events/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the `webhook_events` table. If the table is not empty, all the data it contains will be lost.\n\n*/\n-- DropTable\nDROP TABLE \"webhook_events\";\n\n-- DropEnum\nDROP TYPE \"WebhookSource\";\n"
  },
  {
    "path": "prisma/migrations_backup/20250623144324_add_webhook_events/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"webhook_events\" (\n    \"id\" TEXT NOT NULL,\n    \"provider\" \"PaymentProcessor\" NOT NULL,\n    \"eventType\" TEXT NOT NULL,\n    \"payload\" JSONB NOT NULL,\n    \"headers\" JSONB,\n    \"processed\" BOOLEAN NOT NULL DEFAULT false,\n    \"processedAt\" TIMESTAMP(3),\n    \"retryCount\" INTEGER NOT NULL DEFAULT 0,\n    \"maxRetries\" INTEGER NOT NULL DEFAULT 3,\n    \"error\" TEXT,\n    \"resultingAction\" TEXT,\n    \"relatedUserId\" TEXT,\n    \"relatedPaymentId\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"webhook_events_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE INDEX \"webhook_events_provider_processed_idx\" ON \"webhook_events\"(\"provider\", \"processed\");\n\n-- CreateIndex\nCREATE INDEX \"webhook_events_relatedUserId_idx\" ON \"webhook_events\"(\"relatedUserId\");\n\n-- CreateIndex\nCREATE INDEX \"webhook_events_createdAt_idx\" ON \"webhook_events\"(\"createdAt\");\n"
  },
  {
    "path": "prisma/migrations_backup/20250625155932_add_admin/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"ProgramLevel\" AS ENUM ('BEGINNER', 'INTERMEDIATE', 'ADVANCED', 'EXPERT');\n\n-- CreateTable\nCREATE TABLE \"programs\" (\n    \"id\" TEXT NOT NULL,\n    \"slug\" TEXT NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"titleEn\" TEXT NOT NULL,\n    \"titleEs\" TEXT NOT NULL,\n    \"titlePt\" TEXT NOT NULL,\n    \"titleRu\" TEXT NOT NULL,\n    \"titleZhCn\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"descriptionEn\" TEXT NOT NULL,\n    \"descriptionEs\" TEXT NOT NULL,\n    \"descriptionPt\" TEXT NOT NULL,\n    \"descriptionRu\" TEXT NOT NULL,\n    \"descriptionZhCn\" TEXT NOT NULL,\n    \"category\" TEXT NOT NULL,\n    \"image\" TEXT NOT NULL,\n    \"level\" \"ProgramLevel\" NOT NULL,\n    \"type\" \"ExerciseAttributeValueEnum\" NOT NULL,\n    \"durationWeeks\" INTEGER NOT NULL DEFAULT 4,\n    \"sessionsPerWeek\" INTEGER NOT NULL DEFAULT 3,\n    \"sessionDurationMin\" INTEGER NOT NULL DEFAULT 30,\n    \"equipment\" \"ExerciseAttributeValueEnum\"[] DEFAULT ARRAY[]::\"ExerciseAttributeValueEnum\"[],\n    \"isPremium\" BOOLEAN NOT NULL DEFAULT true,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"emoji\" TEXT,\n    \"participantCount\" INTEGER NOT NULL DEFAULT 0,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"programs_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_coaches\" (\n    \"id\" TEXT NOT NULL,\n    \"programId\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"image\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL DEFAULT 0,\n\n    CONSTRAINT \"program_coaches_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_weeks\" (\n    \"id\" TEXT NOT NULL,\n    \"programId\" TEXT NOT NULL,\n    \"weekNumber\" INTEGER NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"titleEn\" TEXT NOT NULL,\n    \"titleEs\" TEXT NOT NULL,\n    \"titlePt\" TEXT NOT NULL,\n    \"titleRu\" TEXT NOT NULL,\n    \"titleZhCn\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"descriptionEn\" TEXT NOT NULL,\n    \"descriptionEs\" TEXT NOT NULL,\n    \"descriptionPt\" TEXT NOT NULL,\n    \"descriptionRu\" TEXT NOT NULL,\n    \"descriptionZhCn\" TEXT NOT NULL,\n\n    CONSTRAINT \"program_weeks_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_sessions\" (\n    \"id\" TEXT NOT NULL,\n    \"weekId\" TEXT NOT NULL,\n    \"sessionNumber\" INTEGER NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"titleEn\" TEXT NOT NULL,\n    \"titleEs\" TEXT NOT NULL,\n    \"titlePt\" TEXT NOT NULL,\n    \"titleRu\" TEXT NOT NULL,\n    \"titleZhCn\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"descriptionEn\" TEXT NOT NULL,\n    \"descriptionEs\" TEXT NOT NULL,\n    \"descriptionPt\" TEXT NOT NULL,\n    \"descriptionRu\" TEXT NOT NULL,\n    \"descriptionZhCn\" TEXT NOT NULL,\n    \"equipment\" \"ExerciseAttributeValueEnum\"[] DEFAULT ARRAY[]::\"ExerciseAttributeValueEnum\"[],\n    \"estimatedMinutes\" INTEGER NOT NULL,\n    \"isPremium\" BOOLEAN NOT NULL DEFAULT true,\n\n    CONSTRAINT \"program_sessions_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_session_exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"sessionId\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL,\n    \"instructions\" TEXT NOT NULL,\n    \"instructionsEn\" TEXT NOT NULL,\n    \"instructionsEs\" TEXT NOT NULL,\n    \"instructionsPt\" TEXT NOT NULL,\n    \"instructionsRu\" TEXT NOT NULL,\n    \"instructionsZhCn\" TEXT NOT NULL,\n\n    CONSTRAINT \"program_session_exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"program_suggested_sets\" (\n    \"id\" TEXT NOT NULL,\n    \"programSessionExerciseId\" TEXT NOT NULL,\n    \"setIndex\" INTEGER NOT NULL,\n    \"types\" \"WorkoutSetType\"[] DEFAULT ARRAY[]::\"WorkoutSetType\"[],\n    \"valuesInt\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"valuesSec\" INTEGER[] DEFAULT ARRAY[]::INTEGER[],\n    \"units\" \"WorkoutSetUnit\"[] DEFAULT ARRAY[]::\"WorkoutSetUnit\"[],\n\n    CONSTRAINT \"program_suggested_sets_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"user_program_enrollments\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"programId\" TEXT NOT NULL,\n    \"enrolledAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"currentWeek\" INTEGER NOT NULL DEFAULT 1,\n    \"currentSession\" INTEGER NOT NULL DEFAULT 1,\n    \"completedSessions\" INTEGER NOT NULL DEFAULT 0,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"completedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"user_program_enrollments_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"user_session_progress\" (\n    \"id\" TEXT NOT NULL,\n    \"enrollmentId\" TEXT NOT NULL,\n    \"sessionId\" TEXT NOT NULL,\n    \"startedAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"completedAt\" TIMESTAMP(3),\n    \"workoutSessionId\" TEXT,\n\n    CONSTRAINT \"user_session_progress_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slug_key\" ON \"programs\"(\"slug\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_weeks_programId_weekNumber_key\" ON \"program_weeks\"(\"programId\", \"weekNumber\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_sessionNumber_key\" ON \"program_sessions\"(\"weekId\", \"sessionNumber\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_session_exercises_sessionId_order_key\" ON \"program_session_exercises\"(\"sessionId\", \"order\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_suggested_sets_programSessionExerciseId_setIndex_key\" ON \"program_suggested_sets\"(\"programSessionExerciseId\", \"setIndex\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_program_enrollments_userId_programId_key\" ON \"user_program_enrollments\"(\"userId\", \"programId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_session_progress_workoutSessionId_key\" ON \"user_session_progress\"(\"workoutSessionId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_session_progress_enrollmentId_sessionId_key\" ON \"user_session_progress\"(\"enrollmentId\", \"sessionId\");\n\n-- AddForeignKey\nALTER TABLE \"program_coaches\" ADD CONSTRAINT \"program_coaches_programId_fkey\" FOREIGN KEY (\"programId\") REFERENCES \"programs\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_weeks\" ADD CONSTRAINT \"program_weeks_programId_fkey\" FOREIGN KEY (\"programId\") REFERENCES \"programs\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_sessions\" ADD CONSTRAINT \"program_sessions_weekId_fkey\" FOREIGN KEY (\"weekId\") REFERENCES \"program_weeks\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_session_exercises\" ADD CONSTRAINT \"program_session_exercises_sessionId_fkey\" FOREIGN KEY (\"sessionId\") REFERENCES \"program_sessions\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_session_exercises\" ADD CONSTRAINT \"program_session_exercises_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"program_suggested_sets\" ADD CONSTRAINT \"program_suggested_sets_programSessionExerciseId_fkey\" FOREIGN KEY (\"programSessionExerciseId\") REFERENCES \"program_session_exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_program_enrollments\" ADD CONSTRAINT \"user_program_enrollments_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_program_enrollments\" ADD CONSTRAINT \"user_program_enrollments_programId_fkey\" FOREIGN KEY (\"programId\") REFERENCES \"programs\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_session_progress\" ADD CONSTRAINT \"user_session_progress_enrollmentId_fkey\" FOREIGN KEY (\"enrollmentId\") REFERENCES \"user_program_enrollments\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_session_progress\" ADD CONSTRAINT \"user_session_progress_sessionId_fkey\" FOREIGN KEY (\"sessionId\") REFERENCES \"program_sessions\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_session_progress\" ADD CONSTRAINT \"user_session_progress_workoutSessionId_fkey\" FOREIGN KEY (\"workoutSessionId\") REFERENCES \"workout_sessions\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250625195907_add_program_visibility/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"ProgramVisibility\" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');\n\n-- AlterTable\nALTER TABLE \"programs\" ADD COLUMN     \"visibility\" \"ProgramVisibility\" NOT NULL DEFAULT 'DRAFT';\n"
  },
  {
    "path": "prisma/migrations_backup/20250626102058_add_i18n_slugs_on_program/migration.sql",
    "content": "/*\n  Warnings:\n\n  - A unique constraint covering the columns `[weekId,slug]` on the table `program_sessions` will be added. If there are existing duplicate values, this will fail.\n  - A unique constraint covering the columns `[weekId,slugEn]` on the table `program_sessions` will be added. If there are existing duplicate values, this will fail.\n  - A unique constraint covering the columns `[weekId,slugEs]` on the table `program_sessions` will be added. If there are existing duplicate values, this will fail.\n  - A unique constraint covering the columns `[weekId,slugPt]` on the table `program_sessions` will be added. If there are existing duplicate values, this will fail.\n  - A unique constraint covering the columns `[weekId,slugRu]` on the table `program_sessions` will be added. If there are existing duplicate values, this will fail.\n  - A unique constraint covering the columns `[weekId,slugZhCn]` on the table `program_sessions` will be added. If there are existing duplicate values, this will fail.\n  - A unique constraint covering the columns `[slugEn]` on the table `programs` will be added. If there are existing duplicate values, this will fail.\n  - A unique constraint covering the columns `[slugEs]` on the table `programs` will be added. If there are existing duplicate values, this will fail.\n  - A unique constraint covering the columns `[slugPt]` on the table `programs` will be added. If there are existing duplicate values, this will fail.\n  - A unique constraint covering the columns `[slugRu]` on the table `programs` will be added. If there are existing duplicate values, this will fail.\n  - A unique constraint covering the columns `[slugZhCn]` on the table `programs` will be added. If there are existing duplicate values, this will fail.\n  - Added the required column `slug` to the `program_sessions` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `slugEn` to the `program_sessions` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `slugEs` to the `program_sessions` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `slugPt` to the `program_sessions` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `slugRu` to the `program_sessions` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `slugZhCn` to the `program_sessions` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `slugEn` to the `programs` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `slugEs` to the `programs` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `slugPt` to the `programs` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `slugRu` to the `programs` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `slugZhCn` to the `programs` table without a default value. This is not possible if the table is not empty.\n\n*/\n-- AlterTable\nALTER TABLE \"program_sessions\" ADD COLUMN     \"slug\" TEXT NOT NULL,\nADD COLUMN     \"slugEn\" TEXT NOT NULL,\nADD COLUMN     \"slugEs\" TEXT NOT NULL,\nADD COLUMN     \"slugPt\" TEXT NOT NULL,\nADD COLUMN     \"slugRu\" TEXT NOT NULL,\nADD COLUMN     \"slugZhCn\" TEXT NOT NULL;\n\n-- AlterTable\nALTER TABLE \"programs\" ADD COLUMN     \"slugEn\" TEXT NOT NULL,\nADD COLUMN     \"slugEs\" TEXT NOT NULL,\nADD COLUMN     \"slugPt\" TEXT NOT NULL,\nADD COLUMN     \"slugRu\" TEXT NOT NULL,\nADD COLUMN     \"slugZhCn\" TEXT NOT NULL;\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slug_key\" ON \"program_sessions\"(\"weekId\", \"slug\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugEn_key\" ON \"program_sessions\"(\"weekId\", \"slugEn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugEs_key\" ON \"program_sessions\"(\"weekId\", \"slugEs\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugPt_key\" ON \"program_sessions\"(\"weekId\", \"slugPt\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugRu_key\" ON \"program_sessions\"(\"weekId\", \"slugRu\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"program_sessions_weekId_slugZhCn_key\" ON \"program_sessions\"(\"weekId\", \"slugZhCn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugEn_key\" ON \"programs\"(\"slugEn\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugEs_key\" ON \"programs\"(\"slugEs\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugPt_key\" ON \"programs\"(\"slugPt\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugRu_key\" ON \"programs\"(\"slugRu\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"programs_slugZhCn_key\" ON \"programs\"(\"slugZhCn\");\n"
  },
  {
    "path": "prisma/migrations_backup/20250626134345_remove_emoji_on_program/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `emoji` on the `programs` table. All the data in the column will be lost.\n\n*/\n-- AlterTable\nALTER TABLE \"programs\" DROP COLUMN \"emoji\";\n"
  },
  {
    "path": "prisma/migrations_backup/20250626182857_cleanup_billing_system/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `description` on the `subscription_plans` table. All the data in the column will be lost.\n  - You are about to drop the column `externalProductId` on the `subscription_plans` table. All the data in the column will be lost.\n  - You are about to drop the column `features` on the `subscription_plans` table. All the data in the column will be lost.\n  - You are about to drop the column `isVisibleInSelfHosted` on the `subscription_plans` table. All the data in the column will be lost.\n  - You are about to drop the column `name` on the `subscription_plans` table. All the data in the column will be lost.\n  - You are about to drop the column `revenueCatProductId` on the `subscription_plans` table. All the data in the column will be lost.\n  - You are about to drop the `app_configuration` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `webhook_events` table. If the table is not empty, all the data it contains will be lost.\n  - Made the column `currency` on table `subscription_plans` required. This step will fail if there are existing NULL values in that column.\n\n*/\n-- DropIndex\nDROP INDEX \"subscription_plans_revenueCatProductId_key\";\n\n-- AlterTable\nALTER TABLE \"subscription_plans\" DROP COLUMN \"description\",\nDROP COLUMN \"externalProductId\",\nDROP COLUMN \"features\",\nDROP COLUMN \"isVisibleInSelfHosted\",\nDROP COLUMN \"name\",\nDROP COLUMN \"revenueCatProductId\",\nADD COLUMN     \"availableRegions\" TEXT[] DEFAULT ARRAY[]::TEXT[],\nADD COLUMN     \"interval\" TEXT NOT NULL DEFAULT 'month',\nALTER COLUMN \"currency\" SET NOT NULL;\n\n-- DropTable\nDROP TABLE \"app_configuration\";\n\n-- DropTable\nDROP TABLE \"webhook_events\";\n\n-- DropEnum\nDROP TYPE \"BillingMode\";\n\n-- CreateTable\nCREATE TABLE \"plan_provider_mappings\" (\n    \"id\" TEXT NOT NULL,\n    \"planId\" TEXT NOT NULL,\n    \"provider\" \"PaymentProcessor\" NOT NULL,\n    \"externalId\" TEXT NOT NULL,\n    \"region\" TEXT,\n    \"metadata\" JSONB,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"plan_provider_mappings_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE INDEX \"plan_provider_mappings_provider_externalId_idx\" ON \"plan_provider_mappings\"(\"provider\", \"externalId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"plan_provider_mappings_planId_provider_region_key\" ON \"plan_provider_mappings\"(\"planId\", \"provider\", \"region\");\n\n-- AddForeignKey\nALTER TABLE \"plan_provider_mappings\" ADD CONSTRAINT \"plan_provider_mappings_planId_fkey\" FOREIGN KEY (\"planId\") REFERENCES \"subscription_plans\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250626204136_remove_payment_table/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the `payments` table. If the table is not empty, all the data it contains will be lost.\n\n*/\n-- DropForeignKey\nALTER TABLE \"payments\" DROP CONSTRAINT \"payments_subscriptionId_fkey\";\n\n-- DropTable\nDROP TABLE \"payments\";\n\n-- DropEnum\nDROP TYPE \"PaymentStatus\";\n"
  },
  {
    "path": "prisma/migrations_backup/20250626205121_remove_legacy_premium_fields/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `isPremium` on the `user` table. All the data in the column will be lost.\n  - You are about to drop the column `premiumUntil` on the `user` table. All the data in the column will be lost.\n\n*/\n-- AlterTable\nALTER TABLE \"user\" DROP COLUMN \"isPremium\",\nDROP COLUMN \"premiumUntil\";\n"
  },
  {
    "path": "prisma/migrations_backup/20250626205904_remove_payment_table_keep_ispremium/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"user\" ADD COLUMN     \"isPremium\" BOOLEAN DEFAULT false;\n"
  },
  {
    "path": "prisma/migrations_backup/20250707114920_add_user_favorite_exercises/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"user_favorite_exercises\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"exerciseId\" TEXT NOT NULL,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"user_favorite_exercises_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"user_favorite_exercises_userId_exerciseId_key\" ON \"user_favorite_exercises\"(\"userId\", \"exerciseId\");\n\n-- AddForeignKey\nALTER TABLE \"user_favorite_exercises\" ADD CONSTRAINT \"user_favorite_exercises_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"user_favorite_exercises\" ADD CONSTRAINT \"user_favorite_exercises_exerciseId_fkey\" FOREIGN KEY (\"exerciseId\") REFERENCES \"exercises\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations_backup/20250708214116_add_rating_to_wkt_sessions/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workout_sessions\" ADD COLUMN     \"rating\" INTEGER,\nADD COLUMN     \"ratingComment\" TEXT;\n"
  },
  {
    "path": "prisma/migrations_backup/20250709_add_revenuecat_fields/migration.sql",
    "content": "-- Add RevenueCat transaction and product IDs to Subscription table\nALTER TABLE \"subscriptions\" ADD COLUMN \"revenueCatTransactionId\" TEXT;\nALTER TABLE \"subscriptions\" ADD COLUMN \"revenueCatProductId\" TEXT;\n\n-- Create RevenueCat webhook event logging table\nCREATE TABLE \"revenuecat_webhook_events\" (\n    \"id\" TEXT NOT NULL,\n    \"eventType\" TEXT NOT NULL,\n    \"eventTimestamp\" TIMESTAMP(3) NOT NULL,\n    \"appUserId\" TEXT NOT NULL,\n    \"environment\" TEXT NOT NULL,\n    \"productId\" TEXT,\n    \"transactionId\" TEXT,\n    \"originalTransactionId\" TEXT,\n    \"entitlementIds\" TEXT,\n    \"processed\" BOOLEAN NOT NULL DEFAULT false,\n    \"processingError\" TEXT,\n    \"retryCount\" INTEGER NOT NULL DEFAULT 0,\n    \"rawEventData\" JSONB NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"revenuecat_webhook_events_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- Create indexes for efficient querying\nCREATE INDEX \"revenuecat_webhook_events_appUserId_idx\" ON \"revenuecat_webhook_events\"(\"appUserId\");\nCREATE INDEX \"revenuecat_webhook_events_eventType_idx\" ON \"revenuecat_webhook_events\"(\"eventType\");\nCREATE INDEX \"revenuecat_webhook_events_processed_idx\" ON \"revenuecat_webhook_events\"(\"processed\");\nCREATE INDEX \"revenuecat_webhook_events_eventTimestamp_idx\" ON \"revenuecat_webhook_events\"(\"eventTimestamp\");"
  },
  {
    "path": "prisma/migrations_backup/migration_lock.toml",
    "content": "# Please do not edit this file manually\n# It should be added in your version-control system (e.g., Git)\nprovider = \"postgresql\"\n"
  },
  {
    "path": "prisma/schema.prisma",
    "content": "// This is your Prisma schema file\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider = \"postgresql\"\n  url      = env(\"DATABASE_URL\")\n}\n\nenum UserRole {\n  admin\n  user\n}\n\nmodel User {\n  id            String      @id\n  firstName     String      @default(\"\")\n  lastName      String      @default(\"\")\n  name          String\n  email         String      @unique\n  emailVerified Boolean\n  image         String?\n  locale        String?     @default(\"fr\")\n  createdAt     DateTime\n  updatedAt     DateTime\n  sessions      Session[]\n  accounts      Account[]\n  feedbacks     Feedbacks[]\n\n  role           UserRole?        @default(user)\n  banned         Boolean?         @default(false)\n  banReason      String?\n  banExpires     DateTime?\n  WorkoutSession WorkoutSession[]\n  FavoriteExercises UserFavoriteExercise[]\n\n  // Subscription fields\n  subscriptions Subscription[]\n  licenses      License[]\n  isPremium     Boolean?       @default(false)\n\n  // Training programs\n  programEnrollments UserProgramEnrollment[]\n\n  @@map(\"user\")\n}\n\nmodel UserFavoriteExercise {\n  id         String   @id @default(cuid())\n  userId     String\n  user       User     @relation(fields: [userId], references: [id], onDelete: Cascade)\n  exerciseId String\n  exercise   Exercise @relation(fields: [exerciseId], references: [id], onDelete: Cascade)\n  updatedAt  DateTime @updatedAt\n\n  @@unique([userId, exerciseId])\n  @@map(\"user_favorite_exercises\")\n}\n\nmodel Session {\n  id        String   @id\n  expiresAt DateTime\n  token     String   @unique\n  createdAt DateTime\n  updatedAt DateTime\n  ipAddress String?\n  userAgent String?\n  userId    String\n  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n  impersonatedBy String?\n\n  @@map(\"session\")\n}\n\nmodel Account {\n  id                    String    @id\n  accountId             String\n  providerId            String\n  userId                String\n  user                  User      @relation(fields: [userId], references: [id], onDelete: Cascade)\n  accessToken           String?\n  refreshToken          String?\n  idToken               String?\n  accessTokenExpiresAt  DateTime?\n  refreshTokenExpiresAt DateTime?\n  scope                 String?\n  password              String?\n  createdAt             DateTime\n  updatedAt             DateTime\n\n  @@map(\"account\")\n}\n\nmodel Verification {\n  id         String    @id\n  identifier String\n  value      String\n  expiresAt  DateTime\n  createdAt  DateTime?\n  updatedAt  DateTime?\n\n  @@map(\"verification\")\n}\n\nmodel Feedbacks {\n  id      String  @id @default(cuid())\n  review  Int\n  message String\n  email   String?\n  userId  String?\n  user    User?   @relation(fields: [userId], references: [id], onDelete: SetNull)\n\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  @@map(\"feedbacks\")\n}\n\nmodel Exercise {\n  id                String   @id @default(cuid())\n  name              String\n  nameEn            String?\n  description       String?  @db.Text\n  descriptionEn     String?  @db.Text\n  fullVideoUrl      String?  @db.Text\n  fullVideoImageUrl String?  @db.Text\n  introduction      String?  @db.Text\n  introductionEn    String?  @db.Text\n  slug              String?  @unique\n  slugEn            String?  @unique\n  createdAt         DateTime @default(now())\n  updatedAt         DateTime @updatedAt\n\n  // Relations\n  attributes             ExerciseAttribute[]\n  WorkoutSessionExercise WorkoutSessionExercise[]\n  ProgramSessionExercise ProgramSessionExercise[]\n  favoritesByUsers       UserFavoriteExercise[]\n\n  @@map(\"exercises\")\n}\n\nmodel ExerciseAttributeName {\n  id        String                    @id @default(cuid())\n  name      ExerciseAttributeNameEnum @unique\n  createdAt DateTime                  @default(now())\n  updatedAt DateTime                  @updatedAt\n\n  // Relations\n  values     ExerciseAttributeValue[]\n  attributes ExerciseAttribute[]\n\n  @@map(\"exercise_attribute_names\")\n}\n\nmodel ExerciseAttributeValue {\n  id              String                     @id @default(cuid())\n  attributeNameId String\n  value           ExerciseAttributeValueEnum\n  createdAt       DateTime                   @default(now())\n  updatedAt       DateTime                   @updatedAt\n\n  // Relations\n  attributeName ExerciseAttributeName @relation(fields: [attributeNameId], references: [id])\n  attributes    ExerciseAttribute[]\n\n  @@unique([attributeNameId, value])\n  @@map(\"exercise_attribute_values\")\n}\n\nmodel ExerciseAttribute {\n  id               String   @id @default(cuid())\n  exerciseId       String\n  attributeNameId  String\n  attributeValueId String\n  createdAt        DateTime @default(now())\n  updatedAt        DateTime @updatedAt\n\n  // Relations\n  exercise       Exercise               @relation(fields: [exerciseId], references: [id], onDelete: Cascade)\n  attributeName  ExerciseAttributeName  @relation(fields: [attributeNameId], references: [id])\n  attributeValue ExerciseAttributeValue @relation(fields: [attributeValueId], references: [id])\n\n  @@unique([exerciseId, attributeNameId, attributeValueId])\n  @@map(\"exercise_attributes\")\n}\n\n// Enums\nenum ExercisePrivacy {\n  PUBLIC\n  PRIVATE\n}\n\n// Noms d'attributs\nenum ExerciseAttributeNameEnum {\n  TYPE\n  PRIMARY_MUSCLE\n  SECONDARY_MUSCLE\n  EQUIPMENT\n  MECHANICS_TYPE\n}\n\n// Toutes les valeurs possibles\nenum ExerciseAttributeValueEnum {\n  // Types d'exercices\n  BODYWEIGHT\n  STRENGTH\n  POWERLIFTING\n  CALISTHENIC\n  PLYOMETRICS\n  STRETCHING\n  STRONGMAN\n  CARDIO\n  STABILIZATION\n  POWER\n  RESISTANCE\n  CROSSFIT\n  WEIGHTLIFTING\n\n  // Groupes musculaires\n  BICEPS\n  SHOULDERS\n  CHEST\n  BACK\n  GLUTES\n  TRICEPS\n  HAMSTRINGS\n  QUADRICEPS\n  FOREARMS\n  CALVES\n  TRAPS\n  ABDOMINALS\n  NECK\n  LATS\n  ADDUCTORS\n  ABDUCTORS\n  OBLIQUES\n  GROIN\n  FULL_BODY\n  ROTATOR_CUFF\n  HIP_FLEXOR\n  ACHILLES_TENDON\n  FINGERS\n\n  // Équipements\n  DUMBBELL\n  KETTLEBELLS\n  BARBELL\n  SMITH_MACHINE\n  BODY_ONLY\n  OTHER\n  BANDS\n  EZ_BAR\n  MACHINE\n  DESK\n  PULLUP_BAR\n  NONE\n  CABLE\n  MEDICINE_BALL\n  SWISS_BALL\n  FOAM_ROLL\n  WEIGHT_PLATE\n  TRX\n  BOX\n  ROPES\n  SPIN_BIKE\n  STEP\n  BOSU\n  TYRE\n  SANDBAG\n  POLE\n  BENCH\n  WALL\n  BAR\n  RACK\n  CAR\n  SLED\n  CHAIN\n  SKIERG\n  ROPE\n  NA\n\n  // Types de mécanique\n  ISOLATION\n  COMPOUND\n}\n\nmodel WorkoutSession {\n  id        String                       @id @default(cuid())\n  userId    String\n  user      User                         @relation(fields: [userId], references: [id])\n  startedAt DateTime\n  endedAt   DateTime?\n  duration  Int? // en secondes\n  exercises WorkoutSessionExercise[]\n  muscles   ExerciseAttributeValueEnum[] @default([])\n\n  rating        Int? // 1-5 star rating\n  ratingComment String? @db.Text // Optional comment for rating\n\n  // Program progress tracking\n  userSessionProgress UserSessionProgress?\n\n  @@map(\"workout_sessions\")\n}\n\nmodel WorkoutSessionExercise {\n  id               String         @id @default(cuid())\n  workoutSessionId String\n  exerciseId       String\n  order            Int\n  workoutSession   WorkoutSession @relation(fields: [workoutSessionId], references: [id], onDelete: Cascade)\n  exercise         Exercise       @relation(fields: [exerciseId], references: [id])\n  sets             WorkoutSet[]\n\n  @@map(\"workout_session_exercises\")\n}\n\nmodel WorkoutSet {\n  id                       String                 @id @default(cuid())\n  workoutSessionExerciseId String\n  setIndex                 Int\n  type                     WorkoutSetType\n  types                    WorkoutSetType[]       @default([])\n  valuesInt                Int[]                  @default([])\n  valuesSec                Int[]                  @default([])\n  units                    WorkoutSetUnit[]       @default([])\n  completed                Boolean                @default(false)\n  workoutSessionExercise   WorkoutSessionExercise @relation(fields: [workoutSessionExerciseId], references: [id], onDelete: Cascade)\n\n  @@map(\"workout_sets\")\n}\n\nenum WorkoutSetType {\n  TIME\n  WEIGHT\n  REPS\n  BODYWEIGHT\n  NA\n}\n\nenum WorkoutSetUnit {\n  kg\n  lbs\n}\n\n// ========================================\n// BILLING & SUBSCRIPTION MODELS\n// ========================================\n\n// Plans d'abonnement - provider-agnostic, minimal KISS approach\nmodel SubscriptionPlan {\n  id String @id @default(cuid())\n\n  // Pricing - internal source of truth\n  priceMonthly Decimal? @db.Decimal(10, 2)\n  priceYearly  Decimal? @db.Decimal(10, 2)\n  currency     String   @default(\"EUR\")\n  interval     String   @default(\"month\") // month, year\n\n  // Availability\n  isActive         Boolean  @default(true)\n  availableRegions String[] @default([]) // [\"EU\", \"US\", \"UK\"]\n\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  // Relations\n  subscriptions    Subscription[]\n  providerMappings PlanProviderMapping[]\n\n  @@map(\"subscription_plans\")\n}\n\n// Provider mapping for external payment systems\nmodel PlanProviderMapping {\n  id       String           @id @default(cuid())\n  planId   String\n  provider PaymentProcessor\n\n  // External provider IDs\n  externalId String // price_1ABC for Stripe, variant_123 for LemonSqueezy, etc.\n  region     String? // EU, US, UK for regional pricing\n\n  // Provider-specific metadata\n  metadata Json? // Additional provider-specific data\n\n  isActive Boolean @default(true)\n\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  // Relations\n  plan SubscriptionPlan @relation(fields: [planId], references: [id], onDelete: Cascade)\n\n  @@unique([planId, provider, region])\n  @@index([provider, externalId])\n  @@map(\"plan_provider_mappings\")\n}\n\n// Abonnements utilisateurs (source de vérité : RevenueCat pour mobile)\nmodel Subscription {\n  id     String @id @default(cuid())\n  userId String\n  user   User   @relation(fields: [userId], references: [id])\n\n  planId String\n  plan   SubscriptionPlan @relation(fields: [planId], references: [id])\n\n  // RevenueCat data (simplified)\n  revenueCatUserId String? // The RevenueCat user ID for sync\n\n  // Status\n  status SubscriptionStatus\n\n  // Dates importantes\n  startedAt        DateTime\n  currentPeriodEnd DateTime?\n  cancelledAt      DateTime?\n\n  // Platform info\n  platform Platform?\n\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  @@unique([userId, platform])\n  @@map(\"subscriptions\")\n}\n\n// Licences pour self-hosted (alternative aux subscriptions)\nmodel License {\n  id  String @id @default(cuid())\n  key String @unique\n\n  // Propriétaire\n  userId String?\n  user   User?   @relation(fields: [userId], references: [id])\n\n  // Validité\n  validFrom  DateTime  @default(now())\n  validUntil DateTime?\n\n  // Limites\n  maxUsers Int?  @default(1)\n  features Json?\n\n  // Activation\n  activatedAt   DateTime?\n  lastCheckedAt DateTime?\n\n  createdAt DateTime @default(now())\n\n  @@map(\"licenses\")\n}\n\n// RevenueCat webhook event logging for debugging and monitoring\nmodel RevenueCatWebhookEvent {\n  id String @id @default(cuid())\n\n  // Event metadata\n  eventType        String  // INITIAL_PURCHASE, RENEWAL, CANCELLATION, etc.\n  eventTimestamp   DateTime\n  appUserId        String\n  environment      String  // SANDBOX or PRODUCTION\n  \n  // Transaction details\n  productId        String?\n  transactionId    String?\n  originalTransactionId String?\n  entitlementIds   String? // JSON array as string\n  \n  // Processing metadata\n  processed        Boolean @default(false)\n  processingError  String? @db.Text\n  retryCount       Int @default(0)\n  \n  // Raw event data for debugging\n  rawEventData     Json\n  \n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  @@index([appUserId])\n  @@index([eventType])\n  @@index([processed])\n  @@index([eventTimestamp])\n  @@map(\"revenuecat_webhook_events\")\n}\n\n// ========================================\n// BILLING ENUMS\n// ========================================\n\nenum SubscriptionStatus {\n  ACTIVE\n  TRIAL\n  CANCELLED\n  EXPIRED\n  PAUSED\n}\n\nenum Platform {\n  WEB\n  IOS\n  ANDROID\n}\n\nenum PaymentProcessor {\n  STRIPE\n  PAYPAL\n  LEMONSQUEEZY\n  PADDLE\n  APPLE_PAY\n  GOOGLE_PAY\n  REVENUECAT\n  NONE // self-hosted\n  OTHER\n}\n\n// ========================================\n// TRAINING PROGRAMS\n// ========================================\n\nmodel Program {\n  id                 String                       @id @default(cuid())\n  slug               String                       @unique\n  slugEn             String                       @unique\n  slugEs             String                       @unique\n  slugPt             String                       @unique\n  slugRu             String                       @unique\n  slugZhCn           String                       @unique\n  title              String\n  titleEn            String\n  titleEs            String\n  titlePt            String\n  titleRu            String\n  titleZhCn          String\n  description        String                       @db.Text\n  descriptionEn      String                       @db.Text\n  descriptionEs      String                       @db.Text\n  descriptionPt      String                       @db.Text\n  descriptionRu      String                       @db.Text\n  descriptionZhCn    String                       @db.Text\n  category           String\n  image              String\n  level              ProgramLevel\n  type               ExerciseAttributeValueEnum\n  durationWeeks      Int                          @default(4)\n  sessionsPerWeek    Int                          @default(3)\n  sessionDurationMin Int                          @default(30)\n  equipment          ExerciseAttributeValueEnum[] @default([])\n  isPremium          Boolean                      @default(true)\n  isActive           Boolean                      @default(true)\n  visibility         ProgramVisibility            @default(DRAFT)\n  participantCount   Int                          @default(0)\n\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  // Relations\n  weeks       ProgramWeek[]\n  enrollments UserProgramEnrollment[]\n  coaches     ProgramCoach[]\n\n  @@map(\"programs\")\n}\n\nmodel ProgramCoach {\n  id        String @id @default(cuid())\n  programId String\n  name      String\n  image     String\n  order     Int    @default(0)\n\n  program Program @relation(fields: [programId], references: [id], onDelete: Cascade)\n\n  @@map(\"program_coaches\")\n}\n\nmodel ProgramWeek {\n  id              String @id @default(cuid())\n  programId       String\n  weekNumber      Int\n  title           String\n  titleEn         String\n  titleEs         String\n  titlePt         String\n  titleRu         String\n  titleZhCn       String\n  description     String @db.Text\n  descriptionEn   String @db.Text\n  descriptionEs   String @db.Text\n  descriptionPt   String @db.Text\n  descriptionRu   String @db.Text\n  descriptionZhCn String @db.Text\n\n  program  Program          @relation(fields: [programId], references: [id], onDelete: Cascade)\n  sessions ProgramSession[]\n\n  @@unique([programId, weekNumber])\n  @@map(\"program_weeks\")\n}\n\nmodel ProgramSession {\n  id               String                       @id @default(cuid())\n  weekId           String\n  sessionNumber    Int\n  title            String\n  titleEn          String\n  titleEs          String\n  titlePt          String\n  titleRu          String\n  titleZhCn        String\n  slug             String\n  slugEn           String\n  slugEs           String\n  slugPt           String\n  slugRu           String\n  slugZhCn         String\n  description      String                       @db.Text\n  descriptionEn    String                       @db.Text\n  descriptionEs    String                       @db.Text\n  descriptionPt    String                       @db.Text\n  descriptionRu    String                       @db.Text\n  descriptionZhCn  String                       @db.Text\n  equipment        ExerciseAttributeValueEnum[] @default([])\n  estimatedMinutes Int\n  isPremium        Boolean                      @default(true)\n\n  week         ProgramWeek              @relation(fields: [weekId], references: [id], onDelete: Cascade)\n  exercises    ProgramSessionExercise[]\n  userProgress UserSessionProgress[]\n\n  @@unique([weekId, sessionNumber])\n  @@unique([weekId, slug])\n  @@unique([weekId, slugEn])\n  @@unique([weekId, slugEs])\n  @@unique([weekId, slugPt])\n  @@unique([weekId, slugRu])\n  @@unique([weekId, slugZhCn])\n  @@map(\"program_sessions\")\n}\n\nmodel ProgramSessionExercise {\n  id               String @id @default(cuid())\n  sessionId        String\n  exerciseId       String\n  order            Int\n  instructions     String @db.Text\n  instructionsEn   String @db.Text\n  instructionsEs   String @db.Text\n  instructionsPt   String @db.Text\n  instructionsRu   String @db.Text\n  instructionsZhCn String @db.Text\n\n  session       ProgramSession        @relation(fields: [sessionId], references: [id], onDelete: Cascade)\n  exercise      Exercise              @relation(fields: [exerciseId], references: [id])\n  suggestedSets ProgramSuggestedSet[]\n\n  @@unique([sessionId, order])\n  @@map(\"program_session_exercises\")\n}\n\nmodel ProgramSuggestedSet {\n  id                       String           @id @default(cuid())\n  programSessionExerciseId String\n  setIndex                 Int\n  types                    WorkoutSetType[] @default([])\n  valuesInt                Int[]            @default([])\n  valuesSec                Int[]            @default([])\n  units                    WorkoutSetUnit[] @default([])\n\n  programSessionExercise ProgramSessionExercise @relation(fields: [programSessionExerciseId], references: [id], onDelete: Cascade)\n\n  @@unique([programSessionExerciseId, setIndex])\n  @@map(\"program_suggested_sets\")\n}\n\n// User enrollment and progress tracking\nmodel UserProgramEnrollment {\n  id                String    @id @default(cuid())\n  userId            String\n  programId         String\n  enrolledAt        DateTime  @default(now())\n  currentWeek       Int       @default(1)\n  currentSession    Int       @default(1)\n  completedSessions Int       @default(0)\n  isActive          Boolean   @default(true)\n  completedAt       DateTime?\n\n  user            User                  @relation(fields: [userId], references: [id], onDelete: Cascade)\n  program         Program               @relation(fields: [programId], references: [id])\n  sessionProgress UserSessionProgress[]\n\n  @@unique([userId, programId])\n  @@map(\"user_program_enrollments\")\n}\n\nmodel UserSessionProgress {\n  id               String    @id @default(cuid())\n  enrollmentId     String\n  sessionId        String\n  startedAt        DateTime  @default(now())\n  completedAt      DateTime?\n  workoutSessionId String?   @unique // Link to actual workout session\n\n  enrollment     UserProgramEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)\n  session        ProgramSession        @relation(fields: [sessionId], references: [id])\n  workoutSession WorkoutSession?       @relation(fields: [workoutSessionId], references: [id])\n\n  @@unique([enrollmentId, sessionId])\n  @@map(\"user_session_progress\")\n}\n\nenum ProgramLevel {\n  BEGINNER\n  INTERMEDIATE\n  ADVANCED\n  EXPERT\n}\n\nenum ProgramVisibility {\n  DRAFT\n  PUBLISHED\n  ARCHIVED\n}\n"
  },
  {
    "path": "public/_ads.txt",
    "content": "google.com, pub-3437447245301146, DIRECT, f08c47fec0942fa0"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"background_color\": \"#f3f4f6\",\n  \"categories\": [\"health\", \"fitness\", \"sports\"],\n  \"description\": \"Your personal workout companion - track workouts, build routines, and stay motivated\",\n  \"display\": \"standalone\",\n  \"icons\": [\n    {\n      \"src\": \"/images/favicon-16x16.png\",\n      \"sizes\": \"16x16\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"/images/favicon-32x32.png\",\n      \"sizes\": \"32x32\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"/apple-touch-icon.png\",\n      \"sizes\": \"180x180\",\n      \"type\": \"image/png\",\n      \"purpose\": \"any maskable\"\n    },\n    {\n      \"src\": \"/android-chrome-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\",\n      \"purpose\": \"any maskable\"\n    },\n    {\n      \"src\": \"/android-chrome-512x512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image/png\",\n      \"purpose\": \"any maskable\"\n    }\n  ],\n  \"lang\": \"en\",\n  \"name\": \"Workout Cool\",\n  \"orientation\": \"portrait\",\n  \"scope\": \"/\",\n  \"short_name\": \"Workout Cool\",\n  \"start_url\": \"/\",\n  \"theme_color\": \"#FF5722\"\n}\n"
  },
  {
    "path": "public/sw.js",
    "content": "const CACHE_NAME = \"1.2.5\";\nconst urlsToCache = [\n  \"/manifest.json\",\n  \"/images/favicon-32x32.png\",\n  \"/images/favicon-16x16.png\",\n  \"/apple-touch-icon.png\",\n  \"/android-chrome-192x192.png\",\n  \"/android-chrome-512x512.png\",\n  \"/logo.png\",\n];\n\n// Install event - cache resources\nself.addEventListener(\"install\", (event) => {\n  self.skipWaiting(); // 🔥 force install\n  event.waitUntil(\n    caches.open(CACHE_NAME).then((cache) => {\n      return cache.addAll(urlsToCache);\n    }),\n  );\n});\n\n// Fetch event - network first with cache fallback\nself.addEventListener(\"fetch\", (event) => {\n  // Skip non-GET requests\n  if (event.request.method !== \"GET\") {\n    return;\n  }\n\n  // Skip OAuth/auth related URLs\n  const url = new URL(event.request.url);\n  if (\n    url.pathname.startsWith(\"/api/auth\") ||\n    url.pathname.startsWith(\"/auth\") ||\n    url.hostname !== self.location.hostname ||\n    url.pathname.startsWith(\"/api/stripe\") ||\n    url.protocol === \"chrome-extension:\"\n  ) {\n    return;\n  }\n\n  event.respondWith(fetch(event.request));\n});\n\n// Activate event - clean up old caches\nself.addEventListener(\"activate\", (event) => {\n  event.waitUntil(\n    caches.keys().then((cacheNames) => {\n      return Promise.all(\n        cacheNames.map((cacheName) => {\n          if (cacheName !== CACHE_NAME) {\n            return caches.delete(cacheName);\n          }\n        }),\n      );\n    }),\n  );\n\n  self.clients.claim();\n});\n"
  },
  {
    "path": "scripts/check-pricing-config.ts",
    "content": "#!/usr/bin/env ts-node\n\nimport { PrismaClient } from \"@prisma/client\";\n\nconst prisma = new PrismaClient();\n\nasync function checkPricingConfig() {\n  console.log(\"🔍 Checking pricing configuration...\\n\");\n\n  try {\n    // Get all subscription plans\n    const plans = await prisma.subscriptionPlan.findMany({\n      include: {\n        providerMappings: true,\n      },\n      orderBy: [{ currency: \"asc\" }, { priceMonthly: \"asc\" }],\n    });\n\n    console.log(`📊 Found ${plans.length} subscription plans:\\n`);\n\n    // Group by currency\n    const plansByCurrency = plans.reduce(\n      (acc, plan) => {\n        if (!acc[plan.currency]) acc[plan.currency] = [];\n        acc[plan.currency].push(plan);\n        return acc;\n      },\n      {} as Record<string, typeof plans>,\n    );\n\n    // Display plans by currency\n    for (const [currency, currencyPlans] of Object.entries(plansByCurrency)) {\n      console.log(`💰 ${currency} Plans:`);\n      console.log(\"─\".repeat(50));\n\n      for (const plan of currencyPlans) {\n        const price = plan.priceMonthly?.toNumber() || plan.priceYearly?.toNumber() || 0;\n        const interval = plan.interval;\n        const regions = plan.availableRegions.join(\", \");\n        const mappingsCount = plan.providerMappings.length;\n\n        console.log(`📌 ${plan.id}`);\n        console.log(`   Price: ${price} ${currency}/${interval}`);\n        console.log(`   Regions: ${regions}`);\n        console.log(`   Active: ${plan.isActive ? \"✅\" : \"❌\"}`);\n        console.log(`   Provider mappings: ${mappingsCount}`);\n\n        if (plan.providerMappings.length > 0) {\n          console.log(\"   Stripe prices:\");\n          for (const mapping of plan.providerMappings) {\n            console.log(`     - ${mapping.region}: ${mapping.externalId} (${mapping.isActive ? \"active\" : \"inactive\"})`);\n          }\n        }\n        console.log(\"\");\n      }\n    }\n\n    // Check for missing configurations\n    console.log(\"\\n⚠️  Configuration checks:\");\n    console.log(\"─\".repeat(50));\n\n    const plansWithoutMappings = plans.filter((p) => p.providerMappings.length === 0);\n    if (plansWithoutMappings.length > 0) {\n      console.log(`❌ ${plansWithoutMappings.length} plans without Stripe mappings:`);\n      plansWithoutMappings.forEach((p) => console.log(`   - ${p.id}`));\n    } else {\n      console.log(\"✅ All plans have Stripe mappings\");\n    }\n\n    // Region coverage check\n    const regions = [\"EU\", \"US\", \"UK\", \"BR\", \"RU\", \"CN\", \"LATAM\"];\n    const coveredRegions = new Set(plans.flatMap((p) => p.availableRegions));\n    const missingRegions = regions.filter((r) => !coveredRegions.has(r));\n\n    if (missingRegions.length > 0) {\n      console.log(`\\n❌ Missing coverage for regions: ${missingRegions.join(\", \")}`);\n    } else {\n      console.log(\"\\n✅ All regions have pricing coverage\");\n    }\n\n    // Summary\n    console.log(\"\\n📈 Summary:\");\n    console.log(\"─\".repeat(50));\n    console.log(`Total plans: ${plans.length}`);\n    console.log(`Currencies: ${Object.keys(plansByCurrency).join(\", \")}`);\n    console.log(`Covered regions: ${Array.from(coveredRegions).join(\", \")}`);\n  } catch (error) {\n    console.error(\"❌ Error checking pricing config:\", error);\n    throw error;\n  } finally {\n    await prisma.$disconnect();\n  }\n}\n\n// Run if called directly\nif (require.main === module) {\n  checkPricingConfig()\n    .then(() => process.exit(0))\n    .catch((error) => {\n      console.error(error);\n      process.exit(1);\n    });\n}\n\nexport default checkPricingConfig;\n"
  },
  {
    "path": "scripts/import-exercises-with-attributes.prompt.md",
    "content": "Generate 50 unique fitness exercises in CSV format with the following columns:\n\n`id,name,name_en,description,description_en,full_video_url,full_video_image_url,introduction,introduction_en,slug,slug_en,attribute_name,attribute_value`\n\n## Requirements:\n\n- Each exercise should have multiple rows (one per attribute)\n- Use REALISTICS YouTube URLs for videos (format: https://www.youtube.com/watch?v=VIDEO_ID) from\n  @https://www.youtube.com/@fit-distance/videos\n- Use corresponding thumbnail URLs (format: https://img.youtube.com/vi/VIDEO_ID/maxresdefault.jpg)\n- Include HTML tags in descriptions (like <p>, <strong>, <em>)\n- Create slugs in kebab-case format\n\nUse these attribute types and ONLY these: `TYPE`: `STRENGTH, CARDIO, PLYOMETRICS, STRETCHING, FLEXIBILITY, POWERLIFTING` `PRIMARY_MUSCLE`:\n`QUADRICEPS, CHEST, BACK, SHOULDERS, BICEPS, TRICEPS, HAMSTRINGS, GLUTES, CALVES, CORE, FOREARMS` `SECONDARY_MUSCLE`:\n`QUADRICEPS, CHEST, BACK, SHOULDERS, BICEPS, TRICEPS, HAMSTRINGS, GLUTES, CALVES, CORE, FOREARMS` `EQUIPMENT`:\n`BARBELL, DUMBBELL, BODYWEIGHT, MACHINE, CABLE, RESISTANCE_BAND, KETTLEBELLS, MEDICINE_BALL` `MECHANICS_TYPE`: `COMPOUND, ISOLATION`\n\n## Example format:\n\n1,\"Squat avec barre\",\"Barbell Squat\",\"<p>Placez la barre...</p>\",\"<p>Place the\nbarbell...</p>\",https://www.youtube.com/watch?v=abc123,https://img.youtube.com/vi/abc123/maxresdefault.jpg,\"Introduction courte\",\"Short\nintroduction\",\"squat-avec-barre\",\"barbell-squat\",TYPE,STRENGTH 1,\"Squat avec barre\",\"Barbell Squat\",\"<p>Placez la barre...</p>\",\"<p>Place\nthe barbell...</p>\",https://www.youtube.com/watch?v=abc123,https://img.youtube.com/vi/abc123/maxresdefault.jpg,\"Introduction courte\",\"Short\nintroduction\",\"squat-avec-barre\",\"barbell-squat\",PRIMARY_MUSCLE,QUADRICEPS\n\nReply only with the csv.\n"
  },
  {
    "path": "scripts/import-exercises-with-attributes.ts",
    "content": "import path from \"path\";\nimport fs from \"fs\";\n\nimport csv from \"csv-parser\";\nimport { ExerciseAttributeNameEnum, ExerciseAttributeValueEnum, PrismaClient } from \"@prisma/client\";\n\nconst prisma = new PrismaClient();\n\ninterface ExerciseAttributeCSVRow {\n  id: string;\n  name: string;\n  name_en: string;\n  description: string;\n  description_en: string;\n  full_video_url: string;\n  full_video_image_url: string;\n  introduction: string;\n  introduction_en: string;\n  slug: string;\n  slug_en: string;\n  attribute_name: string;\n  attribute_value: string;\n}\n\nfunction cleanValue(value: string): string | null {\n  if (!value || value === \"NULL\" || value.trim() === \"\") return null;\n  return value.trim();\n}\n\nfunction groupExercisesByOriginalId(rows: ExerciseAttributeCSVRow[]) {\n  const exercisesMap = new Map();\n\n  for (const row of rows) {\n    const exerciseId = row.id;\n\n    if (!exercisesMap.has(exerciseId)) {\n      exercisesMap.set(exerciseId, {\n        originalId: exerciseId,\n        name: row.name,\n        nameEn: cleanValue(row.name_en),\n        description: cleanValue(row.description),\n        descriptionEn: cleanValue(row.description_en),\n        fullVideoUrl: cleanValue(row.full_video_url),\n        fullVideoImageUrl: cleanValue(row.full_video_image_url),\n        introduction: cleanValue(row.introduction),\n        introductionEn: cleanValue(row.introduction_en),\n        slug: cleanValue(row.slug),\n        slugEn: cleanValue(row.slug_en),\n        attributes: [],\n      });\n    }\n\n    const exercise = exercisesMap.get(exerciseId);\n    if (row.attribute_name && row.attribute_value) {\n      exercise.attributes.push({\n        attributeName: row.attribute_name,\n        attributeValue: row.attribute_value,\n      });\n    }\n  }\n\n  return Array.from(exercisesMap.values());\n}\n\nasync function ensureAttributeNameExists(name: ExerciseAttributeNameEnum) {\n  let attributeName = await prisma.exerciseAttributeName.findFirst({\n    where: { name },\n  });\n\n  if (!attributeName) {\n    attributeName = await prisma.exerciseAttributeName.create({\n      data: { name },\n    });\n  }\n\n  return attributeName;\n}\n\nfunction normalizeAttributeValue(value: string): ExerciseAttributeValueEnum {\n  const cleaned = value.trim().toUpperCase();\n  if ([\"N/A\", \"NA\", \"NONE\", \"NULL\", \"\"].includes(cleaned)) return \"NA\";\n  if ((Object.values(ExerciseAttributeValueEnum) as string[]).includes(cleaned)) {\n    return cleaned as ExerciseAttributeValueEnum;\n  }\n  throw new Error(`Unknown attribute value: ${value}`);\n}\n\nasync function ensureAttributeValueExists(attributeNameId: string, value: ExerciseAttributeValueEnum) {\n  let attributeValue = await prisma.exerciseAttributeValue.findFirst({\n    where: {\n      attributeNameId,\n      value,\n    },\n  });\n\n  if (!attributeValue) {\n    attributeValue = await prisma.exerciseAttributeValue.create({\n      data: {\n        attributeNameId,\n        value,\n      },\n    });\n  }\n\n  return attributeValue;\n}\n\nasync function importExercisesFromCSV(filePath: string) {\n  const rows: ExerciseAttributeCSVRow[] = [];\n\n  return new Promise<void>((resolve, reject) => {\n    fs.createReadStream(filePath)\n      .pipe(csv())\n      .on(\"data\", (row: ExerciseAttributeCSVRow) => {\n        rows.push(row);\n      })\n      .on(\"end\", async () => {\n        console.log(`📋 ${rows.length} lines found in the CSV`);\n\n        try {\n          const exercises = groupExercisesByOriginalId(rows);\n          console.log(`🏋️  ${exercises.length} unique exercises found`);\n\n          let imported = 0;\n          let errors = 0;\n\n          for (const exercise of exercises) {\n            try {\n              console.log(`\\n🔄 Processing \"${exercise.name}\"...`);\n\n              // Create or update the exercise (simplified version)\n              const createdExercise = await prisma.exercise.upsert({\n                where: { slug: exercise.slug || `exercise-${exercise.originalId}` },\n                update: {\n                  name: exercise.name,\n                  nameEn: exercise.nameEn,\n                  description: exercise.description,\n                  descriptionEn: exercise.descriptionEn,\n                  fullVideoUrl: exercise.fullVideoUrl,\n                  fullVideoImageUrl: exercise.fullVideoImageUrl,\n                  introduction: exercise.introduction,\n                  introductionEn: exercise.introductionEn,\n                  slugEn: exercise.slugEn,\n                },\n                create: {\n                  name: exercise.name,\n                  nameEn: exercise.nameEn,\n                  description: exercise.description,\n                  descriptionEn: exercise.descriptionEn,\n                  fullVideoUrl: exercise.fullVideoUrl,\n                  fullVideoImageUrl: exercise.fullVideoImageUrl,\n                  introduction: exercise.introduction,\n                  introductionEn: exercise.introductionEn,\n                  slug: exercise.slug || `exercise-${exercise.originalId}`,\n                  slugEn: exercise.slugEn,\n                },\n              });\n\n              // Remove old attributes\n              await prisma.exerciseAttribute.deleteMany({\n                where: { exerciseId: createdExercise.id },\n              });\n\n              // Create new attributes\n              for (const attr of exercise.attributes) {\n                try {\n                  const attributeName = await ensureAttributeNameExists(attr.attributeName);\n                  const attributeValue = await ensureAttributeValueExists(attributeName.id, normalizeAttributeValue(attr.attributeValue));\n\n                  await prisma.exerciseAttribute.create({\n                    data: {\n                      exerciseId: createdExercise.id,\n                      attributeNameId: attributeName.id,\n                      attributeValueId: attributeValue.id,\n                    },\n                  });\n\n                  console.log(`   ✅ Attribute: ${attr.attributeName} = ${attr.attributeValue}`);\n                } catch (attrError) {\n                  console.error(\"   ❌ Attribute error:\", attrError);\n                }\n              }\n\n              console.log(`✅ \"${exercise.name}\" imported with ${exercise.attributes.length} attributes`);\n              imported++;\n            } catch (error) {\n              console.error(`❌ Error for \"${exercise.name}\":`, error);\n              errors++;\n            }\n          }\n\n          console.log(\"\\n📊 Summary:\");\n          console.log(`   ✅ Imported: ${imported}`);\n          console.log(`   ❌ Errors: ${errors}`);\n\n          resolve();\n        } catch (error) {\n          reject(error);\n        }\n      })\n      .on(\"error\", reject);\n  });\n}\n\nasync function main() {\n  try {\n    console.log(\"🚀 Import exercises (simplified version)...\\n\");\n\n    const csvFilePath = process.argv[2];\n\n    if (!csvFilePath) {\n      console.error(\"❌ Please provide a CSV file path as argument\");\n      console.log(\"Usage: npm run import:exercises-full <path-to-csv-file>\");\n      process.exit(1);\n    }\n\n    if (!fs.existsSync(csvFilePath)) {\n      console.error(`❌ File not found: ${csvFilePath}`);\n      process.exit(1);\n    }\n\n    if (path.extname(csvFilePath).toLowerCase() !== \".csv\") {\n      console.error(`❌ File must be a CSV file, got: ${path.extname(csvFilePath)}`);\n      process.exit(1);\n    }\n\n    console.log(`📁 Importing from: ${csvFilePath}`);\n\n    await importExercisesFromCSV(csvFilePath);\n\n    // Final stats\n    const totalExercises = await prisma.exercise.count();\n    const totalAttributes = await prisma.exerciseAttribute.count();\n\n    console.log(\"\\n📈 Final database:\");\n    console.log(`   🏋️  Exercises: ${totalExercises}`);\n    console.log(`   🏷️  Attributes: ${totalAttributes}`);\n\n    console.log(\"\\n🎉 Import completed!\");\n  } catch (error) {\n    console.error(\"💥 Error:\", error);\n    process.exit(1);\n  } finally {\n    await prisma.$disconnect();\n  }\n}\n\nmain();\n"
  },
  {
    "path": "scripts/seed-leaderboard-data.ts",
    "content": "#!/usr/bin/env ts-node\nimport dayjs from \"dayjs\";\nimport { PrismaClient, ExerciseAttributeValueEnum } from \"@prisma/client\";\nconst prisma = new PrismaClient();\n\n/**\n * Seed leaderboard data with sample users and workout sessions\n * Creates a variety of workout streaks to demonstrate the leaderboard functionality\n */\nasync function seedLeaderboardData() {\n  console.log(\"🌱 Seeding leaderboard data with sample users and workout sessions...\");\n\n  try {\n    // Sample users data - simplified to focus on total workout sessions\n    const usersData = [\n      {\n        id: \"user_warrior_sarah\",\n        name: \"Sarah Warrior\",\n        email: \"sarah.warrior@example.com\",\n        image: \"https://api.dicebear.com/7.x/micah/svg?seed=Sarah\",\n        totalSessions: 150,\n      },\n      {\n        id: \"user_consistent_mary\",\n        name: \"Mary Consistency\",\n        email: \"mary.consistent@example.com\",\n        image: \"https://api.dicebear.com/7.x/micah/svg?seed=Mary\",\n        totalSessions: 120,\n      },\n      {\n        id: \"user_streak_champion\",\n        name: \"Alex Thunder\",\n        email: \"alex.thunder@example.com\",\n        image: \"https://api.dicebear.com/7.x/micah/svg?seed=Alex\",\n        totalSessions: 90,\n      },\n      {\n        id: \"user_comeback_lisa\",\n        name: \"Lisa Comeback\",\n        email: \"lisa.comeback@example.com\",\n        image: \"https://api.dicebear.com/7.x/micah/svg?seed=Lisa\",\n        totalSessions: 80,\n      },\n      {\n        id: \"user_fitness_john\",\n        name: \"John Fitness\",\n        email: \"john.fitness@example.com\",\n        image: \"https://api.dicebear.com/7.x/micah/svg?seed=John\",\n        totalSessions: 64,\n      },\n      {\n        id: \"user_yoga_emma\",\n        name: \"Emma Zen\",\n        email: \"emma.zen@example.com\",\n        image: \"https://api.dicebear.com/7.x/micah/svg?seed=Emma\",\n        totalSessions: 56,\n      },\n      {\n        id: \"user_machine_tom\",\n        name: \"Tom Machine\",\n        email: \"tom.machine@example.com\",\n        image: \"https://api.dicebear.com/7.x/micah/svg?seed=Tom\",\n        totalSessions: 50,\n      },\n      {\n        id: \"user_beginner_mike\",\n        name: \"Mike Beginner\",\n        email: \"mike.beginner@example.com\",\n        image: \"https://api.dicebear.com/7.x/micah/svg?seed=Mike\",\n        totalSessions: 24,\n      },\n    ];\n\n    // Create users\n    console.log(\"👤 Creating sample users...\");\n    for (const userData of usersData) {\n      await prisma.user.upsert({\n        where: { id: userData.id },\n        update: {\n          name: userData.name,\n          email: userData.email,\n          image: userData.image,\n        },\n        create: {\n          id: userData.id,\n          name: userData.name,\n          email: userData.email,\n          image: userData.image,\n          emailVerified: true,\n          createdAt: dayjs().subtract(60, \"days\").toDate(),\n          updatedAt: new Date(),\n        },\n      });\n    }\n\n    console.log(\"💪 Creating workout sessions to generate streaks...\");\n\n    // Create workout sessions for each user based on their streak data\n    for (const userData of usersData) {\n      console.log(`  📋 Creating sessions for ${userData.name}...`);\n\n      const sessionsToCreate = [];\n\n      // Create workout sessions to match the total sessions count\n      for (let i = 0; i < userData.totalSessions; i++) {\n        const daysAgo = Math.floor(Math.random() * 59) + 1; // 1-60 days ago\n        const sessionDate = dayjs().subtract(daysAgo, \"days\");\n\n        // Force time to be at least 2 hours ago to account for timezone differences\n        const maxHour = Math.min(22, dayjs().hour() - 2); // At least 2 hours ago\n        const startTime = sessionDate.hour(Math.floor(Math.random() * Math.max(1, maxHour)) + 6);\n\n        const sessionDuration = 30 + Math.floor(Math.random() * 60);\n\n        sessionsToCreate.push({\n          userId: userData.id,\n          startedAt: startTime.toDate(),\n          endedAt: startTime.add(sessionDuration, \"minutes\").toDate(), // Always set endedAt for completed sessions\n          duration: sessionDuration * 60, // Convert to seconds\n          muscles: [ExerciseAttributeValueEnum.CHEST, ExerciseAttributeValueEnum.SHOULDERS, ExerciseAttributeValueEnum.TRICEPS], // Sample muscle groups\n        });\n      }\n\n      // Create all sessions for this user\n      for (const sessionData of sessionsToCreate) {\n        await prisma.workoutSession.create({\n          data: sessionData,\n        });\n      }\n    }\n    console.log(`\n📊 Summary:\n- Created ${usersData.length} sample users with workout sessions\n- Generated realistic completed workout sessions with proper endedAt timestamps\n- Leaderboard rankings by total workouts:\n  🥇 Sarah Warrior: 150 workouts\n  🥈 Mary Consistency: 120 workouts\n  🥉 Alex Thunder: 90 workouts\n    `);\n  } catch (error) {\n    console.error(\"❌ Error seeding leaderboard data:\", error);\n    throw error;\n  } finally {\n    await prisma.$disconnect();\n  }\n}\n\n// Run the seed function\nif (require.main === module) {\n  seedLeaderboardData()\n    .then(() => {\n      console.log(\"🎉 Leaderboard data seeded successfully!\\n\");\n      process.exit(0);\n    })\n    .catch((error) => {\n      console.error(\"💥 Leaderboard seeding failed:\", error);\n      process.exit(1);\n    });\n}\n\nexport default seedLeaderboardData;\n"
  },
  {
    "path": "scripts/seed-multi-region-plans.ts",
    "content": "#!/usr/bin/env ts-node\n\nimport { PrismaClient, PaymentProcessor } from \"@prisma/client\";\n\nimport { env } from \"@/env\";\n\nconst prisma = new PrismaClient();\n\ninterface RegionPricing {\n  region: string;\n  currency: string;\n  monthlyPrice: number;\n  yearlyPrice: number;\n  stripeMonthlyPriceId?: string;\n  stripeYearlyPriceId?: string;\n}\n\n// Prix adaptés par région\nconst regionPricing: RegionPricing[] = [\n  {\n    region: \"EU\",\n    currency: \"EUR\",\n    monthlyPrice: 7.9,\n    yearlyPrice: 49,\n    stripeMonthlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_EU,\n    stripeYearlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_EU,\n  },\n  {\n    region: \"US\",\n    currency: \"USD\",\n    monthlyPrice: 9.99,\n    yearlyPrice: 59,\n    stripeMonthlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_US,\n    stripeYearlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_US,\n  },\n  {\n    region: \"LATAM\", // Espagnol + Portugais (hors Brésil)\n    currency: \"USD\",\n    monthlyPrice: 4.99,\n    yearlyPrice: 29,\n    stripeMonthlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_LATAM,\n    stripeYearlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_LATAM,\n  },\n  {\n    region: \"BR\", // Brésil spécifiquement\n    currency: \"BRL\",\n    monthlyPrice: 19.9,\n    yearlyPrice: 119,\n    stripeMonthlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_BR,\n    stripeYearlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_BR,\n  },\n  {\n    region: \"RU\", // Russie\n    currency: \"RUB\",\n    monthlyPrice: 299,\n    yearlyPrice: 1790,\n    stripeMonthlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_RU,\n    stripeYearlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_RU,\n  },\n  {\n    region: \"CN\", // Chine\n    currency: \"CNY\",\n    monthlyPrice: 39,\n    yearlyPrice: 239,\n    stripeMonthlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_CN,\n    stripeYearlyPriceId: env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_CN,\n  },\n];\n\nasync function seedMultiRegionPlans() {\n  console.log(\"🌍 Seeding multi-region subscription plans...\");\n\n  try {\n    // Pour chaque région, créer les plans\n    for (const pricing of regionPricing) {\n      console.log(`\\n📍 Creating plans for ${pricing.region}...`);\n\n      // Plan mensuel\n      const monthlyPlanId = `premium-monthly-${pricing.region.toLowerCase()}`;\n      // const monthlyPlan = await prisma.subscriptionPlan.upsert({\n      //   where: { id: monthlyPlanId },\n      //   update: {\n      //     priceMonthly: pricing.monthlyPrice,\n      //     currency: pricing.currency,\n      //   },\n      //   create: {\n      //     id: monthlyPlanId,\n      //     priceMonthly: pricing.monthlyPrice,\n      //     priceYearly: 0,\n      //     currency: pricing.currency,\n      //     interval: \"month\",\n      //     isActive: true,\n      //     availableRegions: [pricing.region],\n      //   },\n      // });\n\n      // Plan annuel\n      const yearlyPlanId = `premium-yearly-${pricing.region.toLowerCase()}`;\n      // const yearlyPlan = await prisma.subscriptionPlan.upsert({\n      //   where: { id: yearlyPlanId },\n      //   update: {\n      //     priceYearly: pricing.yearlyPrice,\n      //     currency: pricing.currency,\n      //   },\n      //   create: {\n      //     id: yearlyPlanId,\n      //     priceMonthly: 0,\n      //     priceYearly: pricing.yearlyPrice,\n      //     currency: pricing.currency,\n      //     interval: \"year\",\n      //     isActive: true,\n      //     availableRegions: [pricing.region],\n      //   },\n      // });\n\n      // Créer les mappings Stripe si les IDs existent\n      if (pricing.stripeMonthlyPriceId) {\n        await prisma.planProviderMapping.upsert({\n          where: {\n            planId_provider_region: {\n              planId: monthlyPlanId,\n              provider: PaymentProcessor.STRIPE,\n              region: pricing.region,\n            },\n          },\n          update: {},\n          create: {\n            planId: monthlyPlanId,\n            provider: PaymentProcessor.STRIPE,\n            externalId: pricing.stripeMonthlyPriceId,\n            region: pricing.region,\n            isActive: true,\n          },\n        });\n      }\n\n      if (pricing.stripeYearlyPriceId) {\n        await prisma.planProviderMapping.upsert({\n          where: {\n            planId_provider_region: {\n              planId: yearlyPlanId,\n              provider: PaymentProcessor.STRIPE,\n              region: pricing.region,\n            },\n          },\n          update: {},\n          create: {\n            planId: yearlyPlanId,\n            provider: PaymentProcessor.STRIPE,\n            externalId: pricing.stripeYearlyPriceId,\n            region: pricing.region,\n            isActive: true,\n          },\n        });\n      }\n\n      console.log(`✅ Created plans for ${pricing.region}:`);\n      console.log(`   - Monthly: ${pricing.monthlyPrice} ${pricing.currency}`);\n      console.log(`   - Yearly: ${pricing.yearlyPrice} ${pricing.currency}`);\n    }\n\n    console.log(\"\\n✅ Multi-region plans seeded successfully!\");\n    console.log(`\n📊 Summary:\n- Created plans for ${regionPricing.length} regions\n- Currencies: EUR, USD, BRL, RUB, CNY\n- Total plans: ${regionPricing.length * 2} (monthly + yearly)\n\n🔧 Next steps:\n1. Create corresponding Stripe prices for each region\n2. Update environment variables with Stripe price IDs\n3. Test with different Accept-Language headers\n    `);\n  } catch (error) {\n    console.error(\"❌ Error seeding plans:\", error);\n    throw error;\n  } finally {\n    await prisma.$disconnect();\n  }\n}\n\n// Run if called directly\nif (require.main === module) {\n  seedMultiRegionPlans()\n    .then(() => process.exit(0))\n    .catch((error) => {\n      console.error(error);\n      process.exit(1);\n    });\n}\n\nexport default seedMultiRegionPlans;\n"
  },
  {
    "path": "scripts/seed-subscription-plans-simple.ts",
    "content": "#!/usr/bin/env ts-node\n\nimport { PrismaClient, PaymentProcessor } from \"@prisma/client\";\n\nimport { env } from \"@/env\";\n\nconst prisma = new PrismaClient();\n\n/**\n * Seed subscription plans - KISS approach\n * Simplified version without translations (handled client-side)\n */\nasync function seedSubscriptionPlans() {\n  console.log(\"🌱 Seeding subscription plans (KISS approach)...\");\n\n  try {\n    // Create monthly plan\n    const monthlyPlan = await prisma.subscriptionPlan.upsert({\n      where: { id: \"premium-monthly\" },\n      update: {},\n      create: {\n        id: \"premium-monthly\",\n        priceMonthly: 7.9,\n        priceYearly: 0,\n        currency: \"EUR\",\n        interval: \"month\",\n        isActive: true,\n        availableRegions: [\"EU\", \"US\", \"UK\"],\n      },\n    });\n\n    // Create yearly plan\n    const yearlyPlan = await prisma.subscriptionPlan.upsert({\n      where: { id: \"premium-yearly\" },\n      update: {},\n      create: {\n        id: \"premium-yearly\",\n        priceMonthly: 0,\n        priceYearly: 49.0,\n        currency: \"EUR\",\n        interval: \"year\",\n        isActive: true,\n        availableRegions: [\"EU\", \"US\", \"UK\"],\n      },\n    });\n\n    console.log(\"✅ Created subscription plans:\", {\n      monthly: monthlyPlan.id,\n      yearly: yearlyPlan.id,\n    });\n\n    // Create Stripe provider mappings\n    console.log(\"🔗 Creating Stripe provider mappings...\");\n\n    // Monthly plan Stripe mapping\n    await prisma.planProviderMapping.upsert({\n      where: {\n        planId_provider_region: {\n          planId: monthlyPlan.id,\n          provider: PaymentProcessor.STRIPE,\n          region: \"EU\",\n        },\n      },\n      update: {},\n      create: {\n        planId: monthlyPlan.id,\n        provider: PaymentProcessor.STRIPE,\n        externalId: env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_EU || \"price_monthly_fallback\",\n        region: \"EU\",\n        metadata: {\n          description: \"Stripe monthly subscription for EU region\",\n        },\n        isActive: true,\n      },\n    });\n\n    // Yearly plan Stripe mapping\n    await prisma.planProviderMapping.upsert({\n      where: {\n        planId_provider_region: {\n          planId: yearlyPlan.id,\n          provider: PaymentProcessor.STRIPE,\n          region: \"EU\",\n        },\n      },\n      update: {},\n      create: {\n        planId: yearlyPlan.id,\n        provider: PaymentProcessor.STRIPE,\n        externalId: env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_EU || \"price_yearly_fallback\",\n        region: \"EU\",\n        metadata: {\n          description: \"Stripe yearly subscription for EU region\",\n        },\n        isActive: true,\n      },\n    });\n\n    console.log(\"✅ Created Stripe provider mappings\");\n\n    console.log(\"✅ Subscription plans seeded successfully!\");\n    console.log(`\n📋 Summary:\n- Created 2 subscription plans (monthly: €7.90, yearly: €49.00)\n- Created Stripe provider mappings for EU region\n- Names, descriptions, features handled client-side with i18n\n- Ready for multi-region and multi-provider expansion\n\n🔧 Next steps:\n1. Set up your Stripe price IDs in environment variables:\n   - NEXT_PUBLIC_STRIPE_PRICE_MONTHLY\n   - NEXT_PUBLIC_STRIPE_PRICE_YEARLY\n2. Test the premium system with: npm run dev\n3. Visit /premium to see the new plans\n    `);\n  } catch (error) {\n    console.error(\"❌ Error seeding subscription plans:\", error);\n    throw error;\n  } finally {\n    await prisma.$disconnect();\n  }\n}\n\n// Run the seed function\nif (require.main === module) {\n  seedSubscriptionPlans()\n    .then(() => {\n      console.log(\"🎉 Seeding completed!\");\n      process.exit(0);\n    })\n    .catch((error) => {\n      console.error(\"💥 Seeding failed:\", error);\n      process.exit(1);\n    });\n}\n\nexport default seedSubscriptionPlans;\n"
  },
  {
    "path": "scripts/seed-workout-data-advanced.ts",
    "content": "import { prisma } from \"../src/shared/lib/prisma\";\n\n// Configuration\nconst USER_ID = \"bwZuBQO4cJgBX6NiZaXgv81vKfgBQcFe\";\nconst BENCH_PRESS_ID = \"cmbw9xso904p69kv1vwuadhx6\"; // Développé couché à la barre prise large\n\ninterface WorkoutPattern {\n  dayOfWeek: number; // 0 = Sunday, 1 = Monday, etc.\n  hour: number;\n  exercisePatterns: ExercisePattern[];\n}\n\ninterface ExercisePattern {\n  exerciseId: string;\n  sets: SetPattern[];\n  progressionRate: number; // % increase per week\n}\n\ninterface SetPattern {\n  repsRange: [number, number];\n  weightPercentage: number; // Percentage of working weight\n}\n\n// Realistic workout patterns\nconst workoutPatterns: WorkoutPattern[] = [\n  {\n    dayOfWeek: 1, // Monday - Chest day\n    hour: 10,\n    exercisePatterns: [\n      {\n        exerciseId: BENCH_PRESS_ID,\n        sets: [\n          { repsRange: [12, 15], weightPercentage: 60 }, // Warm-up\n          { repsRange: [10, 12], weightPercentage: 70 }, // Warm-up\n          { repsRange: [8, 10], weightPercentage: 85 }, // Working set\n          { repsRange: [6, 8], weightPercentage: 100 }, // Working set\n          { repsRange: [6, 8], weightPercentage: 100 }, // Working set\n          { repsRange: [8, 10], weightPercentage: 90 }, // Back-off set\n        ],\n        progressionRate: 2.5, // 2.5% per week\n      },\n    ],\n  },\n  {\n    dayOfWeek: 4, // Thursday - Upper body\n    hour: 16,\n    exercisePatterns: [\n      {\n        exerciseId: BENCH_PRESS_ID,\n        sets: [\n          { repsRange: [12, 15], weightPercentage: 50 }, // Warm-up\n          { repsRange: [10, 12], weightPercentage: 65 }, // Warm-up\n          { repsRange: [8, 10], weightPercentage: 80 }, // Lighter day\n          { repsRange: [8, 10], weightPercentage: 80 },\n          { repsRange: [10, 12], weightPercentage: 75 },\n        ],\n        progressionRate: 2.5,\n      },\n    ],\n  },\n];\n\nasync function seedAdvancedWorkoutData(weeksToGenerate: number = 12, startingWeight: number = 60) {\n  console.log(`Starting to seed ${weeksToGenerate} weeks of workout data...`);\n  console.log(`Starting bench press weight: ${startingWeight}kg`);\n\n  try {\n    const today = new Date();\n    let totalSessionsCreated = 0;\n    let totalSetsCreated = 0;\n\n    // Generate data week by week\n    for (let week = weeksToGenerate - 1; week >= 0; week--) {\n      const weekStart = new Date(today);\n      weekStart.setDate(today.getDate() - week * 7);\n\n      // Calculate current working weight with progression\n      const weeksCompleted = weeksToGenerate - 1 - week;\n      const progressionMultiplier = Math.pow(1 + workoutPatterns[0].exercisePatterns[0].progressionRate / 100, weeksCompleted);\n      const currentWorkingWeight = startingWeight * progressionMultiplier;\n\n      console.log(`\\nWeek ${weeksCompleted + 1}: Working weight = ${currentWorkingWeight.toFixed(1)}kg`);\n\n      // Generate sessions for each pattern in the week\n      for (const pattern of workoutPatterns) {\n        // Calculate the date for this workout\n        const sessionDate = new Date(weekStart);\n        const daysUntilWorkout = (pattern.dayOfWeek - weekStart.getDay() + 7) % 7;\n        sessionDate.setDate(weekStart.getDate() + daysUntilWorkout);\n        sessionDate.setHours(pattern.hour, 0, 0, 0);\n\n        // Skip if the date is in the future\n        if (sessionDate > today) continue;\n\n        // Add some randomness to simulate real life (10% chance to skip a workout)\n        if (Math.random() < 0.1 && week > 0) {\n          console.log(`  Skipped workout on ${sessionDate.toLocaleDateString()} (simulating missed session)`);\n          continue;\n        }\n\n        // Create workout session\n        const duration = 45 + Math.floor(Math.random() * 30); // 45-75 minutes\n        const workoutSession = await prisma.workoutSession.create({\n          data: {\n            userId: USER_ID,\n            startedAt: sessionDate,\n            endedAt: new Date(sessionDate.getTime() + duration * 60 * 1000),\n            duration: duration * 60,\n          },\n        });\n\n        totalSessionsCreated++;\n        console.log(`  Created session on ${sessionDate.toLocaleDateString()} (${pattern.dayOfWeek === 1 ? \"Heavy\" : \"Light\"} day)`);\n\n        // Create exercises and sets for this session\n        for (let exerciseIndex = 0; exerciseIndex < pattern.exercisePatterns.length; exerciseIndex++) {\n          const exercisePattern = pattern.exercisePatterns[exerciseIndex];\n\n          const workoutSessionExercise = await prisma.workoutSessionExercise.create({\n            data: {\n              workoutSessionId: workoutSession.id,\n              exerciseId: exercisePattern.exerciseId,\n              order: exerciseIndex,\n            },\n          });\n\n          // Create sets according to the pattern\n          for (let setIndex = 0; setIndex < exercisePattern.sets.length; setIndex++) {\n            const setPattern = exercisePattern.sets[setIndex];\n\n            // Calculate actual weight for this set\n            let setWeight = (currentWorkingWeight * setPattern.weightPercentage) / 100;\n\n            // Add small random variation (±2.5%)\n            setWeight *= 0.975 + Math.random() * 0.05;\n\n            // Round to nearest 2.5kg\n            setWeight = Math.round(setWeight / 2.5) * 2.5;\n            setWeight = Math.max(20, setWeight); // Minimum 20kg (empty barbell)\n\n            // Calculate reps with some variation\n            const repsRange = setPattern.repsRange;\n            const reps = repsRange[0] + Math.floor(Math.random() * (repsRange[1] - repsRange[0] + 1));\n\n            // Occasionally fail a rep on heavy sets\n            const failedRep = setPattern.weightPercentage >= 95 && Math.random() < 0.15;\n            const actualReps = failedRep ? Math.max(1, reps - 1) : reps;\n\n            await prisma.workoutSet.create({\n              data: {\n                workoutSessionExerciseId: workoutSessionExercise.id,\n                setIndex: setIndex,\n                types: [\"REPS\", \"WEIGHT\"],\n                type: \"WEIGHT\",\n                valuesInt: [actualReps, Math.round(setWeight)],\n                valuesSec: [],\n                units: [\"kg\"],\n                completed: true,\n              },\n            });\n\n            totalSetsCreated++;\n          }\n\n          console.log(\n            `    Added ${exercisePattern.sets.length} sets (${exercisePattern.sets[0].repsRange[0]}-${exercisePattern.sets[exercisePattern.sets.length - 1].repsRange[1]} reps)`,\n          );\n        }\n      }\n    }\n\n    // Add some recent incomplete sessions\n    console.log(\"\\nAdding recent incomplete/planned sessions...\");\n\n    for (let i = 0; i < 2; i++) {\n      const futureDate = new Date(today);\n      futureDate.setDate(today.getDate() + i + 1);\n      futureDate.setHours(18, 0, 0, 0);\n\n      const workoutSession = await prisma.workoutSession.create({\n        data: {\n          userId: USER_ID,\n          startedAt: futureDate,\n        },\n      });\n\n      await prisma.workoutSessionExercise.create({\n        data: {\n          workoutSessionId: workoutSession.id,\n          exerciseId: BENCH_PRESS_ID,\n          order: 0,\n        },\n      });\n\n      console.log(`  Created planned session for ${futureDate.toLocaleDateString()}`);\n    }\n\n    console.log(\"\\n✅ Successfully seeded workout data!\");\n    console.log(\"\\nSummary:\");\n    console.log(`- Total sessions created: ${totalSessionsCreated}`);\n    console.log(`- Total sets created: ${totalSetsCreated}`);\n    console.log(`- Average sets per session: ${(totalSetsCreated / totalSessionsCreated).toFixed(1)}`);\n    console.log(`- Final working weight: ${(startingWeight * Math.pow(1.025, weeksToGenerate - 1)).toFixed(1)}kg`);\n  } catch (error) {\n    console.error(\"Error seeding data:\", error);\n    throw error;\n  } finally {\n    await prisma.$disconnect();\n  }\n}\n\n// Parse command line arguments\nconst args = process.argv.slice(2);\nconst weeks = args[0] ? parseInt(args[0]) : 12;\nconst startWeight = args[1] ? parseInt(args[1]) : 60;\n\nif (isNaN(weeks) || isNaN(startWeight)) {\n  console.error(\"Usage: tsx seed-workout-data-advanced.ts [weeks] [startingWeight]\");\n  console.error(\"Example: tsx seed-workout-data-advanced.ts 12 60\");\n  process.exit(1);\n}\n\n// Run the seed script\nseedAdvancedWorkoutData(weeks, startWeight).catch((error) => {\n  console.error(\"Fatal error:\", error);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/setup.sh",
    "content": "#!/bin/sh\n\necho \"Running Prisma migrations...\"\nnpx prisma migrate deploy\n\necho \"Generating Prisma client...\"\nnpx prisma generate\n\nif [ \"$SEED_SAMPLE_DATA\" = \"true\"  ]; then\n  echo \"Seed sample data enabled, importing sample data...\"\n    # Import exercises if CSV exists\n    if [ -f \"./data/sample-exercises.csv\" ]; then\n        npx tsx scripts/import-exercises-with-attributes.ts ./data/sample-exercises.csv\n    else\n        echo \"No exercises sample data found, skipping import.\"\n    fi\nelse\n  echo \"Skipping sample data import.\"\nfi\n\necho \"Starting the app...\"\nexec \"$@\"  # runs the CMD from the Dockerfile\n"
  },
  {
    "path": "src/components/ads/AdBlockerForPremium.tsx",
    "content": "\"use client\";\n\nimport Script from \"next/script\";\n\nimport { useUserSubscription } from \"@/features/ads/hooks/useUserSubscription\";\n\nexport function AdBlockerForPremium() {\n  const { isPremium, isPending } = useUserSubscription();\n\n  if (isPending || !isPremium) {\n    return null;\n  }\n\n  return (\n    <Script\n      dangerouslySetInnerHTML={{\n        __html: `\n          // Bloquer les annonces automatiques pour les utilisateurs premium\n          (function() {\n            // Désactiver les requêtes de publicité\n            if (window.adsbygoogle) {\n              window.adsbygoogle.pauseAdRequests = 1;\n            }\n\n            // CSS pour cacher les pubs auto\n            const style = document.createElement('style');\n            style.textContent = \\`\n              .google-auto-placed,\n              [data-ad-layout=\"in-article\"],\n              [data-auto-format],\n              ins.adsbygoogle[data-ad-status=\"unfilled\"],\n              .adsbygoogle-noablate {\n                display: none !important;\n                visibility: hidden !important;\n                height: 0 !important;\n                width: 0 !important;\n                overflow: hidden !important;\n              }\n            \\`;\n            document.head.appendChild(style);\n\n            // Empêcher l'injection de nouvelles pubs\n            const originalPush = Array.prototype.push;\n            if (window.adsbygoogle) {\n              window.adsbygoogle.push = function(...args) {\n                console.log('Blocking auto ad push for premium user');\n                return 0;\n              };\n            }\n          })();\n        `,\n      }}\n      id=\"block-auto-ads\"\n      strategy=\"afterInteractive\"\n    />\n  );\n}\n"
  },
  {
    "path": "src/components/ads/AdPlaceholder.tsx",
    "content": "interface AdPlaceholderProps {\n  width: string;\n  height: string;\n  type?: string;\n}\n\nexport function AdPlaceholder({ width, height, type = \"Ad\" }: AdPlaceholderProps) {\n  return (\n    <div\n      className=\"flex items-center justify-center bg-gray-200 dark:bg-gray-700 border-2 border-dashed border-gray-400 dark:border-gray-500 rounded\"\n      style={{ width, height }}\n    >\n      <span className=\"text-gray-500 dark:text-gray-400 font-medium text-center\">\n        {type} {width} × {height}\n      </span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ads/AdSenseAutoAds.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\n\nimport { useUserSubscription } from \"@/features/ads/hooks/useUserSubscription\";\n\ndeclare global {\n  interface Window {\n    adsbygoogle: any[];\n  }\n}\n\nexport function AdSenseAutoAds() {\n  const { isPremium, isPending } = useUserSubscription();\n\n  useEffect(() => {\n    // Si l'utilisateur est premium, on désactive les pubs automatiques\n    if (isPremium && !isPending) {\n      // Méthode 1: Désactiver via l'API AdSense\n      if (window.adsbygoogle) {\n        // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n        // @ts-ignore\n        window.adsbygoogle.pauseAdRequests = 1;\n      }\n\n      // Méthode 3: Bloquer l'injection de nouvelles pubs via MutationObserver\n      const observer = new MutationObserver((mutations) => {\n        mutations.forEach((mutation) => {\n          mutation.addedNodes.forEach((node) => {\n            if (node instanceof HTMLElement) {\n              if (node.classList.contains(\"adsbygoogle\")) {\n                node.remove();\n              }\n            }\n          });\n        });\n      });\n\n      observer.observe(document.body, {\n        childList: true,\n        subtree: true,\n      });\n\n      // Nettoyer l'observer quand le composant est démonté\n      return () => observer.disconnect();\n    } else if (!isPremium && !isPending) {\n      // Réactiver les pubs pour les utilisateurs non-premium\n      if (window.adsbygoogle) {\n        // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n        // @ts-ignore\n        window.adsbygoogle.pauseAdRequests = 0;\n      }\n    }\n  }, [isPremium, isPending]);\n\n  // Ne rendre rien - ce composant gère seulement la logique\n  return null;\n}\n"
  },
  {
    "path": "src/components/ads/AdWrapper.tsx",
    "content": "\"use client\";\n\nimport { ReactNode } from \"react\";\n\nimport { useUserSubscription } from \"@/features/ads/hooks/useUserSubscription\";\nimport { env } from \"@/env\";\n\ninterface AdWrapperProps {\n  children: ReactNode;\n  fallback?: ReactNode;\n  forceShow?: boolean;\n}\n\nexport function AdWrapper({ children, fallback = null, forceShow = false }: AdWrapperProps) {\n  const { isPremium, isPending } = useUserSubscription();\n\n  if (!env.NEXT_PUBLIC_SHOW_ADS) {\n    return null;\n  }\n\n  // Show ads in development to preview layout\n  if (process.env.NODE_ENV === \"development\") {\n    return <>{children}</>;\n  }\n\n  // Force show ads in development if forceShow is true\n  if (forceShow) {\n    return <>{children}</>;\n  }\n\n  // Don't show ads while loading to prevent layout shift\n  if (isPending) {\n    return <>{fallback}</>;\n  }\n\n  // TODO: don't show ads to self hosted users\n  // Don't show ads to premium users\n  if (isPremium) {\n    return <>{fallback}</>;\n  }\n\n  return <>{children}</>;\n}\n"
  },
  {
    "path": "src/components/ads/EzoicAd.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\n\ninterface EzoicAdProps {\n  placementId: string;\n  className?: string;\n}\n\ndeclare global {\n  interface Window {\n    ezstandalone: {\n      cmd: Array<() => void>;\n      showAds: (placementId?: string | string[]) => void;\n    };\n  }\n}\n\nexport function EzoicAd({ placementId, className = \"\" }: EzoicAdProps) {\n  const divId = `ezoic-pub-ad-placeholder-${placementId}`;\n\n  useEffect(() => {\n    const loadEzoicAd = () => {\n      if (typeof window !== \"undefined\" && window.ezstandalone) {\n        try {\n          window.ezstandalone.cmd = window.ezstandalone.cmd || [];\n          window.ezstandalone.cmd.push(function () {\n            if (window.ezstandalone && window.ezstandalone.showAds) {\n              window.ezstandalone.showAds(placementId);\n            }\n          });\n        } catch (error) {\n          console.error(\"Error loading Ezoic ad:\", error);\n        }\n      }\n    };\n\n    // Delay slightly to ensure Ezoic scripts are loaded\n    const timeoutId = setTimeout(loadEzoicAd, 100);\n\n    return () => clearTimeout(timeoutId);\n  }, [placementId]);\n\n  return <div className={className} id={divId} />;\n}\n"
  },
  {
    "path": "src/components/ads/GoogleAdSense.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\ninterface GoogleAdSenseProps {\n  adClient: string;\n  adSlot: string;\n  adFormat?: string;\n  fullWidthResponsive?: boolean;\n  style?: React.CSSProperties;\n  className?: string;\n}\n\ndeclare global {\n  interface Window {\n    adsbygoogle: any[];\n  }\n}\n\nexport function GoogleAdSense({\n  adClient,\n  adSlot,\n  adFormat = \"auto\",\n  fullWidthResponsive = false,\n  style = { display: \"block\" },\n  className = \"\",\n}: GoogleAdSenseProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    let isAdLoaded = false;\n    let resizeObserver: ResizeObserver | null = null;\n    let checkInterval: NodeJS.Timeout | null = null;\n    let safetyTimeout: NodeJS.Timeout | null = null;\n\n    const loadAdSense = () => {\n      if (!containerRef.current || isAdLoaded) return false;\n\n      const width = containerRef.current.offsetWidth;\n      const computedStyle = window.getComputedStyle(containerRef.current);\n      const isVisible = computedStyle.display !== \"none\" && computedStyle.visibility !== \"hidden\";\n      \n      if (width > 0 && isVisible) {\n        try {\n          (window.adsbygoogle = window.adsbygoogle || []).push({});\n          isAdLoaded = true;\n        } catch (error) {\n          console.error(\"Error loading Google AdSense:\", error);\n        }\n        return true; // Chargement réussi\n      }\n      return false; // Pas encore prêt\n    };\n\n    // Attendre un court délai pour s'assurer que le DOM est prêt\n    const timeoutId = setTimeout(() => {\n      // Méthode avec ResizeObserver (plus élégante)\n      if (typeof ResizeObserver !== \"undefined\") {\n        resizeObserver = new ResizeObserver((entries) => {\n          for (const entry of entries) {\n            if (entry.contentRect.width > 0 && !isAdLoaded) {\n              loadAdSense();\n              if (isAdLoaded && resizeObserver) {\n                resizeObserver.disconnect();\n                break;\n              }\n            }\n          }\n        });\n\n        if (containerRef.current) {\n          // Essayer de charger immédiatement si possible\n          loadAdSense();\n          \n          // Si pas encore chargé, observer les changements\n          if (!isAdLoaded && resizeObserver) {\n            resizeObserver.observe(containerRef.current);\n          }\n        }\n      } else {\n        // Fallback avec setInterval pour les navigateurs anciens\n        checkInterval = setInterval(() => {\n          if (loadAdSense() && checkInterval) {\n            clearInterval(checkInterval);\n          }\n        }, 100);\n\n        // Timeout de sécurité pour éviter les boucles infinies\n        safetyTimeout = setTimeout(() => {\n          if (checkInterval) clearInterval(checkInterval);\n        }, 5000);\n      }\n    }, 100);\n\n    return () => {\n      if (timeoutId) clearTimeout(timeoutId);\n      if (resizeObserver) resizeObserver.disconnect();\n      if (checkInterval) clearInterval(checkInterval);\n      if (safetyTimeout) clearTimeout(safetyTimeout);\n    };\n  }, []);\n\n  return (\n    <div className=\"overflow-hidden\" ref={containerRef} style={{ maxWidth: \"100%\", maxHeight: style?.maxHeight || \"auto\" }}>\n      <ins\n        className={`adsbygoogle ${className}`}\n        data-ad-client={adClient}\n        data-ad-format={adFormat}\n        data-ad-slot={adSlot}\n        data-full-width-responsive={fullWidthResponsive}\n        style={style}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ads/HorizontalAdBanner.tsx",
    "content": "\"use client\";\n\nimport { env } from \"@/env\";\n\nimport { GoogleAdSense } from \"./GoogleAdSense\";\nimport { EzoicAd } from \"./EzoicAd\";\nimport { AdWrapper } from \"./AdWrapper\";\nimport { AdPlaceholder } from \"./AdPlaceholder\";\n\ninterface HorizontalAdBannerProps {\n  adSlot?: string;\n  ezoicPlacementId?: string;\n}\n\nexport function HorizontalAdBanner({ adSlot, ezoicPlacementId }: HorizontalAdBannerProps) {\n  const isDevelopment = process.env.NODE_ENV === \"development\";\n  const useEzoic = env.NEXT_PUBLIC_AD_PROVIDER === \"ezoic\" && ezoicPlacementId;\n\n  if (!env.NEXT_PUBLIC_AD_CLIENT && !useEzoic) {\n    return null;\n  }\n\n  return (\n    <AdWrapper>\n      <div\n        className=\"w-full max-w-full\"\n        style={{\n          minHeight: \"auto\",\n          width: \"100%\",\n          maxHeight: \"90px\",\n          height: \"90px\",\n        }}\n      >\n        <div className=\"py-1 flex justify-center w-full\">\n          {isDevelopment ? (\n            <AdPlaceholder height=\"90px\" type=\"Horizontal Ad Banner\" width=\"100%\" />\n          ) : useEzoic ? (\n            <div className=\"responsive-ad-container\">\n              <EzoicAd className=\"w-full h-full\" placementId={ezoicPlacementId} />\n            </div>\n          ) : adSlot ? (\n            <div className=\"responsive-ad-container\">\n              <GoogleAdSense\n                adClient={env.NEXT_PUBLIC_AD_CLIENT as string}\n                adFormat=\"fluid\"\n                adSlot={adSlot}\n                fullWidthResponsive={true}\n                style={{\n                  display: \"block\",\n                  width: \"100%\",\n                  height: \"90px\",\n                }}\n              />\n            </div>\n          ) : null}\n        </div>\n      </div>\n\n      <style jsx>{`\n        .responsive-ad-container {\n          width: 100%;\n          max-width: 320px;\n          height: 50px;\n        }\n\n        @media (min-width: 481px) and (max-width: 768px) {\n          .responsive-ad-container {\n            max-width: 468px;\n            height: 60px;\n          }\n        }\n\n        @media (min-width: 769px) {\n          .responsive-ad-container {\n            max-width: 728px;\n            height: 90px;\n          }\n        }\n      `}</style>\n    </AdWrapper>\n  );\n}\n"
  },
  {
    "path": "src/components/ads/HorizontalBottomBanner.tsx",
    "content": "\"use client\";\n\nimport { HorizontalAdBanner } from \"@/components/ads/HorizontalAdBanner\";\n\ninterface HorizontalBottomBannerProps {\n  adSlot?: string;\n  ezoicPlacementId?: string;\n}\n\nexport function HorizontalBottomBanner({ adSlot, ezoicPlacementId }: HorizontalBottomBannerProps) {\n  return <HorizontalAdBanner adSlot={adSlot} ezoicPlacementId={ezoicPlacementId} />;\n}\n"
  },
  {
    "path": "src/components/ads/HorizontalTopBanner.tsx",
    "content": "import { HorizontalAdBanner } from \"./HorizontalAdBanner\";\n\ninterface HorizontalTopBannerProps {\n  adSlot?: string;\n  ezoicPlacementId?: string;\n}\n\nexport function HorizontalTopBanner({ adSlot, ezoicPlacementId }: HorizontalTopBannerProps) {\n  return <HorizontalAdBanner adSlot={adSlot} ezoicPlacementId={ezoicPlacementId} />;\n}\n"
  },
  {
    "path": "src/components/ads/InArticle.tsx",
    "content": "import { env } from \"@/env\";\nimport { AdPlaceholder } from \"@/components/ads/AdPlaceholder\";\n\nimport { GoogleAdSense } from \"./GoogleAdSense\";\nimport { AdWrapper } from \"./AdWrapper\";\n\nexport function InArticle({ adSlot }: { adSlot: string }) {\n  const isDevelopment = process.env.NODE_ENV === \"development\";\n\n  if (!env.NEXT_PUBLIC_AD_CLIENT) {\n    return null;\n  }\n\n  return (\n    <AdWrapper>\n      <div className=\"w-full max-w-full my-4\">\n        <div className=\"flex justify-center\">\n          {isDevelopment ? (\n            <AdPlaceholder height=\"200px\" type=\"In-Article Ad\" width=\"300px\" />\n          ) : (\n            <GoogleAdSense\n              adClient={env.NEXT_PUBLIC_AD_CLIENT}\n              adFormat=\"fluid\"\n              adSlot={adSlot}\n              className=\"adsbygoogle\"\n              style={{\n                display: \"block\",\n                textAlign: \"center\",\n                width: \"300px\",\n                height: \"200px\",\n              }}\n            />\n          )}\n        </div>\n      </div>\n    </AdWrapper>\n  );\n}\n"
  },
  {
    "path": "src/components/ads/ResponsiveAdBanner.tsx",
    "content": "\"use client\";\n\nimport { env } from \"@/env\";\nimport { AdPlaceholder } from \"@/components/ads/AdPlaceholder\";\n\nimport { GoogleAdSense } from \"./GoogleAdSense\";\nimport { AdWrapper } from \"./AdWrapper\";\n\ninterface ResponsiveAdBannerProps {\n  adSlot: string;\n}\n\nexport function ResponsiveAdBanner({ adSlot }: ResponsiveAdBannerProps) {\n  const isDevelopment = process.env.NODE_ENV === \"development\";\n\n  if (!env.NEXT_PUBLIC_AD_CLIENT) {\n    return null;\n  }\n\n  return (\n    <AdWrapper>\n      <div\n        className=\"w-full max-w-full\"\n        style={{\n          minHeight: \"auto\",\n          width: \"100%\",\n          maxHeight: \"90px\",\n          height: \"90px\",\n        }}\n      >\n        <div className=\"py-1 flex justify-center w-full\">\n          {isDevelopment ? (\n            <AdPlaceholder height=\"90px\" type=\"Ad Banner\" width=\"100%\" />\n          ) : (\n            <div className=\"responsive-ad-container\">\n              <GoogleAdSense\n                adClient={env.NEXT_PUBLIC_AD_CLIENT}\n                adFormat=\"fluid\"\n                adSlot={adSlot}\n                fullWidthResponsive={true}\n                style={{\n                  display: \"block\",\n                  width: \"100%\",\n                  height: \"90px\",\n                }}\n              />\n            </div>\n          )}\n        </div>\n      </div>\n\n      <style jsx>{`\n        .responsive-ad-container {\n          width: 100%;\n          max-width: 320px;\n          height: 50px;\n        }\n\n        @media (min-width: 481px) and (max-width: 768px) {\n          .responsive-ad-container {\n            max-width: 468px;\n            height: 60px;\n          }\n        }\n\n        @media (min-width: 769px) {\n          .responsive-ad-container {\n            max-width: 728px;\n            height: 90px;\n          }\n        }\n      `}</style>\n    </AdWrapper>\n  );\n}\n"
  },
  {
    "path": "src/components/ads/VerticalAdBanner.tsx",
    "content": "import { env } from \"@/env\";\n\nimport { GoogleAdSense } from \"./GoogleAdSense\";\nimport { EzoicAd } from \"./EzoicAd\";\nimport { AdWrapper } from \"./AdWrapper\";\nimport { AdPlaceholder } from \"./AdPlaceholder\";\n\ninterface VerticalAdBannerProps {\n  adSlot: string;\n  ezoicPlacementId?: string;\n  position?: \"left\" | \"right\";\n}\n\nexport function VerticalAdBanner({ adSlot, ezoicPlacementId, position = \"left\" }: VerticalAdBannerProps) {\n  const isDevelopment = process.env.NODE_ENV === \"development\";\n  const useEzoic = env.NEXT_PUBLIC_AD_PROVIDER === \"ezoic\" && ezoicPlacementId;\n\n  if (!env.NEXT_PUBLIC_AD_CLIENT && !useEzoic) {\n    return null;\n  }\n\n  return (\n    <AdWrapper>\n      <div className=\"hidden lg:block w-[160px] h-[600px] sticky top-4\">\n        {isDevelopment ? (\n          <AdPlaceholder height=\"600px\" type={`Vertical Ad (${position})`} width=\"160px\" />\n        ) : useEzoic ? (\n          <EzoicAd className=\"w-[160px] h-[600px]\" placementId={ezoicPlacementId} />\n        ) : (\n          <GoogleAdSense\n            adClient={env.NEXT_PUBLIC_AD_CLIENT as string}\n            adSlot={adSlot}\n            style={{ display: \"block\", width: \"160px\", height: \"600px\" }}\n          />\n        )}\n      </div>\n    </AdWrapper>\n  );\n}\n"
  },
  {
    "path": "src/components/ads/VerticalLeftBanner.tsx",
    "content": "import { env } from \"@/env\";\n\nimport { VerticalAdBanner } from \"./VerticalAdBanner\";\n\nexport function VerticalLeftBanner() {\n  const hasAdSlot = env.NEXT_PUBLIC_VERTICAL_LEFT_BANNER_AD_SLOT;\n  const hasEzoicPlacement = env.NEXT_PUBLIC_EZOIC_VERTICAL_LEFT_PLACEMENT_ID;\n\n  if (!hasAdSlot && !hasEzoicPlacement) {\n    return null;\n  }\n\n  return (\n    <VerticalAdBanner\n      adSlot={env.NEXT_PUBLIC_VERTICAL_LEFT_BANNER_AD_SLOT || \"\"}\n      ezoicPlacementId={env.NEXT_PUBLIC_EZOIC_VERTICAL_LEFT_PLACEMENT_ID}\n      position=\"left\"\n    />\n  );\n}\n"
  },
  {
    "path": "src/components/ads/VerticalRightBanner.tsx",
    "content": "import { env } from \"@/env\";\n\nimport { VerticalAdBanner } from \"./VerticalAdBanner\";\n\nexport function VerticalRightBanner() {\n  const hasAdSlot = env.NEXT_PUBLIC_VERTICAL_RIGHT_BANNER_AD_SLOT;\n  const hasEzoicPlacement = env.NEXT_PUBLIC_EZOIC_VERTICAL_RIGHT_PLACEMENT_ID;\n  \n  if (!hasAdSlot && !hasEzoicPlacement) {\n    return null;\n  }\n\n  return (\n    <VerticalAdBanner\n      adSlot={env.NEXT_PUBLIC_VERTICAL_RIGHT_BANNER_AD_SLOT || \"\"}\n      ezoicPlacementId={env.NEXT_PUBLIC_EZOIC_VERTICAL_RIGHT_PLACEMENT_ID}\n      position=\"right\"\n    />\n  );\n}\n"
  },
  {
    "path": "src/components/ads/index.ts",
    "content": "export { GoogleAdSense } from \"./GoogleAdSense\";\nexport { AdWrapper } from \"./AdWrapper\";\nexport { VerticalAdBanner } from \"./VerticalAdBanner\";\nexport { VerticalLeftBanner } from \"./VerticalLeftBanner\";\nexport { VerticalRightBanner } from \"./VerticalRightBanner\";\nexport { HorizontalTopBanner } from \"./HorizontalTopBanner\";\nexport { HorizontalBottomBanner } from \"./HorizontalBottomBanner\";\nexport { AdBlockerForPremium } from \"./AdBlockerForPremium\";\nexport { InArticle } from \"./InArticle\";\n"
  },
  {
    "path": "src/components/ads/nutripure-affiliate-banner.tsx",
    "content": "\"use client\";\nimport { useEffect, useState, useRef } from \"react\";\nimport Link from \"next/link\";\nimport Image from \"next/image\";\n\nimport { AdWrapper } from \"./AdWrapper\";\n\nconst messageVariants = [\n  // {\n  //   title: \"Nutrition sportive naturelle\",\n  //   description: \"Des milliers de sportifs nous font confiance\",\n  //   cta: \"Découvrir\",\n  //   badge: \"🇫🇷 Made in France\",\n  //   socialProof: true,\n  // },\n  {\n    title: \"Whey protéine sans additifs\",\n    description: \"100% traçable pour optimiser votre récupération\",\n    cta: \"En savoir plus\",\n    badge: \"⭐ 4.8/5 (1k+ avis)\",\n    socialProof: true,\n  },\n  {\n    title: \"Compléments haute qualité\",\n    description: \"Multi-vitamines, Omega 3 et Magnésium pour athlètes\",\n    cta: \"Explorer\",\n    badge: \"🌿 100% Naturel\",\n  },\n  {\n    title: \"Recommandé par les coachs\",\n    description: \"La référence française en nutrition sportive naturelle\",\n    cta: \"Voir les produits\",\n    badge: \"🏆 Qualité premium\",\n    socialProof: true,\n  },\n];\n\ninterface NutripureAffiliateBannerProps {\n  context?: \"workout\" | \"nutrition\" | \"recovery\" | \"general\";\n  position?: \"top\" | \"middle\" | \"bottom\";\n}\n\nexport function NutripureAffiliateBanner({ context = \"general\", position = \"middle\" }: NutripureAffiliateBannerProps) {\n  const affiliateUrl = \"https://c3po.link/QVupuZ8DYw\";\n  const [currentVariant, setCurrentVariant] = useState(0);\n  const bannerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    let filteredVariants = messageVariants;\n\n    // Personnalisation selon le contexte\n    if (context === \"workout\" || context === \"recovery\") {\n      // Prioriser les messages avec protéines et récupération\n      filteredVariants = messageVariants.filter((_, index) => [1, 3].includes(index));\n    } else if (context === \"nutrition\") {\n      // Prioriser les compléments et vitamines\n      filteredVariants = messageVariants.filter((_, index) => [2, 3].includes(index));\n    }\n\n    // Pour le placement en haut, prioriser les messages avec preuve sociale\n    if (position === \"top\") {\n      const socialProofMessages = filteredVariants.filter((msg) => msg.socialProof);\n      if (socialProofMessages.length > 0) {\n        filteredVariants = socialProofMessages;\n      }\n    }\n\n    const randomIndex = Math.floor(Math.random() * filteredVariants.length);\n    const selectedMessage = filteredVariants[randomIndex];\n    const originalIndex = messageVariants.indexOf(selectedMessage);\n    setCurrentVariant(originalIndex);\n  }, [context, position]);\n\n  const message = messageVariants[currentVariant];\n\n  return (\n    <AdWrapper>\n      <div\n        className=\"w-full max-w-full\"\n        ref={bannerRef}\n        style={{\n          minHeight: \"auto\",\n          width: \"100%\",\n          maxHeight: \"90px\",\n          height: \"90px\",\n        }}\n      >\n        <div className=\"py-1 flex justify-center w-full\">\n          <div className=\"responsive-nutripure-container\">\n            <Link className=\"block w-full h-full\" href={affiliateUrl} rel=\"noopener noreferrer sponsored\" target=\"_blank\">\n              <div className=\"nutripure-banner\">\n                {/* Mobile Layout */}\n                <div className=\"mobile-layout\">\n                  <div className=\"image-section\">\n                    <Image\n                      alt=\"Nutripure\"\n                      className=\"object-contain max-h-[90%] ml-1\"\n                      fill\n                      sizes=\"50px\"\n                      src=\"/images/nutripure-logo.webp\"\n                    />\n                  </div>\n                  <div className=\"content-section\">\n                    <div className=\"text-wrapper\">\n                      <span className=\"badge\">{message.badge}</span>\n                      <h4 className=\"title\">{message.title}</h4>\n                      <p className=\"description\">{message.description}</p>\n                    </div>\n                    <span className=\"cta\">{message.cta}</span>\n                  </div>\n                </div>\n\n                {/* Tablet/Desktop Layout */}\n                <div className=\"desktop-layout\">\n                  <div className=\"image-section\">\n                    <Image alt=\"Nutripure\" className=\"object-contain p-2\" fill sizes=\"90px\" src=\"/images/nutripure-logo.webp\" />\n                  </div>\n                  <div className=\"content-section\">\n                    <div className=\"badge-wrapper\">\n                      <span className=\"badge\">{message.badge}</span>\n                    </div>\n                    <div className=\"text-content\">\n                      <h4 className=\"title\">{message.title}</h4>\n                      <p className=\"description\">{message.description}</p>\n                    </div>\n                    <div className=\"cta-wrapper\">\n                      <span className=\"cta\">\n                        {message.cta}\n                        <svg className=\"arrow-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                          <path d=\"M13 7l5 5m0 0l-5 5m5-5H6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} />\n                        </svg>\n                      </span>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </Link>\n          </div>\n        </div>\n      </div>\n\n      <style jsx>{`\n        .responsive-nutripure-container {\n          width: 100%;\n        }\n\n        .nutripure-banner {\n          width: 100%;\n          height: 100%;\n          background: linear-gradient(135deg, #000 0%, #b8e0ff 100%);\n          border: 1px solid #000;\n          border-radius: 0;\n          overflow: hidden;\n          transition: all 0.3s ease;\n          position: relative;\n          box-shadow: 0 4px 12px rgba(107, 182, 232, 0.25);\n        }\n\n        @media (prefers-color-scheme: dark) {\n          .nutripure-banner {\n            background: linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%);\n            border: 1px solid #000;\n            box-shadow: 0 2px 8px rgba(160, 211, 243, 0.2);\n          }\n        }\n\n        .nutripure-banner:hover {\n          transform: translateY(-2px);\n          box-shadow: 0 8px 20px rgba(107, 182, 232, 0.35);\n          border-color: #5aa5d7;\n        }\n\n        @media (prefers-color-scheme: dark) {\n          .nutripure-banner:hover {\n            box-shadow: 0 6px 16px rgba(160, 211, 243, 0.3);\n            border-color: #a0d3f3;\n          }\n        }\n\n        /* Mobile Layout (default) */\n        .mobile-layout {\n          display: flex;\n          height: 100%;\n          align-items: center;\n          padding: 0;\n        }\n\n        .mobile-layout .image-section {\n          position: relative;\n          width: 50px;\n          height: 80px;\n          flex-shrink: 0;\n          background: rgba(255, 255, 255, 0.95);\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          border-radius: 8px 0 0 8px;\n        }\n\n        .mobile-layout .content-section {\n          flex: 1;\n          padding: 8px 10px;\n          display: flex;\n          align-items: center;\n          justify-content: space-between;\n          gap: 10px;\n        }\n\n        .mobile-layout .text-wrapper {\n          flex: 1;\n          min-width: 0;\n        }\n\n        .mobile-layout .badge {\n          display: inline-block;\n          font-size: 10px;\n          background: #1e3a5f;\n          color: white;\n          padding: 2px 5px;\n          border-radius: 4px;\n          font-weight: 600;\n          margin-bottom: 3px;\n          white-space: nowrap;\n        }\n\n        @media (prefers-color-scheme: dark) {\n          .mobile-layout .badge {\n            background: #000000;\n          }\n        }\n\n        .mobile-layout .title {\n          font-size: 13px;\n          font-weight: 700;\n          color: #1e3a5f;\n          line-height: 1.2;\n          margin-bottom: 2px;\n        }\n\n        @media (prefers-color-scheme: dark) {\n          .mobile-layout .title {\n            color: #000000;\n          }\n        }\n\n        .mobile-layout .description {\n          font-size: 11px;\n          color: #2c4f70;\n          line-height: 1.3;\n          overflow: hidden;\n          text-overflow: ellipsis;\n          display: -webkit-box;\n          -webkit-line-clamp: 2;\n          -webkit-box-orient: vertical;\n        }\n\n        @media (prefers-color-scheme: dark) {\n          .mobile-layout .description {\n            color: #555555;\n          }\n        }\n\n        .mobile-layout .cta {\n          font-size: 12px;\n          font-weight: 600;\n          color: white;\n          background: linear-gradient(135deg, #1e3a5f 0%, #2c4f70 100%);\n          padding: 8px 12px;\n          border-radius: 6px;\n          white-space: nowrap;\n          flex-shrink: 0;\n          box-shadow: 0 2px 4px rgba(30, 58, 95, 0.2);\n          position: relative;\n          overflow: hidden;\n        }\n\n        .mobile-layout .cta::before {\n          content: \"\";\n          position: absolute;\n          top: -50%;\n          left: -100%;\n          width: 200%;\n          height: 200%;\n          background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);\n          transform: rotate(45deg);\n          animation: shine 3s infinite;\n        }\n\n        @keyframes shine {\n          0% {\n            left: -100%;\n          }\n          100% {\n            left: 100%;\n          }\n        }\n\n        @media (prefers-color-scheme: dark) {\n          .mobile-layout .cta {\n            background: #a0d3f3;\n            color: #000000;\n            box-shadow: none;\n          }\n        }\n\n        /* Tablet/Desktop Layout (hidden by default) */\n        .desktop-layout {\n          display: none;\n        }\n\n        /* Tablet styles */\n        @media (min-width: 481px) and (max-width: 768px) {\n          .responsive-nutripure-container {\n            max-width: 100%;\n            height: 90px;\n          }\n\n          .mobile-layout .image-section {\n            width: 90px;\n            height: 90px;\n            background: rgba(255, 255, 255, 0.95);\n          }\n\n          .mobile-layout .title {\n            font-size: 14px;\n          }\n\n          .mobile-layout .description {\n            font-size: 12px;\n          }\n\n          .mobile-layout .badge {\n            display: inline-block;\n            font-size: 11px;\n            background: #1e3a5f;\n            color: white;\n            padding: 3px 7px;\n            border-radius: 6px;\n            font-weight: 600;\n          }\n\n          @media (prefers-color-scheme: dark) {\n            .mobile-layout .badge {\n              background: #000000;\n            }\n          }\n\n          .mobile-layout .cta {\n            font-size: 13px;\n            padding: 8px 14px;\n            position: relative;\n            overflow: hidden;\n          }\n\n          .mobile-layout .cta::after {\n            content: \"\";\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            width: 0;\n            height: 0;\n            border-radius: 50%;\n            background: rgba(255, 255, 255, 0.5);\n            transform: translate(-50%, -50%);\n          }\n\n          @media (prefers-color-scheme: dark) {\n            .mobile-layout .cta {\n              background: #a0d3f3;\n              color: #000000;\n            }\n          }\n        }\n\n        /* Desktop styles */\n        @media (min-width: 769px) {\n          .responsive-nutripure-container {\n            max-width: 728px;\n            height: 90px;\n          }\n\n          .mobile-layout {\n            display: none;\n          }\n\n          .desktop-layout {\n            display: flex;\n            height: 100%;\n            align-items: center;\n            padding: 0;\n          }\n\n          .desktop-layout .image-section {\n            position: relative;\n            width: 90px;\n            height: 90px;\n            flex-shrink: 0;\n            background: rgba(255, 255, 255, 0.95);\n            border-radius: 10px 0 0 10px;\n          }\n\n          .desktop-layout .content-section {\n            flex: 1;\n            padding: 12px 16px;\n            display: flex;\n            align-items: center;\n            gap: 16px;\n          }\n\n          .desktop-layout .badge-wrapper {\n            flex-shrink: 0;\n          }\n\n          .desktop-layout .badge {\n            font-size: 12px;\n            background: #1e3a5f;\n            color: white;\n            padding: 5px 10px;\n            border-radius: 8px;\n            font-weight: 600;\n            white-space: nowrap;\n            box-shadow: 0 2px 6px rgba(30, 58, 95, 0.2);\n          }\n\n          @media (prefers-color-scheme: dark) {\n            .desktop-layout .badge {\n              background: #000000;\n              box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n            }\n          }\n\n          .desktop-layout .text-content {\n            flex: 1;\n          }\n\n          .desktop-layout .title {\n            font-size: 16px;\n            font-weight: 700;\n            color: #1e3a5f;\n            margin-bottom: 4px;\n          }\n\n          @media (prefers-color-scheme: dark) {\n            .desktop-layout .title {\n              color: #000000;\n            }\n          }\n\n          .desktop-layout .description {\n            font-size: 12px;\n            color: #2c4f70;\n            line-height: 1.4;\n            font-weight: 400;\n          }\n\n          @media (prefers-color-scheme: dark) {\n            .desktop-layout .description {\n              color: #333333;\n            }\n          }\n\n          .desktop-layout .cta-wrapper {\n            flex-shrink: 0;\n          }\n\n          .desktop-layout .cta {\n            display: inline-flex;\n            align-items: center;\n            gap: 6px;\n            background: linear-gradient(135deg, #1e3a5f 0%, #2c4f70 100%);\n            color: white;\n            padding: 10px 20px;\n            border-radius: 25px;\n            font-size: 14px;\n            font-weight: 600;\n            transition: all 0.3s ease;\n            box-shadow: 0 4px 12px rgba(30, 58, 95, 0.3);\n            position: relative;\n            overflow: hidden;\n            animation: gentle-glow 4s ease-in-out infinite;\n          }\n\n          @keyframes gentle-glow {\n            0%,\n            100% {\n              box-shadow: 0 4px 12px rgba(30, 58, 95, 0.3);\n            }\n            50% {\n              box-shadow: 0 4px 20px rgba(30, 58, 95, 0.5);\n            }\n          }\n\n          .desktop-layout .cta::before {\n            content: \"\";\n            position: absolute;\n            top: -2px;\n            left: -2px;\n            right: -2px;\n            bottom: -2px;\n            background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);\n            background-size: 200% 200%;\n            animation: shimmer 3s linear infinite;\n            border-radius: 25px;\n            opacity: 0;\n            transition: opacity 0.3s ease;\n          }\n\n          .desktop-layout .cta:hover::before {\n            opacity: 1;\n          }\n\n          @keyframes shimmer {\n            0% {\n              background-position: 200% 50%;\n            }\n            100% {\n              background-position: -200% 50%;\n            }\n          }\n\n          .desktop-layout .cta:hover {\n            background: linear-gradient(135deg, #2c4f70 0%, #3a5f85 100%);\n            transform: scale(1.05) translateY(-1px);\n            box-shadow: 0 8px 20px rgba(30, 58, 95, 0.4);\n          }\n\n          .desktop-layout .arrow-icon {\n            transition: transform 0.3s ease;\n          }\n\n          .desktop-layout .cta:hover .arrow-icon {\n            transform: translateX(3px);\n            animation: arrow-bounce 1s ease-in-out infinite;\n          }\n\n          @keyframes arrow-bounce {\n            0%,\n            100% {\n              transform: translateX(3px);\n            }\n            50% {\n              transform: translateX(6px);\n            }\n          }\n\n          @media (prefers-color-scheme: dark) {\n            .desktop-layout .cta {\n              background: #a0d3f3;\n              color: #000000;\n              box-shadow: 0 4px 12px rgba(160, 211, 243, 0.3);\n            }\n\n            .desktop-layout .cta::before {\n              background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.3) 50%, transparent 70%);\n            }\n\n            .desktop-layout .cta:hover {\n              background: #8bc4e6;\n              box-shadow: 0 6px 16px rgba(160, 211, 243, 0.4);\n            }\n          }\n\n          .arrow-icon {\n            width: 14px;\n            height: 14px;\n          }\n        }\n      `}</style>\n    </AdWrapper>\n  );\n}\n"
  },
  {
    "path": "src/components/premium/RemoveAdsText.tsx",
    "content": "\"use client\";\n\nimport { useRouter } from \"next/navigation\";\nimport { Ban } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\n\nexport function RemoveAdsText() {\n  const router = useRouter();\n  const t = useI18n();\n\n  const handleClick = () => {\n    router.push(\"/premium\");\n  };\n\n  return (\n    <button\n      className=\"flex items-center gap-0 text-[11px] sm:text-xs md:font-medium text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded-full transition-all duration-200 hover:scale-105 whitespace-nowrap hover:underline\"\n      onClick={handleClick}\n    >\n      <Ban className=\"w-3 h-3 flex-shrink-0 mr-1\" />\n      <span className=\"whitespace-nowrap\">{t(\"commons.remove_ads\")}</span>\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/components/pwa/ServiceWorkerRegistration.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\n\nexport function ServiceWorkerRegistration() {\n  useEffect(() => {\n    if (\"serviceWorker\" in navigator) {\n      navigator.serviceWorker\n        .register(\"/sw.js\")\n        .then((registration) => {\n          console.log(\"SW registered: \", registration);\n          // Check for updates\n          registration.update();\n        })\n        .catch((registrationError) => {\n          console.log(\"SW registration failed: \", registrationError);\n        });\n    }\n  }, []);\n\n  return null;\n}"
  },
  {
    "path": "src/components/seo/SEOHead.tsx",
    "content": "import React from \"react\";\n\nimport { generateStructuredData, StructuredDataScript } from \"@/shared/lib/structured-data\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { SiteConfig } from \"@/shared/config/site-config\";\n\nimport type { Metadata } from \"next\";\n\ninterface SEOHeadProps {\n  title?: string;\n  description?: string;\n  keywords?: string[];\n  locale?: string;\n  canonical?: string;\n  ogImage?: string;\n  ogType?: \"website\" | \"article\";\n  noIndex?: boolean;\n  structuredData?: {\n    type: \"Article\" | \"SoftwareApplication\" | \"Calculator\";\n    author?: string;\n    datePublished?: string;\n    dateModified?: string;\n    calculatorData?: {\n      calculatorType: \"calorie\" | \"macro\" | \"bmi\" | \"heart-rate\" | \"heart-rate-zones\" | \"one-rep-max\" | \"rest-timer\";\n      inputFields: string[];\n      outputFields: string[];\n      formula?: string;\n      accuracy?: string;\n      targetAudience?: string[];\n      relatedCalculators?: string[];\n    };\n  };\n}\n\nexport function generateSEOMetadata({\n  title,\n  description,\n  keywords = [],\n  locale = \"en\",\n  canonical,\n  ogImage,\n  ogType = \"website\",\n  noIndex = false,\n}: SEOHeadProps): Metadata {\n  const baseUrl = getServerUrl();\n  const fullTitle = title ? `${title}` : SiteConfig.title;\n  const finalDescription = description || SiteConfig.description;\n  const finalCanonical = canonical || baseUrl;\n  const finalOgImage = ogImage || `${baseUrl}/images/default-og-image_${locale === \"zh-CN\" ? \"zh\" : locale}.jpg`;\n  const allKeywords = [...SiteConfig.keywords, ...keywords];\n\n  return {\n    title: fullTitle,\n    description: finalDescription,\n    keywords: allKeywords,\n    robots: noIndex\n      ? {\n          index: false,\n          follow: false,\n        }\n      : {\n          index: true,\n          follow: true,\n          googleBot: {\n            index: true,\n            follow: true,\n            \"max-snippet\": -1,\n            \"max-image-preview\": \"large\",\n            \"max-video-preview\": -1,\n          },\n        },\n    alternates: {\n      canonical: finalCanonical,\n      languages: {\n        \"fr-FR\": `${baseUrl}/fr`,\n        \"en-US\": `${baseUrl}/en`,\n        \"es-ES\": `${baseUrl}/es`,\n        \"pt-PT\": `${baseUrl}/pt`,\n        \"ru-RU\": `${baseUrl}/ru`,\n        \"zh-CN\": `${baseUrl}/zh-CN`,\n        \"x-default\": baseUrl,\n      },\n    },\n    openGraph: {\n      title: fullTitle,\n      description: finalDescription,\n      url: finalCanonical,\n      siteName: SiteConfig.title,\n      locale:\n        locale === \"en\"\n          ? \"en_US\"\n          : locale === \"es\"\n            ? \"es_ES\"\n            : locale === \"pt\"\n              ? \"pt_PT\"\n              : locale === \"ru\"\n                ? \"ru_RU\"\n                : locale === \"zh-CN\"\n                  ? \"zh_CN\"\n                  : \"fr_FR\",\n      alternateLocale: [\n        \"fr_FR\",\n        \"fr_CA\",\n        \"fr_CH\",\n        \"fr_BE\",\n        \"en_US\",\n        \"en_GB\",\n        \"en_CA\",\n        \"en_AU\",\n        \"es_ES\",\n        \"es_MX\",\n        \"es_AR\",\n        \"es_CL\",\n        \"pt_PT\",\n        \"pt_BR\",\n        \"ru_RU\",\n        \"ru_BY\",\n        \"ru_KZ\",\n        \"zh_CN\",\n        \"zh_TW\",\n        \"zh_HK\",\n      ].filter(\n        (alt) =>\n          alt !==\n          (locale === \"en\"\n            ? \"en_US\"\n            : locale === \"es\"\n              ? \"es_ES\"\n              : locale === \"pt\"\n                ? \"pt_PT\"\n                : locale === \"ru\"\n                  ? \"ru_RU\"\n                  : locale === \"zh-CN\"\n                    ? \"zh_CN\"\n                    : \"fr_FR\"),\n      ),\n      images: [\n        {\n          url: finalOgImage,\n          width: SiteConfig.seo.ogImage.width,\n          height: SiteConfig.seo.ogImage.height,\n          alt: title || SiteConfig.title,\n        },\n      ],\n      type: ogType,\n    },\n    twitter: {\n      card: \"summary_large_image\",\n      site: SiteConfig.seo.twitterHandle,\n      creator: SiteConfig.seo.twitterHandle,\n      title: fullTitle,\n      description: finalDescription,\n      images: [\n        {\n          url: finalOgImage,\n          width: SiteConfig.seo.ogImage.width,\n          height: SiteConfig.seo.ogImage.height,\n          alt: title || SiteConfig.title,\n        },\n      ],\n    },\n  };\n}\n\ninterface SEOScriptsProps extends SEOHeadProps {\n  children?: React.ReactNode;\n  // Add hreflang support\n  hreflangPath?: string; // e.g., \"/tools/heart-rate-zones\"\n}\n\nexport function SEOScripts({\n  title,\n  description,\n  locale = \"en\",\n  canonical,\n  ogImage,\n  structuredData,\n  hreflangPath,\n  children,\n}: SEOScriptsProps) {\n  const baseUrl = getServerUrl();\n  const finalCanonical = canonical || baseUrl;\n  const finalOgImage = ogImage || `${baseUrl}/images/default-og-image_${locale === \"zh-CN\" ? \"zh\" : locale}.jpg`;\n\n  let structuredDataObj;\n  if (structuredData) {\n    structuredDataObj = generateStructuredData({\n      type: structuredData.type,\n      locale,\n      title,\n      description,\n      url: finalCanonical,\n      image: finalOgImage,\n      author: structuredData.author,\n      datePublished: structuredData.datePublished,\n      dateModified: structuredData.dateModified,\n      calculatorData: structuredData.calculatorData,\n    });\n  }\n\n  // Generate hreflang tags if path is provided\n  const hreflangTags = hreflangPath ? (\n    <>\n      <link href={`${baseUrl}/en${hreflangPath}`} hrefLang=\"en\" rel=\"alternate\" />\n      <link href={`${baseUrl}/es${hreflangPath}`} hrefLang=\"es\" rel=\"alternate\" />\n      <link href={`${baseUrl}/fr${hreflangPath}`} hrefLang=\"fr\" rel=\"alternate\" />\n      <link href={`${baseUrl}/pt${hreflangPath}`} hrefLang=\"pt\" rel=\"alternate\" />\n      <link href={`${baseUrl}/ru${hreflangPath}`} hrefLang=\"ru\" rel=\"alternate\" />\n      <link href={`${baseUrl}/zh-CN${hreflangPath}`} hrefLang=\"zh-CN\" rel=\"alternate\" />\n      <link href={`${baseUrl}/en${hreflangPath}`} hrefLang=\"x-default\" rel=\"alternate\" />\n    </>\n  ) : null;\n\n  return (\n    <>\n      {structuredDataObj && <StructuredDataScript data={structuredDataObj} />}\n      {hreflangTags}\n      {children}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/seo/breadcrumbs.tsx",
    "content": "\"use client\";\nimport Link from \"next/link\";\nimport { ChevronRight, Home } from \"lucide-react\";\n\nimport { StructuredDataScript } from \"@/shared/lib/structured-data\";\n\ninterface BreadcrumbItem {\n  label: string;\n  href?: string;\n  current?: boolean;\n}\n\ninterface BreadcrumbsProps {\n  items: BreadcrumbItem[];\n}\n\nexport function Breadcrumbs({ items }: BreadcrumbsProps) {\n  // Generate BreadcrumbList structured data\n  const breadcrumbStructuredData = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"BreadcrumbList\",\n    itemListElement: items.map((item, index) => ({\n      \"@type\": \"ListItem\",\n      position: index + 1,\n      name: item.label,\n      item: item.href ? `https://www.workout.cool${item.href}` : undefined,\n    })),\n  };\n\n  return (\n    <>\n      <StructuredDataScript data={breadcrumbStructuredData} />\n      <nav aria-label=\"Breadcrumb\" className=\"m-2 overflow-x-auto min-h-5 flex items-center\">\n        <ol className=\"flex items-center space-x-1 sm:space-x-2 text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap min-w-max\">\n          {items.map((item, index) => (\n            <li className=\"flex items-center\" key={index}>\n              {index > 0 && <ChevronRight aria-hidden=\"true\" className=\"h-4 w-4 text-gray-400\" />}\n              {item.current ? (\n                <span aria-current=\"page\" className=\"font-medium text-gray-900 dark:text-white\">\n                  {item.label}\n                </span>\n              ) : item.href ? (\n                <Link className=\"hover:text-gray-900 dark:hover:text-white transition-colors\" href={item.href}>\n                  {index === 0 ? (\n                    <span className=\"flex items-center\">\n                      <Home aria-hidden=\"true\" className=\"h-4 w-4 mr-1\" />\n                      {item.label}\n                    </span>\n                  ) : (\n                    item.label\n                  )}\n                </Link>\n              ) : (\n                <span>{item.label}</span>\n              )}\n            </li>\n          ))}\n        </ol>\n      </nav>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/seo/duration-badge.tsx",
    "content": "import { Clock } from \"lucide-react\";\n\ninterface DurationBadgeProps {\n  durationWeeks: number;\n  sessionsPerWeek: number;\n  sessionDurationMin: number;\n  locale: string;\n  className?: string;\n}\n\nexport function DurationBadge({ \n  durationWeeks, \n  sessionsPerWeek, \n  sessionDurationMin, \n  locale,\n  className = \"\" \n}: DurationBadgeProps) {\n  const totalMinutes = durationWeeks * sessionsPerWeek * sessionDurationMin;\n  const totalHours = Math.round(totalMinutes / 60);\n\n  const formatDuration = () => {\n    if (locale === \"en\") {\n      return `${durationWeeks} weeks • ${totalHours}h total`;\n    } else if (locale === \"es\") {\n      return `${durationWeeks} semanas • ${totalHours}h total`;\n    } else if (locale === \"pt\") {\n      return `${durationWeeks} semanas • ${totalHours}h total`;\n    } else if (locale === \"ru\") {\n      return `${durationWeeks} недель • ${totalHours}ч всего`;\n    } else if (locale === \"zh-CN\") {\n      return `${durationWeeks} 周 • 总共${totalHours}小时`;\n    } else {\n      return `${durationWeeks} semaines • ${totalHours}h total`;\n    }\n  };\n\n  return (\n    <div className={`inline-flex items-center space-x-1 text-sm text-gray-600 dark:text-gray-400 ${className}`}>\n      <Clock className=\"h-4 w-4\" />\n      <span>{formatDuration()}</span>\n      \n      {/* Hidden structured data for SEO */}\n      <div className=\"sr-only\">\n        <span itemProp=\"timeRequired\">PT{totalMinutes}M</span>\n        <span itemProp=\"duration\">P{durationWeeks}W</span>\n      </div>\n    </div>\n  );\n}"
  },
  {
    "path": "src/components/seo/rich-snippet-rating.tsx",
    "content": "import { Star } from \"lucide-react\";\n\ninterface RichSnippetRatingProps {\n  rating: number;\n  reviewCount: number;\n  className?: string;\n}\n\nexport function RichSnippetRating({ rating, reviewCount, className = \"\" }: RichSnippetRatingProps) {\n  const fullStars = Math.floor(rating);\n  const hasHalfStar = rating % 1 >= 0.5;\n  const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);\n\n  return (\n    <div className={`flex items-center space-x-1 ${className}`}>\n      {/* Stars */}\n      <div aria-label={`${rating} out of 5 stars`} className=\"flex items-center\" role=\"img\">\n        {/* Full Stars */}\n        {Array.from({ length: fullStars }).map((_, i) => (\n          <Star className=\"h-4 w-4 fill-yellow-400 text-yellow-400\" key={`full-${i}`} />\n        ))}\n\n        {/* Half Star */}\n        {hasHalfStar && (\n          <div className=\"relative\">\n            <Star className=\"h-4 w-4 text-gray-300\" />\n            <div className=\"absolute inset-0 overflow-hidden\" style={{ width: \"50%\" }}>\n              <Star className=\"h-4 w-4 fill-yellow-400 text-yellow-400\" />\n            </div>\n          </div>\n        )}\n\n        {/* Empty Stars */}\n        {Array.from({ length: emptyStars }).map((_, i) => (\n          <Star className=\"h-4 w-4 text-gray-300\" key={`empty-${i}`} />\n        ))}\n      </div>\n\n      {/* Rating Text */}\n      <span className=\"text-sm font-medium text-gray-900 dark:text-white\">{rating.toFixed(1)}</span>\n\n      {/* Review Count */}\n      <span className=\"text-sm text-gray-500 dark:text-gray-400\">\n        ({reviewCount} {reviewCount === 1 ? \"avis\" : \"avis\"})\n      </span>\n\n      {/* Hidden structured data for SEO */}\n      <div className=\"sr-only\">\n        <span itemProp=\"ratingValue\">{rating}</span>\n        <span itemProp=\"bestRating\">5</span>\n        <span itemProp=\"ratingCount\">{reviewCount}</span>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/seo/session-rich-snippets.tsx",
    "content": "import { Clock, Dumbbell, Timer } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\n\ninterface SessionRichSnippetsProps {\n  duration: number;\n  exerciseCount: number;\n  totalSets: number;\n  className?: string;\n}\n\nexport function SessionRichSnippets({ duration, exerciseCount, totalSets, className = \"\" }: SessionRichSnippetsProps) {\n  const t = useI18n();\n\n  return (\n    <div className={`flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400 ${className}`}>\n      {/* Duration */}\n      <div className=\"flex items-center gap-1\">\n        <Clock size={16} />\n        <span>\n          ~{duration} {t(\"programs.min_short\")}\n        </span>\n      </div>\n\n      {/* Exercise Count */}\n      <div className=\"flex items-center gap-1\">\n        <Dumbbell size={16} />\n        <span>\n          {exerciseCount} {t(\"programs.exercises\")}\n        </span>\n      </div>\n\n      {/* Total Sets */}\n      <div className=\"flex items-center gap-1\">\n        <Timer size={16} />\n        <span>\n          {totalSets} {t(\"programs.set\", { count: totalSets })}\n        </span>\n      </div>\n\n      {/* Hidden structured data for SEO */}\n      <div className=\"sr-only\">\n        <span itemProp=\"duration\">{duration}</span>\n        <span itemProp=\"exerciseCount\">{exerciseCount}</span>\n        <span itemProp=\"workoutSets\">{totalSets}</span>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/svg/BrokenLink.tsx",
    "content": "import * as React from \"react\";\nimport { ComponentPropsWithoutRef } from \"react\";\n\nexport type BrokenLinkIconProps = ComponentPropsWithoutRef<\"svg\"> & { size?: number };\n\nexport const BrokenLinkIcon = ({ size = 32, ...props }: BrokenLinkIconProps) => {\n  return (\n    <svg height={size} version=\"1.1\" viewBox=\"0 0 512 512\" width={size} x=\"0\" xmlns=\"http://www.w3.org/2000/svg\" y=\"0\" {...props}>\n      <g>\n        <path\n          d=\"M224.18 287.82c-40.942-40.941-107.551-40.941-148.493 0l-44.98 44.98c-40.941 40.942-40.941 107.552 0 148.493 40.941 40.941 107.55 40.941 148.492 0l44.98-44.98c40.942-40.942 40.942-107.551 0-148.493zm-87.407 151.047c-17.609 17.606-46.035 17.61-63.64 0-17.61-17.605-17.606-46.035 0-63.64l44.98-44.98c17.61-17.606 46.032-17.606 63.64 0 17.61 17.605 17.606 46.03 0 63.64zm0 0\"\n          data-original=\"#6e6e6e\"\n          fill=\"#6E6E6E\"\n          opacity=\"1\"\n        ></path>\n        <path\n          d=\"m224.18 436.313-44.98 44.98c-40.942 40.941-107.552 40.941-148.493 0l42.426-42.426c17.605 17.61 46.031 17.606 63.64 0l44.98-44.98c17.606-17.61 17.606-46.032 0-63.64l42.427-42.427c40.941 40.942 40.941 107.551 0 148.492zm0 0\"\n          data-original=\"#5a5a5a\"\n          fill=\"#5A5A5A\"\n          opacity=\"1\"\n        ></path>\n        <path\n          d=\"M481.293 30.707c-40.941-40.941-107.55-40.941-148.492 0l-44.98 44.98c-40.942 40.942-40.942 107.551 0 148.493 40.94 40.941 107.55 40.941 148.492 0l44.98-44.98c40.941-40.942 40.941-107.552 0-148.493zm-87.406 151.047c-17.606 17.605-46.032 17.605-63.64 0-17.61-17.606-17.606-46.035 0-63.64l44.98-44.981c17.609-17.61 46.03-17.61 63.64 0 17.61 17.605 17.61 46.031 0 63.64zm0 0\"\n          data-original=\"#6e6e6e\"\n          fill=\"#6E6E6E\"\n          opacity=\"1\"\n        ></path>\n        <g fill=\"#FFD400\">\n          <path\n            d=\"m409.242 305.422-9.488 28.465-63.64-21.215 9.488-28.461zM312.645 336.102l21.21 63.64-28.464 9.488-21.211-63.64zM206.586 102.762l21.21 63.64-28.464 9.489-21.21-63.641zM175.89 199.348l-9.484 28.464-63.64-21.21 9.484-28.465zm0 0\"\n            data-original=\"#ffd400\"\n            fill=\"#FFD400\"\n            opacity=\"1\"\n          ></path>\n        </g>\n        <path\n          d=\"m481.293 179.2-44.98 44.98c-40.942 40.941-107.551 40.941-148.493 0l42.426-42.426c17.606 17.605 46.035 17.605 63.64 0l44.981-44.98c17.61-17.61 17.61-46.036 0-63.641l42.426-42.426c40.941 40.941 40.941 107.55 0 148.492zm0 0\"\n          data-original=\"#5a5a5a\"\n          fill=\"#5A5A5A\"\n          opacity=\"1\"\n        ></path>\n        <path\n          d=\"M372.672 139.328c-17.606-17.61-46.031-17.61-63.64 0l-74.247 74.246 63.64 63.637 74.247-74.246c17.61-17.606 17.61-46.031 0-63.637zm0 0\"\n          data-original=\"#ffd400\"\n          fill=\"#FFD400\"\n          opacity=\"1\"\n        ></path>\n        <path\n          d=\"m372.672 202.965-74.246 74.246-31.82-31.816 106.066-106.067c17.61 17.606 17.61 46.031 0 63.637zm0 0\"\n          data-original=\"#ff9f00\"\n          fill=\"#FF9F00\"\n          opacity=\"1\"\n        ></path>\n        <path\n          d=\"m213.574 234.785-74.246 74.246c-17.605 17.61-17.605 46.035 0 63.64 17.606 17.61 46.035 17.606 63.64 0l74.247-74.245zm0 0\"\n          data-original=\"#ffd400\"\n          fill=\"#FFD400\"\n          opacity=\"1\"\n        ></path>\n        <path\n          d=\"m245.395 266.605 31.816 31.82-74.246 74.247c-17.606 17.605-46.031 17.61-63.637 0zm0 0\"\n          data-original=\"#ff9f00\"\n          fill=\"#FF9F00\"\n          opacity=\"1\"\n        ></path>\n      </g>\n    </svg>\n  );\n};\n"
  },
  {
    "path": "src/components/svg/Calendly.tsx",
    "content": "/* eslint-disable @next/next/no-img-element */\nimport * as React from \"react\";\n\n// On utilise l'image SVG du dossier public\nexport const CalendlyIcon: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = (props) => (\n  <img\n    alt=\"Calendly\"\n    height={20}\n    src=\"/icons/calendly.svg\"\n    style={{ display: \"inline-block\", verticalAlign: \"middle\" }}\n    width={20}\n    {...props}\n  />\n);\n"
  },
  {
    "path": "src/components/svg/CircleSvg.tsx",
    "content": "import { cn } from \"@/shared/lib/utils\";\n\nimport type { ComponentPropsWithoutRef } from \"react\";\n\nexport const CircleSvg = ({ className, ...props }: ComponentPropsWithoutRef<\"svg\">) => {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className={cn(\"fill-foreground absolute left-0 top-0 -z-10 w-[calc(100%+1rem)]\", className)}\n      height=\"62\"\n      preserveAspectRatio=\"none\"\n      viewBox=\"0 0 223 62\"\n      width=\"223\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n    >\n      <path d=\"M45.654 53.62c17.666 3.154 35.622 4.512 53.558 4.837 17.94.288 35.91-.468 53.702-2.54 8.89-1.062 17.742-2.442 26.455-4.352 8.684-1.945 17.338-4.3 25.303-7.905 3.94-1.81 7.79-3.962 10.634-6.777 1.38-1.41 2.424-2.994 2.758-4.561.358-1.563-.078-3.143-1.046-4.677-.986-1.524-2.43-2.96-4.114-4.175a37.926 37.926 0 0 0-5.422-3.32c-3.84-1.977-7.958-3.563-12.156-4.933-8.42-2.707-17.148-4.653-25.95-6.145-8.802-1.52-17.702-2.56-26.622-3.333-17.852-1.49-35.826-1.776-53.739-.978-8.953.433-17.898 1.125-26.79 2.22-8.887 1.095-17.738 2.541-26.428 4.616-4.342 1.037-8.648 2.226-12.853 3.676-4.197 1.455-8.314 3.16-12.104 5.363-1.862 1.13-3.706 2.333-5.218 3.829-1.52 1.47-2.79 3.193-3.285 5.113-.528 1.912-.127 3.965.951 5.743 1.07 1.785 2.632 3.335 4.348 4.68 2.135 1.652 3.2 2.672 2.986 3.083-.18.362-1.674.114-4.08-1.638-1.863-1.387-3.63-3.014-4.95-5.09C.94 35.316.424 34.148.171 32.89c-.275-1.253-.198-2.579.069-3.822.588-2.515 2.098-4.582 3.76-6.276 1.673-1.724 3.612-3.053 5.57-4.303 3.96-2.426 8.177-4.278 12.457-5.868 4.287-1.584 8.654-2.89 13.054-4.036 8.801-2.292 17.74-3.925 26.716-5.19C70.777 2.131 79.805 1.286 88.846.723c18.087-1.065 36.236-.974 54.325.397 9.041.717 18.07 1.714 27.042 3.225 8.972 1.485 17.895 3.444 26.649 6.253 4.37 1.426 8.697 3.083 12.878 5.243a42.11 42.11 0 0 1 6.094 3.762c1.954 1.44 3.823 3.2 5.283 5.485a12.515 12.515 0 0 1 1.63 3.88c.164.706.184 1.463.253 2.193-.063.73-.094 1.485-.247 2.195-.652 2.886-2.325 5.141-4.09 6.934-3.635 3.533-7.853 5.751-12.083 7.688-8.519 3.778-17.394 6.09-26.296 7.998-8.917 1.86-17.913 3.152-26.928 4.104-18.039 1.851-36.17 2.295-54.239 1.622-18.062-.713-36.112-2.535-53.824-6.23-5.941-1.31-5.217-2.91.361-1.852\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "src/components/svg/DiscordSvg.tsx",
    "content": "export const DiscordSvg = ({ className, ...props }: React.SVGProps<SVGSVGElement>) => (\n  <svg className={className} fill=\"currentColor\" viewBox=\"0 0 24 24\" {...props}>\n    <path d=\"M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z\" />\n  </svg>\n);\n"
  },
  {
    "path": "src/components/svg/DotPattern.tsx",
    "content": "import { cn } from \"@/shared/lib/utils\";\n\nimport type { ComponentPropsWithoutRef } from \"react\";\n\nexport type DotPatternProps = ComponentPropsWithoutRef<\"div\">;\n\nexport const DotPattern = ({ children, className, ...props }: DotPatternProps) => {\n  return (\n    <div className={cn(\"relative w-full\", className)} {...props}>\n      <div\n        className=\"dot-pattern absolute inset-0 -translate-x-4 translate-y-3\"\n        style={{\n          // @ts-expect-error CSS Variable\n          \"--dot-background\": \"transparent\",\n          \"--dot-color\": \"hsl(var(--primary))\",\n        }}\n      />\n      <div className=\"relative\">{children}</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/svg/GoogleSvg.tsx",
    "content": "import type { ComponentPropsWithoutRef } from \"react\";\n\nexport type GoogleSvgProps = ComponentPropsWithoutRef<\"svg\"> & { size?: number };\n\nexport const GoogleSvg = ({ size = 32, ...props }: GoogleSvgProps) => {\n  return (\n    <svg height={size} viewBox=\"0 0 24 24\" width={size} xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <path\n        d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"\n        fill=\"#4285F4\"\n      />\n      <path\n        d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"\n        fill=\"#34A853\"\n      />\n      <path\n        d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"\n        fill=\"#FBBC05\"\n      />\n      <path\n        d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"\n        fill=\"#EA4335\"\n      />\n      <path d=\"M1 1h22v22H1z\" fill=\"none\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "src/components/svg/IconCheckboxCheck.tsx",
    "content": "const IconCheckboxCheck = ({ className }: any) => {\n  return (\n    <svg className={className} fill=\"none\" height=\"6\" viewBox=\"0 0 6 6\" width=\"6\" xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        d=\"M1 3.00008L2.33333 4.33341L5 1.66675\"\n        stroke=\"currentcolor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.42222\"\n      />\n    </svg>\n  );\n};\nexport default IconCheckboxCheck;\n"
  },
  {
    "path": "src/components/svg/LogoSvg.tsx",
    "content": "import type { ComponentPropsWithoutRef } from \"react\";\n\nexport type LogoSvgProps = ComponentPropsWithoutRef<\"svg\"> & { size?: number };\n\nexport const LogoSvg = ({ size = 32, ...props }: LogoSvgProps) => {\n  return (\n    <svg height={size} viewBox=\"0 0 100 40\" width={size} xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      {/* Disque gauche */}\n      <circle className=\"opacity-90\" cx=\"15\" cy=\"20\" fill=\"currentColor\" r=\"12\" />\n\n      {/* Barre centrale */}\n      <rect fill=\"currentColor\" height=\"4\" rx=\"2\" width=\"76\" x=\"12\" y=\"18\" />\n\n      {/* Disque droit */}\n      <circle className=\"opacity-90\" cx=\"85\" cy=\"20\" fill=\"currentColor\" r=\"12\" />\n\n      {/* Poignée centrale (optionnel pour plus de détail) */}\n      <rect className=\"opacity-50\" fill=\"none\" height=\"8\" rx=\"4\" stroke=\"currentColor\" strokeWidth=\"1\" width=\"20\" x=\"40\" y=\"16\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "src/components/svg/UnderlineSvg.tsx",
    "content": "import type { ComponentPropsWithoutRef } from \"react\";\n\nexport const UnderlineSvg = (props: ComponentPropsWithoutRef<\"svg\">) => {\n  return (\n    <svg fill=\"none\" height=\"36\" viewBox=\"0 0 255 36\" width=\"255\" xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <path\n        d=\"M2.99975 17.6351C116.771 12.3405 178.178 12.7036 252 18.0966\"\n        stroke=\"currentColor\"\n        stroke-linecap=\"round\"\n        stroke-width=\"5.75696\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "src/components/svg/VerifiedBadge.tsx",
    "content": "import type { ComponentPropsWithoutRef } from \"react\";\n\nexport const VerifiedBadge = (props: ComponentPropsWithoutRef<\"svg\">) => {\n  return (\n    <svg viewBox=\"0 0 18 18\" xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <path d=\"M8.572.149L6.32 1.89l-2.702.104a.7.7 0 00-.637.478l-.92 2.757L.258 7.036a.7.7 0 00-.105.855L1.476 10.1l-.444 2.664a.7.7 0 00.412.757l2.602 1.128 1.159 2.413a.7.7 0 00.835.366L9 16.524l2.954.902a.7.7 0 00.848-.394l.968-2.264 2.793-1.248a.7.7 0 00.405-.755l-.445-2.665 1.325-2.209a.7.7 0 00-.105-.855L15.937 5.23l-.919-2.757a.7.7 0 00-.637-.478l-2.702-.104L9.43.149a.7.7 0 00-.857 0zM5.853 9.147L7.5 10.793l4.646-4.646a.5.5 0 11.707.707L7.5 12.207 5.146 9.854a.5.5 0 01.707-.707z\"></path>\n    </svg>\n  );\n};\n"
  },
  {
    "path": "src/components/svg/Youtube.tsx",
    "content": "/* eslint-disable @next/next/no-img-element */\nimport * as React from \"react\";\n\nexport const YoutubeIcon: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = (props) => (\n  <img\n    alt=\"Youtube\"\n    height={props.height ?? 20}\n    src=\"/icons/youtube.svg\"\n    style={{ display: \"inline-block\", verticalAlign: \"middle\" }}\n    width={props.width ?? 20}\n    {...props}\n  />\n);\n"
  },
  {
    "path": "src/components/ui/404-page-not-found.tsx",
    "content": "\"use client\";\n\nimport router from \"next/router\";\n\nimport { useI18n } from \"locales/client\";\nimport { Button } from \"@/components/ui/button\";\n\nexport function NotFoundPage() {\n  const t = useI18n();\n\n  return (\n    <section className=\"font-serif flex min-h-screen items-center justify-center bg-white\">\n      <div className=\"container mx-auto\">\n        <div className=\"flex justify-center\">\n          <div className=\"w-full text-center sm:w-10/12 md:w-8/12\">\n            <div\n              aria-hidden=\"true\"\n              className=\"h-[250px] bg-[url(https://cdn.dribbble.com/users/285475/screenshots/2083086/dribbble_1.gif)] bg-contain bg-center bg-no-repeat sm:h-[350px] md:h-[400px]\"\n            >\n              <h1 className=\"pt-6 text-center text-6xl text-black sm:pt-8 sm:text-7xl md:text-8xl\">404</h1>\n            </div>\n\n            <div className=\"mt-[-50px]\">\n              <h3 className=\"mb-4 text-2xl font-bold text-black sm:text-3xl\">{t(\"commons.looks_like_you_are_lost\")}</h3>\n              <p className=\"mb-6 text-black sm:mb-5\">{t(\"commons.the_page_you_are_looking_for_is_not_available\")}</p>\n\n              <Button className=\"my-5 bg-green-600 hover:bg-green-700\" onClick={() => router.push(\"/\")} variant=\"default\">\n                {t(\"commons.go_to_home\")}\n              </Button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/Bento.tsx",
    "content": "import { cn } from \"@/shared/lib/utils\";\n\nimport { Typography } from \"./typography\";\n\nexport const BentoGrid = ({ className, children }: { className?: string; children?: React.ReactNode }) => {\n  return <div className={cn(\"mx-auto grid max-w-7xl grid-cols-1 gap-4 md:auto-rows-[13rem] md:grid-cols-3\", className)}>{children}</div>;\n};\n\nexport const BentoGridItem = ({\n  className,\n  title,\n  description,\n  header,\n  icon,\n}: {\n  className?: string;\n  title?: string | React.ReactNode;\n  description?: string | React.ReactNode;\n  header?: React.ReactNode;\n  icon?: React.ReactNode;\n}) => {\n  return (\n    <div\n      className={cn(\n        \"group/bento shadow-input bg-card border-border row-span-1 flex flex-col justify-between space-y-4 rounded-xl border p-4 transition duration-200 hover:shadow-xl dark:shadow-none\",\n        className,\n      )}\n    >\n      {header}\n      <div className=\"flex flex-col gap-2\">\n        {icon}\n        <Typography variant=\"large\">{title}</Typography>\n        <Typography variant=\"muted\">{description}</Typography>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/ui/ToastSonner.tsx",
    "content": "\"use client\";\n\nimport { Toaster as Sonner } from \"sonner\";\nimport { useTheme } from \"next-themes\";\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>;\n\nconst ToastSonner = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme();\n\n  return (\n    <Sonner\n      className=\"toaster group\"\n      closeButton={true}\n      theme={theme as ToasterProps[\"theme\"]}\n      toastOptions={{\n        duration: 5000,\n        classNames: {\n          toast:\n            \"group overflow-visible toast group-[.toaster]:bg-white group-[.toaster]:!border-0 group-[.toaster]:p-0 group-[.toaster]:block group-[.toaster]:text-foreground group-[.toaster]:shadow-3xl \",\n          description: \"group-[.toast]:text-black text-sm/tight\",\n          actionButton: \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n          cancelButton: \"!text-black\",\n          closeButton:\n            \"text-black dark:text-white [&>svg]:size-[15px] border-0 hover:opacity-70 !top-[12px]  absolute  ml-auto [--toast-close-button-end:2px] \",\n        },\n      }}\n      {...props}\n    />\n  );\n};\n\nexport { ToastSonner };\n"
  },
  {
    "path": "src/components/ui/accordion.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => <AccordionPrimitive.Item className={cn(\"border-b border-black\", className)} ref={ref} {...props} />);\nAccordionItem.displayName = \"AccordionItem\";\n\nconst AccordionTrigger = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Header className=\"flex\">\n    <AccordionPrimitive.Trigger\n      className={cn(\n        \"font-mono group flex flex-1 items-center justify-between py-4 text-left transition-colors hover:underline\",\n        \"data-[state=open]:bg-accent/40\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    >\n      <span>{children}</span>\n      <ChevronDown\n        className={cn(\n          \"ml-2 size-5 shrink-0 transition-transform duration-200 ease-linear\",\n          \"group-data-[state=closed]:rotate-0 group-data-[state=open]:rotate-180\",\n          \"text-current\",\n        )}\n      />\n    </AccordionPrimitive.Trigger>\n  </AccordionPrimitive.Header>\n));\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;\n\nconst AccordionContent = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Content\n    className={cn(\n      \"overflow-hidden transition-[max-height,padding] duration-200 ease-in-out data-[state=closed]:pt-0 data-[state=open]:pt-2\",\n      \"data-[state=closed]:max-h-0\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  >\n    <div className=\"text-muted-foreground font-mono text-sm\">{children}</div>\n  </AccordionPrimitive.Content>\n));\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nexport { Accordion, AccordionContent, AccordionItem, AccordionTrigger };\n"
  },
  {
    "path": "src/components/ui/alert-dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\n\nimport { cn } from \"@/shared/lib/utils\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\nconst AlertDialog = AlertDialogPrimitive.Root;\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Overlay\n    className={cn(\n      \"bg-background/80 fixed inset-0 z-50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPortal>\n    <AlertDialogOverlay />\n    <AlertDialogPrimitive.Content\n      className={cn(\n        \"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    />\n  </AlertDialogPortal>\n));\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col space-y-2 text-center sm:text-left\", className)} {...props} />\n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nconst AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", className)} {...props} />\n);\nAlertDialogFooter.displayName = \"AlertDialogFooter\";\n\nconst AlertDialogTitle = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => <AlertDialogPrimitive.Title className={cn(\"text-lg font-semibold\", className)} ref={ref} {...props} />);\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Description className={cn(\"text-muted-foreground text-sm\", className)} ref={ref} {...props} />\n));\nAlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogAction = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Action>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} ref={ref} {...props} />);\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Cancel className={cn(buttonVariants({ variant: \"outline\" }), \"mt-2 sm:mt-0\", className)} ref={ref} {...props} />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n  AlertDialog,\n  AlertDialogPortal,\n  AlertDialogOverlay,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n};\n"
  },
  {
    "path": "src/components/ui/alert.tsx",
    "content": "import * as React from \"react\";\nimport { AlertCircle, AlertTriangle, CheckCircle2, Info, XCircle, type LucideIcon } from \"lucide-react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-background text-foreground\",\n        error:\n          \"border-red-500/50 bg-red-50/50 text-red-600 dark:border-red-500/30 dark:bg-red-900/30 dark:text-red-400 [&>svg]:text-red-600 dark:[&>svg]:text-red-400\",\n        warning:\n          \"border-yellow-500/50 bg-yellow-50/50 text-yellow-700 dark:border-yellow-500/30 dark:bg-yellow-900/30 dark:text-yellow-400 [&>svg]:text-yellow-600 dark:[&>svg]:text-yellow-400\",\n        success:\n          \"border-green-500/50 bg-green-50/50 text-green-600 dark:border-green-500/30 dark:bg-green-900/30 dark:text-green-400 [&>svg]:text-green-600 dark:[&>svg]:text-green-400\",\n        info: \"border-blue-500/50 bg-blue-50/50 text-blue-600 dark:border-blue-500/30 dark:bg-blue-900/30 dark:text-blue-400 [&>svg]:text-blue-600 dark:[&>svg]:text-blue-400\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst iconMap: Record<string, LucideIcon> = {\n  error: XCircle,\n  warning: AlertTriangle,\n  success: CheckCircle2,\n  info: Info,\n  default: AlertCircle,\n};\n\ninterface AlertProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof alertVariants> {\n  icon?: LucideIcon;\n}\n\nconst Alert = React.forwardRef<HTMLDivElement, AlertProps>(\n  ({ className, variant = \"default\", children, icon: CustomIcon, ...props }, ref) => {\n    const Icon = CustomIcon || iconMap[variant || \"default\"];\n\n    return (\n      <div className={cn(alertVariants({ variant }), className)} ref={ref} role=\"alert\" {...props}>\n        <Icon className=\"h-4 w-4\" />\n        {children}\n      </div>\n    );\n  },\n);\nAlert.displayName = \"Alert\";\n\nconst AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => (\n  <h5 className={cn(\"mb-1 font-medium leading-none tracking-tight\", className)} ref={ref} {...props} />\n));\nAlertTitle.displayName = \"AlertTitle\";\n\nconst AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, ...props }, ref) => <div className={cn(\"text-sm [&_p]:leading-relaxed\", className)} ref={ref} {...props} />,\n);\nAlertDescription.displayName = \"AlertDescription\";\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "src/components/ui/animated-button/ShinyButton.tsx",
    "content": "\"use client\";\n/* eslint-disable max-len */\nimport React, { HTMLAttributes } from \"react\";\n\ntype Props = HTMLAttributes<HTMLButtonElement> & {\n  children?: React.ReactNode;\n  wantGradient?: boolean;\n};\n\nexport const ShinyButton = ({ className, children, wantGradient = true, ...restProps }: Props) => {\n  const gradient = \"bg-gradient-to-r from-indigo-500 via-pink-500 to-yellow-500 hover:from-indigo-600 hover:via-pink-600 hover:to-red-600\";\n\n  const [isVibrating, setIsVibrating] = React.useState(true);\n\n  React.useEffect(() => {\n    const interval = setInterval(() => {\n      setIsVibrating(!isVibrating);\n    }, 5000);\n\n    return () => {\n      clearInterval(interval);\n    };\n  }, [isVibrating]);\n\n  return (\n    <button\n      className={`\n      ${wantGradient ? gradient : \"\"}\n      z-10 group relative ${\n        isVibrating ? \"vibrate-on-click\" : \"\"\n      } overflow-hidden focus:outline-none text-white font-bold shadow-md rounded-full px-4 py-2 transition-all duration-300 ease-in-out border-transparent ${className}`}\n      type=\"button\"\n      {...restProps}\n    >\n      {/* <div className=\"shimmer z-10 \"></div> */}\n\n      <span className=\"z-10  opacity-0 group-hover:opacity-100 absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full group-hover:-translate-y-2 transition-all duration-300 ease-in-out\">\n        Go ! 🚀\n      </span>\n      <span className=\"z-10 opacity-100 group-hover:opacity-0 transition-opacity duration-300 ease-in-out\">{children}</span>\n      <span\n        aria-hidden\n        className=\"absolute inset-0 -z-10 scale-x-[2.0] blur before:absolute before:inset-0 before:top-1/2 before:aspect-square before:animate-disco3s before:bg-gradient-conic before:from-primary-500 before:via-red-500 before:to-secondary-400 opacity-40\"\n      />\n    </button>\n  );\n};\n"
  },
  {
    "path": "src/components/ui/aspect-ratio.tsx",
    "content": "\"use client\";\n\nimport * as AspectRatioPrimitive from \"@radix-ui/react-aspect-ratio\";\n\nconst AspectRatio = AspectRatioPrimitive.Root;\n\nexport { AspectRatio };\n"
  },
  {
    "path": "src/components/ui/avatar.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst Avatar = React.forwardRef<React.ElementRef<typeof AvatarPrimitive.Root>, React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>>(\n  ({ className, ...props }, ref) => (\n    <AvatarPrimitive.Root className={cn(\"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\", className)} ref={ref} {...props} />\n  ),\n);\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image className={cn(\"aspect-square h-full w-full\", className)} ref={ref} {...props} />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    className={cn(\"bg-muted flex h-full w-full items-center justify-center rounded-full\", className)}\n    ref={ref}\n    {...props}\n  />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
  },
  {
    "path": "src/components/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center gap-1.5 rounded-lg px-2 py-2 text-xs/[10px] shrink-0 font-medium whitespace-nowrap transition text-black [&>svg]:size-3.5 [&>svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-black text-white\",\n        primary: \"bg-primary text-white\",\n        outline:\n          \"bg-white shadow-[0_1px_2px_0_rgba(113,116,152,0.1),0_0_0_1px_rgba(227,225,222,0.4)] text-gray dark:text-gray-500 dark:bg-black-dark\",\n        success: \"bg-success text-white\",\n        pending: \"bg-warning text-white\",\n        danger: \"bg-danger text-white\",\n        orange: \"bg-light-orange\",\n        green: \"bg-success-light\",\n        blue: \"bg-light-blue\",\n        purple: \"bg-light-purple\",\n        red: \"bg-danger-light\",\n        grey: \"bg-gray text-white\",\n        \"grey-700\": \"bg-gray-700 text-white\",\n        \"grey-600\": \"bg-gray-600 text-white\",\n        \"grey-500\": \"bg-gray-500 text-white\",\n        \"grey-400\": \"bg-gray-400\",\n        \"grey-300\": \"bg-gray-300\",\n      },\n      size: {\n        large: \"px-2 py-2.5\",\n        icon: \"py-1.5\",\n        small: \"px-1.5 py-[3px] leading-[12px] rounded-full\",\n        number: \"text-[10px]/[8px] px-1.5 py-1 font-semibold\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, size, ...props }: BadgeProps) {\n  return <div className={cn(badgeVariants({ variant, size }), className)} {...props} />;\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "src/components/ui/bottom-sheet-vaul.tsx",
    "content": "\"use client\";\n\nimport { Drawer as VaulDrawer } from \"vaul\";\nimport React, { useState, useEffect } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@/shared/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface BottomSheetProps {\n  isOpen: boolean;\n  onCloseAction: () => void; // Renommé pour correspondre à l'usage\n  title: string;\n  children: React.ReactNode;\n  phoneContainerId?: string; // Garde l'ID pour trouver le conteneur du portail\n  className?: string; // Ajout pour plus de flexibilité de style\n  contentClassName?: string; // Ajout pour le style du contenu interne\n  // Ajoute d'autres props de vaul si nécessaire (e.g., snapPoints, dismissible)\n}\n\nexport function BottomSheetVaul({\n  isOpen,\n  onCloseAction,\n  title,\n  children,\n  phoneContainerId = \"phone-preview-container\", // Default ID\n  className,\n  contentClassName,\n}: BottomSheetProps) {\n  const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);\n\n  // Trouve le conteneur du portail quand le composant est monté ou l'ID change\n  useEffect(() => {\n    const container = document.getElementById(phoneContainerId);\n    setPortalContainer(container);\n  }, [phoneContainerId]);\n\n  // Gère le changement d'état ouvert/fermé\n  const handleOpenChange = (open: boolean) => {\n    if (!open) {\n      onCloseAction();\n    }\n    // Vaul gère l'état interne 'open', on déclenche seulement la fermeture via la prop\n  };\n\n  // Ne rend rien côté serveur ou si le conteneur du portail n'est pas trouvé\n  if (!portalContainer) {\n    return null;\n  }\n\n  return (\n    <VaulDrawer.Root\n      container={portalContainer}\n      onOpenChange={handleOpenChange}\n      // direction=\"bottom\" // est la direction par défaut\n      // dismissible={true} // est vrai par défaut\n      open={isOpen}\n    >\n      <VaulDrawer.Portal container={portalContainer}>\n        {/* Overlay assombri */}\n        <VaulDrawer.Overlay className=\"fixed inset-0 z-[9998] bg-black/40\" />\n\n        {/* Contenu du tiroir */}\n        <VaulDrawer.Content\n          className={cn(\n            \"bg-background fixed bottom-0 left-0 right-0 z-[9999] mt-24 flex h-[80%] flex-col rounded-t-[10px] bg-white outline-none\", // Utilise bg-background de shadcn/tailwind\n            className, // Permet de surcharger le style externe\n          )}\n        >\n          {/* Barre de préhension (optionnel mais bon pour l'UX mobile) */}\n          {/* <div className=\"mx-auto mt-3 h-1.5 w-12 flex-shrink-0 rounded-full bg-slate-300\" /> */}\n\n          {/* En-tête */}\n          <VaulDrawer.Title className=\"sticky top-0 z-10 flex items-center justify-between rounded-t-[10px] border-b bg-gray-300/50 px-4 py-3\">\n            <span className=\"text-foreground font-medium\">{title}</span>\n            <VaulDrawer.Close asChild>\n              <Button aria-label=\"Fermer\" className=\"[&>svg]:size-[20px]\" size=\"large\" type=\"button\" variant=\"ghost\">\n                <X className=\"h-7 w-7\" />\n              </Button>\n            </VaulDrawer.Close>\n          </VaulDrawer.Title>\n\n          {/* Contenu scrollable */}\n          <div\n            className={cn(\n              \"flex-1 overflow-auto bg-[--page-bg] p-4\", // Ajout de padding par défaut\n              contentClassName, // Permet de surcharger le style interne\n            )}\n          >\n            {children}\n          </div>\n        </VaulDrawer.Content>\n      </VaulDrawer.Portal>\n    </VaulDrawer.Root>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/bottom-sheet.tsx",
    "content": "// src/components/ui/phone-drawer.tsx\n\"use client\"; // Nécessaire car utilise useState, useEffect, etc.\n\nimport { createPortal } from \"react-dom\";\nimport React, { useState, useEffect } from \"react\";\nimport { X } from \"lucide-react\";\nimport { motion, AnimatePresence, useDragControls, PanInfo } from \"framer-motion\";\n\n// --- DrawerHeader Component (peut rester ici ou être importé si partagé) ---\nconst DrawerHeader = ({\n  title,\n  onClose,\n  dragControls,\n}: {\n  title: string;\n  onClose: () => void;\n  dragControls: ReturnType<typeof useDragControls>;\n}) => (\n  <motion.div\n    className=\"sticky top-0 z-10 flex cursor-grab items-center justify-between rounded-lg border-b bg-gray-100 px-4 py-3 active:cursor-grabbing\"\n    onPointerDown={(event) => dragControls.start(event)}\n  >\n    <span className=\"font-medium text-gray-700\">{title}</span>\n    <button\n      className=\"cursor-pointer rounded-full p-1 text-gray-500 hover:bg-gray-200 hover:text-gray-800\"\n      onClick={onClose}\n      onPointerDown={(e) => e.stopPropagation()}\n      type=\"button\"\n    >\n      <X className=\"h-5 w-5\" />\n      <span className=\"sr-only\">Fermer</span>\n    </button>\n  </motion.div>\n);\n// --- End DrawerHeader ---\n\ninterface PhoneDrawerProps {\n  isOpen: boolean;\n  onCloseAction: () => void;\n  title: string;\n  children: React.ReactNode; // Accepte n'importe quel contenu React\n  phoneContainerId?: string; // Optional ID for the container\n}\n\nexport function BottomSheet({\n  isOpen,\n  onCloseAction,\n  title,\n  children,\n  phoneContainerId = \"phone-preview-container\", // Default ID\n}: PhoneDrawerProps) {\n  const [phoneContainer, setPhoneContainer] = useState<HTMLElement | null>(null);\n  const [containerScrollTop, setContainerScrollTop] = useState(0);\n  const [containerClientHeight, setContainerClientHeight] = useState(0);\n  const dragControls = useDragControls();\n\n  // Find the phone container element on mount and add scroll listener\n  useEffect(() => {\n    const container = document.getElementById(phoneContainerId);\n    if (container) {\n      setPhoneContainer(container);\n      const updateScrollState = () => {\n        setContainerScrollTop(container.scrollTop);\n        setContainerClientHeight(container.clientHeight);\n      };\n      container.addEventListener(\"scroll\", updateScrollState);\n      updateScrollState(); // Initial state\n      return () => container.removeEventListener(\"scroll\", updateScrollState);\n    }\n  }, [phoneContainerId]); // Re-run if ID changes\n\n  // Effect to control scroll based on drawer state for BOTH container and body\n  useEffect(() => {\n    if (!phoneContainer) return;\n\n    // Store original styles\n    const originalBodyOverflow = document.body.style.overflow;\n    const originalContainerOverflow = phoneContainer.style.overflow;\n\n    if (isOpen) {\n      // Apply lock styles\n      document.body.style.overflow = \"hidden\";\n      phoneContainer.style.overflow = \"hidden\"; // Keep locking the container too\n    } else {\n      // Restore original styles\n      document.body.style.overflow = originalBodyOverflow;\n      phoneContainer.style.overflow = originalContainerOverflow;\n    }\n\n    // Cleanup function to restore original styles on unmount\n    return () => {\n      document.body.style.overflow = originalBodyOverflow;\n      if (phoneContainer) {\n        // Check if container still exists on cleanup\n        phoneContainer.style.overflow = originalContainerOverflow;\n      }\n    };\n  }, [isOpen, phoneContainer]); // Re-run when isOpen or phoneContainer changes\n\n  // Function to handle the end of a drag gesture\n  const handleDragEnd = (event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {\n    const dragDistance = info.offset.y;\n    const velocity = info.velocity.y;\n\n    if (dragDistance > 50 || velocity > 300) {\n      onCloseAction(); // Use the passed onCloseAction handler\n    }\n  };\n\n  // Get current scroll/height just before potential render\n  // (Helps ensure the position is correct if container scrolls while drawer is closed)\n  useEffect(() => {\n    if (isOpen && phoneContainer) {\n      setContainerScrollTop(phoneContainer.scrollTop);\n      setContainerClientHeight(phoneContainer.clientHeight);\n    }\n  }, [isOpen, phoneContainer]);\n\n  // JSX for the drawer content, to be portaled\n  const DrawerPortalContent = (\n    <AnimatePresence>\n      {isOpen && (\n        <div\n          className=\"absolute inset-x-0 z-[9999]\"\n          style={{\n            top: `${containerScrollTop}px`,\n            height: `${containerClientHeight}px`,\n            pointerEvents: \"auto\",\n          }}\n        >\n          <motion.div\n            animate={{ opacity: 1 }}\n            className=\"absolute inset-0 bg-black/40\"\n            exit={{ opacity: 0 }}\n            initial={{ opacity: 0 }}\n            onClick={onCloseAction}\n            transition={{ duration: 0.3 }}\n          />\n          <motion.div\n            animate={{ y: 0 }}\n            className=\"absolute inset-x-0 bottom-0 flex h-[80%] flex-col rounded-t-[10px] bg-white outline-none\"\n            drag=\"y\"\n            dragConstraints={{ top: 0 }}\n            dragControls={dragControls}\n            dragElastic={0.2}\n            dragListener={false}\n            exit={{ y: \"100%\" }}\n            initial={{ y: \"100%\" }}\n            onDragEnd={handleDragEnd}\n            transition={{ type: \"spring\", damping: 25, stiffness: 300 }}\n          >\n            <DrawerHeader dragControls={dragControls} onClose={onCloseAction} title={title} />\n            {/* Render children passed as props */}\n            <div className=\"flex-1 overflow-auto bg-[--page-bg]\">{children}</div>\n          </motion.div>\n        </div>\n      )}\n    </AnimatePresence>\n  );\n\n  // Render the portal only if the container exists\n  return phoneContainer ? createPortal(DrawerPortalContent, phoneContainer) : null;\n}\n"
  },
  {
    "path": "src/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"@radix-ui/react-slot\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst buttonVariants = cva(\n  \"hover:scale-[0.98] inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-lg px-2.5 py-2 text-center text-xs/4 font-medium outline-none transition duration-300 disabled:pointer-events-none disabled:opacity-30 disabled:hover:cursor-not-allowed [&>svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-white hover:bg-primary/90\", // Simplified hover\n        destructive: \"bg-red-600 text-white hover:bg-red-600/90\", // Destructive variant\n        secondary: \"bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700\", // Secondary variant\n        ghost: \"hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-100\", // Ghost variant\n        link: \"text-primary underline-offset-4 hover:underline\", // Link variant\n        black: \"bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90\", // Adjusted hover for black\n        outline: \"border border-primary bg-transparent text-primary shadow-sm hover:bg-primary/5\", // Adjusted outline\n        \"outline-black\":\n          \"border border-black bg-transparent text-black shadow-sm hover:bg-black/5 dark:border-white dark:text-white dark:hover:bg-white/5\", // Adjusted outline-black\n        \"outline-general\":\n          \"border border-gray-300 bg-transparent text-black shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:text-white dark:hover:bg-gray-800\", // Adjusted outline-general\n      },\n      size: {\n        extraSmall: \"rounded-md p-1 text-xs\",\n        small: \"rounded-md px-2 text-xs\",\n        default: \"text-md p-1 px-3\",\n        large: \"text-md px-3 py-2\",\n        extralarge: \"text-md rounded-[10px] px-3.5 py-[11px] font-semibold [&>svg]:size-[18px]\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {\n  const Comp = asChild ? Slot : \"button\";\n  return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;\n});\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "src/components/ui/card-styled.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst cardVariants = cva(\"w-full relative\", {\n  variants: {\n    variant: {\n      default: [\"border rounded-lg\", \"border-zinc-200 dark:border-zinc-800\", \"bg-white dark:bg-zinc-950\"],\n      dots: [\"relative mx-auto w-full\", \"rounded-lg border border-dashed\", \"border-zinc-300 dark:border-zinc-800\", \"px-4 sm:px-6 md:px-8\"],\n      gradient: [\"relative mx-auto w-full\", \"px-4 sm:px-6 md:px-8\"],\n      plus: [\"border border-dashed\", \"border-zinc-400 dark:border-zinc-700\", \"relative\"],\n      neubrutalism: [\n        \"border-[0.5px]\",\n        \"border-zinc-400 dark:border-white/70\",\n        \"relative\",\n        \"shadow-[4px_4px_0px_0px_rgba(0,0,0)]\",\n        \"dark:shadow-[3px_3px_0px_0px_rgba(255,255,255,0.7)]\",\n      ],\n      inner: [\"border-[0.5px] rounded-sm p-2\", \"border-zinc-300 dark:border-zinc-800\"],\n      lifted: [\n        \"border rounded-xl\",\n        \"border-zinc-400 dark:border-zinc-700\",\n        \"relative\",\n        \"shadow-[0px_5px_0px_0px_rgba(0,0,0,0.7)]\",\n        \"dark:shadow-[0px_4px_0px_0px_rgba(255,255,255,0.5)]\",\n        \"bg-zinc-50 dark:bg-zinc-900/50\",\n      ],\n      corners: [\"border-2 rounded-md\", \"border-zinc-100 dark:border-zinc-700\", \"relative\"],\n    },\n  },\n  defaultVariants: {\n    variant: \"default\",\n  },\n});\n\nexport interface CardProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof cardVariants> {\n  title?: string;\n  description?: string;\n  isHtml?: boolean;\n}\n\nconst CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (\n  <div className={cn(\"p-3\", className)} ref={ref} {...props}>\n    {props.children}\n  </div>\n));\nCardContent.displayName = \"CardContent\";\n\nconst Card = React.forwardRef<HTMLDivElement, CardProps>(({ className, variant, title, description, isHtml, children, ...props }, ref) => {\n  const DotsPattern = () => {\n    const sharedClasses = \"rounded-full outline outline-8 dark:outline-gray-950 sm:my-6 md:my-8 size-1 my-4 outline-gray-50 bg-green-400\";\n\n    return (\n      <>\n        <div className=\"absolute left-0 top-4 -z-0 h-px w-full bg-zinc-400 sm:top-6 md:top-8 dark:bg-zinc-700\" />\n        <div className=\"absolute bottom-4 left-0 z-0 h-px w-full bg-zinc-400 sm:bottom-6 md:bottom-8 dark:bg-zinc-700\" />\n\n        <div className=\"relative w-full border-x border-zinc-400 dark:border-zinc-700\">\n          <div className=\"absolute z-0 grid h-full w-full items-center\">\n            <section className=\"absolute z-0 grid h-full w-full grid-cols-2 place-content-between\">\n              <div className={`${sharedClasses} -translate-x-[2.5px]`} />\n              <div className={`${sharedClasses} translate-x-[2.5px] place-self-end`} />\n              <div className={`${sharedClasses} -translate-x-[2.5px]`} />\n              <div className={`${sharedClasses} translate-x-[2.5px] place-self-end`} />\n            </section>\n          </div>\n\n          <div className=\"relative z-20 mx-auto py-8\">\n            <CardContent>\n              {title && <h3 className=\"mb-1 text-lg font-bold text-[--card-text] dark:text-[--card-text]\">{title}</h3>}\n              {description &&\n                (isHtml ? (\n                  <div dangerouslySetInnerHTML={{ __html: description }} />\n                ) : (\n                  <p className=\"text-gray-700 dark:text-gray-300\">{description}</p>\n                ))}\n\n              {children}\n            </CardContent>\n          </div>\n        </div>\n      </>\n    );\n  };\n\n  const GradientLines = () => (\n    <>\n      <div className=\"absolute left-0 top-4 -z-0 h-px w-full bg-gradient-to-l from-zinc-200 via-zinc-400 to-zinc-600 sm:top-6 md:top-8 dark:from-zinc-900 dark:via-zinc-700 dark:to-zinc-500\" />\n      <div className=\"absolute bottom-4 left-0 z-0 h-px w-full bg-gradient-to-r from-zinc-200 via-zinc-400 to-zinc-600 sm:bottom-6 md:bottom-8 dark:from-zinc-900 dark:via-zinc-700 dark:to-zinc-500\" />\n      <div className=\"border-gradient-x relative w-full border-x\">\n        <div className=\"absolute inset-y-0 left-0 w-px bg-gradient-to-t from-zinc-200 via-zinc-400 to-zinc-600 dark:from-zinc-900 dark:via-zinc-700 dark:to-zinc-500\" />\n        <div className=\"absolute inset-y-0 right-0 w-px bg-gradient-to-t from-zinc-200 via-zinc-400 to-zinc-600 dark:from-zinc-900 dark:via-zinc-700 dark:to-zinc-500\" />\n        <div className=\"relative z-20 mx-auto py-8\">{content}</div>\n      </div>\n    </>\n  );\n\n  const PlusIcons = () => (\n    <>\n      <svg\n        className=\"absolute -left-3 -top-3 size-6 text-black dark:text-white\"\n        fill=\"none\"\n        height={24}\n        stroke=\"currentColor\"\n        strokeWidth=\"1\"\n        viewBox=\"0 0 24 24\"\n        width={24}\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path d=\"M12 6v12m6-6H6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n      </svg>\n      <svg\n        className=\"absolute -right-3 -top-3 size-6 text-black dark:text-white\"\n        fill=\"none\"\n        height={24}\n        stroke=\"currentColor\"\n        strokeWidth=\"1\"\n        viewBox=\"0 0 24 24\"\n        width={24}\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path d=\"M12 6v12m6-6H6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n      </svg>\n      <svg\n        className=\"absolute -bottom-3 -left-3 size-6 text-black dark:text-white\"\n        fill=\"none\"\n        height={24}\n        stroke=\"currentColor\"\n        strokeWidth=\"1\"\n        viewBox=\"0 0 24 24\"\n        width={24}\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path d=\"M12 6v12m6-6H6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n      </svg>\n      <svg\n        className=\"absolute -bottom-3 -right-3 size-6 text-black dark:text-white\"\n        fill=\"none\"\n        height={24}\n        stroke=\"currentColor\"\n        strokeWidth=\"1\"\n        viewBox=\"0 0 24 24\"\n        width={24}\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path d=\"M12 6v12m6-6H6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n      </svg>\n    </>\n  );\n\n  const CornerBorders = () => (\n    <>\n      <div className=\"absolute -left-0.5 -top-0.5 size-6 rounded-tl-md border-l-2 border-t-2 border-zinc-700 dark:border-zinc-200\" />\n      <div className=\"absolute -right-0.5 -top-0.5 size-6 rounded-tr-md border-r-2 border-t-2 border-zinc-700 dark:border-zinc-200\" />\n      <div className=\"absolute -bottom-0.5 -left-0.5 size-6 rounded-bl-md border-b-2 border-l-2 border-zinc-700 dark:border-zinc-200\" />\n      <div className=\"absolute -bottom-0.5 -right-0.5 size-6 rounded-br-md border-b-2 border-r-2 border-zinc-700 dark:border-zinc-200\" />\n    </>\n  );\n\n  const InnerContent = () => {\n    if (variant === \"dots\") return <DotsPattern />;\n    if (variant === \"gradient\") return <GradientLines />;\n    if (variant === \"plus\") return <PlusIcons />;\n    if (variant === \"corners\") return <CornerBorders />;\n    return null;\n  };\n\n  const content = (\n    <CardContent>\n      {title && <h3 className=\"mb-1 text-lg font-bold text-[--card-text] dark:text-[--card-text]\">{title}</h3>}\n      {description &&\n        (isHtml ? (\n          <div dangerouslySetInnerHTML={{ __html: description }} />\n        ) : (\n          <p className=\"text-gray-700 dark:text-gray-300\">{description}</p>\n        ))}\n      {children}\n    </CardContent>\n  );\n\n  if (variant === \"dots\") {\n    return (\n      <div className={cn(cardVariants({ variant, className }))} ref={ref} {...props}>\n        <div className=\"absolute left-0 top-4 -z-0 h-px w-full bg-zinc-400 sm:top-6 md:top-8 dark:bg-zinc-700\" />\n        <div className=\"absolute bottom-4 left-0 z-0 h-px w-full bg-zinc-400 sm:bottom-6 md:bottom-8 dark:bg-zinc-700\" />\n        <div className=\"relative w-full border-x border-zinc-400 dark:border-zinc-700\">\n          <div className=\"absolute z-0 grid h-full w-full items-center\">\n            <section className=\"absolute z-0 grid h-full w-full grid-cols-2 place-content-between\">\n              <div className=\"my-4 size-1 -translate-x-[2.5px] rounded-full bg-green-400 outline outline-8 outline-gray-50 sm:my-6 md:my-8 dark:outline-gray-950\" />\n              <div className=\"my-4 size-1 translate-x-[2.5px] place-self-end rounded-full bg-green-400 outline outline-8 outline-gray-50 sm:my-6 md:my-8 dark:outline-gray-950\" />\n              <div className=\"my-4 size-1 -translate-x-[2.5px] rounded-full bg-green-400 outline outline-8 outline-gray-50 sm:my-6 md:my-8 dark:outline-gray-950\" />\n              <div className=\"my-4 size-1 translate-x-[2.5px] place-self-end rounded-full bg-green-400 outline outline-8 outline-gray-50 sm:my-6 md:my-8 dark:outline-gray-950\" />\n            </section>\n          </div>\n          <div className=\"relative z-20 mx-auto py-8\">{content}</div>\n        </div>\n      </div>\n    );\n  }\n\n  if (variant === \"inner\") {\n    return (\n      <div className={cn(cardVariants({ variant, className }))} ref={ref} {...props}>\n        <div className=\"rounded-sm border border-zinc-300 bg-gradient-to-br from-white to-zinc-200/60 shadow-[2px_0_8px_rgba(0,_0,_0,_0.15)] dark:border-zinc-900/50 dark:from-zinc-950 dark:to-zinc-900/60 dark:shadow-inner\">\n          {content}\n        </div>\n      </div>\n    );\n  }\n\n  if (variant === \"gradient\") {\n    return (\n      <div className={cn(cardVariants({ variant, className }))} ref={ref} {...props}>\n        <GradientLines />\n      </div>\n    );\n  }\n\n  return (\n    <div className={cn(cardVariants({ variant, className }))} ref={ref} {...props}>\n      <InnerContent />\n      {content}\n    </div>\n  );\n});\nCard.displayName = \"Card\";\n\nexport { Card, CardContent, cardVariants };\n"
  },
  {
    "path": "src/components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (\n  <div className={cn(\"rounded-lg bg-white shadow-3xl dark:bg-black-dark dark:shadow-sm\", className)} ref={ref} {...props} />\n));\nCard.displayName = \"Card\";\n\nconst CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (\n  <div className={cn(\"\", className)} ref={ref} {...props} />\n));\nCardHeader.displayName = \"CardHeader\";\n\nconst CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => (\n  <h3 className={cn(\"text-2xl font-semibold leading-none tracking-tight\", className)} ref={ref} {...props} />\n));\nCardTitle.displayName = \"CardTitle\";\n\nconst CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, ...props }, ref) => <p className={cn(\"\", className)} ref={ref} {...props} />,\n);\nCardDescription.displayName = \"CardDescription\";\n\nconst CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (\n  <div className={cn(\"\", className)} ref={ref} {...props} />\n));\nCardContent.displayName = \"CardContent\";\n\nconst CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (\n  <div className={cn(\"\", className)} ref={ref} {...props} />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };\n"
  },
  {
    "path": "src/components/ui/collapsible.tsx",
    "content": "\"use client\";\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\";\n\nconst Collapsible = CollapsiblePrimitive.Root;\n\nconst CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;\n\nconst CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent };\n"
  },
  {
    "path": "src/components/ui/dialog-stack.tsx",
    "content": "\"use client\";\n\nimport { Children, cloneElement, createContext, useContext, useEffect, useState } from \"react\";\nimport * as Portal from \"@radix-ui/react-portal\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nimport type { ButtonHTMLAttributes, Dispatch, HTMLAttributes, MouseEventHandler, ReactElement, ReactNode, SetStateAction } from \"react\";\n\ntype DialogStackContextType = {\n  activeIndex: number;\n  setActiveIndex: Dispatch<SetStateAction<number>>;\n  totalDialogs: number;\n  setTotalDialogs: Dispatch<SetStateAction<number>>;\n  isOpen: boolean;\n  setIsOpen: Dispatch<SetStateAction<boolean>>;\n  clickable: boolean;\n};\n\nconst DialogStackContext = createContext<DialogStackContextType>({\n  activeIndex: 0,\n  setActiveIndex: () => {},\n  totalDialogs: 0,\n  setTotalDialogs: () => {},\n  isOpen: false,\n  setIsOpen: () => {},\n  clickable: false,\n});\n\ntype DialogStackChildProps = {\n  index?: number;\n};\n\nexport const DialogStack = ({\n  children,\n  className,\n  open = false,\n  onOpenChange,\n  clickable = false,\n  ...props\n}: HTMLAttributes<HTMLDivElement> & {\n  open?: boolean;\n  clickable?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}) => {\n  const [activeIndex, setActiveIndex] = useState(0);\n  const [isOpen, setIsOpen] = useState(open);\n\n  useEffect(() => {\n    onOpenChange?.(isOpen);\n  }, [isOpen, onOpenChange]);\n\n  return (\n    <DialogStackContext.Provider\n      value={{\n        activeIndex,\n        setActiveIndex,\n        totalDialogs: 0,\n        setTotalDialogs: () => {},\n        isOpen,\n        setIsOpen,\n        clickable,\n      }}\n    >\n      <div className={className} {...props}>\n        {children}\n      </div>\n    </DialogStackContext.Provider>\n  );\n};\n\nexport const DialogStackTrigger = ({\n  children,\n  className,\n  onClick,\n  asChild,\n  ...props\n}: ButtonHTMLAttributes<HTMLButtonElement> & { asChild?: boolean }) => {\n  const context = useContext(DialogStackContext);\n\n  if (!context) {\n    throw new Error(\"DialogStackTrigger must be used within a DialogStack\");\n  }\n\n  const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => {\n    context.setIsOpen(true);\n    onClick?.(e);\n  };\n\n  if (asChild && children) {\n    return cloneElement(children as ReactElement, {\n      // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n      // @ts-ignore\n      onClick: handleClick,\n      // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n      // @ts-ignore\n      className: cn(className, (children as ReactElement).props.className),\n      ...props,\n    });\n  }\n\n  return (\n    <button\n      className={cn(\n        \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium\",\n        \"ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2\",\n        \"focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n        \"text-primary-foreground bg-primary hover:bg-primary/90\",\n        \"h-10 px-4 py-2\",\n        className,\n      )}\n      onClick={handleClick}\n      {...props}\n    >\n      {children}\n    </button>\n  );\n};\n\nexport const DialogStackOverlay = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => {\n  const context = useContext(DialogStackContext);\n\n  if (!context) {\n    throw new Error(\"DialogStackOverlay must be used within a DialogStack\");\n  }\n\n  if (!context.isOpen) {\n    return null;\n  }\n\n  return (\n    // biome-ignore lint/nursery/noStaticElementInteractions: \"This is a clickable overlay\"\n    <div\n      className={cn(\n        \"fixed inset-0 z-50 bg-black/80\",\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out\",\n        \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n        className,\n      )}\n      onClick={() => context.setIsOpen(false)}\n      {...props}\n    />\n  );\n};\n\nexport const DialogStackBody = ({\n  children,\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement> & {\n  children: ReactElement<DialogStackChildProps>[] | ReactElement<DialogStackChildProps>;\n}) => {\n  const context = useContext(DialogStackContext);\n  const [totalDialogs, setTotalDialogs] = useState(Children.count(children));\n\n  if (!context) {\n    throw new Error(\"DialogStackBody must be used within a DialogStack\");\n  }\n\n  if (!context.isOpen) {\n    return null;\n  }\n\n  return (\n    <DialogStackContext.Provider\n      value={{\n        ...context,\n        totalDialogs,\n        setTotalDialogs,\n      }}\n    >\n      <Portal.Root>\n        <div\n          className={cn(\n            \"pointer-events-none fixed inset-0 z-50 mx-auto flex w-full max-w-lg flex-col items-center justify-center\",\n            className,\n          )}\n          {...props}\n        >\n          <div className=\"pointer-events-auto relative flex w-full flex-col items-center justify-center\">\n            {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}\n            {/* @ts-ignore */}\n            {Children.map(children, (child, index) => cloneElement(child as ReactElement, { index }))}\n          </div>\n        </div>\n      </Portal.Root>\n    </DialogStackContext.Provider>\n  );\n};\n\nexport const DialogStackContent = ({\n  children,\n  className,\n  index = 0,\n  offset = 10,\n  ...props\n}: HTMLAttributes<HTMLDivElement> & {\n  index?: number;\n  offset?: number;\n}) => {\n  const context = useContext(DialogStackContext);\n\n  if (!context) {\n    throw new Error(\"DialogStackContent must be used within a DialogStack\");\n  }\n\n  if (!context.isOpen) {\n    return null;\n  }\n\n  const handleClick = () => {\n    if (context.clickable && context.activeIndex > index) {\n      context.setActiveIndex(index ?? 0);\n    }\n  };\n\n  const distanceFromActive = index - context.activeIndex;\n  const translateY = distanceFromActive < 0 ? `-${Math.abs(distanceFromActive) * offset}px` : `${Math.abs(distanceFromActive) * offset}px`;\n\n  return (\n    // biome-ignore lint/nursery/noStaticElementInteractions: \"This is a clickable dialog\"\n    <div\n      className={cn(\n        \"bg-background h-auto w-full rounded-lg border p-6 shadow-lg transition-all duration-300\",\n\n        className,\n      )}\n      onClick={handleClick}\n      style={{\n        top: 0,\n        transform: `translateY(${translateY})`,\n        width: `calc(100% - ${Math.abs(distanceFromActive) * 10}px)`,\n        zIndex: 50 - Math.abs(context.activeIndex - (index ?? 0)),\n        position: distanceFromActive ? \"absolute\" : \"relative\",\n        opacity: distanceFromActive > 0 ? 0 : 1,\n        cursor: context.clickable && context.activeIndex > index ? \"pointer\" : \"default\",\n      }}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"h-full w-full transition-all duration-300\",\n          context.activeIndex !== index && \"pointer-events-none select-none opacity-0\",\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  );\n};\n\nexport const DialogStackTitle = ({ children, className, ...props }: HTMLAttributes<HTMLHeadingElement>) => (\n  <h2 className={cn(\"text-lg font-semibold leading-none tracking-tight\", className)} {...props}>\n    {children}\n  </h2>\n);\n\nexport const DialogStackDescription = ({ children, className, ...props }: HTMLAttributes<HTMLParagraphElement>) => (\n  <p className={cn(\"text-muted-foreground text-sm\", className)} {...props}>\n    {children}\n  </p>\n);\n\nexport const DialogStackHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col space-y-1.5 text-center sm:text-left\", className)} {...props} />\n);\n\nexport const DialogStackFooter = ({ children, className, ...props }: HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex items-center justify-end space-x-2 pt-4\", className)} {...props}>\n    {children}\n  </div>\n);\n\nexport const DialogStackNext = ({\n  children,\n  className,\n  asChild,\n  ...props\n}: {\n  asChild?: boolean;\n} & HTMLAttributes<HTMLButtonElement>) => {\n  const context = useContext(DialogStackContext);\n\n  if (!context) {\n    throw new Error(\"DialogStackNext must be used within a DialogStack\");\n  }\n\n  const handleNext = () => {\n    if (context.activeIndex < context.totalDialogs - 1) {\n      context.setActiveIndex(context.activeIndex + 1);\n    }\n  };\n\n  if (asChild && children) {\n    return cloneElement(children as ReactElement, {\n      onClick: handleNext,\n      // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n      // @ts-ignore\n      className: cn(className, (children as ReactElement).props.className),\n      ...props,\n    });\n  }\n\n  return (\n    <button\n      className={cn(\n        \"ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n        className,\n      )}\n      disabled={context.activeIndex >= context.totalDialogs - 1}\n      onClick={handleNext}\n      type=\"button\"\n      {...props}\n    >\n      {children || \"Next\"}\n    </button>\n  );\n};\n\nexport const DialogStackPrevious = ({\n  children,\n  className,\n  asChild,\n  ...props\n}: {\n  children?: ReactNode;\n  className?: string;\n  asChild?: boolean;\n} & HTMLAttributes<HTMLButtonElement>) => {\n  const context = useContext(DialogStackContext);\n\n  if (!context) {\n    throw new Error(\"DialogStackPrevious must be used within a DialogStack\");\n  }\n\n  const handlePrevious = () => {\n    if (context.activeIndex > 0) {\n      context.setActiveIndex(context.activeIndex - 1);\n    }\n  };\n\n  if (asChild && children) {\n    return cloneElement(children as ReactElement, {\n      onClick: handlePrevious,\n      // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n      // @ts-ignore\n      className: cn(className, (children as ReactElement).props.className),\n      ...props,\n    });\n  }\n\n  return (\n    <button\n      className={cn(\n        \"ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n        className,\n      )}\n      disabled={context.activeIndex <= 0}\n      onClick={handlePrevious}\n      type=\"button\"\n      {...props}\n    >\n      {children || \"Previous\"}\n    </button>\n  );\n};\n"
  },
  {
    "path": "src/components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { X } from \"lucide-react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-[1000] grid max-h-full w-[90%] max-w-md translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto border bg-white p-5 shadow-sm duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-gray-300/20 dark:bg-black-dark [&>div]:rounded-none sm:[&>div]:rounded-lg\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-[18px] rounded-sm text-black transition-opacity hover:opacity-70 focus:outline-none disabled:pointer-events-none ltr:right-5 rtl:left-5 dark:text-white dark:bg-slate-900\">\n        <X className=\"h-6 w-6 flex-none p-1 hover:rounded-lg hover:bg-gray-300 dark:hover:bg-gray-700\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col space-y-1.5 text-center sm:text-left\", className)} {...props} />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", className)} {...props} />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title className={cn(\"text-lg font-semibold leading-none text-black dark:text-white\", className)} ref={ref} {...props} />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description className={cn(\"text-muted-foreground text-sm\", className)} ref={ref} {...props} />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "src/components/ui/divider.tsx",
    "content": "import { cn } from \"@/shared/lib/utils\";\n\nimport type { ComponentProps } from \"react\";\n\nexport type DividerProps = ComponentProps<\"div\">;\n\nexport const Divider = ({ className, children, ...props }: DividerProps) => {\n  return (\n    <span className={cn(\"relative flex justify-center\", className)} {...props}>\n      <div className=\"absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-transparent bg-gradient-to-r from-transparent via-gray-500 to-transparent opacity-75\"></div>\n\n      <span className=\"bg-muted z-10 mx-1 rounded-full border px-2 py-1 text-center text-xs\">{children}</span>\n    </span>\n  );\n};\n"
  },
  {
    "path": "src/components/ui/donation-alert.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\n\ninterface DonationAlertProps {\n  className?: string;\n}\n\nexport const DonationAlert = ({ className }: DonationAlertProps) => {\n  const t = useI18n();\n\n  return (\n    <Alert\n      className={cn(\n        \"flex items-center bg-gray-300 border-gray-400 text-gray-800 dark:bg-slate-600 dark:border-slate-500 dark:text-slate-200\",\n        className,\n      )}\n      variant=\"info\"\n    >\n      <AlertDescription className=\"flex items-center gap-1 italic text-base\">\n        <span className=\"whitespace-pre-line\">\n          {t(\"donation_alert.title\")}{\" \"}\n          <Link\n            className=\"font-medium text-gray-900 underline hover:text-gray-700 dark:text-gray-200\"\n            href=\"https://ko-fi.com/workoutcool\"\n            target=\"_blank\"\n          >\n            Ko-fi\n          </Link>{\" \"}\n          or{\" \"}\n          <Link\n            className=\"font-medium text-gray-900 underline hover:text-gray-700 dark:text-gray-200\"\n            href=\"https://github.com/sponsors/snouzy\"\n            target=\"_blank\"\n          >\n            GitHub Sponsors\n          </Link>\n          .\n        </span>\n      </AlertDescription>\n    </Alert>\n  );\n};\n"
  },
  {
    "path": "src/components/ui/dropdown-menu.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Check, Circle } from \"lucide-react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\nDropdownMenu.displayName = DropdownMenuPrimitive.Root.displayName;\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    className={cn(\n      \"focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm !outline-none [&[data-state=open]>svg]:!rotate-180\",\n      inset && \"pl-8\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  >\n    {children}\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 6, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      className={cn(\n        \"z-50 min-w-[300px] space-y-1.5 overflow-y-auto rounded-b-lg bg-white p-1.5 text-gray-700 shadow-3xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border dark:border-white/10 dark:bg-black-dark dark:text-gray-500 dark:shadow-sm\",\n        className,\n      )}\n      ref={ref}\n      sideOffset={sideOffset}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    className={cn(\n      \"focus:bg-accent focus:text-accent-foreground relative cursor-pointer select-none items-center rounded-lg px-3 py-2.5 text-sm/none font-medium outline-none transition-colors hover:bg-gray-400 hover:text-black data-[disabled]:opacity-50 dark:hover:bg-gray-200/10 dark:hover:text-white\",\n      inset && \"pl-8\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    checked={checked}\n    className={cn(\n      \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs/tight outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-3 w-3\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    className={cn(\n      \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label className={cn(\"px-2 py-1.5 text-sm font-semibold\", inset && \"pl-8\", className)} ref={ref} {...props} />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator className={cn(\"mx-3 h-px bg-gray-300 dark:bg-gray-700/50\", className)} ref={ref} {...props} />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {\n  return <span className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)} {...props} />;\n};\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\";\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
  },
  {
    "path": "src/components/ui/form.tsx",
    "content": "import { Controller, FormProvider, useForm, useFormContext } from \"react-hook-form\";\nimport * as React from \"react\";\nimport { TriangleAlert } from \"lucide-react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\n\nimport { Label } from \"./label\";\n\nimport type { z, ZodSchema } from \"zod\";\nimport type { ControllerProps, FieldPath, FieldValues, SubmitHandler, UseFormProps, UseFormReturn } from \"react-hook-form\";\nimport type * as LabelPrimitive from \"@radix-ui/react-label\";\n\ntype FormProps<T extends FieldValues> = Omit<React.ComponentProps<\"form\">, \"onSubmit\"> & {\n  form: UseFormReturn<T>;\n  onSubmit: SubmitHandler<T>;\n  disabled?: boolean;\n};\n\nconst Form = <T extends FieldValues>({ form, onSubmit, children, className, disabled, ...props }: FormProps<T>) => (\n  <FormProvider {...form}>\n    <form onSubmit={form.handleSubmit(onSubmit)} {...props} className={className}>\n      <fieldset className={className} disabled={disabled || form.formState.isSubmitting}>\n        {children}\n      </fieldset>\n    </form>\n  </FormProvider>\n);\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);\n\nconst FormField = <TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState, formState } = useFormContext();\n\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext.name) {\n    throw new Error(\"useFormField should be used within <FormField>\");\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);\n\nconst FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => {\n  const id = React.useId();\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div className={cn(\"space-y-2\", className)} ref={ref} {...props} />\n    </FormItemContext.Provider>\n  );\n});\nFormItem.displayName = \"FormItem\";\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField();\n\n  return <Label className={cn(error && \"text-destructive\", className)} htmlFor={formItemId} ref={ref} {...props} />;\n});\nFormLabel.displayName = \"FormLabel\";\n\nconst FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } = useFormField();\n\n  return (\n    <Slot\n      aria-describedby={error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`}\n      aria-invalid={!!error}\n      id={formItemId}\n      ref={ref}\n      {...props}\n    />\n  );\n});\nFormControl.displayName = \"FormControl\";\n\nconst FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, ...props }, ref) => {\n    const { formDescriptionId } = useFormField();\n\n    return <p className={cn(\"text-muted-foreground text-sm\", className)} id={formDescriptionId} ref={ref} {...props} />;\n  },\n);\nFormDescription.displayName = \"FormDescription\";\n\nconst FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ children, ...props }, ref) => {\n  const t = useI18n();\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error.message) : children;\n\n  if (!body) {\n    return null;\n  }\n\n  return (\n    <div className=\"!mt-2.5 flex items-center gap-2\" id={formMessageId} ref={ref} {...props}>\n      <TriangleAlert className=\"size-[18px] shrink-0 text-danger dark:text-danger/70\" />\n      <p className=\"text-xs/tight font-medium text-danger\">{t(body as keyof typeof t)}</p>\n    </div>\n  );\n});\nFormMessage.displayName = \"FormMessage\";\n\ntype UseZodFormProps<Schema extends ZodSchema> = Exclude<UseFormProps<z.infer<Schema>>, \"resolver\"> & {\n  schema: Schema;\n};\n\nfunction useZodForm<Schema extends ZodSchema>({ schema, ...formProps }: UseZodFormProps<Schema>): UseFormReturn<z.infer<Schema>> {\n  return useForm<z.infer<Schema>>({\n    ...formProps,\n    resolver: zodResolver(schema as any),\n  });\n}\n\nexport { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField, useZodForm };\n"
  },
  {
    "path": "src/components/ui/hover-card.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst HoverCard = HoverCardPrimitive.Root;\n\nconst HoverCardTrigger = HoverCardPrimitive.Trigger;\n\nconst HoverCardContent = React.forwardRef<\n  React.ElementRef<typeof HoverCardPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 6, ...props }, ref) => (\n  <HoverCardPrimitive.Content\n    align={align}\n    className={cn(\n      \"z-50 max-w-80 rounded-lg border bg-white p-4 text-black shadow-3xl outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 sm:max-w-[400px] dark:border-gray-300/20 dark:bg-black-dark dark:text-white dark:shadow-sm\",\n      className,\n    )}\n    ref={ref}\n    sideOffset={sideOffset}\n    {...props}\n  />\n));\nHoverCardContent.displayName = HoverCardPrimitive.Content.displayName;\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent };\n"
  },
  {
    "path": "src/components/ui/input-password-strength.tsx",
    "content": "\"use client\";\n\nimport { useId, useMemo, forwardRef } from \"react\";\nimport { Check, X } from \"lucide-react\";\n\nimport { Input, InputProps } from \"@/components/ui/input\";\n\nexport const InputPasswordStrength = forwardRef<HTMLInputElement, InputProps>(({ onChange, value, ...props }, ref) => {\n  const id = useId();\n\n  const checkStrength = (pass: string) => {\n    const requirements = [\n      { regex: /.{8,}/, text: \"At least 8 characters\" },\n      { regex: /[0-9]/, text: \"At least 1 number\" },\n      { regex: /[a-z]/, text: \"At least 1 lowercase letter\" },\n      { regex: /[A-Z]/, text: \"At least 1 uppercase letter\" },\n    ];\n\n    return requirements.map((req) => ({\n      met: req.regex.test(pass || \"\"),\n      text: req.text,\n    }));\n  };\n\n  const strength = checkStrength(value as string);\n\n  const strengthScore = useMemo(() => {\n    return strength.filter((req) => req.met).length;\n  }, [strength]);\n\n  const getStrengthColor = (score: number) => {\n    if (score === 0) return \"bg-border\";\n    if (score <= 1) return \"bg-red-500\";\n    if (score <= 2) return \"bg-orange-500\";\n    if (score === 3) return \"bg-amber-500\";\n    return \"bg-emerald-500\";\n  };\n\n  const getStrengthText = (score: number) => {\n    if (score === 0) return \"Enter a password\";\n    if (score <= 2) return \"Weak password\";\n    if (score === 3) return \"Medium password\";\n    return \"Strong password\";\n  };\n\n  return (\n    <div className=\"min-w-[300px]\">\n      <div className=\"space-y-2\">\n        <div className=\"relative\">\n          <Input\n            aria-describedby={`${id}-description`}\n            aria-invalid={strengthScore < 4}\n            className=\"pe-9\"\n            id={id}\n            onChange={onChange}\n            placeholder=\"Password\"\n            ref={ref}\n            type={\"password\"}\n            value={value}\n            {...props}\n          />\n        </div>\n      </div>\n\n      <div\n        aria-label=\"Password strength\"\n        aria-valuemax={4}\n        aria-valuemin={0}\n        aria-valuenow={strengthScore}\n        className=\"bg-border mb-4 mt-3 h-1 w-full overflow-hidden rounded-full\"\n        role=\"progressbar\"\n      >\n        <div\n          className={`h-full ${getStrengthColor(strengthScore)} transition-all duration-500 ease-out`}\n          style={{ width: `${(strengthScore / 4) * 100}%` }}\n        />\n      </div>\n\n      <p className=\"text-foreground mb-2 text-sm font-medium\" id={`${id}-description`}>\n        {getStrengthText(strengthScore)}. Must contain:\n      </p>\n\n      <ul aria-label=\"Password requirements\" className=\"space-y-1.5\">\n        {strength.map((req, index) => (\n          <li className=\"flex items-center gap-2\" key={index}>\n            {req.met ? (\n              <Check aria-hidden=\"true\" className=\"text-emerald-500\" size={16} />\n            ) : (\n              <X aria-hidden=\"true\" className=\"text-muted-foreground/80\" size={16} />\n            )}\n            <span className={`text-xs ${req.met ? \"text-emerald-600\" : \"text-muted-foreground\"}`}>\n              {req.text}\n              <span className=\"sr-only\">{req.met ? \" - Requirement met\" : \" - Requirement not met\"}</span>\n            </span>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n});\n\nInputPasswordStrength.displayName = \"InputPasswordStrength\";\n"
  },
  {
    "path": "src/components/ui/input.tsx",
    "content": "\"use client\";\nimport * as React from \"react\";\nimport { EyeIcon, EyeOffIcon } from \"lucide-react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"@radix-ui/react-slot\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst inputVariants = cva(\n  \"shadow-3xl relative w-full rounded-lg px-3.5 py-2.5 text-sm/[10px] font-medium text-black dark:text-white outline-none placeholder:font-normal placeholder:text-gray-500 focus:ring-1 focus:ring-black disabled:pointer-events-none disabled:opacity-30 ltr:text-left rtl:text-right\",\n  {\n    variants: {\n      variant: {\n        default: \"\",\n        Search: \"border border-gray-300 py-[7px] pl-8 pr-2 text-xs shadow-sm placeholder:text-black dark:placeholder:text-white\",\n        \"input-form\":\n          \"pr-9 outline-offset-0 focus:outline-[4px] focus:outline-primary/20 focus:ring-1 focus:ring-primary dark:focus:ring-primary\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>, VariantProps<typeof inputVariants> {\n  asChild?: boolean;\n  iconLeft?: React.ReactNode;\n  iconRight?: React.ReactNode;\n  small?: boolean;\n}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, variant, type, asChild = false, small = false, iconLeft = null, iconRight = null, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"input\";\n    const [fieldType, setFieldType] = React.useState(type);\n    const isIconLeft = iconLeft ? \"rtl:pr-9 ltr:pl-9\" : \"\";\n    const isIconRight = iconLeft ? \"rtl:pl-9 ltr:pr-9\" : \"\";\n\n    return (\n      <div className=\"relative\">\n        <Comp\n          className={cn(\n            inputVariants({\n              variant,\n              className,\n            }),\n            isIconLeft,\n            isIconRight,\n          )}\n          ref={ref}\n          type={fieldType}\n          {...props}\n        />\n        {!!iconLeft && (\n          <span\n            className={cn(\n              \"text-grey peer-focus:text-dark pointer-events-none absolute top-0 flex h-9 w-9 items-center justify-center ltr:left-0 rtl:right-0\",\n              small ? \"h-[30px] w-[30px]\" : \"h-9 w-9\",\n            )}\n          >\n            {iconLeft}\n          </span>\n        )}\n\n        {!!iconRight && (\n          <span\n            className={cn(\n              \"text-grey peer-focus:text-dark pointer-events-none absolute top-0 flex items-center justify-center ltr:right-0 rtl:left-0\",\n              small ? \"h-[30px] w-[30px]\" : \"h-9 w-9\",\n            )}\n          >\n            {iconRight}\n          </span>\n        )}\n\n        {type === \"password\" && (\n          <button\n            className=\"text-grey peer-focus:text-dark absolute top-0 flex h-9 w-9 items-center justify-center ltr:right-0 rtl:left-0\"\n            onClick={() => setFieldType(fieldType === \"password\" ? \"text\" : \"password\")}\n            tabIndex={-1}\n            type=\"button\"\n          >\n            {fieldType === \"password\" ? <EyeIcon height={18} width={18} /> : <EyeOffIcon className=\"text-grey\" height={18} width={18} />}\n          </button>\n        )}\n      </div>\n    );\n  },\n);\nInput.displayName = \"Input\";\n\nexport { Input, inputVariants };\n"
  },
  {
    "path": "src/components/ui/iphone-mockup.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport Image from \"next/image\";\nimport iPhone from \"@public/images/iphone.png\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\ninterface IPhoneMockupProps {\n  children?: React.ReactNode;\n  className?: string;\n  screenClassName?: string;\n  showNotch?: boolean;\n  width?: number;\n  height?: number;\n}\n\nexport function IPhoneMockup({ children, className, screenClassName, showNotch = true, width = 298, height = 601 }: IPhoneMockupProps) {\n  return (\n    <div className={cn(\"relative\", className)} style={{ width, height }}>\n      {/* iPhone frame image */}\n      <Image alt=\"iPhone mockup frame\" className=\"pointer-events-none select-none\" height={height} priority src={iPhone} width={width} />\n\n      {/* Screen content container */}\n      <div\n        className={cn(\n          \"absolute inset-0 overflow-hidden\",\n          \"flex flex-col items-center\",\n          // Adjust these values based on your iPhone image\n          \"bottom-[8%] left-[6%] right-[6%] top-[2%]\",\n          \"h-[96%]\",\n          \"rounded-[30px]\",\n          screenClassName,\n        )}\n      >\n        {/* Notch - if enabled */}\n        {showNotch && <div className=\"absolute left-1/2 top-0 z-10 h-[4%] w-[30%] -translate-x-1/2 rounded-b-xl bg-black\" />}\n\n        {/* Content */}\n        <div className=\"w-full flex-1 overflow-hidden\">{children}</div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/label.tsx",
    "content": "import * as React from \"react\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst labelVariants = cva(\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\");\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => <LabelPrimitive.Root className={cn(labelVariants(), className)} ref={ref} {...props} />);\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "src/components/ui/link.tsx",
    "content": "import { forwardRef } from \"react\";\nimport NextLink from \"next/link\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nimport type { ComponentProps } from \"react\";\n\ninterface LinkProps extends ComponentProps<typeof NextLink> {\n  variant?: \"default\" | \"nav\" | \"footer\" | \"button\";\n  size?: \"sm\" | \"base\" | \"lg\";\n}\n\nexport const Link = forwardRef<HTMLAnchorElement, LinkProps>(\n  ({ className, variant = \"default\", size = \"base\", children, ...props }, ref) => {\n    const variants = {\n      default: \"link link-hover text-base-content hover:text-primary transition-colors dark:text-gray-200 dark:hover:text-primary\",\n      nav: \"link link-hover text-base-content/80 hover:text-base-content transition-colors dark:text-gray-200 dark:hover:text-primary\",\n      footer: \"link link-hover text-base-content/70 hover:text-base-content transition-colors dark:text-gray-200 dark:hover:text-primary\",\n      button: \"btn btn-link no-underline hover:underline\",\n    };\n\n    const sizes = {\n      sm: \"text-sm\",\n      base: \"text-base\",\n      lg: \"text-lg\",\n    };\n\n    return (\n      <NextLink className={cn(variants[variant], sizes[size], className)} ref={ref} {...props}>\n        {children}\n      </NextLink>\n    );\n  },\n);\n\nLink.displayName = \"Link\";\n"
  },
  {
    "path": "src/components/ui/loader.tsx",
    "content": "import { Loader2 } from \"lucide-react\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nimport type { LucideProps } from \"lucide-react\";\n\nexport const Loader = ({ className, ...props }: LucideProps) => {\n  return <Loader2 {...props} className={cn(className, \"animate-spin\")} />;\n};\n"
  },
  {
    "path": "src/components/ui/local-alert.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\n\ninterface LocalAlertProps {\n  className?: string;\n}\n\nexport const LocalAlert = ({ className }: LocalAlertProps) => {\n  const t = useI18n();\n\n  return (\n    <Alert className={cn(\"bg-blue-100 border-0 text-black\", className)} variant=\"info\">\n      <AlertDescription className=\"flex flex-wrap items-center gap-1 italic text-base\">\n        {t(\"profile.alert.title\")}\n        <br className=\"sm:hidden\" />\n        <Link className=\"ml-1 mr-1 font-medium text-blue-700 underline\" href={paths.signUp}>\n          {t(\"profile.alert.create_account\")}\n        </Link>\n        {t(\"commons.or\").toLocaleLowerCase()}\n        <Link className=\"ml-1 font-medium text-purple-700 underline\" href={paths.signIn}>\n          {t(\"profile.alert.log_in\")}\n        </Link>\n        {t(\"profile.alert.to_ensure_it_is_not_getting_lost\")}\n      </AlertDescription>\n    </Alert>\n  );\n};\n"
  },
  {
    "path": "src/components/ui/moving-border.tsx",
    "content": "\"use client\";\nimport React, { useRef } from \"react\";\nimport { motion, useAnimationFrame, useMotionTemplate, useMotionValue, useTransform } from \"framer-motion\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nexport function Button({\n  borderRadius = \"1.75rem\",\n  children,\n  as: Component = \"button\",\n  containerClassName,\n  borderClassName,\n  duration,\n  style,\n  className,\n  ...otherProps\n}: {\n  borderRadius?: string;\n  children: React.ReactNode;\n  as?: any;\n  containerClassName?: string;\n  borderClassName?: string;\n  duration?: number;\n  backgroundColor?: string;\n  style?: React.CSSProperties;\n  className?: string;\n  [key: string]: any;\n}) {\n  return (\n    <Component\n      className={cn(\"relative h-16 overflow-hidden bg-transparent p-[1px] text-xl hover:cursor-pointer\", containerClassName)}\n      style={{ borderRadius }}\n      {...otherProps}\n    >\n      <MovingBorder duration={duration} rx=\"30%\" ry=\"30%\">\n        <div className={cn(\"w-30 h-20 bg-[radial-gradient(#ff5722_40%,transparent_60%)] opacity-[0.8]\", borderClassName)} />\n      </MovingBorder>\n\n      <div\n        className={cn(\n          \"relative flex h-full w-full items-center justify-center border border-orange-800 text-sm text-white antialiased backdrop-blur-xl\",\n          className,\n        )}\n        style={{\n          ...style,\n          borderRadius: `calc(${borderRadius} * 0.96)`,\n        }}\n      >\n        {children}\n      </div>\n    </Component>\n  );\n}\n\nexport const MovingBorder = ({\n  children,\n  duration = 3000,\n  rx,\n  ry,\n  ...otherProps\n}: {\n  children: React.ReactNode;\n  duration?: number;\n  rx?: string;\n  ry?: string;\n  [key: string]: any;\n}) => {\n  // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n  // @ts-ignore\n  const pathRef = useRef<any>();\n  const progress = useMotionValue<number>(0);\n\n  useAnimationFrame((time) => {\n    const length = pathRef.current?.getTotalLength();\n    if (length) {\n      const pxPerMillisecond = length / duration;\n      progress.set((time * pxPerMillisecond) % length);\n    }\n  });\n\n  const x = useTransform(progress, (val) => pathRef.current?.getPointAtLength(val).x);\n  const y = useTransform(progress, (val) => pathRef.current?.getPointAtLength(val).y);\n\n  const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`;\n\n  return (\n    <>\n      <svg\n        className=\"absolute h-full w-full\"\n        height=\"100%\"\n        preserveAspectRatio=\"none\"\n        width=\"100%\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        {...otherProps}\n      >\n        <rect fill=\"none\" height=\"100%\" ref={pathRef} rx={rx} ry={ry} width=\"100%\" />\n      </svg>\n      <motion.div\n        style={{\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n          display: \"inline-block\",\n          transform,\n        }}\n      >\n        {children}\n      </motion.div>\n    </>\n  );\n};\n"
  },
  {
    "path": "src/components/ui/navigation-menu.tsx",
    "content": "import * as React from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cva } from \"class-variance-authority\";\nimport * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst NavigationMenu = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>\n>(({ className, children, ...props }, ref) => {\n  return (\n    <NavigationMenuPrimitive.Root\n      className={cn(\"relative z-10 flex max-w-max flex-1 items-center justify-center transition-all\", className)}\n      dir=\"ltr\"\n      ref={ref}\n      {...props}\n    >\n      {children}\n      <NavigationMenuViewport />\n    </NavigationMenuPrimitive.Root>\n  );\n});\nNavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;\n\nconst NavigationMenuList = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.List\n    className={cn(\n      \"group flex flex-1 list-none flex-col justify-center gap-2.5 space-x-1 rounded-lg bg-white p-2 font-semibold shadow-3xl sm:flex-row sm:items-center sm:px-5 sm:py-3 dark:border dark:border-gray-300/20 dark:bg-black-dark dark:shadow-sm\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nNavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;\n\nconst NavigationMenuItem = NavigationMenuPrimitive.Item;\n\nconst navigationMenuTriggerStyle = cva(\n  \"group flex w-full items-center justify-center rounded-lg p-2.5 text-sm/[18px] font-semibold data-[state=open]:bg-light-theme dark:data-[state=open]:bg-black data-[state=open]:text-black dark:data-[state=open]:text-white disabled:pointer-events-none disabled:opacity-50 hover:bg-light-theme transition dark:hover:bg-black dark:hover:text-white\",\n);\n\nconst NavigationMenuTrigger = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <NavigationMenuPrimitive.Trigger className={cn(navigationMenuTriggerStyle(), \"group relative\", className)} ref={ref} {...props}>\n    {children}{\" \"}\n    <ChevronDown aria-hidden=\"true\" className=\"relative top-[1px] size-[18px] group-data-[state=open]:duration-500 ltr:ml-1.5 rtl:mr-1.5\" />\n    <span className=\"group-data-[state=open]:zoom-in-90 group-data-[state=open]:after:absolute group-data-[state=open]:after:-bottom-[42px] group-data-[state=open]:after:right-2 group-data-[state=open]:after:hidden group-data-[state=open]:after:size-5 group-data-[state=open]:after:rotate-[36deg] group-data-[state=open]:after:skew-y-12 group-data-[state=open]:after:rounded-tl-md group-data-[state=open]:after:bg-white sm:group-data-[state=open]:after:block dark:group-data-[state=open]:after:bg-black-dark\"></span>\n  </NavigationMenuPrimitive.Trigger>\n));\nNavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;\n\nconst NavigationMenuContent = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.Content\n    className={cn(\n      \"left-0 top-0 w-full min-w-max data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nNavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;\n\nconst NavigationMenuLink = NavigationMenuPrimitive.Link;\n\nconst NavigationMenuViewport = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>\n>(({ className, ...props }, ref) => (\n  <div className={cn(\"absolute left-1/2 top-full flex -translate-x-1/2 justify-center\")}>\n    <NavigationMenuPrimitive.Viewport\n      className={cn(\n        \"origin-top-center relative mt-4 h-[var(--radix-navigation-menu-viewport-height)] w-full min-w-max overflow-hidden rounded-lg bg-white data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)] dark:bg-black-dark\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    />\n  </div>\n));\nNavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;\n\nconst NavigationMenuIndicator = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.Indicator\n    className={cn(\n      \"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  >\n    <div className=\"bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md\" />\n  </NavigationMenuPrimitive.Indicator>\n));\nNavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;\n\nexport {\n  navigationMenuTriggerStyle,\n  NavigationMenu,\n  NavigationMenuList,\n  NavigationMenuItem,\n  NavigationMenuContent,\n  NavigationMenuTrigger,\n  NavigationMenuLink,\n  NavigationMenuIndicator,\n  NavigationMenuViewport,\n};\n"
  },
  {
    "path": "src/components/ui/next-top-loader.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport * as NProgress from \"nprogress\";\n\nexport type NextTopLoaderProps = {\n  /**\n   * Color for the TopLoader.\n   * @default \"#29d\"\n   */\n  color?: string;\n  /**\n   * The initial position for the TopLoader in percentage, 0.08 is 8%.\n   * @default 0.08\n   */\n  initialPosition?: number;\n  /**\n   * The increment delay speed in milliseconds.\n   * @default 200\n   */\n  crawlSpeed?: number;\n  /**\n   * The height for the TopLoader in pixels (px).\n   * @default 3\n   */\n  height?: number;\n  /**\n   * Auto increamenting behaviour for the TopLoader.\n   * @default true\n   */\n  crawl?: boolean;\n  /**\n   * To show spinner or not.\n   * @default true\n   */\n  showSpinner?: boolean;\n  /**\n   * Animation settings using easing (a CSS easing string).\n   * @default \"ease\"\n   */\n  easing?: string;\n  /**\n   * Animation speed in ms for the TopLoader.\n   * @default 200\n   */\n  speed?: number;\n  /**\n   * Defines a shadow for the TopLoader.\n   * @default \"0 0 10px ${color},0 0 5px ${color}\"\n   *\n   * @ you can disable it by setting it to `false`\n   */\n  shadow?: string | false;\n  /**\n   * Timeout in ms before the TopLoader will appear.\n   *\n   * @default 0\n   */\n  delay?: number;\n};\n\nconst isAnchorOfCurrentUrl = (currentUrl: string, newUrl: string) => {\n  const currentUrlObj = new URL(currentUrl);\n  const newUrlObj = new URL(newUrl);\n  const currentHash = currentUrlObj.hash;\n  const newHash = newUrlObj.hash;\n\n  return (\n    currentUrlObj.hostname === newUrlObj.hostname &&\n    currentUrlObj.pathname === newUrlObj.pathname &&\n    currentUrlObj.search === newUrlObj.search &&\n    currentHash !== newHash &&\n    currentUrlObj.href.replace(currentHash, \"\") === newUrlObj.href.replace(newHash, \"\")\n  );\n};\n\nexport const NextTopLoader = ({\n  color = \"#FF5722\",\n  height = 3,\n  showSpinner = true,\n  crawl = true,\n  crawlSpeed = 200,\n  initialPosition = 0.08,\n  easing = \"ease\",\n  speed = 200,\n  shadow,\n  delay = 0,\n}: NextTopLoaderProps) => {\n  const boxShadow =\n    !shadow && shadow !== undefined ? \"\" : shadow ? `box-shadow:${shadow}` : `box-shadow:0 0 10px ${color},0 0 5px ${color}`;\n\n  const styles = (\n    <style>\n      { }\n      {`#nprogress{pointer-events:none}#nprogress .bar{background:${color};position:fixed;z-index:1031;top:0;left:0;width:100%;height:${height}px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;${boxShadow};opacity:1;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translate(0px,-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:2px solid transparent;border-top-color:${color};border-left-color:${color};border-radius:50%;-webkit-animation:nprogress-spinner 400ms linear infinite;animation:nprogress-spinner 400ms linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}`}\n    </style>\n  );\n\n  useEffect(() => {\n    NProgress.configure({\n      showSpinner,\n      trickle: crawl,\n      trickleSpeed: crawlSpeed,\n      minimum: initialPosition,\n      easing,\n      speed,\n    });\n\n    const handleNProgressStart = () => {\n      let isDone = false;\n      setTimeout(() => {\n        if (!isDone) {\n          NProgress.start();\n        }\n      }, 100);\n\n      const originalPushState = window.history.pushState;\n      window.history.pushState = function (...args) {\n        isDone = true;\n        NProgress.done();\n        for (const el of Array.from(document.querySelectorAll(\"html\"))) {\n          el.classList.remove(\"nprogress-busy\");\n        }\n        return originalPushState.apply(window.history, args);\n      };\n    };\n\n    const handleQuickProgress = () => {\n      if (delay === 0) {\n        NProgress.start();\n        NProgress.done();\n        for (const el of Array.from(document.querySelectorAll(\"html\"))) {\n          el.classList.remove(\"nprogress-busy\");\n        }\n      }\n    };\n\n    const handleClick = (event: MouseEvent) => {\n      // if ctrl or cmd key is pressed, don't intercept\n      if (event.ctrlKey || event.metaKey) return;\n\n      try {\n        const target = event.target as HTMLElement;\n        const anchor = target.closest(\"a\");\n\n        if (!anchor) return;\n\n        const currentUrl = window.location.href;\n        const newUrl = anchor.href;\n        const isExternalLink = anchor.target === \"_blank\";\n        const isAnchor = isAnchorOfCurrentUrl(currentUrl, newUrl);\n\n        if (newUrl === currentUrl || isAnchor || isExternalLink) {\n          handleQuickProgress();\n        } else {\n          handleNProgressStart();\n        }\n      } catch {\n        handleQuickProgress();\n      }\n    };\n\n    document.addEventListener(\"click\", handleClick);\n\n    return () => {\n      document.removeEventListener(\"click\", handleClick);\n    };\n  }, []);\n\n  return styles;\n};\n"
  },
  {
    "path": "src/components/ui/pagination.tsx",
    "content": "import * as React from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\n\nimport { cn } from \"@/shared/lib/utils\";\nimport { ButtonProps, buttonVariants } from \"@/components/ui/button\";\n\nconst Pagination = ({ className, ...props }: React.ComponentProps<\"nav\">) => (\n  <nav aria-label=\"pagination\" className={cn(\"mx-auto flex w-full justify-end\", className)} role=\"navigation\" {...props} />\n);\nPagination.displayName = \"Pagination\";\n\nconst PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(({ className, ...props }, ref) => (\n  <ul\n    className={cn(\"flex flex-row items-center overflow-hidden rounded-lg bg-white shadow-sm dark:bg-black-dark\", className)}\n    ref={ref}\n    {...props}\n  />\n));\nPaginationContent.displayName = \"PaginationContent\";\n\nconst PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(({ className, ...props }, ref) => (\n  <li className={cn(\"\", className)} ref={ref} {...props} />\n));\nPaginationItem.displayName = \"PaginationItem\";\n\ntype PaginationLinkProps = {\n  isActive?: boolean;\n} & Pick<ButtonProps, \"size\"> &\n  React.ComponentProps<\"a\">;\n\nconst PaginationLink = ({\n  className,\n  isActive,\n\n  ...props\n}: PaginationLinkProps) => (\n  <a\n    aria-current={isActive ? \"page\" : undefined}\n    className={cn(\n      \"grid size-[30px] place-content-center !rounded-none text-xs text-[#707079] !shadow-none !ring-0 hover:!border-x hover:border-gray-300 dark:border-gray-300/20 dark:hover:border-gray-300/20\",\n      buttonVariants({\n        variant: isActive ? \"outline-general\" : \"outline-general\",\n      }),\n      className,\n      isActive &&\n        \"border-x border-gray-300 bg-[#F7F7F8] text-black hover:bg-[#F7F7F8] dark:border-gray-300/20 dark:bg-black dark:text-white dark:hover:bg-black\",\n    )}\n    {...props}\n  />\n);\nPaginationLink.displayName = \"PaginationLink\";\n\nconst PaginationPrevious = ({ className, disabled, ...props }: React.ComponentProps<typeof PaginationLink> & { disabled?: boolean }) => (\n  <PaginationLink\n    aria-label=\"Go to previous page\"\n    className={cn(\n      \"gap-1 rounded-l-lg border-r border-gray-300 pl-2.5 hover:!border-l-0\",\n      className,\n      disabled && \"pointer-events-none cursor-not-allowed opacity-50\",\n    )}\n    size=\"default\"\n    {...props}\n  >\n    <ChevronLeft className=\"size-4 text-black rtl:rotate-180 dark:text-white\" />\n  </PaginationLink>\n);\nPaginationPrevious.displayName = \"PaginationPrevious\";\n\nconst PaginationNext = ({ className, disabled, ...props }: React.ComponentProps<typeof PaginationLink> & { disabled?: boolean }) => (\n  <PaginationLink\n    aria-label=\"Go to next page\"\n    className={cn(\n      \"gap-1 rounded-r-lg border-l border-gray-300 pr-2.5 hover:!border-r-0\",\n      className,\n      disabled && \"pointer-events-none cursor-not-allowed opacity-50\",\n    )}\n    size=\"default\"\n    {...props}\n  >\n    <ChevronRight className=\"size-4 text-black rtl:rotate-180 dark:text-white\" />\n  </PaginationLink>\n);\nPaginationNext.displayName = \"PaginationNext\";\n\nexport { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious };\n"
  },
  {
    "path": "src/components/ui/phone-frame-preview.tsx",
    "content": "import React from \"react\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\ninterface PhoneFramePreviewProps extends React.HTMLAttributes<HTMLDivElement> {\n  children: React.ReactNode;\n}\n\nexport function PhoneFramePreview({ children, className, ...props }: PhoneFramePreviewProps) {\n  return (\n    <div\n      className={cn(\n        \"relative mx-auto flex aspect-[9/18] h-auto w-full max-w-[330px] overflow-hidden rounded-[2.5rem] border-[3px] border-black bg-gray-900 shadow-xl\",\n        className,\n      )}\n      {...props}\n    >\n      {/* Notch */}\n      <div className=\"absolute left-1/2 top-0 z-[50] h-4 w-28 -translate-x-1/2 rounded-b-lg bg-black\"></div>\n      {/* Screen Content */}\n      <div className=\"relative h-full w-full overflow-hidden rounded-[2rem] bg-white\">\n        {children}\n        {/* Home Indicator Bar */}\n        <div className=\"absolute bottom-2 left-1/2 z-[50] h-1 w-1/3 -translate-x-1/2 rounded-full bg-gray-400\"></div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/popover.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 6, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      align={align}\n      className={cn(\n        \"z-50 w-[250px] rounded-lg bg-white p-4 font-medium text-popover-foreground shadow-3xl outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ltr:text-left rtl:text-right dark:border dark:border-white/10 dark:bg-black-dark dark:shadow-sm\",\n        className,\n      )}\n      ref={ref}\n      sideOffset={sideOffset}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent };\n"
  },
  {
    "path": "src/components/ui/premium-gate.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport Link from \"next/link\";\nimport { Crown, Sparkles } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { useUserSubscription } from \"@/features/ads/hooks/useUserSubscription\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface PremiumGateProps {\n  children: React.ReactNode;\n  feature?: string;\n  fallback?: React.ReactNode;\n  showUpgradePrompt?: boolean;\n  onUpgradePress?: () => void;\n  upgradeMessage?: string;\n  className?: string;\n}\n\n/**\n * PremiumGate Component\n *\n * Gates content behind premium subscription status\n * Shows upgrade prompt or custom fallback for non-premium users\n */\nexport function PremiumGate({\n  children,\n  feature,\n  fallback,\n  showUpgradePrompt = true,\n  onUpgradePress,\n  upgradeMessage,\n  className,\n}: PremiumGateProps) {\n  const { isPremium, isPending } = useUserSubscription();\n\n  // Show loading state while checking premium status\n  if (isPending) {\n    return (\n      <div className={cn(\"flex items-center justify-center p-8\", className)}>\n        <Skeleton className=\"h-32 w-full max-w-md\" />\n      </div>\n    );\n  }\n\n  // Grant access if user has premium\n  if (isPremium) {\n    return <>{children}</>;\n  }\n\n  // Show custom fallback if provided\n  if (fallback) {\n    return <>{fallback}</>;\n  }\n\n  // Show upgrade prompt if enabled\n  if (showUpgradePrompt) {\n    return <PremiumUpgradePrompt className={className} feature={feature} message={upgradeMessage} onUpgradePress={onUpgradePress} />;\n  }\n\n  // Default: hide content\n  return null;\n}\n\ninterface PremiumUpgradePromptProps {\n  feature?: string;\n  message?: string;\n  onUpgradePress?: () => void;\n  className?: string;\n}\n\nfunction PremiumUpgradePrompt({ feature, message, onUpgradePress, className }: PremiumUpgradePromptProps) {\n  const t = useI18n();\n  const defaultMessage = message || t(\"premium.upgrade_to_access_feature\");\n\n  const handleUpgradePress = () => {\n    // Track premium gate view if analytics available\n    if (typeof window !== \"undefined\" && feature) {\n      // TODO: Add analytics tracking\n      console.log(\"Premium gate viewed:\", feature);\n    }\n\n    if (onUpgradePress) {\n      onUpgradePress();\n    }\n  };\n\n  return (\n    <Card className={cn(\"max-w-md mx-auto\", className)}>\n      <CardHeader className=\"text-center\">\n        <div className=\"flex justify-center mb-4\">\n          <div className=\"p-3 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-full\">\n            <Crown className=\"h-8 w-8 text-white\" />\n          </div>\n        </div>\n        <CardTitle className=\"text-2xl font-bold\">{t(\"premium.premium_feature\")}</CardTitle>\n        <CardDescription className=\"text-base mt-2\">{defaultMessage}</CardDescription>\n      </CardHeader>\n      <CardContent className=\"text-center\">\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"flex items-center justify-center gap-2 text-sm text-muted-foreground\">\n            <Sparkles className=\"h-4 w-4\" />\n            <span>{t(\"premium.unlock_all_features\")}</span>\n          </div>\n\n          <Link href=\"/premium\" onClick={handleUpgradePress}>\n            <Button className=\"w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700\" size=\"large\">\n              {t(\"commons.upgrade_to_premium\")}\n            </Button>\n          </Link>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\n// Utility component for inline premium indicators\nexport function PremiumBadge({ size = \"small\", className }: { size?: \"small\" | \"medium\" | \"large\"; className?: string }) {\n  const sizeClasses = {\n    small: \"px-2 py-0.5 text-xs\",\n    medium: \"px-3 py-1 text-sm\",\n    large: \"px-4 py-1.5 text-base\",\n  };\n\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center gap-1 rounded-md bg-gradient-to-r from-yellow-400 to-orange-500 text-white font-semibold\",\n        sizeClasses[size],\n        className,\n      )}\n    >\n      <Crown className={cn(size === \"small\" ? \"h-3 w-3\" : size === \"medium\" ? \"h-4 w-4\" : \"h-5 w-5\")} />\n      PREMIUM\n    </span>\n  );\n}\n\n// Higher-order component for premium feature gating\nexport function withPremiumGate<P extends object>(\n  WrappedComponent: React.ComponentType<P>,\n  gateOptions?: Omit<PremiumGateProps, \"children\">,\n) {\n  return function PremiumGatedComponent(props: P) {\n    return (\n      <PremiumGate {...gateOptions}>\n        <WrappedComponent {...props} />\n      </PremiumGate>\n    );\n  };\n}\n"
  },
  {
    "path": "src/components/ui/premium-upsell-alert.tsx",
    "content": "import React from \"react\";\nimport Link from \"next/link\";\nimport { Sparkles, Zap, Ban } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { AdWrapper } from \"@/components/ads\";\n\ninterface PremiumUpsellAlertProps {\n  className?: string;\n}\n\nexport const PremiumUpsellAlert = ({ className }: PremiumUpsellAlertProps) => {\n  const t = useI18n();\n\n  return (\n    <AdWrapper\n      fallback={\n        <Alert\n          className={cn(\n            \"flex items-center bg-gradient-to-r from-purple-500/10 to-blue-500/10 border-purple-400/50 dark:from-purple-600/20 dark:to-blue-600/20 dark:border-purple-500/50\",\n            className,\n          )}\n          variant=\"info\"\n        >\n          <AlertDescription className=\"flex items-center justify-between w-full\">\n            <div className=\"flex items-center gap-3\">\n              <Sparkles className=\"h-5 w-5 text-purple-600 dark:text-purple-400\" />\n              <span className=\"text-base font-medium text-gray-900 dark:text-gray-100\">{t(\"premium.already_premium\")}</span>\n            </div>\n          </AlertDescription>\n        </Alert>\n      }\n    >\n      <div\n        className={cn(\n          \"p-2 flex items-center bg-gradient-to-r from-yellow-500/10 to-orange-500/10 border-yellow-400/50 dark:from-yellow-600/20 dark:to-orange-600/20 dark:border-yellow-500/50\",\n          className,\n        )}\n      >\n        <div className=\"flex flex-col sm:flex-row items-center justify-between gap-3 w-full\">\n          <div className=\"flex items-center gap-3\">\n            <Zap className=\"h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0\" />\n            <span className=\"text-base font-medium text-gray-900 dark:text-gray-100\">{t(\"donation_alert.title\")}</span>\n          </div>\n          <div className=\"flex items-center gap-2\">\n            <div className=\"flex items-center gap-1 text-sm text-gray-600 dark:text-gray-400\">\n              <Ban className=\"h-4 w-4\" />\n              <span>{t(\"premium.no_ads\")}</span>\n            </div>\n            <Link href=\"/premium\">\n              <Button\n                className=\"bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700\"\n                size=\"small\"\n                variant=\"default\"\n              >\n                {t(\"premium.upgrade\")}\n              </Button>\n            </Link>\n          </div>\n        </div>\n      </div>\n    </AdWrapper>\n  );\n};\n"
  },
  {
    "path": "src/components/ui/radio-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Circle } from \"lucide-react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\n\nimport { cn } from \"@/shared/lib/utils\";\nimport IconCheckboxCheck from \"@/components/svg/IconCheckboxCheck\";\n\nconst radioGroupVariants = cva(\n  \"aspect-square size-3 flex  items-center justify-center shrink-0 rounded-full ring-[1.5px] ring-gray-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-40\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-white dark:bg-transparent\",\n        outline: \"data-[state=checked]:!bg-white\",\n      },\n      color: {\n        default: \"data-[state=checked]:ring-black dark:data-[state=checked]:ring-white\",\n        primary: \"data-[state=checked]:ring-primary\",\n        success: \"data-[state=checked]:ring-success\",\n        pending: \"data-[state=checked]:ring-warning\",\n        danger: \"data-[state=checked]:ring-danger\",\n        outlineBlack: \"data-[state=checked]:ring-black\",\n        outlinePrimary: \"data-[state=checked]:ring-primary\",\n        outlineSuccess: \"data-[state=checked]:ring-success\",\n        outlinePending: \"data-[state=checked]:ring-warning\",\n        outlineDanger: \"data-[state=checked]:ring-danger\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      color: \"default\",\n    },\n  },\n);\n\nconst radioGroupCircleVariants = cva(\n  \"size-1.5 rounded-full bg-current text-transparent ring-[3.5px] ring-current disabled:cursor-not-allowed disabled:opacity-40\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-white dark:bg-black\",\n        outline: \"data-[state=checked]:!bg-white\",\n      },\n      color: {\n        default: \"ring-black dark:ring-white\",\n        primary: \"ring-primary\",\n        success: \"ring-success\",\n        pending: \"ring-warning\",\n        danger: \"ring-danger\",\n        outlineBlack: \"ring-[3px] ring-white bg-black\",\n        outlinePrimary: \"ring-[3px] ring-white bg-primary\",\n        outlineSuccess: \"ring-[3px] ring-white bg-success\",\n        outlinePending: \"ring-[3px] ring-white bg-warning\",\n        outlineDanger: \"ring-[3px] ring-white bg-danger\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      color: \"default\",\n    },\n  },\n);\n\nconst RadioGroup = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return <RadioGroupPrimitive.Root className={cn(\"grid gap-4\", className)} dir=\"ltr\" {...props} ref={ref} />;\n});\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n\nconst RadioGroupItem = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> & VariantProps<typeof radioGroupVariants>\n>(({ className, variant, color, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Item className={cn(\"\", radioGroupVariants({ variant, color, className }))} ref={ref} {...props}>\n      <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n        <Circle className={cn(radioGroupCircleVariants({ variant, color, className }))} />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n});\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\nconst RadioGroupCheck = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Item\n      className={cn(\n        \"aspect-square size-3 rounded-full border-[1.5px] border-gray-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-0 data-[state=checked]:bg-black dark:data-[state=checked]:bg-white\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n        <IconCheckboxCheck className={cn(\"size-1.5 text-white dark:text-black\")} />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n});\nRadioGroupCheck.displayName = RadioGroupPrimitive.Item.displayName;\n\nexport { RadioGroup, RadioGroupItem, RadioGroupCheck };\n"
  },
  {
    "path": "src/components/ui/scroll-area.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => {\n  return (\n    <ScrollAreaPrimitive.Root\n      className={cn(\"relative overflow-hidden rounded-lg bg-white shadow-3xl dark:bg-black-dark dark:shadow-none\", className)}\n      dir=\"ltr\"\n      ref={ref}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport className=\"h-full w-full\">{children}</ScrollAreaPrimitive.Viewport>\n      <ScrollBar />\n      <ScrollAreaPrimitive.Corner />\n    </ScrollAreaPrimitive.Root>\n  );\n});\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    className={cn(\n      \"mr-2 flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" && \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" && \"h-1.5 flex-col border-t border-t-transparent p-[1px]\",\n      className,\n    )}\n    orientation={orientation}\n    ref={ref}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-gray-300 dark:bg-gray-300/20\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n));\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "src/components/ui/select.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronDown, ChevronsUpDown } from \"lucide-react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst Select = SelectPrimitive.Root;\n\nSelect.displayName = SelectPrimitive.Root.displayName;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {\n    icons?: string;\n  }\n>(({ className, children, icons, ...props }, ref) => {\n  return (\n    <SelectPrimitive.Trigger\n      className={cn(\n        \"flex w-full items-center justify-between gap-1.5 rounded-lg px-3.5 py-2.5 text-left text-sm/[18px] font-medium text-gray shadow-3xl transition-all placeholder:text-gray focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white/5 dark:text-gray-500 dark:shadow-sm dark:ring-1 dark:ring-white/10 [&>span]:line-clamp-1 [&[data-state=open]>svg]:rotate-180 [&[data-state=open]>svg]:text-black dark:[&[data-state=open]>svg]:text-white [&[data-state=open]]:text-black dark:[&[data-state=open]]:text-white\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        {icons === \"shorting\" ? (\n          <>\n            <ChevronsUpDown className=\"size-4 shrink-0 !rotate-0 text-gray transition dark:text-gray-500\" />\n          </>\n        ) : (\n          <ChevronsUpDown className=\"h-4 w-4 text-gray transition dark:text-gray-500\" />\n        )}\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n});\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton className={cn(\"flex cursor-default items-center justify-center py-1\", className)} ref={ref} {...props}>\n    <ChevronDown className=\"h-4 w-4 rotate-180\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton className={cn(\"flex cursor-default items-center justify-center py-1\", className)} ref={ref} {...props}>\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      className={cn(\n        \"relative z-[1001] max-h-96 w-full overflow-hidden rounded-lg bg-white text-sm/4 font-medium text-gray shadow-3xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border dark:border-white/10 dark:bg-black-dark dark:text-gray-500 dark:shadow-sm\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className,\n      )}\n      position={position}\n      ref={ref}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" && \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label className={cn(\"py-1.5 pl-8 pr-2 text-sm font-semibold\", className)} ref={ref} {...props} />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    className={cn(\n      \"relative mt-px flex w-full cursor-pointer select-none items-center rounded-lg py-1.5 pl-2 pr-2 text-sm/5 font-medium outline-none ring-0 first:mt-0 hover:bg-gray-400 hover:text-black data-[disabled]:pointer-events-none data-[state=checked]:bg-gray-400 data-[state=checked]:text-black data-[disabled]:opacity-50 dark:hover:bg-gray-200/10 dark:hover:text-white dark:data-[state=checked]:bg-gray-200/10 dark:data-[state=checked]:text-white\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  >\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator className={cn(\"bg-muted -mx-1 my-1 h-px\", className)} ref={ref} {...props} />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "src/components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(({ className, orientation = \"horizontal\", decorative = true, ...props }, ref) => (\n  <SeparatorPrimitive.Root\n    className={cn(\"bg-border shrink-0\", orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\", className)}\n    decorative={decorative}\n    orientation={orientation}\n    ref={ref}\n    {...props}\n  />\n));\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "src/components/ui/sheet.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { X } from \"lucide-react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst Sheet = SheetPrimitive.Root;\n\nconst SheetTrigger = SheetPrimitive.Trigger;\n\nconst SheetClose = SheetPrimitive.Close;\n\nconst SheetPortal = SheetPrimitive.Portal;\n\nconst SheetOverlay = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nSheetOverlay.displayName = SheetPrimitive.Overlay.displayName;\n\nconst sheetVariants = cva(\n  \"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n  {\n    variants: {\n      side: {\n        top: \"inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top\",\n        bottom: \"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom\",\n        left: \"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm\",\n        right:\n          \"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm\",\n      },\n    },\n    defaultVariants: {\n      side: \"right\",\n    },\n  },\n);\n\ninterface SheetContentProps extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, VariantProps<typeof sheetVariants> {}\n\nconst SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(\n  ({ side = \"right\", className, children, ...props }, ref) => (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content className={cn(sheetVariants({ side }), className)} ref={ref} {...props}>\n        {children}\n        <SheetPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none\">\n          <X className=\"h-4 w-4\" />\n          <span className=\"sr-only\">Close</span>\n        </SheetPrimitive.Close>\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  ),\n);\nSheetContent.displayName = SheetPrimitive.Content.displayName;\n\nconst SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col space-y-2 text-center sm:text-left\", className)} {...props} />\n);\nSheetHeader.displayName = \"SheetHeader\";\n\nconst SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", className)} {...props} />\n);\nSheetFooter.displayName = \"SheetFooter\";\n\nconst SheetTitle = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Title className={cn(\"text-foreground text-lg font-semibold\", className)} ref={ref} {...props} />\n));\nSheetTitle.displayName = SheetPrimitive.Title.displayName;\n\nconst SheetDescription = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Description className={cn(\"text-muted-foreground text-sm\", className)} ref={ref} {...props} />\n));\nSheetDescription.displayName = SheetPrimitive.Description.displayName;\n\nexport { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription };\n"
  },
  {
    "path": "src/components/ui/shine-border.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\ntype TColorProp = string | string[];\n\ninterface ShineBorderProps {\n  borderRadius?: number;\n  borderWidth?: number;\n  duration?: number;\n  color?: TColorProp;\n  className?: string;\n  children: React.ReactNode;\n}\n\n/**\n * @name Shine Border\n * @description It is an animated background border effect component with easy to use and configurable props.\n * @param borderRadius defines the radius of the border.\n * @param borderWidth defines the width of the border.\n * @param duration defines the animation duration to be applied on the shining border\n * @param color a string or string array to define border color.\n * @param className defines the class name to be applied to the component\n * @param children contains react node elements.\n */\nexport function ShineBorder({\n  borderRadius = 8,\n  borderWidth = 1,\n  duration = 14,\n  color = \"#000000\",\n  className,\n  children,\n}: ShineBorderProps) {\n  return (\n    <div\n      className={cn(\n        \"min-h-[60px] w-fit min-w-[300px] place-items-center rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white\",\n        className,\n      )}\n      style={\n        {\n          \"--border-radius\": `${borderRadius}px`,\n        } as React.CSSProperties\n      }\n    >\n      <div\n        className={\"before:bg-shine-size motion-safe:before:animate-shine before:absolute before:inset-0 before:aspect-square before:size-full before:rounded-[--border-radius] before:p-[--border-width] before:will-change-[background-position] before:content-[\\\"\\\"] before:![-webkit-mask-composite:xor] before:[background-image:--background-radial-gradient] before:[background-size:300%_300%] before:![mask-composite:exclude] before:[mask:--mask-linear-gradient]\"}\n        style={\n          {\n            \"--border-width\": `${borderWidth}px`,\n            \"--border-radius\": `${borderRadius}px`,\n            \"--duration\": `${duration}s`,\n            \"--mask-linear-gradient\": \"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)\",\n            \"--background-radial-gradient\": `radial-gradient(transparent,transparent, ${color instanceof Array ? color.join(\",\") : color},transparent,transparent)`,\n          } as React.CSSProperties\n        }\n      ></div>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/simple-select.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { cn } from \"@/shared/lib/utils\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\n\nexport interface SelectOption<T extends string = string> {\n  value: T;\n  label: string;\n}\n\ninterface SimpleSelectProps<T extends string = string> {\n  value: T;\n  onValueChange: (value: T) => void;\n  options: SelectOption<T>[];\n  placeholder?: string;\n  className?: string;\n  // Additional props for better customization\n  disabled?: boolean;\n  \"aria-label\"?: string;\n}\n\n/**\n * A simple, reusable select component built on top of shadcn/ui Select\n *\n * @example\n * ```tsx\n * const options = [\n *   { value: \"apple\", label: \"Apple\" },\n *   { value: \"banana\", label: \"Banana\" }\n * ];\n *\n * <SimpleSelect\n *   value={selectedFruit}\n *   onValueChange={setSelectedFruit}\n *   options={options}\n *   placeholder=\"Select a fruit\"\n * />\n * ```\n */\nexport function SimpleSelect<T extends string = string>({\n  value,\n  onValueChange,\n  options,\n  placeholder,\n  className,\n  disabled = false,\n  \"aria-label\": ariaLabel,\n}: SimpleSelectProps<T>) {\n  return (\n    <Select disabled={disabled} onValueChange={(newValue) => onValueChange(newValue as T)} value={value}>\n      <SelectTrigger aria-label={ariaLabel} className={cn(\"w-full\", className)}>\n        <SelectValue placeholder={placeholder} />\n      </SelectTrigger>\n      <SelectContent>\n        {options.map((option) => (\n          <SelectItem key={option.value} value={option.value}>\n            {option.label}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/skeleton.tsx",
    "content": "import React from \"react\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\ninterface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {\n  width?: string | number;\n  height?: string | number;\n  rounded?: string;\n}\n\nexport function Skeleton({ width, height, rounded = \"rounded\", className, ...props }: SkeletonProps) {\n  return (\n    <div\n      className={cn(\"animate-pulse bg-gray-200 dark:bg-gray-700\", rounded, className)}\n      style={{\n        width,\n        height,\n        ...props.style,\n      }}\n      {...props}\n    />\n  );\n}\n"
  },
  {
    "path": "src/components/ui/slider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\n\nimport { cn } from \"@/shared/lib/utils\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\n\nconst Slider = React.forwardRef<\n  React.ElementRef<typeof SliderPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {\n    showTooltip?: boolean;\n    tooltipContent?: (value: number) => React.ReactNode;\n  }\n>(({ className, showTooltip = false, tooltipContent, ...props }, ref) => {\n  const [showTooltipState, setShowTooltipState] = React.useState(false);\n  const [internalValue, setInternalValue] = React.useState<number[]>((props.defaultValue as number[]) ?? (props.value as number[]) ?? [0]);\n\n  React.useEffect(() => {\n    if (props.value !== undefined) {\n      setInternalValue(props.value as number[]);\n    }\n  }, [props.value]);\n\n  const handleValueChange = (newValue: number[]) => {\n    setInternalValue(newValue);\n    props.onValueChange?.(newValue);\n  };\n\n  const handlePointerDown = () => {\n    if (showTooltip) {\n      setShowTooltipState(true);\n    }\n  };\n\n  const handlePointerUp = React.useCallback(() => {\n    if (showTooltip) {\n      setShowTooltipState(false);\n    }\n  }, [showTooltip]);\n\n  React.useEffect(() => {\n    if (showTooltip) {\n      document.addEventListener(\"pointerup\", handlePointerUp);\n      return () => {\n        document.removeEventListener(\"pointerup\", handlePointerUp);\n      };\n    }\n  }, [showTooltip, handlePointerUp]);\n\n  const renderThumb = (value: number) => {\n    const thumb = (\n      <SliderPrimitive.Thumb\n        className=\"text-focus-visible:outline-ring/40 block h-5 w-5 cursor-ew-resize rounded-full border-2 border-primary bg-white transition-colors focus-visible:outline focus-visible:outline-[3px] data-[disabled]:cursor-not-allowed\"\n        onPointerDown={handlePointerDown}\n      />\n    );\n\n    if (!showTooltip) return thumb;\n\n    return (\n      <TooltipProvider>\n        <Tooltip open={showTooltipState}>\n          <TooltipTrigger asChild>{thumb}</TooltipTrigger>\n          <TooltipContent className=\"px-2 py-1 text-xs\" side={props.orientation === \"vertical\" ? \"right\" : \"top\"} sideOffset={8}>\n            <p>{tooltipContent ? tooltipContent(value) : value}</p>\n          </TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n    );\n  };\n\n  return (\n    <SliderPrimitive.Root\n      className={cn(\n        \"relative flex w-full touch-none select-none items-center data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col data-[disabled]:opacity-50\",\n        className,\n      )}\n      onValueChange={handleValueChange}\n      ref={ref}\n      {...props}\n    >\n      <SliderPrimitive.Track className=\"relative grow overflow-hidden rounded-full bg-primary/20 data-[orientation=horizontal]:h-2 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-2\">\n        <SliderPrimitive.Range className=\"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\" />\n      </SliderPrimitive.Track>\n      {internalValue?.map((value, index) => <React.Fragment key={index}>{renderThumb(value)}</React.Fragment>)}\n    </SliderPrimitive.Root>\n  );\n});\nSlider.displayName = SliderPrimitive.Root.displayName;\n\nexport { Slider };\n"
  },
  {
    "path": "src/components/ui/sonner.tsx",
    "content": "\"use client\";\n\nimport { Toaster as Sonner } from \"sonner\";\nimport { useTheme } from \"next-themes\";\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>;\n\nconst ToastSonner = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme();\n\n  return (\n    <Sonner\n      className=\"toaster group\"\n      closeButton={true}\n      theme={theme as ToasterProps[\"theme\"]}\n      toastOptions={{\n        classNames: {\n          toast:\n            \"group overflow-hidden toast group-[.toaster]:bg-white group-[.toaster]:!border-0 group-[.toaster]:p-0 group-[.toaster]:block group-[.toaster]:text-foreground group-[.toaster]:shadow-3xl \",\n          description: \"group-[.toast]:text-black text-sm/tight\",\n          actionButton: \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n          cancelButton: \"!text-black\",\n          closeButton:\n            \"text-black dark:text-white [&>svg]:size-[18px] border-0 hover:opacity-70 top-[22px] absolute rtl:left-2 ltr:right-2 ltr:ml-auto rtl:mr-auto\",\n        },\n      }}\n      {...props}\n    />\n  );\n};\n\nexport { ToastSonner };\n"
  },
  {
    "path": "src/components/ui/star-button.tsx",
    "content": "import { Star } from \"lucide-react\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\ninterface StarButtonProps {\n  isActive: boolean;\n  isLoading: boolean;\n  onClick?: (e: React.MouseEvent) => void;\n  className?: string;\n  children?: React.ReactNode;\n}\n\nexport function StarButton({ isActive, isLoading, onClick, className, children }: StarButtonProps) {\n  return (\n    <button\n      className={cn(\"transition-colors duration-200 text-yellow-500 btn btn-neutral btn-sm\", className)}\n      disabled={isLoading}\n      onClick={onClick}\n    >\n      <Star className={cn(\"transition-all duration-200 h-5 w-5\", isActive ? \"fill-current\" : \"fill-none\", isLoading && \"animate-pulse\")} />\n      {children}\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/switch.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst switchVariants = cva(\n  \"peer inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full transition disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-black data-[state=checked]:bg-black data-[state=unchecked]:bg-gray-500/40 outline-none\",\n  {\n    variants: {\n      variant: {\n        default: \"\",\n        outline: \"h-2 w-[22px]\",\n        large: \"h-6 w-12\",\n      },\n      color: {\n        default: \"data-[state=checked]:ring-black dark:data-[state=checked]:bg-white \",\n        primary: \"data-[state=checked]:bg-primary\",\n        success: \"data-[state=checked]:bg-success\",\n        pending: \"data-[state=checked]:bg-warning\",\n        danger: \"data-[state=checked]:bg-danger\",\n        outlineBlack: \"data-[state=checked]:bg-black/20 dark:data-[state=checked]:bg-white/50\",\n        outlinePrimary: \"data-[state=checked]:bg-primary/20\",\n        outlineSuccess: \"data-[state=checked]:bg-success/20\",\n        outlinePending: \"data-[state=checked]:bg-warning/20\",\n        outlineDanger: \"data-[state=checked]:bg-danger/20\",\n        info: \"data-[state=checked]:bg-blue-400\",\n        warning: \"data-[state=checked]:bg-yellow-400\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      color: \"default\",\n    },\n  },\n);\n\nconst switchThumbVariants = cva(\n  \"bg-transparent pointer-events-none block size-3 rounded-full border-[3.6px] border-white shadow-[0_0_2.4px_0_rgba(0,0,0,0.10)]  transition-transform ltr:data-[state=checked]:translate-x-3.5 ltr:data-[state=unchecked]:translate-x-0.5 rtl:data-[state=checked]:-translate-x-3.5 rtl:data-[state=unchecked]:translate-x-0.5\",\n  {\n    variants: {\n      variant: {\n        default: \"\",\n        outline: \"border-0 data-[state=unchecked]:-translate-x-0.5 data-[state=checked]:translate-x-3\",\n        large: \"size-5 border-[4px]\",\n      },\n      color: {\n        default: \"dark:border-black\",\n        primary: \"\",\n        success: \"\",\n        pending: \"\",\n        danger: \"\",\n        outlineBlack: \"data-[state=checked]:bg-black dark:data-[state=checked]:bg-white data-[state=unchecked]:bg-gray-500\",\n        outlinePrimary: \"data-[state=checked]:bg-primary data-[state=unchecked]:bg-gray-500\",\n        outlineSuccess: \"data-[state=checked]:bg-success data-[state=unchecked]:bg-gray-500\",\n        outlinePending: \"data-[state=checked]:bg-warning data-[state=unchecked]:bg-gray-500\",\n        outlineDanger: \"data-[state=checked]:bg-danger data-[state=unchecked]:bg-gray-500\",\n        info: \"data-[state=checked]:bg-blue-400 data-[state=unchecked]:bg-gray-500\",\n        warning: \"data-[state=checked]:bg-yellow-400 data-[state=unchecked]:bg-gray-500\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      color: \"default\",\n    },\n  },\n);\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & VariantProps<typeof switchVariants>\n>(({ className, variant, color, ...props }, ref) => (\n  <SwitchPrimitives.Root className={cn(switchVariants({ variant, color, className }))} {...props} ref={ref}>\n    <SwitchPrimitives.Thumb className={cn(switchThumbVariants({ variant, color, className }))} />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nconst SwitchOutline = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"focus-visible:ring-ring focus-visible:ring-offset-background peer inline-flex h-7 w-[50px] shrink-0 cursor-pointer items-center rounded-full border border-gray-300 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-transparent data-[state=unchecked]:bg-transparent\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"pointer-events-none block size-1.5 rounded-full bg-transparent shadow-lg ring-[5px] ring-black transition duration-300 data-[state=checked]:ring-black ltr:data-[state=checked]:translate-x-8 ltr:data-[state=unchecked]:translate-x-2.5 rtl:data-[state=checked]:-translate-x-8 rtl:data-[state=unchecked]:-translate-x-2.5 dark:ring-white dark:data-[state=checked]:ring-white\",\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitchOutline.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch, SwitchOutline };\n"
  },
  {
    "path": "src/components/ui/table.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => (\n  <div className=\"relative w-full overflow-auto\">\n    <table className={cn(\"w-full caption-bottom text-sm\", className)} ref={ref} {...props} />\n  </div>\n));\nTable.displayName = \"Table\";\n\nconst TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(\n  ({ className, ...props }, ref) => (\n    <thead\n      className={cn(\n        \"[&_tr]:border-y [&_tr]:border-gray-300 [&_tr]:hover:bg-white dark:[&_tr]:border-gray dark:[&_tr]:bg-white/[6%] dark:[&_tr]:hover:bg-white/[6%]\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    />\n  ),\n);\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(\n  ({ className, ...props }, ref) => (\n    <tbody className={cn(\"divide-y divide-gray-300 dark:divide-gray-300/10\", className)} ref={ref} {...props} />\n  ),\n);\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(\n  ({ className, ...props }, ref) => <tfoot className={cn(\"bg-muted/50 font-medium\", className)} ref={ref} {...props} />,\n);\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(({ className, ...props }, ref) => (\n  <tr\n    className={cn(\n      \"bg-white transition-colors hover:bg-gray-200 data-[state=selected]:bg-gray-200 dark:bg-black-dark dark:hover:bg-gray-200/10 dark:data-[state=selected]:bg-gray-200/10\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (\n  <th\n    className={cn(\n      \"whitespace-nowrap p-4 align-middle text-xs/tight font-medium ltr:text-left ltr:first:pl-5 ltr:last:pr-5 rtl:text-right rtl:first:pr-5 rtl:last:pl-5 [&:has([role=checkbox])]:w-0 ltr:[&:has([role=checkbox])]:pr-0 rtl:[&:has([role=checkbox])]:pl-0\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (\n  <td\n    className={cn(\n      \"whitespace-nowrap px-4 py-3.5 align-middle text-xs/tight font-medium text-black last:text-right ltr:first:pl-5 ltr:last:pr-5 rtl:first:pr-5 rtl:last:pl-5 dark:text-white ltr:[&:has([role=checkbox])]:pr-0 rtl:[&:has([role=checkbox])]:pl-0\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(\n  ({ className, ...props }, ref) => <caption className={cn(\"text-muted-foreground mt-4 text-sm\", className)} ref={ref} {...props} />,\n);\nTableCaption.displayName = \"TableCaption\";\n\nexport { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };\n"
  },
  {
    "path": "src/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst Tabs = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Root>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>>(\n  ({ className, ...props }, ref) => {\n    return <TabsPrimitive.Root className={cn(\"\", className)} dir=\"ltr\" ref={ref} {...props} />;\n  },\n);\nTabs.displayName = TabsPrimitive.Root.displayName;\n\nconst TabsList = React.forwardRef<React.ElementRef<typeof TabsPrimitive.List>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>>(\n  ({ className, ...props }, ref) => <TabsPrimitive.List className={cn(\"\", className)} ref={ref} {...props} />,\n);\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    className={cn(\n      \"group flex items-center gap-1.5 whitespace-nowrap rounded-lg p-2.5 font-medium transition-all hover:bg-light-theme hover:text-black focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-light-theme data-[state=active]:text-black dark:hover:bg-black dark:hover:text-white dark:data-[state=active]:bg-black dark:data-[state=active]:text-white [&>svg]:size-[18px] [&>svg]:shrink-0 [&[data-state=active]>svg]:text-primary\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    className={cn(\"focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2\", className)}\n    ref={ref}\n    {...props}\n  />\n));\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nexport interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {\n  return (\n    <textarea\n      className={cn(\n        \"min-h-24 w-full rounded-lg px-3.5 py-3 font-medium leading-tight text-black shadow-3xl outline-none placeholder:font-medium placeholder:text-gray-500/70 focus:ring-1 focus:ring-black disabled:cursor-not-allowed disabled:opacity-50 dark:bg-black-dark dark:bg-white/5 dark:text-white dark:shadow-sm dark:ring-1 dark:ring-white/10 dark:placeholder:text-gray-500 focus:dark:ring-white\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    />\n  );\n});\nTextarea.displayName = \"Textarea\";\n\nexport { Textarea };\n"
  },
  {
    "path": "src/components/ui/theme-provider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\n\nexport function ThemeProvider({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "src/components/ui/timer.tsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\n\nexport function Timer({\n  isRunning,\n  initialSeconds = 0,\n  onChange,\n}: {\n  isRunning: boolean;\n  initialSeconds?: number;\n  onChange?: (seconds: number) => void;\n}) {\n  const [seconds, setSeconds] = useState(initialSeconds);\n  const intervalRef = useRef<NodeJS.Timeout | null>(null);\n\n  useEffect(() => {\n    setSeconds(initialSeconds);\n  }, [initialSeconds]);\n\n  useEffect(() => {\n    if (isRunning) {\n      intervalRef.current = setInterval(() => {\n        setSeconds((s) => {\n          const next = s + 1;\n          onChange?.(next);\n          return next;\n        });\n      }, 1000);\n    } else if (intervalRef.current) {\n      clearInterval(intervalRef.current);\n      intervalRef.current = null;\n    }\n    return () => {\n      if (intervalRef.current) clearInterval(intervalRef.current);\n    };\n  }, [isRunning, onChange]);\n\n  // Format mm:ss ou hh:mm:ss\n  const format = () => {\n    const h = Math.floor(seconds / 3600);\n    const m = Math.floor((seconds % 3600) / 60);\n    const s = seconds % 60;\n    if (h > 0) return `${h.toString().padStart(2, \"0\")}:${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n    return `${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`;\n  };\n\n  return <span>{format()}</span>;\n}\n"
  },
  {
    "path": "src/components/ui/title-with-dot.tsx",
    "content": "import { cn } from \"@/shared/lib/utils\";\n\nexport function TitleWithDot({ title, className }: { title: string; className?: string }) {\n  return (\n    <div className={cn(\"mb-5 flex items-center gap-2\", className)}>\n      <span className=\"inline-block h-2 w-2 rounded-full bg-gradient-to-r from-indigo-500 to-blue-500\" />\n      <h3 className=\"text-base font-semibold text-gray-900\">{title}</h3>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/toast.tsx",
    "content": "\"use client\";\n\nimport { toast } from \"sonner\";\nimport * as React from \"react\";\nimport Image from \"next/image\";\nimport { X, CheckCircle2, AlertTriangle, Info, XCircle } from \"lucide-react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\n\nimport Logo from \"@public/logo.png\";\nimport { cn } from \"@/shared/lib/utils\";\n\nconst ToastProvider = ToastPrimitives.Provider;\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 transition-all duration-300 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[492px]\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg p-4 duration-300 transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-all data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full shadow-3xl\",\n\n  {\n    variants: {\n      variant: {\n        default: \"bg-white text-black dark:bg-black-dark dark:text-white\",\n        black: \"bg-black text-white\",\n        destructive: \"destructive group border-danger-light border bg-white text-danger\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return <ToastPrimitives.Root className={cn(toastVariants({ variant }), className)} ref={ref} {...props} />;\n});\nToast.displayName = ToastPrimitives.Root.displayName;\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => <ToastPrimitives.Action className={cn(\"inline-block\", className)} ref={ref} {...props} />);\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    className={cn(\n      \"text-foreground/50 hover:text-foreground absolute top-3.5 rounded-md p-1 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 ltr:right-3.5 rtl:left-3.5\",\n      className,\n    )}\n    ref={ref}\n    toast-close=\"\"\n    {...props}\n  >\n    <X className=\"size-[18px]\" />\n  </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => <ToastPrimitives.Title className={cn(\"text-sm font-semibold\", className)} ref={ref} {...props} />);\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\nconst ToastIcon = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => <ToastPrimitives.Title className={cn(\"size-5\", className)} ref={ref} {...props} />);\nToastIcon.displayName = ToastPrimitives.Title.displayName;\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    className={cn(\"text-xs/[18px] font-semibold text-gray dark:text-gray-600\", className)}\n    ref={ref}\n    {...props}\n  />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>;\n\ntype BrandedToastVariant = \"default\" | \"success\" | \"error\" | \"info\" | \"warning\";\n\nconst variantStyles: Record<BrandedToastVariant, { icon: React.ReactNode; color: string }> = {\n  default: {\n    icon: <Info className=\"h-4 w-4 text-primary\" />,\n    color: \"bg-primary/5 border-primary\",\n  },\n  success: {\n    icon: <CheckCircle2 className=\"h-4 w-4 text-green-600\" />,\n    color: \"bg-green-50 border-green-600\",\n  },\n  error: {\n    icon: <XCircle className=\"h-4 w-4 text-danger\" />,\n    color: \"bg-danger/10 border-danger\",\n  },\n  info: {\n    icon: <Info className=\"h-4 w-4 text-blue-600\" />,\n    color: \"bg-blue-50 border-blue-600\",\n  },\n  warning: {\n    icon: <AlertTriangle className=\"h-4 w-4 text-yellow-600\" />,\n    color: \"bg-yellow-50 border-yellow-600\",\n  },\n};\n\ninterface BrandedToastOptions {\n  title: string;\n  subtitle?: string;\n  variant?: BrandedToastVariant;\n}\n\nconst BrandedToastContent = ({ title, subtitle, variant = \"default\" }: BrandedToastOptions) => {\n  const { icon, color } = variantStyles[variant];\n\n  return (\n    <ToastDescription className={\"dark:bg-black-dark dark:text-white\"}>\n      <div className={`-mt-0.5 flex items-center gap-2 border-b px-4 py-3 ${color}`}>\n        <Image alt=\"logo\" height={24} src={Logo} width={24} />\n      </div>\n      <div className=\"flex items-center p-4\">\n        {icon}\n        <p className=\"text-1sm pl-2 font-sans text-black dark:text-white\">{title}</p>\n      </div>\n      {subtitle && <p className=\"!text-xs/20 p-4 pt-0 font-sans text-black/50 dark:text-white\">{subtitle}</p>}\n    </ToastDescription>\n  );\n};\n\nfunction brandedToast(options: BrandedToastOptions) {\n  toast(<BrandedToastContent {...options} />);\n}\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastIcon,\n  ToastAction,\n  brandedToast,\n};\n"
  },
  {
    "path": "src/components/ui/toaster.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/shared/lib/utils\";\nimport { useToast } from \"@/components/ui/use-toast\";\nimport { Toast, ToastProvider, ToastTitle, ToastViewport } from \"@/components/ui/toast\";\n\nexport function Toaster() {\n  const { toasts } = useToast();\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, icon, image, title, description, action, close, actionClassName, ...props }) {\n        return (\n          <Toast key={id} {...props} className={cn(\"\", !!image && \"p-0\", props.className)}>\n            <div>\n              {image && <div>{image}</div>}\n              <div className={cn(\"flex items-start gap-2.5\", image ? \"p-4\" : \"p-0\")}>\n                {icon}\n                <div className={cn(\"flex flex-col items-start gap-2\", actionClassName)}>\n                  {title && <ToastTitle>{title}</ToastTitle>}\n                  {description}\n\n                  {action}\n                </div>\n                {close}\n              </div>\n            </div>\n          </Toast>\n        );\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst TooltipRoot = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    className={cn(\n      \"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    ref={ref}\n    sideOffset={sideOffset}\n    {...props}\n  />\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nconst InlineTooltip = ({\n  children,\n  title,\n  delayDuration = 200,\n  className,\n  side,\n}: React.PropsWithChildren<{\n  title: string;\n  delayDuration?: number;\n  className?: string;\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n}>) => {\n  return (\n    <TooltipProvider>\n      <TooltipRoot delayDuration={delayDuration}>\n        <TooltipTrigger asChild={typeof children !== \"string\"}>{children}</TooltipTrigger>\n        <TooltipContent className={cn(\"max-w-md\", className)} side={side}>\n          <p className=\"text-center\">{title}</p>\n        </TooltipContent>\n      </TooltipRoot>\n    </TooltipProvider>\n  );\n};\n\nconst Tooltip = (props: React.ComponentPropsWithoutRef<typeof TooltipRoot>) => {\n  return (\n    <TooltipProvider>\n      <TooltipRoot {...props} />\n    </TooltipProvider>\n  );\n};\n\nexport { InlineTooltip, Tooltip, TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger };\n"
  },
  {
    "path": "src/components/ui/typography.tsx",
    "content": "import { cva } from \"class-variance-authority\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nimport type { ComponentPropsWithoutRef, ElementType, PropsWithChildren } from \"react\";\nimport type { VariantProps } from \"class-variance-authority\";\n\ntype PolymorphicAsProp<E extends ElementType> = {\n  as?: E | React.ComponentType<Omit<ComponentPropsWithoutRef<E>, \"as\">> | React.FunctionComponent<Omit<ComponentPropsWithoutRef<E>, \"as\">>;\n};\n\ntype PolymorphicProps<E extends ElementType> = PropsWithChildren<Omit<ComponentPropsWithoutRef<E>, \"as\"> & PolymorphicAsProp<E>>;\n\nconst typographyVariants = cva(\"\", {\n  variants: {\n    variant: {\n      h1: \"scroll-m-20 font-caption text-4xl font-extrabold tracking-tight lg:text-5xl\",\n      h2: \"scroll-m-20 font-caption text-3xl font-semibold tracking-tight transition-colors first:mt-0\",\n      h3: \"scroll-m-20 font-caption text-xl font-semibold tracking-tight\",\n      p: \"leading-7 [&:not(:first-child)]:mt-6\",\n      base: \"\",\n      quote: \"citation\",\n      code: \"relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold\",\n      lead: \"text-xl text-muted-foreground\",\n      large: \"text-lg font-semibold\",\n      small: \"text-sm font-medium leading-none\",\n      muted: \"text-sm text-muted-foreground\",\n      link: \"font-medium text-primary underline underline-offset-3 hover:underline hover:scale-[0.95] transition-all duration-300\",\n    },\n  },\n  defaultVariants: {\n    variant: \"base\",\n  },\n});\ntype TypographyCvaProps = VariantProps<typeof typographyVariants>;\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst defaultElement = \"p\";\n\nconst defaultElementMapping: Record<NonNullable<TypographyCvaProps[\"variant\"]>, ElementType> = {\n  h1: \"h1\",\n  h2: \"h2\",\n  h3: \"h3\",\n  p: \"p\",\n  quote: \"blockquote\" as \"p\",\n  code: \"code\",\n  lead: \"p\",\n  large: \"p\",\n  small: \"p\",\n  muted: \"p\",\n  link: \"a\",\n  base: \"p\",\n} as const;\n\nexport function Typography<E extends ElementType = typeof defaultElement>({\n  as,\n  children,\n  className,\n  variant,\n  ...restProps\n}: PolymorphicProps<E> & TypographyCvaProps) {\n  const Component: ElementType = as ?? defaultElementMapping[variant ?? \"base\"];\n\n  return (\n    <Component {...(restProps as ComponentPropsWithoutRef<E>)} className={cn(typographyVariants({ variant }), className)}>\n      {children}\n    </Component>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/use-toast.ts",
    "content": "\"use client\";\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\";\n\nimport type { ToastActionElement, ToastProps } from \"@/components/ui/toast\";\n\nconst TOAST_LIMIT = 1;\nconst TOAST_REMOVE_DELAY = 1000;\n\ntype ToasterToast = ToastProps & {\n  id: string;\n  image?: React.ReactNode;\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  action?: ToastActionElement;\n  icon?: React.ReactNode;\n  close?: React.ReactNode;\n  actionClassName?: string;\n};\n\nconst _actionTypes = {\n  ADD_TOAST: \"ADD_TOAST\",\n  UPDATE_TOAST: \"UPDATE_TOAST\",\n  DISMISS_TOAST: \"DISMISS_TOAST\",\n  REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const;\n\nlet count = 0;\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER;\n  return count.toString();\n}\n\ntype ActionType = typeof _actionTypes;\n\ntype Action =\n  | {\n      type: ActionType[\"ADD_TOAST\"];\n      toast: ToasterToast;\n    }\n  | {\n      type: ActionType[\"UPDATE_TOAST\"];\n      toast: Partial<ToasterToast>;\n    }\n  | {\n      type: ActionType[\"DISMISS_TOAST\"];\n      toastId?: ToasterToast[\"id\"];\n    }\n  | {\n      type: ActionType[\"REMOVE_TOAST\"];\n      toastId?: ToasterToast[\"id\"];\n    };\n\ninterface State {\n  toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return;\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId);\n    dispatch({\n      type: \"REMOVE_TOAST\",\n      toastId: toastId,\n    });\n  }, TOAST_REMOVE_DELAY);\n\n  toastTimeouts.set(toastId, timeout);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case \"ADD_TOAST\":\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      };\n\n    case \"UPDATE_TOAST\":\n      return {\n        ...state,\n        toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),\n      };\n\n    case \"DISMISS_TOAST\": {\n      const { toastId } = action;\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId);\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id);\n        });\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t,\n        ),\n      };\n    }\n    case \"REMOVE_TOAST\":\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        };\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      };\n  }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action);\n  listeners.forEach((listener) => {\n    listener(memoryState);\n  });\n}\n\ntype Toast = Omit<ToasterToast, \"id\">;\n\nfunction toast({ ...props }: Toast) {\n  const id = genId();\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: \"UPDATE_TOAST\",\n      toast: { ...props, id },\n    });\n  const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id });\n\n  dispatch({\n    type: \"ADD_TOAST\",\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss();\n      },\n    },\n  });\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  };\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState);\n\n  React.useEffect(() => {\n    listeners.push(setState);\n    return () => {\n      const index = listeners.indexOf(setState);\n      if (index > -1) {\n        listeners.splice(index, 1);\n      }\n    };\n  }, [state]);\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n  };\n}\n\nexport { useToast, toast };\n"
  },
  {
    "path": "src/components/ui/workout-lol.tsx",
    "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nconst workoutLolVariants = cva(\n  \"inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-sm font-mono font-semibold transition-colors duration-200\",\n  {\n    variants: {\n      variant: {\n        default: [\n          \"bg-red-50 text-red-600 ring-1 ring-inset ring-red-200\",\n          \"dark:bg-red-950/30 dark:text-red-400 dark:ring-red-900/30\",\n          \"hover:bg-red-100 dark:hover:bg-red-950/50\",\n        ],\n        muted: [\n          \"bg-slate-100 text-slate-600 ring-1 ring-inset ring-slate-200\",\n          \"dark:bg-slate-900/50 dark:text-slate-400 dark:ring-slate-800\",\n          \"hover:bg-slate-200 dark:hover:bg-slate-900/70\",\n        ],\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\ninterface WorkoutLolProps extends VariantProps<typeof workoutLolVariants> {\n  className?: string;\n  children?: React.ReactNode;\n}\n\nexport const WorkoutLol = ({ className, variant, children }: WorkoutLolProps) => {\n  return <span className={cn(workoutLolVariants({ variant }), className)}>{children || \"workout.lol\"}</span>;\n};\n\nexport const WorkoutLolMuted = ({ className, children }: Omit<WorkoutLolProps, \"variant\">) => (\n  <WorkoutLol className={className} variant=\"muted\">\n    {children}\n  </WorkoutLol>\n);\n"
  },
  {
    "path": "src/components/utils/ErrorBoundaries.tsx",
    "content": "\"use client\";\n\nimport { Component } from \"react\";\n\nimport { logger } from \"@/shared/lib/logger\";\nimport { Card, CardDescription, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\n\nimport type { ErrorInfo, ReactNode } from \"react\";\n\ntype Props = {\n  children?: ReactNode;\n  fallback?: ReactNode | ((error: string) => ReactNode);\n  title?: string;\n  description?: string;\n};\n\ntype State = {\n  hasError: boolean;\n  error: string;\n};\n\n/**\n * ErrorBoundary is a component that catches errors in its children and renders a fallback UI.\n * It's useful for handling errors in the UI and preventing the entire app from crashing.\n * It use class component because it uses componentDidCatch lifecycle method.\n */\nexport class ErrorBoundary extends Component<Props, State> {\n  public state: State = {\n    hasError: false,\n    error: \"\",\n  };\n\n  public static getDerivedStateFromError(error: Error): State {\n    // Update state so the next render will show the fallback UI.\n    logger.error(\"ErrorBoundary: \", error.message);\n    return { hasError: true, error: error.message };\n  }\n\n  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n    logger.error(\"Uncaught error:\", error, errorInfo);\n  }\n\n  public render() {\n    if (!this.state.hasError) {\n      return this.props.children;\n    }\n\n    if (this.props.fallback) {\n      if (typeof this.props.fallback === \"function\") return this.props.fallback(this.state.error);\n\n      return this.props.fallback;\n    }\n\n    const { title = \"Something went wrong\", description = \"Please try again later\" } = this.props;\n\n    return (\n      <Card>\n        <CardHeader>\n          <CardTitle>{title}</CardTitle>\n          <CardDescription>{description}</CardDescription>\n        </CardHeader>\n        <CardFooter>\n          <Button\n            onClick={() => {\n              this.setState({ hasError: false });\n            }}\n            variant=\"outline\"\n          >\n            Retry\n          </Button>\n        </CardFooter>\n      </Card>\n    );\n  }\n}\n"
  },
  {
    "path": "src/components/utils/TailwindIndicator.tsx",
    "content": "export function TailwindIndicator() {\n  if (process.env.NODE_ENV === \"production\") return null;\n\n  return (\n    <div className=\"fixed bottom-1 left-1 z-50 flex size-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white\">\n      <div className=\"block sm:hidden\">xs</div>\n      <div className=\"hidden sm:block md:hidden\">sm</div>\n      <div className=\"hidden md:block lg:hidden\">md</div>\n      <div className=\"hidden lg:block xl:hidden\">lg</div>\n      <div className=\"hidden xl:block 2xl:hidden\">xl</div>\n      <div className=\"hidden 2xl:block\">2xl</div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/version.tsx",
    "content": "import { appVersion } from \"@/shared/lib/version\";\n\nexport const Version = () => (\n  <div className=\"absolute bottom-2 right-4\">\n    <span className=\"text-xs text-gray-500 dark:text-gray-600/50\">\n      v{appVersion}\n    </span>\n  </div>\n\n);"
  },
  {
    "path": "src/entities/exercise/shared/muscles.tsx",
    "content": "import { ExerciseAttributeNameEnum } from \"@prisma/client\";\n\nimport { ExerciseAttribute, ExerciseWithAttributes } from \"@/entities/exercise/types/exercise.types\";\n\nexport const getAttributeName = (attr: ExerciseAttribute) => {\n  return typeof attr.attributeName === \"string\" ? attr.attributeName : attr.attributeName.name;\n};\n\nexport const getAttributeValue = (attr: ExerciseAttribute) => {\n  return typeof attr.attributeValue === \"string\" ? attr.attributeValue : attr.attributeValue.value;\n};\n\nconst getAttributesByName = (attributes: ExerciseAttribute[], name: ExerciseAttributeNameEnum) => {\n  return attributes.filter((attr) => getAttributeName(attr) === name);\n};\n\nexport const getPrimaryMuscle = (attributes: ExerciseAttribute[]): ExerciseAttribute | undefined => {\n  return getAttributesByName(attributes, ExerciseAttributeNameEnum.PRIMARY_MUSCLE)[0];\n};\n\nexport const getSecondaryMuscles = (attributes: ExerciseAttribute[]) => {\n  return getAttributesByName(attributes, ExerciseAttributeNameEnum.SECONDARY_MUSCLE);\n};\n\nexport const getExerciseAttributesValueOf = (exercise: ExerciseWithAttributes, name: ExerciseAttributeNameEnum) => {\n  return getAttributesByName(exercise.attributes, name).map(getAttributeValue);\n};\n"
  },
  {
    "path": "src/entities/exercise/types/exercise.types.ts",
    "content": "import { ExerciseAttributeNameEnum, ExerciseAttributeValueEnum } from \"@prisma/client\";\n\n// import { I18nName, I18nField } from \"@/shared/types/i18n.types\";\n\n// Base exercise type\nexport interface BaseExercise {\n  id: string;\n  fullVideoUrl?: string | null;\n  fullVideoImageUrl?: string | null;\n  introduction: string | null;\n  introductionEn: string | null;\n  name: string;\n  nameEn: string | null;\n  description: string;\n  descriptionEn: string | null;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nexport interface ExerciseAttribute {\n  id: string;\n  exerciseId: string;\n  attributeNameId: string;\n  attributeValueId: string;\n  attributeName: ExerciseAttributeNameEnum | { name: ExerciseAttributeNameEnum; id: string };\n  attributeValue: ExerciseAttributeValueEnum | { value: ExerciseAttributeValueEnum; id: string };\n}\n\n// Exercise with attributes\nexport interface ExerciseWithAttributes extends BaseExercise {\n  attributes: ExerciseAttribute[];\n}\n\n// Suggested set for program exercises\nexport interface SuggestedSet {\n  id: string;\n  programExerciseId: string;\n  setIndex: number;\n  types: string[];\n  valuesInt: number[];\n  valuesSec: number[];\n  units: string[];\n}\n"
  },
  {
    "path": "src/entities/program/types/program.types.ts",
    "content": "import { ProgramLevel, ExerciseAttributeValueEnum, ProgramVisibility, ProgramWeek } from \"@prisma/client\";\n\nimport { I18nText, I18nSlug } from \"@/shared/types/i18n.types\";\nimport { ProgramSessionWithExercises } from \"@/entities/program-session/types/program-session.types\";\n\n// Base program type with all fields\nexport interface BaseProgram extends I18nText, I18nSlug {\n  id: string;\n  category: string;\n  image: string;\n  level: ProgramLevel;\n  type: ExerciseAttributeValueEnum;\n  durationWeeks: number;\n  sessionsPerWeek: number;\n  sessionDurationMin: number;\n  equipment: ExerciseAttributeValueEnum[];\n  isPremium: boolean;\n  visibility: ProgramVisibility;\n  isActive: boolean;\n  participantCount: number;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\n// Simplified program reference\nexport interface ProgramReference {\n  id: string;\n  title: string;\n  slug: string;\n}\n\n// Program with i18n reference (for navigation)\nexport interface ProgramI18nReference extends I18nText, I18nSlug {\n  id: string;\n}\n\n// Coach type\nexport interface ProgramCoach {\n  id: string;\n  name: string;\n  image: string;\n  order: number;\n  programId: string;\n}\n\nexport interface PublicProgramResponse extends BaseProgram {\n  totalEnrollments: number;\n}\n\n// Detailed program response (with weeks and sessions)\nexport interface ProgramDetailResponse extends BaseProgram {\n  coaches: ProgramCoach[];\n  totalEnrollments: number;\n  weeks: ProgramWeekWithSessions[];\n}\n\n// Program week with sessions\nexport interface ProgramWeekWithSessions extends ProgramWeek {\n  sessions: ProgramSessionWithExercises[];\n}\n\n// Session detail response\nexport interface SessionDetailResponse {\n  session: ProgramSessionWithExercises;\n  program: ProgramI18nReference;\n  week: ProgramWeek;\n}\n\n// Progress tracking\nexport interface ProgramProgressResponse {\n  enrollment: {\n    id: string;\n    userId: string;\n    programId: string;\n    currentWeek: number;\n    currentSession: number;\n    enrolledAt: Date;\n    sessionProgress: SessionProgress[];\n  } | null;\n  stats: {\n    totalSessions: number;\n    completedSessions: number;\n    completionPercentage: number;\n    currentWeek: number;\n    currentSession: number;\n  };\n}\n\nexport interface SessionProgress {\n  id: string;\n  enrollmentId: string;\n  sessionId: string;\n  startedAt: Date;\n  completedAt: Date | null;\n  workoutSessionId: string | null;\n  session: {\n    id: string;\n  };\n}\n"
  },
  {
    "path": "src/entities/program-session/types/program-session.types.ts",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { I18nText, I18nSlug, I18nField } from \"@/shared/types/i18n.types\";\nimport { ExerciseWithAttributes, SuggestedSet } from \"@/entities/exercise/types/exercise.types\";\n\n// Base session type\nexport interface BaseProgramSession extends I18nText, I18nSlug {\n  id: string;\n  weekId: string;\n  sessionNumber: number;\n  equipment: ExerciseAttributeValueEnum[];\n  estimatedMinutes: number;\n  isPremium: boolean;\n}\n\n// Program week type\nexport interface ProgramWeek extends I18nText {\n  id: string;\n  programId: string;\n  weekNumber: number;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\n// Program exercise (exercise in the context of a program session)\nexport interface ProgramExercise extends I18nField<\"instructions\"> {\n  id: string;\n  sessionId: string;\n  exerciseId: string;\n  order: number;\n  exercise: ExerciseWithAttributes;\n  suggestedSets: SuggestedSet[];\n}\n\n// Session with exercises\nexport interface ProgramSessionWithExercises extends BaseProgramSession {\n  exercises: ProgramExercise[];\n}\n"
  },
  {
    "path": "src/entities/user/lib/display-name.ts",
    "content": "import { SessionUser } from \"@/entities/user/types/session-user\";\n\nexport function displayName(user: SessionUser): string {\n  return user.name\n    ? user.name\n    : user.email\n        .split(\"@\")[0]\n        .replaceAll(\".\", \" \")\n        .replace(/^\\w/, (c) => c.toUpperCase());\n}\n\nexport function displayFullName({ firstName, lastName }: { firstName: string; lastName: string }): string {\n  return `${firstName} ${lastName}`;\n}\n\nexport function displayFirstNameAndFirstLetterLastName(user: SessionUser): string {\n  return `${user.firstName} ${user.lastName?.charAt(0)}.`;\n}\n\nexport const displayFirstName = (user: SessionUser): string => {\n  return user.firstName;\n};\n\nexport const displayInitials = (user: SessionUser): string => {\n  return user.firstName.charAt(0) + user.lastName.charAt(0);\n};\n"
  },
  {
    "path": "src/entities/user/model/get-server-session-user.ts",
    "content": "import { notFound } from \"next/navigation\";\nimport { headers } from \"next/headers\";\n\nimport { auth } from \"@/features/auth/lib/better-auth\";\nexport class AuthError extends Error {\n  constructor(message: string) {\n    super(message);\n  }\n}\n\nexport const serverAuth = async () => {\n  const session = await auth.api.getSession({ headers: await headers() });\n\n  if (session && session.user) {\n    return session.user;\n  }\n\n  return null;\n};\n\nexport const serverRequiredUser = async () => {\n  const user = await serverAuth();\n\n  if (!user) {\n    notFound();\n  }\n\n  return user;\n};\n"
  },
  {
    "path": "src/entities/user/model/get-users.actions.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\nimport { Prisma, UserRole } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { authenticatedActionClient, ActionError } from \"@/shared/api/safe-actions\";\n\nconst getUsersSchema = z.object({\n  page: z.number().default(1),\n  limit: z.number().default(10),\n  search: z.string().optional(),\n  sortBy: z.enum([\"createdAt\", \"email\"]).optional().default(\"createdAt\"),\n  sortOrder: z.enum([\"asc\", \"desc\"]).optional().default(\"desc\"),\n});\n\nexport const getUsersAction = authenticatedActionClient.schema(getUsersSchema).action(async ({ parsedInput, ctx }) => {\n  try {\n    const { user: authUser } = ctx;\n\n    if (!authUser || authUser.role !== UserRole.admin) {\n      throw new ActionError(\"Access denied. Admin role required.\");\n    }\n\n    const { page, limit, search, sortBy, sortOrder } = parsedInput;\n\n    // Validate input parameters\n    if (page < 1) {\n      throw new ActionError(\"Page number must be positive\");\n    }\n\n    if (limit < 1 || limit > 100) {\n      throw new ActionError(\"Limit must be between 1 and 100\");\n    }\n\n    const where: Prisma.UserWhereInput = search\n      ? {\n          OR: [\n            { id: { contains: search, mode: Prisma.QueryMode.insensitive } },\n            { email: { contains: search, mode: Prisma.QueryMode.insensitive } },\n          ],\n        }\n      : {};\n\n    const selectClause = {\n      id: true,\n      email: true,\n      emailVerified: true,\n      firstName: true,\n      lastName: true,\n      createdAt: true,\n      role: true,\n    };\n\n    const totalCount = await prisma.user.count({ where });\n    const skip = (page - 1) * limit;\n\n    let orderByPrisma: Prisma.UserOrderByWithRelationInput = {};\n    if (sortBy === \"createdAt\") {\n      orderByPrisma = { createdAt: sortOrder };\n    } else if (sortBy === \"email\") {\n      orderByPrisma = { email: sortOrder };\n    }\n\n    const fetchedUsers = await prisma.user.findMany({\n      select: selectClause,\n      where,\n      orderBy: orderByPrisma,\n      skip,\n      take: limit,\n    });\n\n    const usersToReturn = fetchedUsers.map((u) => ({\n      ...u,\n      // Ensure dates are properly serialized\n      createdAt: u.createdAt.toISOString(),\n    }));\n\n    return {\n      users: usersToReturn,\n      pagination: {\n        total: totalCount,\n        pages: Math.ceil(totalCount / limit),\n        page,\n        limit,\n      },\n    };\n  } catch (error) {\n    console.error(\"Error in getUsersAction:\", error);\n\n    if (error instanceof ActionError) {\n      throw error;\n    }\n\n    if (error instanceof Prisma.PrismaClientKnownRequestError) {\n      throw new ActionError(`Database error: ${error.code}`);\n    }\n\n    if (error instanceof Prisma.PrismaClientUnknownRequestError) {\n      throw new ActionError(\"Database connection error\");\n    }\n\n    throw new ActionError(\"Failed to fetch users\");\n  }\n});\n"
  },
  {
    "path": "src/entities/user/model/update-user-locale.ts",
    "content": "\"use client\";\n\nimport { useMutation } from \"@tanstack/react-query\";\n\nimport { useCurrentUser } from \"@/entities/user/model/useCurrentUser\";\nimport { updateUserAction } from \"@/entities/user/model/update-user.action\";\n\ninterface UpdateUserLocaleParams {\n  locale: string;\n}\n\nexport function useUpdateUserLocale() {\n  const user = useCurrentUser();\n\n  return useMutation({\n    mutationFn: async ({ locale }: UpdateUserLocaleParams) => {\n      if (!user) {\n        return;\n      }\n\n      const result = await updateUserAction({ locale });\n\n      if (!result?.data?.success) {\n        throw new Error(\"Failed to update user locale\");\n      }\n\n      return result.data;\n    },\n    onError: (error) => {\n      console.error(\"Failed to update user locale:\", error);\n      // silent fail, ux friendly\n    },\n  });\n}\n"
  },
  {
    "path": "src/entities/user/model/update-user.action.ts",
    "content": "\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { mobileAuthenticatedActionClient } from \"@/shared/api/mobile-safe-actions\";\nimport { updateUserSchema } from \"@/entities/user/schemas/update-user.schema\";\n\nexport const updateUserAction = mobileAuthenticatedActionClient.schema(updateUserSchema).action(async ({ parsedInput, ctx }) => {\n  const { user } = ctx as { user: any };\n\n  if (!user) {\n    throw new Error(\"Unauthenticated\");\n  }\n\n  const { locale, firstName, lastName, image, revalidatePath: path } = parsedInput;\n\n  // Build update object with only provided fields\n  const updateData: Record<string, any> = {};\n  if (locale !== undefined) updateData.locale = locale;\n  if (firstName !== undefined) updateData.firstName = firstName;\n  if (lastName !== undefined) updateData.lastName = lastName;\n  if (image !== undefined) updateData.image = image;\n\n  // Only perform update if there are fields to update\n  if (Object.keys(updateData).length > 0) {\n    await prisma.user.update({\n      where: { id: user.id },\n      data: updateData,\n    });\n  }\n\n  // Revalidate path if provided\n  if (path) {\n    revalidatePath(path);\n  }\n\n  return { success: true };\n});\n"
  },
  {
    "path": "src/entities/user/model/use-auto-locale.ts",
    "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { useChangeLocale, useCurrentLocale } from \"locales/client\";\nimport { useUpdateUserLocale } from \"@/entities/user/model/update-user-locale\";\n\nexport function useAutoLocale() {\n  const currentLocale = useCurrentLocale();\n  const changeLocale = useChangeLocale();\n  const updateUserLocale = useUpdateUserLocale();\n  const hasAutoDetected = useRef(false);\n\n  useEffect(() => {\n    // Only run auto-detection once on mount\n    if (hasAutoDetected.current) return;\n\n    const detectedLocale = document.cookie\n      .split(\"; \")\n      .find((row) => row.startsWith(\"detected-locale=\"))\n      ?.split(\"=\")[1];\n\n    // Only change if we have a detected locale different from current\n    if (detectedLocale && detectedLocale !== currentLocale) {\n      hasAutoDetected.current = true;\n\n      // Change locale on client\n      changeLocale(detectedLocale as any);\n\n      // Save to database silently\n      updateUserLocale.mutate({ locale: detectedLocale });\n    }\n  }, []); // Remove dependencies to run only once\n}\n"
  },
  {
    "path": "src/entities/user/model/useCurrentSession.ts",
    "content": "import { useSession } from \"@/features/auth/lib/auth-client\";\n\nexport const useCurrentSession = () => {\n  const session = useSession();\n  const sessionData = session.data;\n\n  if (!sessionData) {\n    return null;\n  }\n\n  return sessionData;\n};\n"
  },
  {
    "path": "src/entities/user/model/useCurrentUser.ts",
    "content": "import { useSession } from \"@/features/auth/lib/auth-client\";\n\nexport const useCurrentUser = () => {\n  const session = useSession();\n  const user = session.data?.user;\n\n  if (!user) {\n    return null;\n  }\n\n  return user;\n};\n"
  },
  {
    "path": "src/entities/user/schemas/get-user.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const getUsersSchema = z.object({\n  page: z.number().default(1),\n  limit: z.number().default(10),\n  search: z.string().optional(),\n});\n"
  },
  {
    "path": "src/entities/user/schemas/update-user.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const updateUserSchema = z.object({\n  locale: z.string().optional(),\n  firstName: z.string().optional(),\n  lastName: z.string().optional(),\n  image: z.string().optional(),\n  revalidatePath: z.string().optional(),\n});\n\nexport type UpdateUserInput = z.infer<typeof updateUserSchema>;\n"
  },
  {
    "path": "src/entities/user/types/session-user.ts",
    "content": "import { User } from \"@prisma/client\";\n\nimport { authClient } from \"@/features/auth/lib/auth-client\";\n\nexport interface SessionUser extends Omit<User, \"image\" | \"createdAt\" | \"updatedAt\"> {\n  image?: string | null;\n}\n\nexport type Session = typeof authClient.$Infer.Session;\n"
  },
  {
    "path": "src/env.ts",
    "content": "import { z } from \"zod\";\nimport { createEnv } from \"@t3-oss/env-nextjs\";\n\nconst booleanString = z.enum([\"true\", \"false\"]).transform((val) => val === \"true\");\n\n/**\n * This is the schema for the environment variables.\n *\n * Please import **this** file and use the `env` variable\n */\nexport const env = createEnv({\n  server: {\n    BETTER_AUTH_URL: z.string().url(),\n    DATABASE_URL: z.string().url(),\n    GOOGLE_CLIENT_ID: z.string().min(1),\n    GOOGLE_CLIENT_SECRET: z.string().min(1),\n    NODE_ENV: z.enum([\"development\", \"production\", \"test\"]),\n    BETTER_AUTH_SECRET: z.string().min(1),\n    OPENPANEL_SECRET_KEY: z.string().optional(),\n    SMTP_HOST: z.string().optional(),\n    SMTP_PORT: z.coerce.number().positive().optional(),\n    SMTP_USER: z.string().optional(),\n    SMTP_PASS: z.string().optional(),\n    SMTP_FROM: z.string().optional(),\n    //issue fixed in zod 4. See https://github.com/colinhacks/zod/issues/3906\n    SMTP_SECURE: booleanString.default(\"false\"),\n\n    STRIPE_SECRET_KEY: z.string().optional(),\n    STRIPE_WEBHOOK_SECRET: z.string().optional(),\n\n    // RevenueCat configuration\n    REVENUECAT_SECRET_KEY: z.string().optional(),\n    REVENUECAT_WEBHOOK_SECRET: z.string().optional(),\n  },\n  /**\n   * If you add `client` environment variables, you need to add them to\n   * `experimental__runtimeEnv` as well.\n   */\n  client: {\n    NEXT_PUBLIC_OPENPANEL_CLIENT_ID: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(),\n    NEXT_PUBLIC_APP_URL: z.string().url(),\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_EU: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_EU: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_US: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_US: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_LATAM: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_LATAM: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_BR: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_BR: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_RU: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_RU: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_CN: z.string().optional(),\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_CN: z.string().optional(),\n    NEXT_PUBLIC_SHOW_ADS: booleanString.optional(),\n    NEXT_PUBLIC_AD_CLIENT: z.string().optional(),\n    NEXT_PUBLIC_VERTICAL_LEFT_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_VERTICAL_RIGHT_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_EQUIPMENT_SELECTION_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_EXERCISE_SELECTION_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_MUSCLE_SELECTION_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_WORKOUT_SESSION_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_BOTTOM_WORKOUT_SESSION_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_STEPPER_STEP_1_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_STEPPER_STEP_2_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_STEPPER_STEP_3_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_PROGRAMS_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_BOTTOM_PROGRAMS_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_TOOLS_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_BOTTOM_TOOLS_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_CALCULATOR_HUB_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_BOTTOM_CALCULATOR_HUB_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_PROGRAM_DETAILS_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_BOTTOM_PROGRAM_DETAILS_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_PROFILE_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_IN_ARTICLE_BMI_1_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_IN_ARTICLE_BMI_2_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_BMI_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_BOTTOM_BMI_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_HEART_ZONES_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_BOTTOM_HEART_ZONES_BANNER_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_1: z.string().optional(),\n    NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_2: z.string().optional(),\n    NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_3: z.string().optional(),\n    NEXT_PUBLIC_TOP_MIFFLIN_ST_JEOR_CALCULATOR_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_OXFORD_CALCULATOR_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_BOTTOM_OXFORD_CALCULATOR_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_HARRIS_BENEDICT_CALCULATOR_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_KATCH_MCARDLE_CALCULATOR_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_CUNNINGHAM_CALCULATOR_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_TOP_CALORIE_CALCULATOR_COMPARISON_AD_SLOT: z.string().optional(),\n    NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_COMPARISON_AD_SLOT: z.string().optional(),\n    // Ezoic configuration\n    NEXT_PUBLIC_AD_PROVIDER: z.enum([\"adsense\", \"ezoic\"]).optional().default(\"adsense\"),\n    NEXT_PUBLIC_EZOIC_VERTICAL_LEFT_PLACEMENT_ID: z.string().optional(),\n    NEXT_PUBLIC_EZOIC_VERTICAL_RIGHT_PLACEMENT_ID: z.string().optional(),\n    NEXT_PUBLIC_EZOIC_EQUIPMENT_SELECTION_PLACEMENT_ID: z.string().optional(),\n    NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_1_PLACEMENT_ID: z.string().optional(),\n    NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_2_PLACEMENT_ID: z.string().optional(),\n    NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_3_PLACEMENT_ID: z.string().optional(),\n    // GA4\n    NEXT_PUBLIC_GA4_MEASUREMENT_ID: z.string().optional(),\n  },\n\n  experimental__runtimeEnv: {\n    NEXT_PUBLIC_OPENPANEL_CLIENT_ID: process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID,\n    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,\n    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_EU: process.env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_EU,\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_EU: process.env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_EU,\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_US: process.env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_US,\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_US: process.env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_US,\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_LATAM: process.env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_LATAM,\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_LATAM: process.env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_LATAM,\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_BR: process.env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_BR,\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_BR: process.env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_BR,\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_RU: process.env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_RU,\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_RU: process.env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_RU,\n    NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_CN: process.env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY_CN,\n    NEXT_PUBLIC_STRIPE_PRICE_YEARLY_CN: process.env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY_CN,\n    NEXT_PUBLIC_SHOW_ADS: process.env.NEXT_PUBLIC_SHOW_ADS,\n    NEXT_PUBLIC_AD_CLIENT: process.env.NEXT_PUBLIC_AD_CLIENT,\n    NEXT_PUBLIC_VERTICAL_LEFT_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_VERTICAL_LEFT_BANNER_AD_SLOT,\n    NEXT_PUBLIC_VERTICAL_RIGHT_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_VERTICAL_RIGHT_BANNER_AD_SLOT,\n    NEXT_PUBLIC_EQUIPMENT_SELECTION_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_EQUIPMENT_SELECTION_BANNER_AD_SLOT,\n    NEXT_PUBLIC_EXERCISE_SELECTION_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_EXERCISE_SELECTION_BANNER_AD_SLOT,\n    NEXT_PUBLIC_MUSCLE_SELECTION_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_MUSCLE_SELECTION_BANNER_AD_SLOT,\n    NEXT_PUBLIC_TOP_WORKOUT_SESSION_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_WORKOUT_SESSION_BANNER_AD_SLOT,\n    NEXT_PUBLIC_BOTTOM_WORKOUT_SESSION_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_BOTTOM_WORKOUT_SESSION_BANNER_AD_SLOT,\n    NEXT_PUBLIC_TOP_STEPPER_STEP_1_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_STEPPER_STEP_1_BANNER_AD_SLOT,\n    NEXT_PUBLIC_TOP_STEPPER_STEP_2_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_STEPPER_STEP_2_BANNER_AD_SLOT,\n    NEXT_PUBLIC_TOP_STEPPER_STEP_3_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_STEPPER_STEP_3_BANNER_AD_SLOT,\n    NEXT_PUBLIC_TOP_PROGRAMS_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_PROGRAMS_BANNER_AD_SLOT,\n    NEXT_PUBLIC_BOTTOM_PROGRAMS_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_BOTTOM_PROGRAMS_BANNER_AD_SLOT,\n    NEXT_PUBLIC_TOP_TOOLS_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_TOOLS_BANNER_AD_SLOT,\n    NEXT_PUBLIC_BOTTOM_TOOLS_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_BOTTOM_TOOLS_BANNER_AD_SLOT,\n    NEXT_PUBLIC_TOP_CALCULATOR_HUB_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_CALCULATOR_HUB_BANNER_AD_SLOT,\n    NEXT_PUBLIC_BOTTOM_CALCULATOR_HUB_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_BOTTOM_CALCULATOR_HUB_BANNER_AD_SLOT,\n    NEXT_PUBLIC_TOP_PROGRAM_DETAILS_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_PROGRAM_DETAILS_BANNER_AD_SLOT,\n    NEXT_PUBLIC_BOTTOM_PROGRAM_DETAILS_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_BOTTOM_PROGRAM_DETAILS_BANNER_AD_SLOT,\n    NEXT_PUBLIC_TOP_PROFILE_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_PROFILE_BANNER_AD_SLOT,\n    NEXT_PUBLIC_IN_ARTICLE_BMI_1_AD_SLOT: process.env.NEXT_PUBLIC_IN_ARTICLE_BMI_1_AD_SLOT,\n    NEXT_PUBLIC_IN_ARTICLE_BMI_2_AD_SLOT: process.env.NEXT_PUBLIC_IN_ARTICLE_BMI_2_AD_SLOT,\n    NEXT_PUBLIC_TOP_BMI_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_BMI_BANNER_AD_SLOT,\n    NEXT_PUBLIC_BOTTOM_BMI_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_BMI_BANNER_AD_SLOT,\n    NEXT_PUBLIC_TOP_HEART_ZONES_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_TOP_HEART_ZONES_BANNER_AD_SLOT,\n    NEXT_PUBLIC_BOTTOM_HEART_ZONES_BANNER_AD_SLOT: process.env.NEXT_PUBLIC_BOTTOM_HEART_ZONES_BANNER_AD_SLOT,\n    NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_1: process.env.NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_1,\n    NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_2: process.env.NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_2,\n    NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_3: process.env.NEXT_PUBLIC_IN_ARTICLE_HEART_ZONES_AD_SLOT_3,\n    NEXT_PUBLIC_TOP_MIFFLIN_ST_JEOR_CALCULATOR_AD_SLOT: process.env.NEXT_PUBLIC_TOP_MIFFLIN_ST_JEOR_CALCULATOR_AD_SLOT,\n    NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_AD_SLOT: process.env.NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_AD_SLOT,\n    NEXT_PUBLIC_TOP_OXFORD_CALCULATOR_AD_SLOT: process.env.NEXT_PUBLIC_TOP_OXFORD_CALCULATOR_AD_SLOT,\n    NEXT_PUBLIC_BOTTOM_OXFORD_CALCULATOR_AD_SLOT: process.env.NEXT_PUBLIC_BOTTOM_OXFORD_CALCULATOR_AD_SLOT,\n    NEXT_PUBLIC_TOP_HARRIS_BENEDICT_CALCULATOR_AD_SLOT: process.env.NEXT_PUBLIC_TOP_HARRIS_BENEDICT_CALCULATOR_AD_SLOT,\n    NEXT_PUBLIC_TOP_KATCH_MCARDLE_CALCULATOR_AD_SLOT: process.env.NEXT_PUBLIC_TOP_KATCH_MCARDLE_CALCULATOR_AD_SLOT,\n    NEXT_PUBLIC_TOP_CUNNINGHAM_CALCULATOR_AD_SLOT: process.env.NEXT_PUBLIC_TOP_CUNNINGHAM_CALCULATOR_AD_SLOT,\n    NEXT_PUBLIC_TOP_CALORIE_CALCULATOR_COMPARISON_AD_SLOT: process.env.NEXT_PUBLIC_TOP_CALORIE_CALCULATOR_COMPARISON_AD_SLOT,\n    NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_COMPARISON_AD_SLOT: process.env.NEXT_PUBLIC_BOTTOM_CALORIE_CALCULATOR_COMPARISON_AD_SLOT,\n    NEXT_PUBLIC_AD_PROVIDER: process.env.NEXT_PUBLIC_AD_PROVIDER,\n    NEXT_PUBLIC_EZOIC_VERTICAL_LEFT_PLACEMENT_ID: process.env.NEXT_PUBLIC_EZOIC_VERTICAL_LEFT_PLACEMENT_ID,\n    NEXT_PUBLIC_EZOIC_VERTICAL_RIGHT_PLACEMENT_ID: process.env.NEXT_PUBLIC_EZOIC_VERTICAL_RIGHT_PLACEMENT_ID,\n    NEXT_PUBLIC_EZOIC_EQUIPMENT_SELECTION_PLACEMENT_ID: process.env.NEXT_PUBLIC_EZOIC_EQUIPMENT_SELECTION_PLACEMENT_ID,\n    NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_1_PLACEMENT_ID: process.env.NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_1_PLACEMENT_ID,\n    NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_2_PLACEMENT_ID: process.env.NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_2_PLACEMENT_ID,\n    NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_3_PLACEMENT_ID: process.env.NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_3_PLACEMENT_ID,\n    NEXT_PUBLIC_GA4_MEASUREMENT_ID: process.env.NEXT_PUBLIC_GA4_MEASUREMENT_ID,\n  },\n});\n"
  },
  {
    "path": "src/features/admin/layout/admin-header.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { LogOut, Menu, X, UserCog } from \"lucide-react\";\n\nimport Logo from \"@public/logo.png\";\nimport { PLACEHOLDERS } from \"@/shared/constants/placeholders\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { useLogout } from \"@/features/auth/model/useLogout\";\nimport { displayFirstNameAndFirstLetterLastName } from \"@/entities/user/lib/display-name\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"@/components/ui/dropdown-menu\";\nimport { Button } from \"@/components/ui/button\";\n\nimport type { SessionUser } from \"@/entities/user/types/session-user\";\n\ninterface AdminHeaderProps {\n  user: SessionUser;\n}\n\nexport function AdminHeader({ user }: AdminHeaderProps) {\n  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n  const logout = useLogout(paths.root);\n\n  return (\n    <header className=\"border-b border-gray-200 bg-white px-6 py-4 dark:border-gray-700 dark:bg-gray-800\">\n      <div className=\"flex items-center justify-between\">\n        {/* Logo and title */}\n        <div className=\"flex items-center space-x-4\">\n          <Button className=\"md:hidden\" onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} size=\"small\" variant=\"ghost\">\n            {isMobileMenuOpen ? <X className=\"h-5 w-5\" /> : <Menu className=\"h-5 w-5\" />}\n          </Button>\n\n          <Link className=\"flex items-center space-x-3\" href=\"/admin\">\n            <Image alt=\"WorkoutCool Logo\" className=\"h-8 w-8\" height={32} src={Logo} width={32} />\n            <span className=\"hidden text-xl font-semibold text-gray-900 sm:block dark:text-white\">Administration</span>\n          </Link>\n        </div>\n\n        {/* User menu */}\n        <div className=\"flex items-center space-x-4\">\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button className=\"flex items-center space-x-2\" size=\"small\" variant=\"ghost\">\n                <div className=\"h-8 w-8 overflow-hidden rounded-full\">\n                  <Image\n                    alt=\"Profile\"\n                    className=\"h-full w-full object-cover\"\n                    height={32}\n                    src={user.image || PLACEHOLDERS.PROFILE_IMAGE}\n                    width={32}\n                  />\n                </div>\n                <span className=\"hidden text-sm font-medium text-gray-700 sm:block dark:text-gray-300\">\n                  {displayFirstNameAndFirstLetterLastName(user)}\n                </span>\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\" className=\"w-48\">\n              <DropdownMenuItem asChild>\n                <Link className=\"flex items-center space-x-2\" href={`/${paths.profile}`}>\n                  <UserCog className=\"h-4 w-4\" />\n                  <span>Profil</span>\n                </Link>\n              </DropdownMenuItem>\n              <DropdownMenuItem onClick={() => logout.mutate()}>\n                <div className=\"flex items-center space-x-2\">\n                  <LogOut className=\"h-4 w-4\" />\n                  <span>Déconnexion</span>\n                </div>\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </div>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/layout/admin-sidebar/ui/admin-header.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { LogOut, Menu, X, UserCog } from \"lucide-react\";\n\nimport Logo from \"@public/logo.png\";\nimport { PLACEHOLDERS } from \"@/shared/constants/placeholders\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { useLogout } from \"@/features/auth/model/useLogout\";\nimport { displayFirstNameAndFirstLetterLastName } from \"@/entities/user/lib/display-name\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"@/components/ui/dropdown-menu\";\nimport { Button } from \"@/components/ui/button\";\n\nimport type { SessionUser } from \"@/entities/user/types/session-user\";\n\ninterface AdminHeaderProps {\n  user: SessionUser;\n}\n\nexport function AdminHeader({ user }: AdminHeaderProps) {\n  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n  const logout = useLogout(paths.root);\n\n  return (\n    <header className=\"border-b border-gray-200 bg-white px-6 py-4 dark:border-gray-700 dark:bg-gray-800\">\n      <div className=\"flex items-center justify-between\">\n        {/* Logo and title */}\n        <div className=\"flex items-center space-x-4\">\n          <Button className=\"md:hidden\" onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} size=\"small\" variant=\"ghost\">\n            {isMobileMenuOpen ? <X className=\"h-5 w-5\" /> : <Menu className=\"h-5 w-5\" />}\n          </Button>\n\n          <Link className=\"flex items-center space-x-3\" href=\"/admin\">\n            <Image alt=\"WorkoutCool Logo\" className=\"h-8 w-8\" height={32} src={Logo} width={32} />\n            <span className=\"hidden text-xl font-semibold text-gray-900 sm:block dark:text-white\">Administration</span>\n          </Link>\n        </div>\n\n        {/* User menu */}\n        <div className=\"flex items-center space-x-4\">\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button className=\"flex items-center space-x-2\" size=\"small\" variant=\"ghost\">\n                <div className=\"h-8 w-8 overflow-hidden rounded-full\">\n                  <Image\n                    alt=\"Profile\"\n                    className=\"h-full w-full object-cover\"\n                    height={32}\n                    src={user.image || PLACEHOLDERS.PROFILE_IMAGE}\n                    width={32}\n                  />\n                </div>\n                <span className=\"hidden text-sm font-medium text-gray-700 sm:block dark:text-gray-300\">\n                  {displayFirstNameAndFirstLetterLastName(user)}\n                </span>\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\" className=\"w-48\">\n              <DropdownMenuItem asChild>\n                <Link className=\"flex items-center space-x-2\" href={`/${paths.profile}`}>\n                  <UserCog className=\"h-4 w-4\" />\n                  <span>Profil</span>\n                </Link>\n              </DropdownMenuItem>\n              <DropdownMenuItem onClick={() => logout.mutate()}>\n                <div className=\"flex items-center space-x-2\">\n                  <LogOut className=\"h-4 w-4\" />\n                  <span>Déconnexion</span>\n                </div>\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </div>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/layout/admin-sidebar/ui/admin-sidebar.tsx",
    "content": "\"use client\";\n\nimport { usePathname } from \"next/navigation\";\nimport Link from \"next/link\";\nimport { Users, LayoutDashboard, BarChart3, Settings } from \"lucide-react\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nimport version from \"../../../../../../package.json\";\n\nconst navigation = [\n  {\n    name: \"Dashboard\",\n    href: \"/admin/dashboard\",\n    icon: LayoutDashboard,\n    description: \"Overview of the statistics.\",\n  },\n  {\n    name: \"Users\",\n    href: \"/admin/users\",\n    icon: Users,\n    description: \"Manage users.\",\n  },\n  {\n    name: \"Programs\",\n    href: \"/admin/programs\",\n    icon: BarChart3,\n    description: \"Manage programs.\",\n  },\n  {\n    name: \"Settings\",\n    href: \"/admin/settings\",\n    icon: Settings,\n    description: \"Configuration of the system.\",\n  },\n];\n\nexport const AdminSidebar = () => {\n  const pathname = usePathname();\n\n  return (\n    <aside className=\"hidden w-64 flex-col border-r border-gray-200 bg-white md:flex dark:border-gray-700 dark:bg-gray-800\">\n      <div className=\"flex flex-1 flex-col pt-6\">\n        <nav className=\"flex-1 space-y-2 px-4\">\n          <div className=\"mb-6\">\n            <h2 className=\"px-3 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400\">Navigation</h2>\n          </div>\n\n          {navigation.map((item) => {\n            const isActive = pathname === item.href || pathname.startsWith(item.href + \"/\");\n            const Icon = item.icon;\n\n            return (\n              <Link\n                className={cn(\n                  \"group flex items-center rounded-lg px-3 py-3 text-sm font-medium transition-colors\",\n                  isActive\n                    ? \"bg-blue-50 text-blue-700 dark:bg-blue-900/50 dark:text-blue-400\"\n                    : \"text-gray-700 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-white\",\n                )}\n                href={item.href}\n                key={item.name}\n              >\n                <Icon\n                  className={cn(\n                    \"mr-3 h-5 w-5 flex-shrink-0\",\n                    isActive\n                      ? \"text-blue-600 dark:text-blue-400\"\n                      : \"text-gray-400 group-hover:text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-300\",\n                  )}\n                />\n                <div className=\"flex flex-col\">\n                  <span>{item.name}</span>\n                  <span className=\"text-xs text-gray-500 dark:text-gray-400\">{item.description}</span>\n                </div>\n              </Link>\n            );\n          })}\n        </nav>\n\n        {/* Footer */}\n        <div className=\"border-t border-gray-200 p-4 dark:border-gray-700\">\n          <div className=\"text-xs text-gray-500 dark:text-gray-400\">\n            <p>WorkoutCool Admin</p>\n            <p className=\"mt-1\">Version {version.version}</p>\n          </div>\n        </div>\n      </div>\n    </aside>\n  );\n};\n"
  },
  {
    "path": "src/features/admin/programs/actions/add-exercise.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\nimport { UserRole, WorkoutSetType, WorkoutSetUnit } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nimport { ExerciseWithAttributes } from \"../types/program.types\";\n\ninterface SuggestedSetData {\n  setIndex: number;\n  types: WorkoutSetType[];\n  valuesInt?: number[];\n  valuesSec?: number[];\n  units?: WorkoutSetUnit[];\n}\n\ninterface AddExerciseData {\n  sessionId: string;\n  exerciseId: string;\n  order: number;\n  instructions: string;\n  instructionsEn: string;\n  suggestedSets: SuggestedSetData[];\n}\n\nexport async function addExerciseToSession(data: AddExerciseData) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  // TODO: middleware or layout\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  // Check if exercise already exists in this session at this order\n  const existingExercise = await prisma.programSessionExercise.findUnique({\n    where: {\n      sessionId_order: {\n        sessionId: data.sessionId,\n        order: data.order,\n      },\n    },\n  });\n\n  if (existingExercise) {\n    throw new Error(`Un exercice existe déjà à la position ${data.order}`);\n  }\n\n  const programSessionExercise = await prisma.programSessionExercise.create({\n    data: {\n      sessionId: data.sessionId,\n      exerciseId: data.exerciseId,\n      order: data.order,\n      instructions: data.instructions,\n      instructionsEn: data.instructionsEn,\n      instructionsEs: data.instructionsEn, // Default fallback\n      instructionsPt: data.instructionsEn,\n      instructionsRu: data.instructionsEn,\n      instructionsZhCn: data.instructionsEn,\n      suggestedSets: {\n        create: data.suggestedSets.map((set) => ({\n          setIndex: set.setIndex,\n          types: set.types,\n          valuesInt: set.valuesInt || [],\n          valuesSec: set.valuesSec || [],\n          units: set.units || [],\n        })),\n      },\n    },\n    include: {\n      exercise: true,\n      suggestedSets: {\n        orderBy: { setIndex: \"asc\" },\n      },\n    },\n  });\n\n  revalidatePath(\"/admin/programs\");\n\n  return programSessionExercise;\n}\n\nexport async function getExercises(search?: string): Promise<ExerciseWithAttributes[]> {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  // TODO: middleware or layout\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  const where = search\n    ? {\n        OR: [{ name: { contains: search, mode: \"insensitive\" as const } }, { nameEn: { contains: search, mode: \"insensitive\" as const } }],\n      }\n    : {};\n\n  const exercises = await prisma.exercise.findMany({\n    where,\n    include: {\n      attributes: {\n        include: {\n          attributeName: true,\n          attributeValue: true,\n        },\n      },\n    },\n    orderBy: { name: \"asc\" },\n    take: 50,\n  });\n\n  return exercises;\n}\n"
  },
  {
    "path": "src/features/admin/programs/actions/add-session.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\nimport { ExerciseAttributeValueEnum, UserRole } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\ninterface AddSessionData {\n  weekId: string;\n  sessionNumber: number;\n  title: string;\n  titleEn: string;\n  titleEs: string;\n  titlePt: string;\n  titleRu: string;\n  titleZhCn: string;\n  slug: string;\n  slugEn: string;\n  slugEs: string;\n  slugPt: string;\n  slugRu: string;\n  slugZhCn: string;\n  description: string;\n  descriptionEn: string;\n  descriptionEs: string;\n  descriptionPt: string;\n  descriptionRu: string;\n  descriptionZhCn: string;\n  equipment: ExerciseAttributeValueEnum[];\n  estimatedMinutes: number;\n  isPremium: boolean;\n}\n\nexport async function addSessionToWeek(data: AddSessionData) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  // TODO: middleware or layout\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  // Check if session number already exists in this week\n  const existingSession = await prisma.programSession.findUnique({\n    where: {\n      weekId_sessionNumber: {\n        weekId: data.weekId,\n        sessionNumber: data.sessionNumber,\n      },\n    },\n  });\n\n  if (existingSession) {\n    throw new Error(`La séance ${data.sessionNumber} existe déjà dans cette semaine`);\n  }\n\n  const programSession = await prisma.programSession.create({\n    data: {\n      weekId: data.weekId,\n      sessionNumber: data.sessionNumber,\n      title: data.title,\n      titleEn: data.titleEn,\n      titleEs: data.titleEs,\n      titlePt: data.titlePt,\n      titleRu: data.titleRu,\n      titleZhCn: data.titleZhCn,\n      slug: data.slug,\n      slugEn: data.slugEn,\n      slugEs: data.slugEs,\n      slugPt: data.slugPt,\n      slugRu: data.slugRu,\n      slugZhCn: data.slugZhCn,\n      description: data.description,\n      descriptionEn: data.descriptionEn,\n      descriptionEs: data.descriptionEs,\n      descriptionPt: data.descriptionPt,\n      descriptionRu: data.descriptionRu,\n      descriptionZhCn: data.descriptionZhCn,\n      equipment: data.equipment,\n      estimatedMinutes: data.estimatedMinutes,\n      isPremium: data.isPremium,\n    },\n  });\n\n  revalidatePath(\"/admin/programs\");\n\n  return programSession;\n}\n"
  },
  {
    "path": "src/features/admin/programs/actions/add-week.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\nimport { UserRole } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\ninterface AddWeekData {\n  programId: string;\n  weekNumber: number;\n  title: string;\n  titleEn: string;\n  titleEs: string;\n  titlePt: string;\n  titleRu: string;\n  titleZhCn: string;\n  description?: string;\n  descriptionEn?: string;\n  descriptionEs?: string;\n  descriptionPt?: string;\n  descriptionRu?: string;\n  descriptionZhCn?: string;\n}\n\nexport async function addWeekToProgram(data: AddWeekData) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  // TODO: middleware or layout\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  // Check if week number already exists\n  const existingWeek = await prisma.programWeek.findUnique({\n    where: {\n      programId_weekNumber: {\n        programId: data.programId,\n        weekNumber: data.weekNumber,\n      },\n    },\n  });\n\n  if (existingWeek) {\n    throw new Error(`La semaine ${data.weekNumber} existe déjà`);\n  }\n\n  const week = await prisma.programWeek.create({\n    data: {\n      programId: data.programId,\n      weekNumber: data.weekNumber,\n      title: data.title,\n      titleEn: data.titleEn,\n      titleEs: data.titleEs,\n      titlePt: data.titlePt,\n      titleRu: data.titleRu,\n      titleZhCn: data.titleZhCn,\n      description: data.description || \"\",\n      descriptionEn: data.descriptionEn || \"\",\n      descriptionEs: data.descriptionEs || \"\",\n      descriptionPt: data.descriptionPt || \"\",\n      descriptionRu: data.descriptionRu || \"\",\n      descriptionZhCn: data.descriptionZhCn || \"\",\n    },\n  });\n\n  revalidatePath(\"/admin/programs\");\n\n  return week;\n}\n"
  },
  {
    "path": "src/features/admin/programs/actions/create-program.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\nimport { ProgramLevel, ExerciseAttributeValueEnum, UserRole, ProgramVisibility } from \"@prisma/client\";\n\nimport { generateSlug } from \"@/shared/lib/slug\";\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\ninterface CreateProgramData {\n  // Basic info\n  title: string;\n  titleEn: string;\n  titleEs: string;\n  titlePt: string;\n  titleRu: string;\n  titleZhCn: string;\n  description: string;\n  descriptionEn: string;\n  descriptionEs: string;\n  descriptionPt: string;\n  descriptionRu: string;\n  descriptionZhCn: string;\n  category: string;\n  image: string;\n  level: ProgramLevel;\n  type: ExerciseAttributeValueEnum;\n\n  // Program details\n  durationWeeks: number;\n  sessionsPerWeek: number;\n  sessionDurationMin: number;\n  equipment: ExerciseAttributeValueEnum[];\n  isPremium: boolean;\n  emoji?: string;\n\n  // Coaches\n  coaches?: Array<{\n    name: string;\n    image: string;\n    order: number;\n  }>;\n}\n\nexport async function createProgram(data: CreateProgramData) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  // TODO: middleware or layout\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  // Generate slugs for all languages\n  const slug = generateSlug(data.title);\n  const slugEn = generateSlug(data.titleEn);\n  const slugEs = generateSlug(data.titleEs);\n  const slugPt = generateSlug(data.titlePt);\n  const slugRu = generateSlug(data.titleRu);\n  const slugZhCn = generateSlug(data.titleZhCn);\n\n  // Check if any slug already exists\n  const existingProgram = await prisma.program.findFirst({\n    where: {\n      OR: [{ slug }, { slugEn }, { slugEs }, { slugPt }, { slugRu }, { slugZhCn }],\n    },\n  });\n\n  if (existingProgram) {\n    throw new Error(\"Un programme avec ce nom existe déjà dans une des langues\");\n  }\n\n  const program = await prisma.program.create({\n    data: {\n      slug,\n      slugEn,\n      slugEs,\n      slugPt,\n      slugRu,\n      slugZhCn,\n      title: data.title,\n      titleEn: data.titleEn,\n      titleEs: data.titleEs,\n      titlePt: data.titlePt,\n      titleRu: data.titleRu,\n      titleZhCn: data.titleZhCn,\n      description: data.description,\n      descriptionEn: data.descriptionEn,\n      descriptionEs: data.descriptionEs,\n      descriptionPt: data.descriptionPt,\n      descriptionRu: data.descriptionRu,\n      descriptionZhCn: data.descriptionZhCn,\n      category: data.category,\n      image: data.image,\n      level: data.level,\n      type: data.type,\n      durationWeeks: data.durationWeeks,\n      sessionsPerWeek: data.sessionsPerWeek,\n      sessionDurationMin: data.sessionDurationMin,\n      equipment: data.equipment,\n      isPremium: data.isPremium,\n      visibility: ProgramVisibility.DRAFT, // Always start as draft\n      coaches: {\n        create: data.coaches || [],\n      },\n    },\n    include: {\n      coaches: true,\n    },\n  });\n\n  revalidatePath(\"/admin/programs\");\n\n  return program;\n}\n"
  },
  {
    "path": "src/features/admin/programs/actions/delete-program.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\nimport { UserRole } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nexport async function deleteProgram(programId: string) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  // Check if program has enrollments\n  const program = await prisma.program.findUnique({\n    where: { id: programId },\n    include: {\n      enrollments: {\n        take: 1,\n      },\n    },\n  });\n\n  if (!program) {\n    throw new Error(\"Program not found\");\n  }\n\n  if (program.enrollments.length > 0) {\n    throw new Error(\"Cannot delete program with active enrollments\");\n  }\n\n  // Delete program (cascade will handle weeks, sessions, exercises, etc.)\n  await prisma.program.delete({\n    where: { id: programId },\n  });\n\n  revalidatePath(\"/admin/programs\");\n  \n  return { success: true };\n}"
  },
  {
    "path": "src/features/admin/programs/actions/get-programs.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { UserRole, ProgramVisibility } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nimport { ProgramWithStats, ProgramWithFullDetails } from \"../types/program.types\";\n\nexport async function getPrograms(visibility?: ProgramVisibility): Promise<ProgramWithStats[]> {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  // TODO: middleware or layout\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  const programs = await prisma.program.findMany({\n    where: visibility ? { visibility } : undefined,\n    include: {\n      coaches: {\n        orderBy: { order: \"asc\" },\n      },\n      weeks: {\n        include: {\n          sessions: {\n            include: {\n              exercises: {\n                include: {\n                  exercise: true,\n                  suggestedSets: true,\n                },\n                orderBy: { order: \"asc\" },\n              },\n            },\n            orderBy: { sessionNumber: \"asc\" },\n          },\n        },\n        orderBy: { weekNumber: \"asc\" },\n      },\n      enrollments: {\n        select: {\n          id: true,\n        },\n      },\n    },\n    orderBy: { createdAt: \"desc\" },\n  });\n\n  return programs.map((program) => ({\n    ...program,\n    totalEnrollments: program.enrollments.length,\n    totalWeeks: program.weeks.length,\n    totalSessions: program.weeks.reduce((acc, week) => acc + week.sessions.length, 0),\n    totalExercises: program.weeks.reduce(\n      (acc, week) => acc + week.sessions.reduce((sessAcc, session) => sessAcc + session.exercises.length, 0),\n      0,\n    ),\n  }));\n}\n\nexport async function getProgramById(id: string): Promise<ProgramWithFullDetails | null> {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  // TODO: middleware or layout\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  const program = await prisma.program.findUnique({\n    where: { id },\n    include: {\n      coaches: {\n        orderBy: { order: \"asc\" },\n      },\n      weeks: {\n        include: {\n          sessions: {\n            include: {\n              exercises: {\n                include: {\n                  exercise: {\n                    include: {\n                      attributes: {\n                        include: {\n                          attributeName: true,\n                          attributeValue: true,\n                        },\n                      },\n                    },\n                  },\n                  suggestedSets: {\n                    orderBy: { setIndex: \"asc\" },\n                  },\n                },\n                orderBy: { order: \"asc\" },\n              },\n            },\n            orderBy: { sessionNumber: \"asc\" },\n          },\n        },\n        orderBy: { weekNumber: \"asc\" },\n      },\n    },\n  });\n\n  return program;\n}\n"
  },
  {
    "path": "src/features/admin/programs/actions/update-exercise-sets.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\nimport { UserRole, WorkoutSetType, WorkoutSetUnit } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\ninterface SetData {\n  id?: string;\n  setIndex: number;\n  types: WorkoutSetType[];\n  valuesInt: number[];\n  valuesSec: number[];\n  units: WorkoutSetUnit[];\n}\n\nexport async function updateExerciseSets(exerciseId: string, sets: SetData[]) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  try {\n    // Utiliser une transaction pour s'assurer de la cohérence\n    await prisma.$transaction(async (tx) => {\n      // Supprimer tous les sets existants de cet exercice\n      await tx.programSuggestedSet.deleteMany({\n        where: {\n          programSessionExerciseId: exerciseId,\n        },\n      });\n\n      // Créer les nouveaux sets\n      const setsToCreate = sets.map((set) => ({\n        programSessionExerciseId: exerciseId,\n        setIndex: set.setIndex,\n        types: set.types,\n        valuesInt: set.valuesInt,\n        valuesSec: set.valuesSec,\n        units: set.units,\n      }));\n\n      if (setsToCreate.length > 0) {\n        await tx.programSuggestedSet.createMany({\n          data: setsToCreate,\n        });\n      }\n    });\n\n    // Revalider les caches\n    revalidatePath(\"/admin/programs\");\n    \n    return { success: true };\n  } catch (error) {\n    console.error(\"Error updating exercise sets:\", error);\n    throw new Error(\"Erreur lors de la mise à jour des séries\");\n  }\n}"
  },
  {
    "path": "src/features/admin/programs/actions/update-program-visibility.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\nimport { UserRole, ProgramVisibility } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nexport async function updateProgramVisibility(programId: string, visibility: ProgramVisibility) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  // Check if program exists\n  const program = await prisma.program.findUnique({\n    where: { id: programId },\n    select: { \n      id: true, \n      title: true,\n      visibility: true,\n      enrollments: {\n        select: { id: true },\n        take: 1,\n      }\n    },\n  });\n\n  if (!program) {\n    throw new Error(\"Program not found\");\n  }\n\n  // Prevent archiving a program with active enrollments\n  if (visibility === ProgramVisibility.ARCHIVED && program.enrollments.length > 0) {\n    throw new Error(\"Cannot archive a program with active enrollments\");\n  }\n\n  // Update program visibility\n  const updatedProgram = await prisma.program.update({\n    where: { id: programId },\n    data: { visibility },\n  });\n\n  revalidatePath(\"/admin/programs\");\n  revalidatePath(`/admin/programs/${programId}`);\n  \n  return updatedProgram;\n}"
  },
  {
    "path": "src/features/admin/programs/actions/update-program.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\nimport { UserRole, ProgramLevel, ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { generateSlug } from \"@/shared/lib/slug\";\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\ninterface UpdateProgramData {\n  title: string;\n  titleEn: string;\n  titleEs: string;\n  titlePt: string;\n  titleRu: string;\n  titleZhCn: string;\n  description: string;\n  descriptionEn: string;\n  descriptionEs: string;\n  descriptionPt: string;\n  descriptionRu: string;\n  descriptionZhCn: string;\n  category: string;\n  image: string;\n  level: ProgramLevel;\n  type: ExerciseAttributeValueEnum;\n  durationWeeks: number;\n  sessionsPerWeek: number;\n  sessionDurationMin: number;\n  equipment: ExerciseAttributeValueEnum[];\n  isPremium: boolean;\n  emoji?: string;\n  coaches: Array<{\n    id: string;\n    name: string;\n    image: string;\n    order: number;\n  }>;\n}\n\nexport async function updateProgram(programId: string, data: UpdateProgramData) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  try {\n    // Generate new slugs from updated titles\n    const slug = generateSlug(data.title);\n    const slugEn = generateSlug(data.titleEn);\n    const slugEs = generateSlug(data.titleEs);\n    const slugPt = generateSlug(data.titlePt);\n    const slugRu = generateSlug(data.titleRu);\n    const slugZhCn = generateSlug(data.titleZhCn);\n\n    // Check if any slug already exists (excluding current program)\n    const existingProgram = await prisma.program.findFirst({\n      where: {\n        AND: [\n          { id: { not: programId } },\n          {\n            OR: [{ slug }, { slugEn }, { slugEs }, { slugPt }, { slugRu }, { slugZhCn }],\n          },\n        ],\n      },\n    });\n\n    if (existingProgram) {\n      throw new Error(\"Un programme avec ce nom existe déjà dans une des langues\");\n    }\n\n    const updatedProgram = await prisma.$transaction(async (tx) => {\n      // Update the program\n      const program = await tx.program.update({\n        where: {\n          id: programId,\n        },\n        data: {\n          slug,\n          slugEn,\n          slugEs,\n          slugPt,\n          slugRu,\n          slugZhCn,\n          title: data.title,\n          titleEn: data.titleEn,\n          titleEs: data.titleEs,\n          titlePt: data.titlePt,\n          titleRu: data.titleRu,\n          titleZhCn: data.titleZhCn,\n          description: data.description,\n          descriptionEn: data.descriptionEn,\n          descriptionEs: data.descriptionEs,\n          descriptionPt: data.descriptionPt,\n          descriptionRu: data.descriptionRu,\n          descriptionZhCn: data.descriptionZhCn,\n          category: data.category,\n          image: data.image,\n          level: data.level,\n          type: data.type,\n          durationWeeks: data.durationWeeks,\n          sessionsPerWeek: data.sessionsPerWeek,\n          sessionDurationMin: data.sessionDurationMin,\n          equipment: data.equipment,\n          isPremium: data.isPremium,\n        },\n      });\n\n      // Delete existing coaches\n      await tx.programCoach.deleteMany({\n        where: {\n          programId,\n        },\n      });\n\n      // Create new coaches\n      if (data.coaches.length > 0) {\n        const coachesToCreate = data.coaches.map((coach, index) => ({\n          programId,\n          name: coach.name,\n          image: coach.image,\n          order: index,\n        }));\n\n        await tx.programCoach.createMany({\n          data: coachesToCreate,\n        });\n      }\n\n      return program;\n    });\n\n    // Revalider les caches\n    revalidatePath(\"/admin/programs\");\n    revalidatePath(`/admin/programs/${programId}/edit`);\n\n    return updatedProgram;\n  } catch (error) {\n    console.error(\"Error updating program:\", error);\n    throw new Error(\"Erreur lors de la mise à jour du programme\");\n  }\n}\n"
  },
  {
    "path": "src/features/admin/programs/actions/update-session.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\nimport { ExerciseAttributeValueEnum, UserRole } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\ninterface UpdateSessionData {\n  sessionId: string;\n  title: string;\n  titleEn: string;\n  titleEs: string;\n  titlePt: string;\n  titleRu: string;\n  titleZhCn: string;\n  slug: string;\n  slugEn: string;\n  slugEs: string;\n  slugPt: string;\n  slugRu: string;\n  slugZhCn: string;\n  description: string;\n  descriptionEn: string;\n  descriptionEs: string;\n  descriptionPt: string;\n  descriptionRu: string;\n  descriptionZhCn: string;\n  equipment: ExerciseAttributeValueEnum[];\n  estimatedMinutes: number;\n  isPremium: boolean;\n}\n\nexport async function updateSession(data: UpdateSessionData) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  // TODO: middleware or layout\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"UNAUTHORIZED\");\n  }\n\n  // Get current session to access weekId for slug uniqueness check\n  const currentSession = await prisma.programSession.findUnique({\n    where: { id: data.sessionId },\n    select: { weekId: true },\n  });\n\n  if (!currentSession) {\n    throw new Error(\"SESSION_NOT_FOUND\");\n  }\n\n  // Helper function to ensure slug uniqueness\n  async function ensureUniqueSessionSlug(baseSlug: string, field: string): Promise<string> {\n    let slug = baseSlug;\n    let counter = 1;\n\n    while (true) {\n      const existing = await prisma.programSession.findFirst({\n        where: {\n          weekId: currentSession?.weekId,\n          id: { not: data.sessionId },\n          [field]: slug,\n        },\n      });\n\n      if (!existing) {\n        return slug;\n      }\n\n      counter++;\n      slug = `${baseSlug}-${counter}`;\n    }\n  }\n\n  // Ensure all slugs are unique\n  const uniqueSlugs = {\n    slug: await ensureUniqueSessionSlug(data.slug, \"slug\"),\n    slugEn: await ensureUniqueSessionSlug(data.slugEn, \"slugEn\"),\n    slugEs: await ensureUniqueSessionSlug(data.slugEs, \"slugEs\"),\n    slugPt: await ensureUniqueSessionSlug(data.slugPt, \"slugPt\"),\n    slugRu: await ensureUniqueSessionSlug(data.slugRu, \"slugRu\"),\n    slugZhCn: await ensureUniqueSessionSlug(data.slugZhCn, \"slugZhCn\"),\n  };\n\n  const updatedSession = await prisma.programSession.update({\n    where: { id: data.sessionId },\n    data: {\n      title: data.title,\n      titleEn: data.titleEn,\n      titleEs: data.titleEs,\n      titlePt: data.titlePt,\n      titleRu: data.titleRu,\n      titleZhCn: data.titleZhCn,\n      slug: uniqueSlugs.slug,\n      slugEn: uniqueSlugs.slugEn,\n      slugEs: uniqueSlugs.slugEs,\n      slugPt: uniqueSlugs.slugPt,\n      slugRu: uniqueSlugs.slugRu,\n      slugZhCn: uniqueSlugs.slugZhCn,\n      description: data.description,\n      descriptionEn: data.descriptionEn,\n      descriptionEs: data.descriptionEs,\n      descriptionPt: data.descriptionPt,\n      descriptionRu: data.descriptionRu,\n      descriptionZhCn: data.descriptionZhCn,\n      equipment: data.equipment,\n      estimatedMinutes: data.estimatedMinutes,\n      isPremium: data.isPremium,\n    },\n  });\n\n  revalidatePath(\"/admin/programs\");\n\n  return updatedSession;\n}\n"
  },
  {
    "path": "src/features/admin/programs/actions/update-week.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\nimport { UserRole } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\ninterface UpdateWeekData {\n  title: string;\n  titleEn: string;\n  titleEs: string;\n  titlePt: string;\n  titleRu: string;\n  titleZhCn: string;\n  description?: string;\n  descriptionEn?: string;\n  descriptionEs?: string;\n  descriptionPt?: string;\n  descriptionRu?: string;\n  descriptionZhCn?: string;\n}\n\nexport async function updateWeek(weekId: string, data: UpdateWeekData) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session || session.user?.role !== UserRole.admin) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  try {\n    const updatedWeek = await prisma.programWeek.update({\n      where: {\n        id: weekId,\n      },\n      data: {\n        title: data.title,\n        titleEn: data.titleEn,\n        titleEs: data.titleEs,\n        titlePt: data.titlePt,\n        titleRu: data.titleRu,\n        titleZhCn: data.titleZhCn,\n        description: data.description || \"\",\n        descriptionEn: data.descriptionEn || \"\",\n        descriptionEs: data.descriptionEs || \"\",\n        descriptionPt: data.descriptionPt || \"\",\n        descriptionRu: data.descriptionRu || \"\",\n        descriptionZhCn: data.descriptionZhCn || \"\",\n      },\n    });\n\n    // Revalider les caches\n    revalidatePath(\"/admin/programs\");\n\n    return updatedWeek;\n  } catch (error) {\n    console.error(\"Error updating week:\", error);\n    throw new Error(\"Erreur lors de la mise à jour de la semaine\");\n  }\n}\n"
  },
  {
    "path": "src/features/admin/programs/types/program.types.ts",
    "content": "import { \n  Program, \n  ProgramWeek, \n  ProgramSession, \n  ProgramSessionExercise, \n  ProgramSuggestedSet,\n  ProgramCoach,\n  Exercise,\n  ExerciseAttribute,\n  ExerciseAttributeName,\n  ExerciseAttributeValue,\n  UserProgramEnrollment\n} from \"@prisma/client\";\n\n// Type pour getProgramById avec toutes les associations\nexport type ProgramWithFullDetails = Program & {\n  coaches: ProgramCoach[];\n  weeks: (ProgramWeek & {\n    sessions: (ProgramSession & {\n      exercises: (ProgramSessionExercise & {\n        exercise: Exercise & {\n          attributes: (ExerciseAttribute & {\n            attributeName: ExerciseAttributeName;\n            attributeValue: ExerciseAttributeValue;\n          })[];\n        };\n        suggestedSets: ProgramSuggestedSet[];\n      })[];\n    })[];\n  })[];\n};\n\n// Type pour getPrograms avec les propriétés calculées\nexport type ProgramWithStats = Program & {\n  coaches: ProgramCoach[];\n  weeks: (ProgramWeek & {\n    sessions: (ProgramSession & {\n      exercises: (ProgramSessionExercise & {\n        exercise: Exercise;\n        suggestedSets: ProgramSuggestedSet[];\n      })[];\n    })[];\n  })[];\n  enrollments: Pick<UserProgramEnrollment, \"id\">[];\n  // Propriétés calculées\n  totalEnrollments: number;\n  totalWeeks: number;\n  totalSessions: number;\n  totalExercises: number;\n};\n\n// Type pour une semaine avec ses sessions\nexport type WeekWithSessions = ProgramWeek & {\n  sessions: (ProgramSession & {\n    exercises: (ProgramSessionExercise & {\n      exercise: Exercise;\n      suggestedSets: ProgramSuggestedSet[];\n    })[];\n  })[];\n};\n\n// Type pour une session avec ses exercices\nexport type SessionWithExercises = ProgramSession & {\n  exercises: (ProgramSessionExercise & {\n    exercise: Exercise;\n    suggestedSets: ProgramSuggestedSet[];\n  })[];\n};\n\n// Type pour un exercice avec ses attributs complets (pour le modal)\nexport type ExerciseWithAttributes = Exercise & {\n  attributes: (ExerciseAttribute & {\n    attributeName: ExerciseAttributeName;\n    attributeValue: ExerciseAttributeValue;\n  })[];\n};"
  },
  {
    "path": "src/features/admin/programs/ui/add-exercise-modal.tsx",
    "content": "\"use client\";\n\nimport { z } from \"zod\";\nimport { useForm } from \"react-hook-form\";\nimport { useState, useEffect } from \"react\";\nimport { Plus, Trash2, Search } from \"lucide-react\";\nimport { WorkoutSetType, WorkoutSetUnit } from \"@prisma/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\nimport { CreateSuggestedSetData, SUGGESTED_SET_TEMPLATES } from \"@/features/programs/lib/suggested-sets-helpers\";\n\nimport { ExerciseWithAttributes } from \"../types/program.types\";\nimport { addExerciseToSession, getExercises } from \"../actions/add-exercise.action\";\n\nconst exerciseSchema = z.object({\n  exerciseId: z.string().min(1, \"Veuillez sélectionner un exercice\"),\n  instructions: z.string().min(1, \"Les instructions sont requises\"),\n  instructionsEn: z.string().min(1, \"Les instructions en anglais sont requises\"),\n  suggestedSets: z.array(\n    z.object({\n      setIndex: z.number(),\n      types: z.array(z.nativeEnum(WorkoutSetType)),\n      valuesInt: z.array(z.number()).optional(),\n      valuesSec: z.array(z.number()).optional(),\n      units: z.array(z.nativeEnum(WorkoutSetUnit)).optional(),\n    }),\n  ),\n});\n\ntype ExerciseFormData = z.infer<typeof exerciseSchema>;\n\ninterface AddExerciseModalProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  sessionId: string;\n  nextOrder: number;\n}\n\nexport function AddExerciseModal({ open, onOpenChange, sessionId, nextOrder }: AddExerciseModalProps) {\n  const [isLoading, setIsLoading] = useState(false);\n  const [exercises, setExercises] = useState<ExerciseWithAttributes[]>([]);\n  const [searchTerm, setSearchTerm] = useState(\"\");\n  const [selectedExercise, setSelectedExercise] = useState<ExerciseWithAttributes | null>(null);\n  const [suggestedSets, setSuggestedSets] = useState<any[]>([]);\n\n  const {\n    register,\n    handleSubmit,\n    reset,\n    setValue,\n    formState: { errors },\n  } = useForm<ExerciseFormData>({\n    resolver: zodResolver(exerciseSchema),\n    defaultValues: {\n      suggestedSets: [],\n    },\n  });\n\n  // Load exercises\n  useEffect(() => {\n    if (open) {\n      loadExercises();\n    }\n  }, [open, searchTerm]);\n\n  const loadExercises = async () => {\n    try {\n      const data = await getExercises(searchTerm);\n      setExercises(data);\n    } catch (error) {\n      console.error(\"Error loading exercises:\", error);\n    }\n  };\n\n  const selectExercise = (exercise: ExerciseWithAttributes) => {\n    setSelectedExercise(exercise);\n    setValue(\"exerciseId\", exercise.id);\n\n    // Set default suggested sets based on exercise type\n    const defaultSets = SUGGESTED_SET_TEMPLATES.strengthTraining();\n    setSuggestedSets(defaultSets);\n    setValue(\"suggestedSets\", defaultSets);\n  };\n\n  const addSet = () => {\n    const newSet = {\n      setIndex: suggestedSets.length,\n      types: [WorkoutSetType.WEIGHT, WorkoutSetType.REPS],\n      valuesInt: [20, 10],\n      units: [WorkoutSetUnit.kg],\n    };\n    const newSets = [...suggestedSets, newSet];\n    setSuggestedSets(newSets);\n    setValue(\"suggestedSets\", newSets);\n  };\n\n  const removeSet = (index: number) => {\n    const newSets = suggestedSets.filter((_, i) => i !== index);\n    // Reindex sets\n    const reindexedSets = newSets.map((set, i) => ({ ...set, setIndex: i }));\n    setSuggestedSets(reindexedSets);\n    setValue(\"suggestedSets\", reindexedSets);\n  };\n\n  const updateSet = (index: number, field: string, value: any) => {\n    const newSets = [...suggestedSets];\n    newSets[index] = { ...newSets[index], [field]: value };\n    setSuggestedSets(newSets);\n    setValue(\"suggestedSets\", newSets);\n  };\n\n  const getTemplate = (template: string) => {\n    let sets: CreateSuggestedSetData[] = [];\n    switch (template) {\n      case \"strength\":\n        sets = SUGGESTED_SET_TEMPLATES.strengthTraining();\n        break;\n      case \"bodyweight\":\n        sets = SUGGESTED_SET_TEMPLATES.bodyweight();\n        break;\n      case \"timed\":\n        sets = SUGGESTED_SET_TEMPLATES.timed();\n        break;\n    }\n    setSuggestedSets(sets);\n    setValue(\"suggestedSets\", sets);\n  };\n\n  const onSubmit = async (data: ExerciseFormData) => {\n    setIsLoading(true);\n    try {\n      await addExerciseToSession({\n        sessionId,\n        exerciseId: data.exerciseId,\n        order: nextOrder,\n        instructions: data.instructions,\n        instructionsEn: data.instructionsEn,\n        suggestedSets: data.suggestedSets,\n      });\n\n      handleClose();\n      window.location.reload(); // Refresh to show new exercise\n    } catch (error) {\n      console.error(\"Error adding exercise:\", error);\n      alert(\"Erreur lors de l'ajout de l'exercice\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleClose = () => {\n    reset();\n    setSelectedExercise(null);\n    setSuggestedSets([]);\n    setSearchTerm(\"\");\n    onOpenChange(false);\n  };\n\n  const strengthTemplate = () => getTemplate(\"strength\");\n  const bodyweightTemplate = () => getTemplate(\"bodyweight\");\n  const timedTemplate = () => getTemplate(\"timed\");\n\n  return (\n    <>\n      {open && (\n        <div className=\"modal modal-open\">\n          <div className=\"modal-box w-11/12 max-w-4xl h-full max-h-[90vh] flex flex-col\">\n            <div className=\"flex justify-between items-center mb-4\">\n              <h3 className=\"font-bold text-lg\">Ajouter un exercice</h3>\n              <button className=\"btn btn-sm btn-circle btn-ghost\" onClick={handleClose}>\n                ✕\n              </button>\n            </div>\n\n            <div className=\"flex-1 overflow-y-auto space-y-6 h-full\">\n              {/* Exercise Selection */}\n              {!selectedExercise && (\n                <div className=\"card bg-base-100 h-full\">\n                  <div className=\"card-body\">\n                    <h2 className=\"card-title\">Sélectionner un exercice</h2>\n                    <div className=\"space-y-4 h-full\">\n                      <div className=\"relative\">\n                        <Search className=\"absolute left-3 top-3 h-4 w-4 text-base-content/60\" />\n                        <input\n                          className=\"input input-bordered w-full pl-10\"\n                          onChange={(e) => setSearchTerm(e.target.value)}\n                          placeholder=\"Rechercher un exercice...\"\n                          value={searchTerm}\n                        />\n                      </div>\n\n                      <div className=\"grid gap-2  overflow-y-auto\">\n                        {exercises.map((exercise) => (\n                          <div\n                            className=\"flex items-center justify-between p-3 border border-base-300 rounded-lg cursor-pointer hover:bg-base-200\"\n                            key={exercise.id}\n                            onClick={() => selectExercise(exercise)}\n                          >\n                            <div>\n                              <h4 className=\"font-medium\">{exercise.name}</h4>\n                              <p className=\"text-sm text-base-content/60\">{exercise.nameEn}</p>\n                            </div>\n                            <button className=\"btn btn-sm btn-primary\">Sélectionner</button>\n                          </div>\n                        ))}\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              )}\n\n              {/* Selected Exercise & Configuration */}\n              {selectedExercise && (\n                <form className=\"space-y-6\" onSubmit={handleSubmit(onSubmit)}>\n                  {/* Exercise Info */}\n                  <div className=\"card bg-base-100 shadow-xl h-full\">\n                    <div className=\"card-body\">\n                      <div className=\"flex items-center justify-between\">\n                        <div>\n                          <h2 className=\"card-title\">{selectedExercise.name}</h2>\n                          <p className=\"text-sm text-base-content/60\">{selectedExercise.nameEn}</p>\n                        </div>\n                        <button className=\"btn btn-outline\" onClick={() => setSelectedExercise(null)} type=\"button\">\n                          Changer\n                        </button>\n                      </div>\n                    </div>\n                  </div>\n\n                  {/* Instructions */}\n                  <div className=\"card bg-base-100 shadow-xl\">\n                    <div className=\"card-body h-full\">\n                      <h2 className=\"card-title\">Instructions</h2>\n                      <div className=\"space-y-4\">\n                        <div className=\"form-control\">\n                          <label className=\"label\" htmlFor=\"instructions\">\n                            <span className=\"label-text\">Instructions (FR)</span>\n                          </label>\n                          <textarea\n                            className=\"textarea textarea-bordered\"\n                            id=\"instructions\"\n                            {...register(\"instructions\")}\n                            placeholder=\"Instructions spécifiques pour cet exercice dans ce programme...\"\n                            rows={3}\n                          />\n                          {errors.instructions && <div className=\"text-sm text-error mt-1\">{errors.instructions.message}</div>}\n                        </div>\n                        <div className=\"form-control\">\n                          <label className=\"label\" htmlFor=\"instructionsEn\">\n                            <span className=\"label-text\">Instructions (EN)</span>\n                          </label>\n                          <textarea\n                            className=\"textarea textarea-bordered\"\n                            id=\"instructionsEn\"\n                            {...register(\"instructionsEn\")}\n                            placeholder=\"Specific instructions for this exercise in this program...\"\n                            rows={3}\n                          />\n                          {errors.instructionsEn && <div className=\"text-sm text-error mt-1\">{errors.instructionsEn.message}</div>}\n                        </div>\n                      </div>\n                    </div>\n                  </div>\n\n                  {/* Suggested Sets */}\n                  <div className=\"card bg-base-100 shadow-xl\">\n                    <div className=\"card-body\">\n                      <div className=\"flex items-center justify-between mb-4\">\n                        <h2 className=\"card-title\">Séries suggérées</h2>\n                        <div className=\"flex gap-2\">\n                          <button className=\"btn btn-sm btn-outline\" onClick={strengthTemplate} type=\"button\">\n                            Musculation\n                          </button>\n                          <button className=\"btn btn-sm btn-outline\" onClick={bodyweightTemplate} type=\"button\">\n                            Poids du corps\n                          </button>\n                          <button className=\"btn btn-sm btn-outline\" onClick={timedTemplate} type=\"button\">\n                            Chronométré\n                          </button>\n                          <button className=\"btn btn-sm btn-primary\" onClick={addSet} type=\"button\">\n                            <Plus className=\"h-4 w-4 mr-1\" />\n                            Ajouter\n                          </button>\n                        </div>\n                      </div>\n                      <div className=\"space-y-3\">\n                        {suggestedSets.map((set, index) => (\n                          <div className=\"flex items-center gap-3 p-3 border border-base-300 rounded-lg\" key={index}>\n                            <div className=\"w-8 h-8 bg-primary text-primary-content rounded-full flex items-center justify-center text-sm font-semibold\">\n                              {index + 1}\n                            </div>\n                            <div className=\"flex-1 grid grid-cols-5 gap-2\">\n                              <div className=\"form-control\">\n                                <label className=\"label\">\n                                  <span className=\"label-text text-xs\">Type</span>\n                                </label>\n                                <select\n                                  className=\"select select-bordered select-sm\"\n                                  onChange={(e) => {\n                                    const type = e.target.value;\n                                    if (type === WorkoutSetType.WEIGHT) {\n                                      updateSet(index, \"types\", [WorkoutSetType.WEIGHT, WorkoutSetType.REPS]);\n                                    } else {\n                                      updateSet(index, \"types\", [type]);\n                                    }\n                                  }}\n                                  value={set.types?.[0] || \"\"}\n                                >\n                                  <option value=\"\">Sélectionner</option>\n                                  <option value={WorkoutSetType.WEIGHT}>Poids + Reps</option>\n                                  <option value={WorkoutSetType.REPS}>Répétitions seules</option>\n                                  <option value={WorkoutSetType.TIME}>Temps</option>\n                                  <option value={WorkoutSetType.BODYWEIGHT}>Poids du corps</option>\n                                </select>\n                              </div>\n                              \n                              {/* Poids field - only show if WEIGHT type is selected */}\n                              {set.types?.includes(WorkoutSetType.WEIGHT) && (\n                                <div className=\"form-control\">\n                                  <label className=\"label\">\n                                    <span className=\"label-text text-xs\">Poids</span>\n                                  </label>\n                                  <input\n                                    className=\"input input-bordered input-sm\"\n                                    onChange={(e) => {\n                                      const weightValue = parseInt(e.target.value) || 0;\n                                      const repsValue = set.valuesInt?.[1] || 10;\n                                      updateSet(index, \"valuesInt\", [weightValue, repsValue]);\n                                    }}\n                                    placeholder=\"kg\"\n                                    type=\"number\"\n                                    value={set.valuesInt?.[0] || \"\"}\n                                  />\n                                </div>\n                              )}\n                              \n                              {/* Reps field - show for WEIGHT and REPS types */}\n                              {(set.types?.includes(WorkoutSetType.REPS) || set.types?.includes(WorkoutSetType.WEIGHT)) && (\n                                <div className=\"form-control\">\n                                  <label className=\"label\">\n                                    <span className=\"label-text text-xs\">Répétitions</span>\n                                  </label>\n                                  <input\n                                    className=\"input input-bordered input-sm\"\n                                    onChange={(e) => {\n                                      const repsValue = parseInt(e.target.value) || 0;\n                                      if (set.types?.includes(WorkoutSetType.WEIGHT)) {\n                                        const weightValue = set.valuesInt?.[0] || 20;\n                                        updateSet(index, \"valuesInt\", [weightValue, repsValue]);\n                                      } else {\n                                        updateSet(index, \"valuesInt\", [repsValue]);\n                                      }\n                                    }}\n                                    placeholder=\"reps\"\n                                    type=\"number\"\n                                    value={set.types?.includes(WorkoutSetType.WEIGHT) ? set.valuesInt?.[1] || \"\" : set.valuesInt?.[0] || \"\"}\n                                  />\n                                </div>\n                              )}\n                              \n                              {/* Bodyweight field - only show if BODYWEIGHT type is selected */}\n                              {set.types?.includes(WorkoutSetType.BODYWEIGHT) && (\n                                <div className=\"form-control\">\n                                  <label className=\"label\">\n                                    <span className=\"label-text text-xs\">Poids du corps</span>\n                                  </label>\n                                  <input\n                                    className=\"input input-bordered input-sm\"\n                                    placeholder=\"✔\"\n                                    readOnly\n                                    value=\"✔\"\n                                  />\n                                </div>\n                              )}\n                              \n                              {/* Time field - only show if TIME type is selected */}\n                              {set.types?.includes(WorkoutSetType.TIME) && (\n                                <div className=\"form-control\">\n                                  <label className=\"label\">\n                                    <span className=\"label-text text-xs\">Temps (sec)</span>\n                                  </label>\n                                  <input\n                                    className=\"input input-bordered input-sm\"\n                                    onChange={(e) => updateSet(index, \"valuesSec\", [parseInt(e.target.value) || 0])}\n                                    placeholder=\"secondes\"\n                                    type=\"number\"\n                                    value={set.valuesSec?.[0] || \"\"}\n                                  />\n                                </div>\n                              )}\n                              \n                              {/* Unit field - only show if WEIGHT type is selected */}\n                              {set.types?.includes(WorkoutSetType.WEIGHT) && (\n                                <div className=\"form-control\">\n                                  <label className=\"label\">\n                                    <span className=\"label-text text-xs\">Unité</span>\n                                  </label>\n                                  <select\n                                    className=\"select select-bordered select-sm\"\n                                    onChange={(e) => updateSet(index, \"units\", [e.target.value])}\n                                    value={set.units?.[0] || \"\"}\n                                  >\n                                    <option value=\"\">Sélectionner</option>\n                                    <option value={WorkoutSetUnit.kg}>kg</option>\n                                    <option value={WorkoutSetUnit.lbs}>lbs</option>\n                                  </select>\n                                </div>\n                              )}\n                            </div>\n                            <button className=\"btn btn-sm btn-outline\" onClick={() => removeSet(index)} type=\"button\">\n                              <Trash2 className=\"h-4 w-4\" />\n                            </button>\n                          </div>\n                        ))}\n\n                        {suggestedSets.length === 0 && (\n                          <div className=\"text-center py-8 border-2 border-dashed border-base-300 rounded-lg\">\n                            <p className=\"text-base-content/60 mb-3\">Aucune série configurée</p>\n                            <button className=\"btn btn-sm btn-primary\" onClick={addSet} type=\"button\">\n                              <Plus className=\"h-4 w-4 mr-1\" />\n                              Ajouter la première série\n                            </button>\n                          </div>\n                        )}\n                      </div>\n                    </div>\n                  </div>\n\n                  <div className=\"flex justify-end gap-2 pt-4\">\n                    <button className=\"btn btn-outline\" onClick={handleClose} type=\"button\">\n                      Annuler\n                    </button>\n                    <button className=\"btn btn-primary\" disabled={isLoading} type=\"submit\">\n                      {isLoading ? \"Ajout...\" : \"Ajouter l'exercice\"}\n                    </button>\n                  </div>\n                </form>\n              )}\n            </div>\n          </div>\n        </div>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/add-session-modal.tsx",
    "content": "\"use client\";\n\nimport { z } from \"zod\";\nimport { useForm } from \"react-hook-form\";\nimport { useState } from \"react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\nimport { generateSlugsForAllLanguages } from \"@/shared/lib/slug\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Label } from \"@/components/ui/label\";\nimport { Input } from \"@/components/ui/input\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\n\nimport { addSessionToWeek } from \"../actions/add-session.action\";\n\nconst sessionSchema = z.object({\n  title: z.string().min(1, \"Le titre est requis\"),\n  titleEn: z.string().min(1, \"Le titre en anglais est requis\"),\n  titleEs: z.string().min(1, \"Le titre en espagnol est requis\"),\n  titlePt: z.string().min(1, \"Le titre en portugais est requis\"),\n  titleRu: z.string().min(1, \"Le titre en russe est requis\"),\n  titleZhCn: z.string().min(1, \"Le titre en chinois est requis\"),\n  description: z.string().min(1, \"La description est requise\"),\n  descriptionEn: z.string().min(1, \"La description en anglais est requise\"),\n  descriptionEs: z.string().min(1, \"La description en espagnol est requise\"),\n  descriptionPt: z.string().min(1, \"La description en portugais est requise\"),\n  descriptionRu: z.string().min(1, \"La description en russe est requise\"),\n  descriptionZhCn: z.string().min(1, \"La description en chinois est requise\"),\n  estimatedMinutes: z.number().min(5, \"Au moins 5 minutes\"),\n  isPremium: z.boolean(),\n  equipment: z.array(z.nativeEnum(ExerciseAttributeValueEnum)),\n});\n\ntype SessionFormData = z.infer<typeof sessionSchema>;\n\ninterface AddSessionModalProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  weekId: string;\n  nextSessionNumber: number;\n}\n\nconst EQUIPMENT_OPTIONS = [\n  { value: ExerciseAttributeValueEnum.BODY_ONLY, label: \"Poids du corps\" },\n  { value: ExerciseAttributeValueEnum.DUMBBELL, label: \"Haltères\" },\n  { value: ExerciseAttributeValueEnum.BARBELL, label: \"Barre\" },\n  { value: ExerciseAttributeValueEnum.KETTLEBELLS, label: \"Kettlebells\" },\n  { value: ExerciseAttributeValueEnum.BANDS, label: \"Élastiques\" },\n  { value: ExerciseAttributeValueEnum.MACHINE, label: \"Machines\" },\n  { value: ExerciseAttributeValueEnum.CABLE, label: \"Câbles\" },\n];\n\nexport function AddSessionModal({ open, onOpenChange, weekId, nextSessionNumber }: AddSessionModalProps) {\n  const [isLoading, setIsLoading] = useState(false);\n  const [activeTab, setActiveTab] = useState(\"fr\");\n  const [selectedEquipment, setSelectedEquipment] = useState<ExerciseAttributeValueEnum[]>([]);\n\n  const {\n    register,\n    handleSubmit,\n    reset,\n    setValue,\n    formState: { errors },\n  } = useForm<SessionFormData>({\n    resolver: zodResolver(sessionSchema),\n    defaultValues: {\n      title: `Séance ${nextSessionNumber}`,\n      titleEn: `Session ${nextSessionNumber}`,\n      titleEs: `Sesión ${nextSessionNumber}`,\n      titlePt: `Sessão ${nextSessionNumber}`,\n      titleRu: `Сессия ${nextSessionNumber}`,\n      titleZhCn: `第${nextSessionNumber}节`,\n      description: `Description de la séance ${nextSessionNumber}`,\n      descriptionEn: `Description of session ${nextSessionNumber}`,\n      descriptionEs: `Descripción de la sesión ${nextSessionNumber}`,\n      descriptionPt: `Descrição da sessão ${nextSessionNumber}`,\n      descriptionRu: `Описание сессии ${nextSessionNumber}`,\n      descriptionZhCn: `第${nextSessionNumber}节课程描述`,\n      estimatedMinutes: 30,\n      isPremium: true,\n      equipment: [],\n    },\n  });\n\n  const toggleEquipment = (equipment: ExerciseAttributeValueEnum) => {\n    const newEquipment = selectedEquipment.includes(equipment)\n      ? selectedEquipment.filter((e) => e !== equipment)\n      : [...selectedEquipment, equipment];\n\n    setSelectedEquipment(newEquipment);\n    setValue(\"equipment\", newEquipment);\n  };\n\n  const onSubmit = async (data: SessionFormData) => {\n    setIsLoading(true);\n    try {\n      // Generate slugs from titles\n      const slugs = generateSlugsForAllLanguages({\n        title: data.title,\n        titleEn: data.titleEn,\n        titleEs: data.titleEs,\n        titlePt: data.titlePt,\n        titleRu: data.titleRu,\n        titleZhCn: data.titleZhCn,\n      });\n\n      await addSessionToWeek({\n        weekId,\n        sessionNumber: nextSessionNumber,\n        ...data,\n        ...slugs,\n      });\n\n      reset();\n      setSelectedEquipment([]);\n      onOpenChange(false);\n      window.location.reload(); // Refresh to show new session\n    } catch (error) {\n      console.error(\"Error adding session:\", error);\n      alert(\"Erreur lors de l'ajout de la séance\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleClose = () => {\n    reset();\n    setSelectedEquipment([]);\n    setActiveTab(\"fr\");\n    onOpenChange(false);\n  };\n\n  return (\n    <Dialog onOpenChange={handleClose} open={open}>\n      <DialogContent className=\"max-w-2xl max-h-[90vh] overflow-y-auto\">\n        <DialogHeader>\n          <DialogTitle>Ajouter une séance</DialogTitle>\n        </DialogHeader>\n\n        <form className=\"space-y-4\" onSubmit={handleSubmit(onSubmit)}>\n          {/* Language Tabs */}\n          <div className=\"tabs tabs-boxed\">\n            <button className={`tab ${activeTab === \"fr\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"fr\")} type=\"button\">\n              🇫🇷 FR\n            </button>\n            <button className={`tab ${activeTab === \"en\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"en\")} type=\"button\">\n              🇺🇸 EN\n            </button>\n            <button className={`tab ${activeTab === \"es\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"es\")} type=\"button\">\n              🇪🇸 ES\n            </button>\n            <button className={`tab ${activeTab === \"pt\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"pt\")} type=\"button\">\n              🇵🇹 PT\n            </button>\n            <button className={`tab ${activeTab === \"ru\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"ru\")} type=\"button\">\n              🇷🇺 RU\n            </button>\n            <button className={`tab ${activeTab === \"zh\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"zh\")} type=\"button\">\n              🇨🇳 ZH\n            </button>\n          </div>\n\n          {/* French Fields */}\n          {activeTab === \"fr\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"title\">Titre (Français)</Label>\n                <Input id=\"title\" {...register(\"title\")} placeholder={`Séance ${nextSessionNumber}`} />\n                {errors.title && <p className=\"text-sm text-red-500 mt-1\">{errors.title.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"description\">Description (Français)</Label>\n                <Textarea id=\"description\" {...register(\"description\")} placeholder=\"Description de cette séance...\" rows={3} />\n                {errors.description && <p className=\"text-sm text-red-500 mt-1\">{errors.description.message}</p>}\n              </div>\n            </div>\n          )}\n\n          {/* English Fields */}\n          {activeTab === \"en\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"titleEn\">Title (English)</Label>\n                <Input id=\"titleEn\" {...register(\"titleEn\")} placeholder={`Session ${nextSessionNumber}`} />\n                {errors.titleEn && <p className=\"text-sm text-red-500 mt-1\">{errors.titleEn.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"descriptionEn\">Description (English)</Label>\n                <Textarea id=\"descriptionEn\" {...register(\"descriptionEn\")} placeholder=\"Session description...\" rows={3} />\n                {errors.descriptionEn && <p className=\"text-sm text-red-500 mt-1\">{errors.descriptionEn.message}</p>}\n              </div>\n            </div>\n          )}\n\n          {/* Spanish Fields */}\n          {activeTab === \"es\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"titleEs\">Título (Español)</Label>\n                <Input id=\"titleEs\" {...register(\"titleEs\")} placeholder={`Sesión ${nextSessionNumber}`} />\n                {errors.titleEs && <p className=\"text-sm text-red-500 mt-1\">{errors.titleEs.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"descriptionEs\">Descripción (Español)</Label>\n                <Textarea id=\"descriptionEs\" {...register(\"descriptionEs\")} placeholder=\"Descripción de la sesión...\" rows={3} />\n                {errors.descriptionEs && <p className=\"text-sm text-red-500 mt-1\">{errors.descriptionEs.message}</p>}\n              </div>\n            </div>\n          )}\n\n          {/* Portuguese Fields */}\n          {activeTab === \"pt\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"titlePt\">Título (Português)</Label>\n                <Input id=\"titlePt\" {...register(\"titlePt\")} placeholder={`Sessão ${nextSessionNumber}`} />\n                {errors.titlePt && <p className=\"text-sm text-red-500 mt-1\">{errors.titlePt.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"descriptionPt\">Descrição (Português)</Label>\n                <Textarea id=\"descriptionPt\" {...register(\"descriptionPt\")} placeholder=\"Descrição da sessão...\" rows={3} />\n                {errors.descriptionPt && <p className=\"text-sm text-red-500 mt-1\">{errors.descriptionPt.message}</p>}\n              </div>\n            </div>\n          )}\n\n          {/* Russian Fields */}\n          {activeTab === \"ru\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"titleRu\">Название (Русский)</Label>\n                <Input id=\"titleRu\" {...register(\"titleRu\")} placeholder={`Сессия ${nextSessionNumber}`} />\n                {errors.titleRu && <p className=\"text-sm text-red-500 mt-1\">{errors.titleRu.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"descriptionRu\">Описание (Русский)</Label>\n                <Textarea id=\"descriptionRu\" {...register(\"descriptionRu\")} placeholder=\"Описание сессии...\" rows={3} />\n                {errors.descriptionRu && <p className=\"text-sm text-red-500 mt-1\">{errors.descriptionRu.message}</p>}\n              </div>\n            </div>\n          )}\n\n          {/* Chinese Fields */}\n          {activeTab === \"zh\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"titleZhCn\">标题 (中文)</Label>\n                <Input id=\"titleZhCn\" {...register(\"titleZhCn\")} placeholder={`第${nextSessionNumber}节`} />\n                {errors.titleZhCn && <p className=\"text-sm text-red-500 mt-1\">{errors.titleZhCn.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"descriptionZhCn\">描述 (中文)</Label>\n                <Textarea id=\"descriptionZhCn\" {...register(\"descriptionZhCn\")} placeholder=\"课程描述...\" rows={3} />\n                {errors.descriptionZhCn && <p className=\"text-sm text-red-500 mt-1\">{errors.descriptionZhCn.message}</p>}\n              </div>\n            </div>\n          )}\n\n          <div className=\"grid grid-cols-2 gap-4\">\n            <div>\n              <Label htmlFor=\"estimatedMinutes\">Durée estimée (minutes)</Label>\n              <Input id=\"estimatedMinutes\" min=\"5\" type=\"number\" {...register(\"estimatedMinutes\", { valueAsNumber: true })} />\n              {errors.estimatedMinutes && <p className=\"text-sm text-red-500 mt-1\">{errors.estimatedMinutes.message}</p>}\n            </div>\n            <div className=\"flex items-center space-x-2 pt-8\">\n              <Switch defaultChecked={true} id=\"isPremium\" onCheckedChange={(checked) => setValue(\"isPremium\", checked)} />\n              <Label htmlFor=\"isPremium\">Séance premium</Label>\n            </div>\n          </div>\n\n          <div>\n            <Label>Équipement requis</Label>\n            <div className=\"flex flex-wrap gap-2 mt-2\">\n              {EQUIPMENT_OPTIONS.map((option) => (\n                <Badge\n                  className=\"cursor-pointer\"\n                  key={option.value}\n                  onClick={() => toggleEquipment(option.value)}\n                  variant={selectedEquipment.includes(option.value) ? \"default\" : \"outline\"}\n                >\n                  {option.label}\n                </Badge>\n              ))}\n            </div>\n          </div>\n\n          <div className=\"flex justify-end gap-2 pt-4\">\n            <Button onClick={handleClose} type=\"button\" variant=\"outline\">\n              Annuler\n            </Button>\n            <Button disabled={isLoading} type=\"submit\">\n              {isLoading ? \"Ajout...\" : \"Ajouter la séance\"}\n            </Button>\n          </div>\n        </form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/add-week-modal.tsx",
    "content": "\"use client\";\n\nimport { z } from \"zod\";\nimport { useState } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { addWeekToProgram } from \"../actions/add-week.action\";\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst weekSchema = z.object({\n  title: z.string().min(1, \"Le titre est requis\"),\n  titleEn: z.string().min(1, \"Le titre en anglais est requis\"),\n  titleEs: z.string().min(1, \"Le titre en espagnol est requis\"),\n  titlePt: z.string().min(1, \"Le titre en portugais est requis\"),\n  titleRu: z.string().min(1, \"Le titre en russe est requis\"),\n  titleZhCn: z.string().min(1, \"Le titre en chinois est requis\"),\n  description: z.string().optional(),\n  descriptionEn: z.string().optional(),\n  descriptionEs: z.string().optional(),\n  descriptionPt: z.string().optional(),\n  descriptionRu: z.string().optional(),\n  descriptionZhCn: z.string().optional(),\n});\n\ntype WeekFormData = z.infer<typeof weekSchema>;\n\ninterface AddWeekModalProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  programId: string;\n  nextWeekNumber: number;\n}\n\nexport function AddWeekModal({ open, onOpenChange, programId, nextWeekNumber }: AddWeekModalProps) {\n  const [isLoading, setIsLoading] = useState(false);\n  const [activeTab, setActiveTab] = useState(\"fr\");\n  const [formData, setFormData] = useState<WeekFormData>({\n    title: `Semaine ${nextWeekNumber}`,\n    titleEn: `Week ${nextWeekNumber}`,\n    titleEs: `Semana ${nextWeekNumber}`,\n    titlePt: `Semana ${nextWeekNumber}`,\n    titleRu: `Неделя ${nextWeekNumber}`,\n    titleZhCn: `第${nextWeekNumber}周`,\n    description: \"\",\n    descriptionEn: \"\",\n    descriptionEs: \"\",\n    descriptionPt: \"\",\n    descriptionRu: \"\",\n    descriptionZhCn: \"\",\n  });\n\n  const handleSubmit = async (e: React.FormEvent) => {\n    e.preventDefault();\n    setIsLoading(true);\n    try {\n      await addWeekToProgram({\n        programId,\n        weekNumber: nextWeekNumber,\n        ...formData,\n      });\n\n      onOpenChange(false);\n      window.location.reload(); // Refresh to show new week\n    } catch (error) {\n      console.error(\"Error adding week:\", error);\n      alert(\"Erreur lors de l'ajout de la semaine\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleClose = () => {\n    setActiveTab(\"fr\");\n    onOpenChange(false);\n  };\n\n  if (!open) return null;\n\n  return (\n    <div className=\"modal modal-open modal-middle !mt-0\">\n      <div className=\"modal-box max-w-4xl overflow-y-auto\">\n        <div className=\"flex items-center justify-between mb-6\">\n          <h3 className=\"font-bold text-lg\">Ajouter une semaine</h3>\n          <button className=\"btn btn-sm btn-circle btn-ghost\" onClick={handleClose}>\n            <X className=\"h-4 w-4\" />\n          </button>\n        </div>\n\n        <form onSubmit={handleSubmit}>\n          <div className=\"space-y-6\">\n            {/* Language Tabs */}\n            <div className=\"tabs tabs-boxed\">\n              <button className={`tab ${activeTab === \"fr\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"fr\")} type=\"button\">\n                🇫🇷 FR\n              </button>\n              <button className={`tab ${activeTab === \"en\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"en\")} type=\"button\">\n                🇺🇸 EN\n              </button>\n              <button className={`tab ${activeTab === \"es\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"es\")} type=\"button\">\n                🇪🇸 ES\n              </button>\n              <button className={`tab ${activeTab === \"pt\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"pt\")} type=\"button\">\n                🇵🇹 PT\n              </button>\n              <button className={`tab ${activeTab === \"ru\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"ru\")} type=\"button\">\n                🇷🇺 RU\n              </button>\n              <button className={`tab ${activeTab === \"zh\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"zh\")} type=\"button\">\n                🇨🇳 ZH\n              </button>\n            </div>\n\n            {/* French Fields */}\n            {activeTab === \"fr\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Titre (Français)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, title: e.target.value })}\n                    placeholder={`Semaine ${nextWeekNumber}`}\n                    required\n                    type=\"text\"\n                    value={formData.title}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Description (Français)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, description: e.target.value })}\n                    placeholder=\"Description de cette semaine...\"\n                    value={formData.description}\n                  />\n                </div>\n              </div>\n            )}\n\n            {/* English Fields */}\n            {activeTab === \"en\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Title (English)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, titleEn: e.target.value })}\n                    placeholder={`Week ${nextWeekNumber}`}\n                    required\n                    type=\"text\"\n                    value={formData.titleEn}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Description (English)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, descriptionEn: e.target.value })}\n                    placeholder=\"Week description...\"\n                    value={formData.descriptionEn}\n                  />\n                </div>\n              </div>\n            )}\n\n            {/* Spanish Fields */}\n            {activeTab === \"es\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Título (Español)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, titleEs: e.target.value })}\n                    placeholder={`Semana ${nextWeekNumber}`}\n                    required\n                    type=\"text\"\n                    value={formData.titleEs}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Descripción (Español)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, descriptionEs: e.target.value })}\n                    placeholder=\"Descripción de la semana...\"\n                    value={formData.descriptionEs}\n                  />\n                </div>\n              </div>\n            )}\n\n            {/* Portuguese Fields */}\n            {activeTab === \"pt\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Título (Português)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, titlePt: e.target.value })}\n                    placeholder={`Semana ${nextWeekNumber}`}\n                    required\n                    type=\"text\"\n                    value={formData.titlePt}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Descrição (Português)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, descriptionPt: e.target.value })}\n                    placeholder=\"Descrição da semana...\"\n                    value={formData.descriptionPt}\n                  />\n                </div>\n              </div>\n            )}\n\n            {/* Russian Fields */}\n            {activeTab === \"ru\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Название (Русский)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, titleRu: e.target.value })}\n                    placeholder={`Неделя ${nextWeekNumber}`}\n                    required\n                    type=\"text\"\n                    value={formData.titleRu}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Описание (Русский)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, descriptionRu: e.target.value })}\n                    placeholder=\"Описание недели...\"\n                    value={formData.descriptionRu}\n                  />\n                </div>\n              </div>\n            )}\n\n            {/* Chinese Fields */}\n            {activeTab === \"zh\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">标题 (中文)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, titleZhCn: e.target.value })}\n                    placeholder={`第${nextWeekNumber}周`}\n                    required\n                    type=\"text\"\n                    value={formData.titleZhCn}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">描述 (中文)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isLoading}\n                    onChange={(e) => setFormData({ ...formData, descriptionZhCn: e.target.value })}\n                    placeholder=\"本周描述...\"\n                    value={formData.descriptionZhCn}\n                  />\n                </div>\n              </div>\n            )}\n          </div>\n\n          <div className=\"modal-action\">\n            <button className=\"btn btn-ghost\" disabled={isLoading} onClick={handleClose} type=\"button\">\n              Annuler\n            </button>\n            <button className=\"btn btn-primary\" disabled={isLoading} type=\"submit\">\n              {isLoading ? (\n                <>\n                  <span className=\"loading loading-spinner loading-sm\"></span>\n                  Ajout...\n                </>\n              ) : (\n                \"Ajouter la semaine\"\n              )}\n            </button>\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/create-program-button.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Plus } from \"lucide-react\";\n\nimport { CreateProgramModal } from \"./create-program-modal\";\n\nexport function CreateProgramButton() {\n  const [isModalOpen, setIsModalOpen] = useState(false);\n\n  return (\n    <>\n      <button \n        className=\"btn btn-primary\"\n        onClick={() => setIsModalOpen(true)}\n      >\n        <Plus className=\"h-4 w-4\" />\n        Créer un programme\n      </button>\n      \n      <CreateProgramModal\n        onOpenChange={setIsModalOpen}\n        open={isModalOpen}\n      />\n    </>\n  );\n}"
  },
  {
    "path": "src/features/admin/programs/ui/create-program-form.tsx",
    "content": "\"use client\";\n\nimport { z } from \"zod\";\nimport { useForm } from \"react-hook-form\";\nimport { useState } from \"react\";\nimport { Plus, Trash2 } from \"lucide-react\";\nimport { ProgramLevel, ExerciseAttributeValueEnum } from \"@prisma/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\nimport { createProgram } from \"../actions/create-program.action\";\n\nconst programSchema = z.object({\n  // Step 1: Basic info\n  title: z.string().min(1, \"Le titre est requis\"),\n  titleEn: z.string().min(1, \"Le titre en anglais est requis\"),\n  titleEs: z.string().min(1, \"Le titre en espagnol est requis\"),\n  titlePt: z.string().min(1, \"Le titre en portugais est requis\"),\n  titleRu: z.string().min(1, \"Le titre en russe est requis\"),\n  titleZhCn: z.string().min(1, \"Le titre en chinois est requis\"),\n  description: z.string().min(1, \"La description est requise\"),\n  descriptionEn: z.string().min(1, \"La description en anglais est requise\"),\n  descriptionEs: z.string().min(1, \"La description en espagnol est requise\"),\n  descriptionPt: z.string().min(1, \"La description en portugais est requise\"),\n  descriptionRu: z.string().min(1, \"La description en russe est requise\"),\n  descriptionZhCn: z.string().min(1, \"La description en chinois est requise\"),\n  category: z.string().min(1, \"La catégorie est requise\"),\n  image: z.string().url(\"URL d'image invalide\"),\n  level: z.nativeEnum(ProgramLevel),\n  type: z.nativeEnum(ExerciseAttributeValueEnum),\n\n  // Step 2: Configuration\n  durationWeeks: z.number().min(1, \"Au moins 1 semaine\"),\n  sessionsPerWeek: z.number().min(1, \"Au moins 1 séance par semaine\"),\n  sessionDurationMin: z.number().min(5, \"Au moins 5 minutes\"),\n  equipment: z.array(z.nativeEnum(ExerciseAttributeValueEnum)),\n  isPremium: z.boolean(),\n\n  // Step 3: Coaches\n  coaches: z.array(\n    z.object({\n      name: z.string().min(1, \"Le nom est requis\"),\n      image: z.string().url(\"URL d'image invalide\"),\n      order: z.number(),\n    }),\n  ),\n});\n\ntype ProgramFormData = z.infer<typeof programSchema>;\n\ninterface CreateProgramFormProps {\n  currentStep: number;\n  onStepComplete: (step: number) => void;\n  onSuccess: () => void;\n  onCancel: () => void;\n}\n\nconst EQUIPMENT_OPTIONS = [\n  { value: ExerciseAttributeValueEnum.BODY_ONLY, label: \"Poids du corps\" },\n  { value: ExerciseAttributeValueEnum.DUMBBELL, label: \"Haltères\" },\n  { value: ExerciseAttributeValueEnum.BARBELL, label: \"Barre\" },\n  { value: ExerciseAttributeValueEnum.KETTLEBELLS, label: \"Kettlebells\" },\n  { value: ExerciseAttributeValueEnum.BANDS, label: \"Élastiques\" },\n  { value: ExerciseAttributeValueEnum.MACHINE, label: \"Machines\" },\n  { value: ExerciseAttributeValueEnum.CABLE, label: \"Câbles\" },\n];\n\nconst TYPE_OPTIONS = [\n  { value: ExerciseAttributeValueEnum.STRENGTH, label: \"Musculation\" },\n  { value: ExerciseAttributeValueEnum.CARDIO, label: \"Cardio\" },\n  { value: ExerciseAttributeValueEnum.BODYWEIGHT, label: \"Poids du corps\" },\n  { value: ExerciseAttributeValueEnum.STRETCHING, label: \"Étirements\" },\n  { value: ExerciseAttributeValueEnum.CALISTHENIC, label: \"Callisthénie\" },\n];\n\nexport function CreateProgramForm({ currentStep, onStepComplete, onSuccess, onCancel }: CreateProgramFormProps) {\n  const [isLoading, setIsLoading] = useState(false);\n  const [selectedEquipment, setSelectedEquipment] = useState<ExerciseAttributeValueEnum[]>([]);\n  const [activeTab, setActiveTab] = useState(\"fr\");\n\n  const {\n    register,\n    handleSubmit,\n    watch,\n    setValue,\n    formState: { errors },\n  } = useForm<ProgramFormData>({\n    resolver: zodResolver(programSchema),\n    defaultValues: {\n      level: ProgramLevel.BEGINNER,\n      type: ExerciseAttributeValueEnum.STRENGTH,\n      durationWeeks: 4,\n      sessionsPerWeek: 3,\n      sessionDurationMin: 30,\n      isPremium: true,\n      equipment: [],\n      coaches: [],\n      title: \"\",\n      titleEn: \"\",\n      titleEs: \"\",\n      titlePt: \"\",\n      titleRu: \"\",\n      titleZhCn: \"\",\n      description: \"\",\n      descriptionEn: \"\",\n      descriptionEs: \"\",\n      descriptionPt: \"\",\n      descriptionRu: \"\",\n      descriptionZhCn: \"\",\n    },\n  });\n\n  const coaches = watch(\"coaches\") || [];\n\n  const addCoach = () => {\n    const newCoaches = [...coaches, { name: \"\", image: \"\", order: coaches.length }];\n    setValue(\"coaches\", newCoaches);\n  };\n\n  const removeCoach = (index: number) => {\n    const newCoaches = coaches.filter((_, i) => i !== index);\n    setValue(\"coaches\", newCoaches);\n  };\n\n  const toggleEquipment = (equipment: ExerciseAttributeValueEnum) => {\n    const newEquipment = selectedEquipment.includes(equipment)\n      ? selectedEquipment.filter((e) => e !== equipment)\n      : [...selectedEquipment, equipment];\n\n    setSelectedEquipment(newEquipment);\n    setValue(\"equipment\", newEquipment);\n  };\n\n  const onSubmit = async (data: ProgramFormData) => {\n    if (currentStep < 3) {\n      onStepComplete(currentStep);\n      return;\n    }\n\n    setIsLoading(true);\n    try {\n      await createProgram(data);\n      onSuccess();\n    } catch (error) {\n      console.error(\"Error creating program:\", error);\n      alert(\"Erreur lors de la création du programme\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const renderStep1 = () => (\n    <div className=\"card bg-base-100 shadow-xl\">\n      <div className=\"card-body\">\n        <h2 className=\"card-title\">Informations générales</h2>\n        \n        {/* Language Tabs */}\n        <div className=\"tabs tabs-boxed mb-6\">\n          <button className={`tab ${activeTab === \"fr\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"fr\")} type=\"button\">\n            🇫🇷 FR\n          </button>\n          <button className={`tab ${activeTab === \"en\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"en\")} type=\"button\">\n            🇺🇸 EN\n          </button>\n          <button className={`tab ${activeTab === \"es\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"es\")} type=\"button\">\n            🇪🇸 ES\n          </button>\n          <button className={`tab ${activeTab === \"pt\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"pt\")} type=\"button\">\n            🇵🇹 PT\n          </button>\n          <button className={`tab ${activeTab === \"ru\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"ru\")} type=\"button\">\n            🇷🇺 RU\n          </button>\n          <button className={`tab ${activeTab === \"zh\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"zh\")} type=\"button\">\n            🇨🇳 ZH\n          </button>\n        </div>\n\n        <div className=\"space-y-4\">\n          {/* French Fields */}\n          {activeTab === \"fr\" && (\n            <div className=\"space-y-4\">\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"title\">\n                  <span className=\"label-text\">Titre (Français)</span>\n                </label>\n                <input className=\"input input-bordered\" id=\"title\" {...register(\"title\")} />\n                {errors.title && <div className=\"text-sm text-error mt-1\">{errors.title.message}</div>}\n              </div>\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"description\">\n                  <span className=\"label-text\">Description (Français)</span>\n                </label>\n                <textarea className=\"textarea textarea-bordered h-24\" id=\"description\" {...register(\"description\")} />\n                {errors.description && <div className=\"text-sm text-error mt-1\">{errors.description.message}</div>}\n              </div>\n            </div>\n          )}\n\n          {/* English Fields */}\n          {activeTab === \"en\" && (\n            <div className=\"space-y-4\">\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"titleEn\">\n                  <span className=\"label-text\">Title (English)</span>\n                </label>\n                <input className=\"input input-bordered\" id=\"titleEn\" {...register(\"titleEn\")} />\n                {errors.titleEn && <div className=\"text-sm text-error mt-1\">{errors.titleEn.message}</div>}\n              </div>\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"descriptionEn\">\n                  <span className=\"label-text\">Description (English)</span>\n                </label>\n                <textarea className=\"textarea textarea-bordered h-24\" id=\"descriptionEn\" {...register(\"descriptionEn\")} />\n                {errors.descriptionEn && <div className=\"text-sm text-error mt-1\">{errors.descriptionEn.message}</div>}\n              </div>\n            </div>\n          )}\n\n          {/* Spanish Fields */}\n          {activeTab === \"es\" && (\n            <div className=\"space-y-4\">\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"titleEs\">\n                  <span className=\"label-text\">Título (Español)</span>\n                </label>\n                <input className=\"input input-bordered\" id=\"titleEs\" {...register(\"titleEs\")} />\n                {errors.titleEs && <div className=\"text-sm text-error mt-1\">{errors.titleEs.message}</div>}\n              </div>\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"descriptionEs\">\n                  <span className=\"label-text\">Descripción (Español)</span>\n                </label>\n                <textarea className=\"textarea textarea-bordered h-24\" id=\"descriptionEs\" {...register(\"descriptionEs\")} />\n                {errors.descriptionEs && <div className=\"text-sm text-error mt-1\">{errors.descriptionEs.message}</div>}\n              </div>\n            </div>\n          )}\n\n          {/* Portuguese Fields */}\n          {activeTab === \"pt\" && (\n            <div className=\"space-y-4\">\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"titlePt\">\n                  <span className=\"label-text\">Título (Português)</span>\n                </label>\n                <input className=\"input input-bordered\" id=\"titlePt\" {...register(\"titlePt\")} />\n                {errors.titlePt && <div className=\"text-sm text-error mt-1\">{errors.titlePt.message}</div>}\n              </div>\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"descriptionPt\">\n                  <span className=\"label-text\">Descrição (Português)</span>\n                </label>\n                <textarea className=\"textarea textarea-bordered h-24\" id=\"descriptionPt\" {...register(\"descriptionPt\")} />\n                {errors.descriptionPt && <div className=\"text-sm text-error mt-1\">{errors.descriptionPt.message}</div>}\n              </div>\n            </div>\n          )}\n\n          {/* Russian Fields */}\n          {activeTab === \"ru\" && (\n            <div className=\"space-y-4\">\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"titleRu\">\n                  <span className=\"label-text\">Название (Русский)</span>\n                </label>\n                <input className=\"input input-bordered\" id=\"titleRu\" {...register(\"titleRu\")} />\n                {errors.titleRu && <div className=\"text-sm text-error mt-1\">{errors.titleRu.message}</div>}\n              </div>\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"descriptionRu\">\n                  <span className=\"label-text\">Описание (Русский)</span>\n                </label>\n                <textarea className=\"textarea textarea-bordered h-24\" id=\"descriptionRu\" {...register(\"descriptionRu\")} />\n                {errors.descriptionRu && <div className=\"text-sm text-error mt-1\">{errors.descriptionRu.message}</div>}\n              </div>\n            </div>\n          )}\n\n          {/* Chinese Fields */}\n          {activeTab === \"zh\" && (\n            <div className=\"space-y-4\">\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"titleZhCn\">\n                  <span className=\"label-text\">标题 (中文)</span>\n                </label>\n                <input className=\"input input-bordered\" id=\"titleZhCn\" {...register(\"titleZhCn\")} />\n                {errors.titleZhCn && <div className=\"text-sm text-error mt-1\">{errors.titleZhCn.message}</div>}\n              </div>\n              <div className=\"form-control\">\n                <label className=\"label\" htmlFor=\"descriptionZhCn\">\n                  <span className=\"label-text\">描述 (中文)</span>\n                </label>\n                <textarea className=\"textarea textarea-bordered h-24\" id=\"descriptionZhCn\" {...register(\"descriptionZhCn\")} />\n                {errors.descriptionZhCn && <div className=\"text-sm text-error mt-1\">{errors.descriptionZhCn.message}</div>}\n              </div>\n            </div>\n          )}\n\n          <div className=\"grid grid-cols-2 gap-4\">\n            <div className=\"form-control\">\n              <label className=\"label\" htmlFor=\"category\">\n                <span className=\"label-text\">Catégorie</span>\n              </label>\n              <input className=\"input input-bordered\" id=\"category\" {...register(\"category\")} placeholder=\"ex: Musculation\" />\n              {errors.category && <div className=\"text-sm text-error mt-1\">{errors.category.message}</div>}\n            </div>\n            <div className=\"form-control\">\n              <label className=\"label\" htmlFor=\"image\">\n                <span className=\"label-text\">URL de l&apos;image</span>\n              </label>\n              <input className=\"input input-bordered\" id=\"image\" {...register(\"image\")} placeholder=\"https://...\" />\n              {errors.image && <div className=\"text-sm text-error mt-1\">{errors.image.message}</div>}\n            </div>\n          </div>\n\n          <div className=\"grid grid-cols-3 gap-4\">\n            <div className=\"form-control\">\n              <label className=\"label\" htmlFor=\"level\">\n                <span className=\"label-text\">Niveau</span>\n              </label>\n              <select\n                className=\"select select-bordered\"\n                defaultValue={ProgramLevel.BEGINNER}\n                onChange={(e) => setValue(\"level\", e.target.value as ProgramLevel)}\n              >\n                <option value={ProgramLevel.BEGINNER}>Débutant</option>\n                <option value={ProgramLevel.INTERMEDIATE}>Intermédiaire</option>\n                <option value={ProgramLevel.ADVANCED}>Avancé</option>\n                <option value={ProgramLevel.EXPERT}>Expert</option>\n              </select>\n            </div>\n            <div className=\"form-control\">\n              <label className=\"label\" htmlFor=\"type\">\n                <span className=\"label-text\">Type</span>\n              </label>\n              <select\n                className=\"select select-bordered\"\n                defaultValue={ExerciseAttributeValueEnum.STRENGTH}\n                onChange={(e) => setValue(\"type\", e.target.value as ExerciseAttributeValueEnum)}\n              >\n                {TYPE_OPTIONS.map((option) => (\n                  <option key={option.value} value={option.value}>\n                    {option.label}\n                  </option>\n                ))}\n              </select>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n\n  const renderStep2 = () => (\n    <div className=\"card bg-base-100 shadow-xl\">\n      <div className=\"card-body\">\n        <h2 className=\"card-title\">Configuration du programme</h2>\n        <div className=\"space-y-6\">\n          <div className=\"grid grid-cols-3 gap-4\">\n            <div className=\"form-control\">\n              <label className=\"label\" htmlFor=\"durationWeeks\">\n                <span className=\"label-text\">Durée (semaines)</span>\n              </label>\n              <input\n                className=\"input input-bordered\"\n                id=\"durationWeeks\"\n                min=\"1\"\n                type=\"number\"\n                {...register(\"durationWeeks\", { valueAsNumber: true })}\n              />\n              {errors.durationWeeks && <div className=\"text-sm text-error mt-1\">{errors.durationWeeks.message}</div>}\n            </div>\n            <div className=\"form-control\">\n              <label className=\"label\" htmlFor=\"sessionsPerWeek\">\n                <span className=\"label-text\">Séances/semaine</span>\n              </label>\n              <input\n                className=\"input input-bordered\"\n                id=\"sessionsPerWeek\"\n                min=\"1\"\n                type=\"number\"\n                {...register(\"sessionsPerWeek\", { valueAsNumber: true })}\n              />\n              {errors.sessionsPerWeek && <div className=\"text-sm text-error mt-1\">{errors.sessionsPerWeek.message}</div>}\n            </div>\n            <div className=\"form-control\">\n              <label className=\"label\" htmlFor=\"sessionDurationMin\">\n                <span className=\"label-text\">Durée séance (min)</span>\n              </label>\n              <input\n                className=\"input input-bordered\"\n                id=\"sessionDurationMin\"\n                min=\"5\"\n                type=\"number\"\n                {...register(\"sessionDurationMin\", { valueAsNumber: true })}\n              />\n              {errors.sessionDurationMin && <div className=\"text-sm text-error mt-1\">{errors.sessionDurationMin.message}</div>}\n            </div>\n          </div>\n\n          <div>\n            <label className=\"label\">\n              <span className=\"label-text\">Équipement requis</span>\n            </label>\n            <div className=\"flex flex-wrap gap-2 mt-2\">\n              {EQUIPMENT_OPTIONS.map((option) => (\n                <div\n                  className={`badge cursor-pointer ${selectedEquipment.includes(option.value) ? \"badge-primary\" : \"badge-outline\"}`}\n                  key={option.value}\n                  onClick={() => toggleEquipment(option.value)}\n                >\n                  {option.label}\n                </div>\n              ))}\n            </div>\n          </div>\n\n          <div className=\"form-control\">\n            <label className=\"label cursor-pointer justify-start gap-2\">\n              <input\n                className=\"toggle toggle-primary\"\n                defaultChecked={true}\n                id=\"isPremium\"\n                onChange={(e) => setValue(\"isPremium\", e.target.checked)}\n                type=\"checkbox\"\n              />\n              <span className=\"label-text\">Programme premium</span>\n            </label>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n\n  const renderStep3 = () => (\n    <div className=\"card bg-base-100 shadow-xl\">\n      <div className=\"card-body\">\n        <div className=\"flex items-center justify-between mb-4\">\n          <h2 className=\"card-title\">Coachs du programme</h2>\n          <button className=\"btn btn-sm btn-primary\" onClick={addCoach} type=\"button\">\n            <Plus className=\"h-4 w-4 mr-1\" />\n            Ajouter\n          </button>\n        </div>\n        <div className=\"space-y-4\">\n          {coaches.length === 0 ? (\n            <p className=\"text-base-content/60 text-center py-8\">Aucun coach ajouté. Cliquez sur &quot;Ajouter&quot; pour commencer.</p>\n          ) : (\n            coaches.map((_, index) => (\n              <div className=\"flex gap-4 items-end\" key={index}>\n                <div className=\"flex-1 form-control\">\n                  <label className=\"label\" htmlFor={`coach-name-${index}`}>\n                    <span className=\"label-text\">Nom</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered\"\n                    id={`coach-name-${index}`}\n                    {...register(`coaches.${index}.name`)}\n                    placeholder=\"Nom du coach\"\n                  />\n                </div>\n                <div className=\"flex-1 form-control\">\n                  <label className=\"label\" htmlFor={`coach-image-${index}`}>\n                    <span className=\"label-text\">URL de l&apos;image</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered\"\n                    id={`coach-image-${index}`}\n                    {...register(`coaches.${index}.image`)}\n                    placeholder=\"https://...\"\n                  />\n                </div>\n                <button className=\"btn btn-outline btn-sm\" onClick={() => removeCoach(index)} type=\"button\">\n                  <Trash2 className=\"h-4 w-4\" />\n                </button>\n              </div>\n            ))\n          )}\n        </div>\n      </div>\n    </div>\n  );\n\n  return (\n    <form className=\"space-y-6\" onSubmit={handleSubmit(onSubmit)}>\n      {currentStep === 1 && renderStep1()}\n      {currentStep === 2 && renderStep2()}\n      {currentStep === 3 && renderStep3()}\n\n      <div className=\"flex justify-between pt-6 border-t border-base-300\">\n        <button className=\"btn btn-outline\" onClick={onCancel} type=\"button\">\n          Annuler\n        </button>\n        <button className=\"btn btn-primary\" disabled={isLoading} type=\"submit\">\n          {currentStep === 3 ? (isLoading ? \"Création...\" : \"Créer le programme\") : \"Suivant\"}\n        </button>\n      </div>\n    </form>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/create-program-modal.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { CheckCircle } from \"lucide-react\";\n\nimport { CreateProgramForm } from \"./create-program-form\";\n\ninterface CreateProgramModalProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nconst STEPS = [\n  { id: 1, title: \"Informations générales\", description: \"Titre, description, niveau...\" },\n  { id: 2, title: \"Configuration\", description: \"Durée, fréquence, équipement...\" },\n  { id: 3, title: \"Coachs\", description: \"Ajouter les coachs du programme\" },\n] as const;\n\nexport function CreateProgramModal({ open, onOpenChange }: CreateProgramModalProps) {\n  const [currentStep, setCurrentStep] = useState(1);\n  const [completedSteps, setCompletedSteps] = useState<number[]>([]);\n\n  const handleStepComplete = (step: number) => {\n    if (!completedSteps.includes(step)) {\n      setCompletedSteps([...completedSteps, step]);\n    }\n    \n    // Move to next step if not last\n    if (step < STEPS.length) {\n      setCurrentStep(step + 1);\n    }\n  };\n\n  const handleClose = () => {\n    setCurrentStep(1);\n    setCompletedSteps([]);\n    onOpenChange(false);\n  };\n\n  const handleSuccess = () => {\n    handleClose();\n    // Refresh the page to show the new program\n    window.location.reload();\n  };\n\n  return (\n    <>\n      {open && (\n        <div className=\"modal modal-open\">\n          <div className=\"modal-box w-11/12 max-w-4xl h-full max-h-[90vh] flex flex-col\">\n            <div className=\"flex justify-between items-center mb-4\">\n              <h3 className=\"font-bold text-lg\">Créer un nouveau programme</h3>\n              <button \n                className=\"btn btn-sm btn-circle btn-ghost\"\n                onClick={handleClose}\n              >\n                ✕\n              </button>\n            </div>\n            \n            {/* Steps indicator */}\n            <div className=\"flex items-center justify-between mb-6 px-4\">\n              {STEPS.map((step, index) => (\n                <div className=\"flex items-center\" key={step.id}>\n                  <div className=\"flex flex-col items-center\">\n                    <div\n                      className={`flex items-center justify-center w-10 h-10 rounded-full border-2 transition-colors ${\n                        completedSteps.includes(step.id)\n                          ? \"bg-success border-success text-white\"\n                          : currentStep === step.id\n                          ? \"bg-primary border-primary text-white\"\n                          : \"border-base-300 text-base-content/50\"\n                      }`}\n                    >\n                      {completedSteps.includes(step.id) ? (\n                        <CheckCircle className=\"w-5 h-5\" />\n                      ) : (\n                        <span className=\"text-sm font-semibold\">{step.id}</span>\n                      )}\n                    </div>\n                    <div className=\"mt-2 text-center\">\n                      <div className=\"text-sm font-medium\">{step.title}</div>\n                      <div className=\"text-xs text-base-content/60\">{step.description}</div>\n                    </div>\n                  </div>\n                  \n                  {index < STEPS.length - 1 && (\n                    <div\n                      className={`w-20 h-0.5 mx-4 ${\n                        completedSteps.includes(step.id) ? \"bg-success\" : \"bg-base-300\"\n                      }`}\n                    />\n                  )}\n                </div>\n              ))}\n            </div>\n\n            {/* Form content */}\n            <div className=\"flex-1 overflow-y-auto\">\n              <CreateProgramForm\n                currentStep={currentStep}\n                onCancel={handleClose}\n                onStepComplete={handleStepComplete}\n                onSuccess={handleSuccess}\n              />\n            </div>\n          </div>\n        </div>\n      )}\n    </>\n  );\n}"
  },
  {
    "path": "src/features/admin/programs/ui/delete-program-button.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { Trash2 } from \"lucide-react\";\n\nimport { deleteProgram } from \"../actions/delete-program.action\";\n\ninterface DeleteProgramButtonProps {\n  programId: string;\n  programTitle: string;\n}\n\nexport function DeleteProgramButton({ programId, programTitle }: DeleteProgramButtonProps) {\n  const [isModalOpen, setIsModalOpen] = useState(false);\n  const [isDeleting, setIsDeleting] = useState(false);\n  const router = useRouter();\n\n  const handleDelete = async () => {\n    setIsDeleting(true);\n    try {\n      await deleteProgram(programId);\n      setIsModalOpen(false);\n      router.refresh();\n    } catch (error) {\n      console.error(\"Error deleting program:\", error);\n      alert(error instanceof Error ? error.message : \"Erreur lors de la suppression\");\n    } finally {\n      setIsDeleting(false);\n    }\n  };\n\n  return (\n    <>\n      <button className=\"btn btn-outline btn-sm px-2\" onClick={() => setIsModalOpen(true)}>\n        <Trash2 className=\"h-4 w-4 text-error\" />\n      </button>\n\n      {isModalOpen && (\n        <div className=\"modal modal-open\">\n          <div className=\"modal-box\">\n            <h3 className=\"font-bold text-lg\">Confirmer la suppression</h3>\n            <p className=\"py-4\">\n              Êtes-vous sûr de vouloir supprimer le programme <strong>{programTitle}</strong> ? Cette action est irréversible. Au bout de 30\n              jours, tu ne pourras plus accéder à ton compte, ni à tes données. Tu devras créer un nouveau compte si tu veux continuer à\n              utiliser l&apos;application.\n            </p>\n            {/* Warning if program has enrollments */}\n            <div className=\"alert alert-warning\">\n              <svg className=\"stroke-current shrink-0 h-6 w-6\" fill=\"none\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n                <path\n                  d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  strokeWidth=\"2\"\n                />\n              </svg>\n              <span>Cette action supprimera toutes les semaines, séances et exercices associés.</span>\n            </div>\n            <div className=\"modal-action\">\n              <button className=\"btn btn-outline\" disabled={isDeleting} onClick={() => setIsModalOpen(false)}>\n                Annuler\n              </button>\n              <button className=\"btn btn-error\" disabled={isDeleting} onClick={handleDelete}>\n                {isDeleting ? (\n                  <>\n                    <span className=\"loading loading-spinner loading-sm\"></span>\n                    Suppression...\n                  </>\n                ) : (\n                  \"Supprimer\"\n                )}\n              </button>\n            </div>\n          </div>\n        </div>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/edit-program-modal.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { X, Plus, Trash2 } from \"lucide-react\";\nimport { ProgramLevel, ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { useI18n } from \"locales/client\";\nimport { allEquipmentValues, getEquipmentTranslation } from \"@/shared/lib/workout-session/equipments\";\n\nimport { updateProgram } from \"../actions/update-program.action\";\n\ninterface EditProgramModalProps {\n  program: {\n    id: string;\n    title: string;\n    titleEn: string;\n    titleEs: string;\n    titlePt: string;\n    titleRu: string;\n    titleZhCn: string;\n    description: string;\n    descriptionEn: string;\n    descriptionEs: string;\n    descriptionPt: string;\n    descriptionRu: string;\n    descriptionZhCn: string;\n    category: string;\n    image: string;\n    level: ProgramLevel;\n    type: ExerciseAttributeValueEnum;\n    durationWeeks: number;\n    sessionsPerWeek: number;\n    sessionDurationMin: number;\n    equipment: ExerciseAttributeValueEnum[];\n    isPremium: boolean;\n    coaches: Array<{\n      id: string;\n      name: string;\n      image: string;\n      order: number;\n    }>;\n  };\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nexport function EditProgramModal({ program, open, onOpenChange }: EditProgramModalProps) {\n  const router = useRouter();\n  const t = useI18n();\n  const [activeTab, setActiveTab] = useState(\"fr\");\n  const [formData, setFormData] = useState({\n    title: program.title,\n    titleEn: program.titleEn,\n    titleEs: program.titleEs,\n    titlePt: program.titlePt,\n    titleRu: program.titleRu,\n    titleZhCn: program.titleZhCn,\n    description: program.description,\n    descriptionEn: program.descriptionEn,\n    descriptionEs: program.descriptionEs,\n    descriptionPt: program.descriptionPt,\n    descriptionRu: program.descriptionRu,\n    descriptionZhCn: program.descriptionZhCn,\n    category: program.category,\n    image: program.image,\n    level: program.level,\n    type: program.type,\n    durationWeeks: program.durationWeeks,\n    sessionsPerWeek: program.sessionsPerWeek,\n    sessionDurationMin: program.sessionDurationMin,\n    equipment: program.equipment,\n    isPremium: program.isPremium,\n    coaches: program.coaches,\n  });\n  const [isSaving, setIsSaving] = useState(false);\n\n  const handleSave = async () => {\n    setIsSaving(true);\n    try {\n      await updateProgram(program.id, formData);\n      setActiveTab(\"fr\");\n      onOpenChange(false);\n      router.refresh();\n    } catch (error) {\n      console.error(\"Error saving program:\", error);\n      alert(error instanceof Error ? error.message : \"Erreur lors de la sauvegarde\");\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  const handleClose = () => {\n    setActiveTab(\"fr\");\n    onOpenChange(false);\n  };\n\n  const handleEquipmentChange = (equipment: ExerciseAttributeValueEnum) => {\n    const newEquipment = formData.equipment.includes(equipment)\n      ? formData.equipment.filter((e) => e !== equipment)\n      : [...formData.equipment, equipment];\n    setFormData({ ...formData, equipment: newEquipment });\n  };\n\n  const addCoach = () => {\n    const newCoaches = [...formData.coaches, { id: `new-${Date.now()}`, name: \"\", image: \"\", order: formData.coaches.length }];\n    setFormData({ ...formData, coaches: newCoaches });\n  };\n\n  const removeCoach = (index: number) => {\n    const newCoaches = formData.coaches.filter((_, i) => i !== index);\n    setFormData({ ...formData, coaches: newCoaches });\n  };\n\n  const updateCoach = (index: number, field: string, value: string) => {\n    const newCoaches = [...formData.coaches];\n    newCoaches[index] = { ...newCoaches[index], [field]: value };\n    setFormData({ ...formData, coaches: newCoaches });\n  };\n\n  if (!open) return null;\n\n  return (\n    <div className=\"modal modal-open modal-middle !mt-0\">\n      <div className=\"modal-box max-w-4xl overflow-y-auto\">\n        <div className=\"flex items-center justify-between mb-6\">\n          <h3 className=\"font-bold text-lg\">Éditer le programme</h3>\n          <button className=\"btn btn-sm btn-circle btn-ghost\" onClick={handleClose}>\n            <X className=\"h-4 w-4\" />\n          </button>\n        </div>\n\n        <div className=\"space-y-6\">\n          {/* Language Tabs */}\n          <div className=\"tabs tabs-boxed\">\n            <button className={`tab ${activeTab === \"fr\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"fr\")} type=\"button\">\n              🇫🇷 FR\n            </button>\n            <button className={`tab ${activeTab === \"en\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"en\")} type=\"button\">\n              🇺🇸 EN\n            </button>\n            <button className={`tab ${activeTab === \"es\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"es\")} type=\"button\">\n              🇪🇸 ES\n            </button>\n            <button className={`tab ${activeTab === \"pt\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"pt\")} type=\"button\">\n              🇵🇹 PT\n            </button>\n            <button className={`tab ${activeTab === \"ru\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"ru\")} type=\"button\">\n              🇷🇺 RU\n            </button>\n            <button className={`tab ${activeTab === \"zh\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"zh\")} type=\"button\">\n              🇨🇳 ZH\n            </button>\n          </div>\n\n          {/* French Fields */}\n          {activeTab === \"fr\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">Titre (Français)</span>\n                </label>\n                <input\n                  className=\"input input-bordered w-full\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, title: e.target.value })}\n                  type=\"text\"\n                  value={formData.title}\n                />\n              </div>\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">Description (Français)</span>\n                </label>\n                <textarea\n                  className=\"textarea textarea-bordered w-full h-24\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, description: e.target.value })}\n                  value={formData.description}\n                />\n              </div>\n            </div>\n          )}\n\n          {/* English Fields */}\n          {activeTab === \"en\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">Title (English)</span>\n                </label>\n                <input\n                  className=\"input input-bordered w-full\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, titleEn: e.target.value })}\n                  type=\"text\"\n                  value={formData.titleEn}\n                />\n              </div>\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">Description (English)</span>\n                </label>\n                <textarea\n                  className=\"textarea textarea-bordered w-full h-24\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, descriptionEn: e.target.value })}\n                  value={formData.descriptionEn}\n                />\n              </div>\n            </div>\n          )}\n\n          {/* Spanish Fields */}\n          {activeTab === \"es\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">Título (Español)</span>\n                </label>\n                <input\n                  className=\"input input-bordered w-full\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, titleEs: e.target.value })}\n                  type=\"text\"\n                  value={formData.titleEs}\n                />\n              </div>\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">Descripción (Español)</span>\n                </label>\n                <textarea\n                  className=\"textarea textarea-bordered w-full h-24\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, descriptionEs: e.target.value })}\n                  value={formData.descriptionEs}\n                />\n              </div>\n            </div>\n          )}\n\n          {/* Portuguese Fields */}\n          {activeTab === \"pt\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">Título (Português)</span>\n                </label>\n                <input\n                  className=\"input input-bordered w-full\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, titlePt: e.target.value })}\n                  type=\"text\"\n                  value={formData.titlePt}\n                />\n              </div>\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">Descrição (Português)</span>\n                </label>\n                <textarea\n                  className=\"textarea textarea-bordered w-full h-24\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, descriptionPt: e.target.value })}\n                  value={formData.descriptionPt}\n                />\n              </div>\n            </div>\n          )}\n\n          {/* Russian Fields */}\n          {activeTab === \"ru\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">Название (Русский)</span>\n                </label>\n                <input\n                  className=\"input input-bordered w-full\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, titleRu: e.target.value })}\n                  type=\"text\"\n                  value={formData.titleRu}\n                />\n              </div>\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">Описание (Русский)</span>\n                </label>\n                <textarea\n                  className=\"textarea textarea-bordered w-full h-24\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, descriptionRu: e.target.value })}\n                  value={formData.descriptionRu}\n                />\n              </div>\n            </div>\n          )}\n\n          {/* Chinese Fields */}\n          {activeTab === \"zh\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">标题 (中文)</span>\n                </label>\n                <input\n                  className=\"input input-bordered w-full\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, titleZhCn: e.target.value })}\n                  type=\"text\"\n                  value={formData.titleZhCn}\n                />\n              </div>\n              <div>\n                <label className=\"label\">\n                  <span className=\"label-text\">描述 (中文)</span>\n                </label>\n                <textarea\n                  className=\"textarea textarea-bordered w-full h-24\"\n                  disabled={isSaving}\n                  onChange={(e) => setFormData({ ...formData, descriptionZhCn: e.target.value })}\n                  value={formData.descriptionZhCn}\n                />\n              </div>\n            </div>\n          )}\n\n          {/* Image et emoji */}\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n            <div>\n              <label className=\"label\">\n                <span className=\"label-text\">Image URL</span>\n              </label>\n              <input\n                className=\"input input-bordered w-full\"\n                disabled={isSaving}\n                onChange={(e) => setFormData({ ...formData, image: e.target.value })}\n                type=\"url\"\n                value={formData.image}\n              />\n            </div>\n          </div>\n\n          {/* Métadonnées */}\n          <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n            <div>\n              <label className=\"label\">\n                <span className=\"label-text\">Catégorie</span>\n              </label>\n              <input\n                className=\"input input-bordered w-full\"\n                disabled={isSaving}\n                onChange={(e) => setFormData({ ...formData, category: e.target.value })}\n                type=\"text\"\n                value={formData.category}\n              />\n            </div>\n            <div>\n              <label className=\"label\">\n                <span className=\"label-text\">Niveau</span>\n              </label>\n              <select\n                className=\"select select-bordered w-full\"\n                disabled={isSaving}\n                onChange={(e) => setFormData({ ...formData, level: e.target.value as ProgramLevel })}\n                value={formData.level}\n              >\n                <option value=\"BEGINNER\">Débutant</option>\n                <option value=\"INTERMEDIATE\">Intermédiaire</option>\n                <option value=\"ADVANCED\">Avancé</option>\n              </select>\n            </div>\n            <div>\n              <label className=\"label\">\n                <span className=\"label-text\">Type</span>\n              </label>\n              <select\n                className=\"select select-bordered w-full\"\n                disabled={isSaving}\n                onChange={(e) => setFormData({ ...formData, type: e.target.value as ExerciseAttributeValueEnum })}\n                value={formData.type}\n              >\n                <option value=\"BODYWEIGHT\">Poids du corps</option>\n                <option value=\"DUMBBELL\">Haltères</option>\n                <option value=\"BARBELL\">Barre</option>\n                <option value=\"KETTLEBELLS\">Kettlebells</option>\n                <option value=\"RESISTANCE_BAND\">Élastiques</option>\n              </select>\n            </div>\n          </div>\n\n          {/* Paramètres du programme */}\n          <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n            <div>\n              <label className=\"label\">\n                <span className=\"label-text\">Durée (semaines)</span>\n              </label>\n              <input\n                className=\"input input-bordered w-full\"\n                disabled={isSaving}\n                min={1}\n                onChange={(e) => setFormData({ ...formData, durationWeeks: parseInt(e.target.value) || 0 })}\n                type=\"number\"\n                value={formData.durationWeeks}\n              />\n            </div>\n            <div>\n              <label className=\"label\">\n                <span className=\"label-text\">Séances/semaine</span>\n              </label>\n              <input\n                className=\"input input-bordered w-full\"\n                disabled={isSaving}\n                min={1}\n                onChange={(e) => setFormData({ ...formData, sessionsPerWeek: parseInt(e.target.value) || 0 })}\n                type=\"number\"\n                value={formData.sessionsPerWeek}\n              />\n            </div>\n            <div>\n              <label className=\"label\">\n                <span className=\"label-text\">Durée séance (min)</span>\n              </label>\n              <input\n                className=\"input input-bordered w-full\"\n                disabled={isSaving}\n                min={1}\n                onChange={(e) => setFormData({ ...formData, sessionDurationMin: parseInt(e.target.value) || 0 })}\n                type=\"number\"\n                value={formData.sessionDurationMin}\n              />\n            </div>\n          </div>\n\n          {/* Équipement */}\n          <div>\n            <label className=\"label\">\n              <span className=\"label-text\">Équipement requis</span>\n            </label>\n            <div className=\"grid grid-cols-2 md:grid-cols-3 gap-2\">\n              {allEquipmentValues.map((equipment) => {\n                const translation = getEquipmentTranslation(equipment, t);\n                return (\n                  <label className=\"label cursor-pointer justify-start gap-2\" key={equipment}>\n                    <input\n                      checked={formData.equipment.includes(equipment)}\n                      className=\"checkbox checkbox-sm\"\n                      disabled={isSaving}\n                      onChange={() => handleEquipmentChange(equipment)}\n                      type=\"checkbox\"\n                    />\n                    <span className=\"label-text text-sm\">{translation.label}</span>\n                  </label>\n                );\n              })}\n            </div>\n          </div>\n\n          {/* Premium */}\n          <div>\n            <label className=\"label cursor-pointer justify-start gap-2\">\n              <input\n                checked={formData.isPremium}\n                className=\"checkbox\"\n                disabled={isSaving}\n                onChange={(e) => setFormData({ ...formData, isPremium: e.target.checked })}\n                type=\"checkbox\"\n              />\n              <span className=\"label-text\">Programme Premium</span>\n            </label>\n          </div>\n\n          {/* Coachs */}\n          <div>\n            <div className=\"flex items-center justify-between mb-4\">\n              <label className=\"label\">\n                <span className=\"label-text font-medium\">Coachs du programme</span>\n              </label>\n              <button className=\"btn btn-sm btn-primary\" disabled={isSaving} onClick={addCoach} type=\"button\">\n                <Plus className=\"h-4 w-4 mr-1\" />\n                Ajouter\n              </button>\n            </div>\n            <div className=\"space-y-4\">\n              {formData.coaches.length === 0 ? (\n                <p className=\"text-base-content/60 text-center py-8\">Aucun coach ajouté. Cliquez sur &quot;Ajouter&quot; pour commencer.</p>\n              ) : (\n                formData.coaches.map((coach, index) => (\n                  <div className=\"flex gap-4 items-end\" key={index}>\n                    <div className=\"flex-1 form-control\">\n                      <label className=\"label\" htmlFor={`coach-name-${index}`}>\n                        <span className=\"label-text\">Nom</span>\n                      </label>\n                      <input\n                        className=\"input input-bordered\"\n                        disabled={isSaving}\n                        id={`coach-name-${index}`}\n                        onChange={(e) => updateCoach(index, \"name\", e.target.value)}\n                        placeholder=\"Nom du coach\"\n                        value={coach.name}\n                      />\n                    </div>\n                    <div className=\"flex-1 form-control\">\n                      <label className=\"label\" htmlFor={`coach-image-${index}`}>\n                        <span className=\"label-text\">URL de l&apos;image</span>\n                      </label>\n                      <input\n                        className=\"input input-bordered\"\n                        disabled={isSaving}\n                        id={`coach-image-${index}`}\n                        onChange={(e) => updateCoach(index, \"image\", e.target.value)}\n                        placeholder=\"https://...\"\n                        value={coach.image}\n                      />\n                    </div>\n                    <button className=\"btn btn-outline btn-sm\" disabled={isSaving} onClick={() => removeCoach(index)} type=\"button\">\n                      <Trash2 className=\"h-4 w-4\" />\n                    </button>\n                  </div>\n                ))\n              )}\n            </div>\n          </div>\n        </div>\n\n        <div className=\"modal-action\">\n          <button className=\"btn btn-ghost\" disabled={isSaving} onClick={handleClose}>\n            Annuler\n          </button>\n          <button className=\"btn btn-primary\" disabled={isSaving} onClick={handleSave}>\n            {isSaving ? (\n              <>\n                <span className=\"loading loading-spinner loading-sm\"></span>\n                Sauvegarde...\n              </>\n            ) : (\n              \"Sauvegarder\"\n            )}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/edit-session-modal.tsx",
    "content": "\"use client\";\n\nimport { z } from \"zod\";\nimport { useForm } from \"react-hook-form\";\nimport { useState } from \"react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\nimport { generateSlugsForAllLanguages } from \"@/shared/lib/slug\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Label } from \"@/components/ui/label\";\nimport { Input } from \"@/components/ui/input\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\n\nimport { SessionWithExercises } from \"../types/program.types\";\nimport { updateSession } from \"../actions/update-session.action\";\n\nconst sessionSchema = z.object({\n  title: z.string().min(1, \"Le titre est requis\"),\n  titleEn: z.string().min(1, \"Le titre en anglais est requis\"),\n  titleEs: z.string().min(1, \"Le titre en espagnol est requis\"),\n  titlePt: z.string().min(1, \"Le titre en portugais est requis\"),\n  titleRu: z.string().min(1, \"Le titre en russe est requis\"),\n  titleZhCn: z.string().min(1, \"Le titre en chinois est requis\"),\n  description: z.string().min(1, \"La description est requise\"),\n  descriptionEn: z.string().min(1, \"La description en anglais est requise\"),\n  descriptionEs: z.string().min(1, \"La description en espagnol est requise\"),\n  descriptionPt: z.string().min(1, \"La description en portugais est requise\"),\n  descriptionRu: z.string().min(1, \"La description en russe est requise\"),\n  descriptionZhCn: z.string().min(1, \"La description en chinois est requise\"),\n  estimatedMinutes: z.number().min(5, \"Au moins 5 minutes\"),\n  isPremium: z.boolean(),\n  equipment: z.array(z.nativeEnum(ExerciseAttributeValueEnum)),\n});\n\ntype SessionFormData = z.infer<typeof sessionSchema>;\n\ninterface EditSessionModalProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  session: SessionWithExercises;\n}\n\nconst EQUIPMENT_OPTIONS = [\n  { value: ExerciseAttributeValueEnum.BODY_ONLY, label: \"Poids du corps\" },\n  { value: ExerciseAttributeValueEnum.DUMBBELL, label: \"Haltères\" },\n  { value: ExerciseAttributeValueEnum.BARBELL, label: \"Barre\" },\n  { value: ExerciseAttributeValueEnum.KETTLEBELLS, label: \"Kettlebells\" },\n  { value: ExerciseAttributeValueEnum.BANDS, label: \"Élastiques\" },\n  { value: ExerciseAttributeValueEnum.MACHINE, label: \"Machines\" },\n  { value: ExerciseAttributeValueEnum.CABLE, label: \"Câbles\" },\n];\n\nexport function EditSessionModal({ open, onOpenChange, session }: EditSessionModalProps) {\n  const [isLoading, setIsLoading] = useState(false);\n  const [activeTab, setActiveTab] = useState(\"fr\");\n  const [selectedEquipment, setSelectedEquipment] = useState<ExerciseAttributeValueEnum[]>(session.equipment);\n\n  const {\n    register,\n    handleSubmit,\n    reset,\n    setValue,\n    formState: { errors },\n  } = useForm<SessionFormData>({\n    resolver: zodResolver(sessionSchema),\n    defaultValues: {\n      title: session.title,\n      titleEn: session.titleEn,\n      titleEs: session.titleEs,\n      titlePt: session.titlePt,\n      titleRu: session.titleRu,\n      titleZhCn: session.titleZhCn,\n      description: session.description,\n      descriptionEn: session.descriptionEn,\n      descriptionEs: session.descriptionEs,\n      descriptionPt: session.descriptionPt,\n      descriptionRu: session.descriptionRu,\n      descriptionZhCn: session.descriptionZhCn,\n      estimatedMinutes: session.estimatedMinutes,\n      isPremium: session.isPremium,\n      equipment: session.equipment,\n    },\n  });\n\n  const toggleEquipment = (equipment: ExerciseAttributeValueEnum) => {\n    const newEquipment = selectedEquipment.includes(equipment)\n      ? selectedEquipment.filter((e) => e !== equipment)\n      : [...selectedEquipment, equipment];\n\n    setSelectedEquipment(newEquipment);\n    setValue(\"equipment\", newEquipment);\n  };\n\n  const onSubmit = async (data: SessionFormData) => {\n    setIsLoading(true);\n    try {\n      // Generate slugs from titles\n      const slugs = generateSlugsForAllLanguages({\n        title: data.title,\n        titleEn: data.titleEn,\n        titleEs: data.titleEs,\n        titlePt: data.titlePt,\n        titleRu: data.titleRu,\n        titleZhCn: data.titleZhCn,\n      });\n\n      await updateSession({\n        sessionId: session.id,\n        ...data,\n        ...slugs,\n      });\n\n      onOpenChange(false);\n      window.location.reload(); // Refresh to show updated session\n    } catch (error) {\n      console.error(\"Error updating session:\", error);\n      alert(\"Erreur lors de la mise à jour de la séance\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleClose = () => {\n    reset();\n    setSelectedEquipment(session.equipment);\n    setActiveTab(\"fr\");\n    onOpenChange(false);\n  };\n\n  return (\n    <Dialog onOpenChange={handleClose} open={open}>\n      <DialogContent className=\"max-w-2xl max-h-[90vh] overflow-y-auto\">\n        <DialogHeader>\n          <DialogTitle>Éditer la séance</DialogTitle>\n        </DialogHeader>\n\n        <form className=\"space-y-4\" onSubmit={handleSubmit(onSubmit)}>\n          {/* Language Tabs */}\n          <div className=\"tabs tabs-boxed\">\n            <button className={`tab ${activeTab === \"fr\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"fr\")} type=\"button\">\n              🇫🇷 FR\n            </button>\n            <button className={`tab ${activeTab === \"en\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"en\")} type=\"button\">\n              🇺🇸 EN\n            </button>\n            <button className={`tab ${activeTab === \"es\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"es\")} type=\"button\">\n              🇪🇸 ES\n            </button>\n            <button className={`tab ${activeTab === \"pt\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"pt\")} type=\"button\">\n              🇵🇹 PT\n            </button>\n            <button className={`tab ${activeTab === \"ru\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"ru\")} type=\"button\">\n              🇷🇺 RU\n            </button>\n            <button className={`tab ${activeTab === \"zh\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"zh\")} type=\"button\">\n              🇨🇳 ZH\n            </button>\n          </div>\n\n          {/* French Fields */}\n          {activeTab === \"fr\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"edit-title\">Titre (Français)</Label>\n                <Input id=\"edit-title\" {...register(\"title\")} />\n                {errors.title && <p className=\"text-sm text-red-500 mt-1\">{errors.title.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"edit-description\">Description (Français)</Label>\n                <Textarea id=\"edit-description\" {...register(\"description\")} rows={3} />\n                {errors.description && <p className=\"text-sm text-red-500 mt-1\">{errors.description.message}</p>}\n              </div>\n            </div>\n          )}\n\n          {/* English Fields */}\n          {activeTab === \"en\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"edit-titleEn\">Title (English)</Label>\n                <Input id=\"edit-titleEn\" {...register(\"titleEn\")} />\n                {errors.titleEn && <p className=\"text-sm text-red-500 mt-1\">{errors.titleEn.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"edit-descriptionEn\">Description (English)</Label>\n                <Textarea id=\"edit-descriptionEn\" {...register(\"descriptionEn\")} rows={3} />\n                {errors.descriptionEn && <p className=\"text-sm text-red-500 mt-1\">{errors.descriptionEn.message}</p>}\n              </div>\n            </div>\n          )}\n\n          {/* Spanish Fields */}\n          {activeTab === \"es\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"edit-titleEs\">Título (Español)</Label>\n                <Input id=\"edit-titleEs\" {...register(\"titleEs\")} />\n                {errors.titleEs && <p className=\"text-sm text-red-500 mt-1\">{errors.titleEs.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"edit-descriptionEs\">Descripción (Español)</Label>\n                <Textarea id=\"edit-descriptionEs\" {...register(\"descriptionEs\")} rows={3} />\n                {errors.descriptionEs && <p className=\"text-sm text-red-500 mt-1\">{errors.descriptionEs.message}</p>}\n              </div>\n            </div>\n          )}\n\n          {/* Portuguese Fields */}\n          {activeTab === \"pt\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"edit-titlePt\">Título (Português)</Label>\n                <Input id=\"edit-titlePt\" {...register(\"titlePt\")} />\n                {errors.titlePt && <p className=\"text-sm text-red-500 mt-1\">{errors.titlePt.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"edit-descriptionPt\">Descrição (Português)</Label>\n                <Textarea id=\"edit-descriptionPt\" {...register(\"descriptionPt\")} rows={3} />\n                {errors.descriptionPt && <p className=\"text-sm text-red-500 mt-1\">{errors.descriptionPt.message}</p>}\n              </div>\n            </div>\n          )}\n\n          {/* Russian Fields */}\n          {activeTab === \"ru\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"edit-titleRu\">Название (Русский)</Label>\n                <Input id=\"edit-titleRu\" {...register(\"titleRu\")} />\n                {errors.titleRu && <p className=\"text-sm text-red-500 mt-1\">{errors.titleRu.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"edit-descriptionRu\">Описание (Русский)</Label>\n                <Textarea id=\"edit-descriptionRu\" {...register(\"descriptionRu\")} rows={3} />\n                {errors.descriptionRu && <p className=\"text-sm text-red-500 mt-1\">{errors.descriptionRu.message}</p>}\n              </div>\n            </div>\n          )}\n\n          {/* Chinese Fields */}\n          {activeTab === \"zh\" && (\n            <div className=\"space-y-4\">\n              <div>\n                <Label htmlFor=\"edit-titleZhCn\">标题 (中文)</Label>\n                <Input id=\"edit-titleZhCn\" {...register(\"titleZhCn\")} />\n                {errors.titleZhCn && <p className=\"text-sm text-red-500 mt-1\">{errors.titleZhCn.message}</p>}\n              </div>\n              <div>\n                <Label htmlFor=\"edit-descriptionZhCn\">描述 (中文)</Label>\n                <Textarea id=\"edit-descriptionZhCn\" {...register(\"descriptionZhCn\")} rows={3} />\n                {errors.descriptionZhCn && <p className=\"text-sm text-red-500 mt-1\">{errors.descriptionZhCn.message}</p>}\n              </div>\n            </div>\n          )}\n\n          <div className=\"grid grid-cols-2 gap-4\">\n            <div>\n              <Label htmlFor=\"edit-estimatedMinutes\">Durée estimée (minutes)</Label>\n              <Input id=\"edit-estimatedMinutes\" min=\"5\" type=\"number\" {...register(\"estimatedMinutes\", { valueAsNumber: true })} />\n              {errors.estimatedMinutes && <p className=\"text-sm text-red-500 mt-1\">{errors.estimatedMinutes.message}</p>}\n            </div>\n            <div className=\"flex items-center space-x-2 pt-8\">\n              <Switch\n                defaultChecked={session.isPremium}\n                id=\"edit-isPremium\"\n                onCheckedChange={(checked) => setValue(\"isPremium\", checked)}\n              />\n              <Label htmlFor=\"edit-isPremium\">Séance premium</Label>\n            </div>\n          </div>\n\n          <div>\n            <Label>Équipement requis</Label>\n            <div className=\"flex flex-wrap gap-2 mt-2\">\n              {EQUIPMENT_OPTIONS.map((option) => (\n                <Badge\n                  className=\"cursor-pointer\"\n                  key={option.value}\n                  onClick={() => toggleEquipment(option.value)}\n                  variant={selectedEquipment.includes(option.value) ? \"default\" : \"outline\"}\n                >\n                  {option.label}\n                </Badge>\n              ))}\n            </div>\n          </div>\n\n          <div className=\"flex justify-end gap-2 pt-4\">\n            <Button onClick={handleClose} type=\"button\" variant=\"outline\">\n              Annuler\n            </Button>\n            <Button disabled={isLoading} type=\"submit\">\n              {isLoading ? \"Mise à jour...\" : \"Mettre à jour\"}\n            </Button>\n          </div>\n        </form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/edit-sets-modal.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { X, Plus, Minus, Trash2 } from \"lucide-react\";\nimport { ProgramSessionExercise, ProgramSuggestedSet } from \"@prisma/client\";\n\nimport { AVAILABLE_WORKOUT_SET_TYPES, MAX_WORKOUT_SET_COLUMNS, WORKOUT_SET_UNITS_TUPLE } from \"@/shared/constants/workout-set-types\";\nimport { WorkoutSetType, WorkoutSetUnit } from \"@/features/workout-session/types/workout-set\";\n\nimport { updateExerciseSets } from \"../actions/update-exercise-sets.action\";\n\ninterface EditSetsModalProps {\n  exercise: ProgramSessionExercise & {\n    exercise: { name: string };\n    suggestedSets: ProgramSuggestedSet[];\n  };\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\ninterface SetData {\n  id?: string;\n  setIndex: number;\n  types: WorkoutSetType[];\n  valuesInt: number[];\n  valuesSec: number[];\n  units: WorkoutSetUnit[];\n}\n\nexport function EditSetsModal({ exercise, open, onOpenChange }: EditSetsModalProps) {\n  const router = useRouter();\n  const [sets, setSets] = useState<SetData[]>(() =>\n    exercise.suggestedSets.length > 0\n      ? exercise.suggestedSets.map((set) => ({\n          id: set.id,\n          setIndex: set.setIndex,\n          types: set.types as WorkoutSetType[],\n          valuesInt: set.valuesInt,\n          valuesSec: set.valuesSec,\n          units: set.units as WorkoutSetUnit[],\n        }))\n      : [\n          {\n            setIndex: 0,\n            types: [\"WEIGHT\", \"REPS\"] as WorkoutSetType[],\n            valuesInt: [20, 10],\n            valuesSec: [],\n            units: [\"kg\"] as WorkoutSetUnit[],\n          },\n        ]\n  );\n\n  const addSet = () => {\n    const newSet: SetData = {\n      setIndex: sets.length,\n      types: [\"WEIGHT\", \"REPS\"] as WorkoutSetType[],\n      valuesInt: [20, 10],\n      valuesSec: [],\n      units: [\"kg\"] as WorkoutSetUnit[],\n    };\n    setSets([...sets, newSet]);\n  };\n\n  const removeSet = (index: number) => {\n    setSets(sets.filter((_, i) => i !== index).map((set, i) => ({ ...set, setIndex: i })));\n  };\n\n  const updateSet = (setIndex: number, data: Partial<SetData>) => {\n    setSets(sets.map((set, i) => (i === setIndex ? { ...set, ...data } : set)));\n  };\n\n  const handleTypeChange = (setIndex: number, columnIndex: number, newType: WorkoutSetType) => {\n    const set = sets[setIndex];\n    const newTypes = [...set.types];\n    newTypes[columnIndex] = newType;\n    updateSet(setIndex, { types: newTypes });\n  };\n\n  const handleValueIntChange = (setIndex: number, columnIndex: number, value: number) => {\n    const set = sets[setIndex];\n    const newValuesInt = [...set.valuesInt];\n    newValuesInt[columnIndex] = value;\n    updateSet(setIndex, { valuesInt: newValuesInt });\n  };\n\n  const handleValueSecChange = (setIndex: number, columnIndex: number, value: number) => {\n    const set = sets[setIndex];\n    const newValuesSec = [...set.valuesSec];\n    newValuesSec[columnIndex] = value;\n    updateSet(setIndex, { valuesSec: newValuesSec });\n  };\n\n  const handleUnitChange = (setIndex: number, columnIndex: number, newUnit: WorkoutSetUnit) => {\n    const set = sets[setIndex];\n    const newUnits = [...set.units];\n    newUnits[columnIndex] = newUnit;\n    updateSet(setIndex, { units: newUnits });\n  };\n\n  const addColumn = (setIndex: number) => {\n    const set = sets[setIndex];\n    if (set.types.length < MAX_WORKOUT_SET_COLUMNS) {\n      const firstAvailableType = AVAILABLE_WORKOUT_SET_TYPES.find((t) => !set.types.includes(t));\n      if (firstAvailableType) {\n        const newTypes = [...set.types, firstAvailableType];\n        updateSet(setIndex, { types: newTypes });\n      }\n    }\n  };\n\n  const removeColumn = (setIndex: number, columnIndex: number) => {\n    const set = sets[setIndex];\n    const newTypes = set.types.filter((_, idx) => idx !== columnIndex);\n    const newValuesInt = set.valuesInt.filter((_, idx) => idx !== columnIndex);\n    const newValuesSec = set.valuesSec.filter((_, idx) => idx !== columnIndex);\n    const newUnits = set.units.filter((_, idx) => idx !== columnIndex);\n\n    updateSet(setIndex, {\n      types: newTypes,\n      valuesInt: newValuesInt,\n      valuesSec: newValuesSec,\n      units: newUnits,\n    });\n  };\n\n  const renderInputForType = (type: WorkoutSetType, setIndex: number, columnIndex: number) => {\n    const set = sets[setIndex];\n\n    switch (type) {\n      case \"TIME\":\n        return (\n          <div className=\"flex gap-1 w-full\">\n            <input\n              className=\"input input-bordered input-sm w-1/2 text-center font-semibold\"\n              min={0}\n              onChange={(e) => handleValueIntChange(setIndex, columnIndex, parseInt(e.target.value) || 0)}\n              placeholder=\"min\"\n              type=\"number\"\n              value={set.valuesInt[columnIndex] ?? \"\"}\n            />\n            <input\n              className=\"input input-bordered input-sm w-1/2 text-center font-semibold\"\n              max={59}\n              min={0}\n              onChange={(e) => handleValueSecChange(setIndex, columnIndex, parseInt(e.target.value) || 0)}\n              placeholder=\"sec\"\n              type=\"number\"\n              value={set.valuesSec[columnIndex] ?? \"\"}\n            />\n          </div>\n        );\n      case \"WEIGHT\":\n        return (\n          <div className=\"flex gap-1 w-full items-center\">\n            <input\n              className=\"input input-bordered input-sm w-1/2 text-center font-semibold\"\n              min={0}\n              onChange={(e) => handleValueIntChange(setIndex, columnIndex, parseInt(e.target.value) || 0)}\n              type=\"number\"\n              value={set.valuesInt[columnIndex] ?? \"\"}\n            />\n            <select\n              className=\"select select-bordered select-sm w-1/2 font-semibold\"\n              onChange={(e) => handleUnitChange(setIndex, columnIndex, e.target.value as WorkoutSetUnit)}\n              value={set.units[columnIndex] ?? \"kg\"}\n            >\n              {WORKOUT_SET_UNITS_TUPLE.map((unit) => (\n                <option key={unit} value={unit}>\n                  {unit}\n                </option>\n              ))}\n            </select>\n          </div>\n        );\n      case \"REPS\":\n        return (\n          <input\n            className=\"input input-bordered input-sm w-full text-center font-semibold\"\n            min={0}\n            onChange={(e) => handleValueIntChange(setIndex, columnIndex, parseInt(e.target.value) || 0)}\n            type=\"number\"\n            value={set.valuesInt[columnIndex] ?? \"\"}\n          />\n        );\n      case \"BODYWEIGHT\":\n        return (\n          <input\n            className=\"input input-bordered input-sm w-full text-center font-semibold\"\n            readOnly\n            value=\"✔\"\n          />\n        );\n      default:\n        return null;\n    }\n  };\n\n  const [isSaving, setIsSaving] = useState(false);\n\n  const handleSave = async () => {\n    setIsSaving(true);\n    try {\n      await updateExerciseSets(exercise.id, sets);\n      onOpenChange(false);\n      router.refresh();\n    } catch (error) {\n      console.error(\"Error saving sets:\", error);\n      alert(error instanceof Error ? error.message : \"Erreur lors de la sauvegarde\");\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  const getTypeLabel = (type: WorkoutSetType): string => {\n    const labels: Record<WorkoutSetType, string> = {\n      TIME: \"Temps\",\n      WEIGHT: \"Poids\",\n      REPS: \"Répétitions\",\n      BODYWEIGHT: \"Poids du corps\",\n      NA: \"N/A\",\n    };\n    return labels[type];\n  };\n\n  if (!open) return null;\n\n  return (\n    <div className=\"modal modal-open\">\n      <div className=\"modal-box max-w-6xl max-h-[90vh] overflow-y-auto\">\n        <div className=\"flex items-center justify-between mb-6\">\n          <h3 className=\"font-bold text-lg\">\n            Éditer les séries - {exercise.exercise.name}\n          </h3>\n          <button className=\"btn btn-sm btn-circle btn-ghost\" onClick={() => onOpenChange(false)}>\n            <X className=\"h-4 w-4\" />\n          </button>\n        </div>\n\n        <div className=\"space-y-6\">\n          {sets.map((set, setIndex) => (\n            <div className=\"card bg-base-200 shadow-sm\" key={set.id || setIndex}>\n              <div className=\"card-body p-4\">\n                <div className=\"flex items-center justify-between mb-4\">\n                  <div className=\"badge badge-primary font-semibold\">\n                    SÉRIE {setIndex + 1}\n                  </div>\n                  <button\n                    className=\"btn btn-sm btn-error btn-outline\"\n                    disabled={sets.length <= 1}\n                    onClick={() => removeSet(setIndex)}\n                  >\n                    <Trash2 className=\"h-4 w-4\" />\n                  </button>\n                </div>\n\n                {/* Columns */}\n                <div className=\"flex flex-col gap-4\">\n                  <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4\">\n                    {set.types.map((type, columnIndex) => {\n                      const availableTypes = AVAILABLE_WORKOUT_SET_TYPES.filter(\n                        (option) => !set.types.includes(option) || option === type\n                      );\n\n                      return (\n                        <div className=\"space-y-2\" key={columnIndex}>\n                          <div className=\"flex items-center gap-1\">\n                            <select\n                              className=\"select select-bordered select-sm font-semibold flex-1\"\n                              onChange={(e) => handleTypeChange(setIndex, columnIndex, e.target.value as WorkoutSetType)}\n                              value={type}\n                            >\n                              {availableTypes.map((availableType) => (\n                                <option key={availableType} value={availableType}>\n                                  {getTypeLabel(availableType)}\n                                </option>\n                              ))}\n                            </select>\n                            {set.types.length > 1 && (\n                              <button\n                                className=\"btn btn-sm btn-error btn-outline\"\n                                onClick={() => removeColumn(setIndex, columnIndex)}\n                              >\n                                <Minus className=\"h-3 w-3\" />\n                              </button>\n                            )}\n                          </div>\n                          {renderInputForType(type, setIndex, columnIndex)}\n                        </div>\n                      );\n                    })}\n                  </div>\n\n                  {/* Add column button */}\n                  {set.types.length < MAX_WORKOUT_SET_COLUMNS && (\n                    <button\n                      className=\"btn btn-sm btn-outline w-full\"\n                      onClick={() => addColumn(setIndex)}\n                    >\n                      <Plus className=\"h-4 w-4 mr-2\" />\n                      Ajouter une colonne\n                    </button>\n                  )}\n                </div>\n              </div>\n            </div>\n          ))}\n\n          <button className=\"btn btn-outline w-full\" onClick={addSet}>\n            <Plus className=\"h-4 w-4 mr-2\" />\n            Ajouter une série\n          </button>\n        </div>\n\n        <div className=\"modal-action\">\n          <button \n            className=\"btn btn-ghost\" \n            disabled={isSaving}\n            onClick={() => onOpenChange(false)}\n          >\n            Annuler\n          </button>\n          <button \n            className=\"btn btn-primary\" \n            disabled={isSaving}\n            onClick={handleSave}\n          >\n            {isSaving ? (\n              <>\n                <span className=\"loading loading-spinner loading-sm\"></span>\n                Sauvegarde...\n              </>\n            ) : (\n              \"Sauvegarder\"\n            )}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}"
  },
  {
    "path": "src/features/admin/programs/ui/edit-week-modal.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { X } from \"lucide-react\";\n\nimport { updateWeek } from \"../actions/update-week.action\";\n\ninterface EditWeekModalProps {\n  week: {\n    id: string;\n    weekNumber: number;\n    title: string;\n    titleEn: string;\n    titleEs: string;\n    titlePt: string;\n    titleRu: string;\n    titleZhCn: string;\n    description: string;\n    descriptionEn: string;\n    descriptionEs: string;\n    descriptionPt: string;\n    descriptionRu: string;\n    descriptionZhCn: string;\n  };\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nexport function EditWeekModal({ week, open, onOpenChange }: EditWeekModalProps) {\n  const router = useRouter();\n  const [activeTab, setActiveTab] = useState(\"fr\");\n  const [formData, setFormData] = useState({\n    title: week.title,\n    titleEn: week.titleEn,\n    titleEs: week.titleEs,\n    titlePt: week.titlePt,\n    titleRu: week.titleRu,\n    titleZhCn: week.titleZhCn,\n    description: week.description,\n    descriptionEn: week.descriptionEn,\n    descriptionEs: week.descriptionEs,\n    descriptionPt: week.descriptionPt,\n    descriptionRu: week.descriptionRu,\n    descriptionZhCn: week.descriptionZhCn,\n  });\n  const [isSaving, setIsSaving] = useState(false);\n\n  const handleSubmit = async (e: React.FormEvent) => {\n    e.preventDefault();\n    setIsSaving(true);\n    try {\n      await updateWeek(week.id, formData);\n      setActiveTab(\"fr\");\n      onOpenChange(false);\n      router.refresh();\n    } catch (error) {\n      console.error(\"Error saving week:\", error);\n      alert(error instanceof Error ? error.message : \"Erreur lors de la sauvegarde\");\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  const handleClose = () => {\n    setActiveTab(\"fr\");\n    onOpenChange(false);\n  };\n\n  if (!open) return null;\n\n  return (\n    <div className=\"modal modal-open modal-middle !mt-0\">\n      <div className=\"modal-box max-w-4xl overflow-y-auto\">\n        <div className=\"flex items-center justify-between mb-6\">\n          <h3 className=\"font-bold text-lg\">Éditer la Semaine {week.weekNumber}</h3>\n          <button className=\"btn btn-sm btn-circle btn-ghost\" onClick={handleClose}>\n            <X className=\"h-4 w-4\" />\n          </button>\n        </div>\n\n        <form onSubmit={handleSubmit}>\n          <div className=\"space-y-6\">\n            {/* Language Tabs */}\n            <div className=\"tabs tabs-boxed\">\n              <button className={`tab ${activeTab === \"fr\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"fr\")} type=\"button\">\n                🇫🇷 FR\n              </button>\n              <button className={`tab ${activeTab === \"en\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"en\")} type=\"button\">\n                🇺🇸 EN\n              </button>\n              <button className={`tab ${activeTab === \"es\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"es\")} type=\"button\">\n                🇪🇸 ES\n              </button>\n              <button className={`tab ${activeTab === \"pt\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"pt\")} type=\"button\">\n                🇵🇹 PT\n              </button>\n              <button className={`tab ${activeTab === \"ru\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"ru\")} type=\"button\">\n                🇷🇺 RU\n              </button>\n              <button className={`tab ${activeTab === \"zh\" ? \"tab-active\" : \"\"}`} onClick={() => setActiveTab(\"zh\")} type=\"button\">\n                🇨🇳 ZH\n              </button>\n            </div>\n\n            {/* French Fields */}\n            {activeTab === \"fr\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Titre (Français)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, title: e.target.value })}\n                    placeholder=\"Titre de la semaine\"\n                    required\n                    type=\"text\"\n                    value={formData.title}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Description (Français)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, description: e.target.value })}\n                    placeholder=\"Description de cette semaine...\"\n                    value={formData.description}\n                  />\n                </div>\n              </div>\n            )}\n\n            {/* English Fields */}\n            {activeTab === \"en\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Title (English)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, titleEn: e.target.value })}\n                    placeholder=\"Week title\"\n                    required\n                    type=\"text\"\n                    value={formData.titleEn}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Description (English)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, descriptionEn: e.target.value })}\n                    placeholder=\"Week description...\"\n                    value={formData.descriptionEn}\n                  />\n                </div>\n              </div>\n            )}\n\n            {/* Spanish Fields */}\n            {activeTab === \"es\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Título (Español)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, titleEs: e.target.value })}\n                    placeholder=\"Título de la semana\"\n                    required\n                    type=\"text\"\n                    value={formData.titleEs}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Descripción (Español)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, descriptionEs: e.target.value })}\n                    placeholder=\"Descripción de la semana...\"\n                    value={formData.descriptionEs}\n                  />\n                </div>\n              </div>\n            )}\n\n            {/* Portuguese Fields */}\n            {activeTab === \"pt\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Título (Português)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, titlePt: e.target.value })}\n                    placeholder=\"Título da semana\"\n                    required\n                    type=\"text\"\n                    value={formData.titlePt}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Descrição (Português)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, descriptionPt: e.target.value })}\n                    placeholder=\"Descrição da semana...\"\n                    value={formData.descriptionPt}\n                  />\n                </div>\n              </div>\n            )}\n\n            {/* Russian Fields */}\n            {activeTab === \"ru\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Название (Русский)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, titleRu: e.target.value })}\n                    placeholder=\"Название недели\"\n                    required\n                    type=\"text\"\n                    value={formData.titleRu}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">Описание (Русский)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, descriptionRu: e.target.value })}\n                    placeholder=\"Описание недели...\"\n                    value={formData.descriptionRu}\n                  />\n                </div>\n              </div>\n            )}\n\n            {/* Chinese Fields */}\n            {activeTab === \"zh\" && (\n              <div className=\"space-y-4\">\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">标题 (中文)</span>\n                  </label>\n                  <input\n                    className=\"input input-bordered w-full\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, titleZhCn: e.target.value })}\n                    placeholder=\"周标题\"\n                    required\n                    type=\"text\"\n                    value={formData.titleZhCn}\n                  />\n                </div>\n                <div>\n                  <label className=\"label\">\n                    <span className=\"label-text\">描述 (中文)</span>\n                  </label>\n                  <textarea\n                    className=\"textarea textarea-bordered w-full h-24\"\n                    disabled={isSaving}\n                    onChange={(e) => setFormData({ ...formData, descriptionZhCn: e.target.value })}\n                    placeholder=\"本周描述...\"\n                    value={formData.descriptionZhCn}\n                  />\n                </div>\n              </div>\n            )}\n          </div>\n\n          <div className=\"modal-action\">\n            <button className=\"btn btn-ghost\" disabled={isSaving} onClick={handleClose} type=\"button\">\n              Annuler\n            </button>\n            <button className=\"btn btn-primary\" disabled={isSaving} type=\"submit\">\n              {isSaving ? (\n                <>\n                  <span className=\"loading loading-spinner loading-sm\"></span>\n                  Sauvegarde...\n                </>\n              ) : (\n                \"Sauvegarder\"\n              )}\n            </button>\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/program-builder.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { Plus, Calendar, Clock, Users, ArrowLeft, Edit } from \"lucide-react\";\n\nimport { ProgramWithFullDetails } from \"../types/program.types\";\nimport { WeekCard } from \"./week-card\";\nimport { VisibilityBadge } from \"./visibility-badge\";\nimport { EditProgramModal } from \"./edit-program-modal\";\nimport { AddWeekModal } from \"./add-week-modal\";\n\ninterface ProgramBuilderProps {\n  program: ProgramWithFullDetails;\n}\n\nexport function ProgramBuilder({ program }: ProgramBuilderProps) {\n  const [isAddWeekModalOpen, setIsAddWeekModalOpen] = useState(false);\n  const [isEditProgramModalOpen, setIsEditProgramModalOpen] = useState(false);\n\n  return (\n    <div className=\"space-y-6\">\n      {/* Header */}\n      <div className=\"flex items-center gap-4\">\n        <Link className=\"btn btn-ghost btn-sm\" href=\"/admin/programs\">\n          <ArrowLeft className=\"h-4 w-4 mr-2\" />\n          Retour aux programmes\n        </Link>\n      </div>\n\n      {/* Program Overview */}\n      <div className=\"card bg-base-100 shadow-xl\">\n        <div className=\"card-body\">\n          <div className=\"flex items-start justify-between\">\n            <div className=\"flex items-start gap-4\">\n              <div className=\"relative w-24 h-24 rounded-lg overflow-hidden\">\n                <Image alt={program.title} className=\"object-cover\" fill src={program.image} />\n              </div>\n              <div>\n                <div className=\"flex items-center gap-2 mb-2\">\n                  <h1 className=\"text-2xl font-bold\">{program.title}</h1>\n                </div>\n                <p className=\"text-base-content/60 mb-3\">{program.description}</p>\n                <div className=\"flex gap-2\">\n                  <VisibilityBadge currentVisibility={program.visibility} programId={program.id} />\n                  <div className={`badge ${program.isPremium ? \"badge-primary\" : \"badge-secondary\"}`}>\n                    {program.isPremium ? \"Premium\" : \"Gratuit\"}\n                  </div>\n                  <div className=\"badge badge-outline\">{program.level}</div>\n                  <div className=\"badge badge-outline\">{program.category}</div>\n                </div>\n              </div>\n            </div>\n            <div className=\"flex flex-col items-end gap-2\">\n              <button className=\"btn btn-sm btn-ghost\" onClick={() => setIsEditProgramModalOpen(true)} title=\"Éditer le programme\">\n                <Edit className=\"h-4 w-4\" />\n              </button>\n              <div className=\"text-right\">\n                <div className=\"text-sm text-base-content/60\">Participants</div>\n                <div className=\"text-2xl font-bold\">{program.participantCount}</div>\n              </div>\n            </div>\n          </div>\n          <div className=\"divider\"></div>\n          <div className=\"grid grid-cols-4 gap-4 text-sm\">\n            <div className=\"flex items-center gap-2\">\n              <Calendar className=\"h-4 w-4 text-base-content/60\" />\n              <span>{program.durationWeeks} semaines</span>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <Clock className=\"h-4 w-4 text-base-content/60\" />\n              <span>{program.sessionDurationMin} min/séance</span>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <Users className=\"h-4 w-4 text-base-content/60\" />\n              <span>{program.sessionsPerWeek} séances/semaine</span>\n            </div>\n            <div>\n              <span className=\"text-base-content/60\">Équipement: </span>\n              <span>{program.equipment.join(\", \") || \"Aucun\"}</span>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Coaches Section */}\n      <div className=\"card bg-base-100 shadow-xl\">\n        <div className=\"card-body\">\n          <div className=\"flex items-center justify-between mb-4\">\n            <h3 className=\"text-lg font-semibold\">Coachs ({program.coaches.length})</h3>\n            <button className=\"btn btn-sm btn-primary\" onClick={() => setIsEditProgramModalOpen(true)} title=\"Éditer les coachs\">\n              <Edit className=\"h-4 w-4 mr-2\" />\n              Éditer les coachs\n            </button>\n          </div>\n\n          {program.coaches.length === 0 ? (\n            <div className=\"flex flex-col items-center justify-center py-8\">\n              <Users className=\"h-12 w-12 text-base-content/60 mb-4\" />\n              <h4 className=\"text-lg font-semibold mb-2\">Aucun coach assigné</h4>\n              <p className=\"text-base-content/60 text-center max-w-md mb-4\">\n                Ajoutez des coachs pour présenter les experts qui accompagneront les utilisateurs.\n              </p>\n              <button className=\"btn btn-primary btn-sm\" onClick={() => setIsEditProgramModalOpen(true)}>\n                <Plus className=\"h-4 w-4 mr-2\" />\n                Ajouter un coach\n              </button>\n            </div>\n          ) : (\n            <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\">\n              {program.coaches.map((coach) => (\n                <div className=\"flex items-center gap-3 p-3 bg-base-200 rounded-lg\" key={coach.id}>\n                  <div className=\"relative w-12 h-12 rounded-full overflow-hidden\">\n                    <Image alt={coach.name} className=\"object-cover\" fill src={coach.image} />\n                  </div>\n                  <div className=\"flex-1\">\n                    <h5 className=\"font-medium\">{coach.name}</h5>\n                    <span className=\"text-xs text-base-content/60\">Coach #{coach.order + 1}</span>\n                  </div>\n                </div>\n              ))}\n            </div>\n          )}\n        </div>\n      </div>\n\n      {/* Content Tabs */}\n      <div className=\"space-y-6\">\n        <div className=\"space-y-6\">\n          {/* Add Week Button */}\n          <div className=\"flex justify-between items-center\">\n            <h3 className=\"text-lg font-semibold\">\n              Semaines ({program.weeks.length}/{program.durationWeeks})\n            </h3>\n            <button className=\"btn btn-primary\" onClick={() => setIsAddWeekModalOpen(true)}>\n              <Plus className=\"h-4 w-4 mr-2\" />\n              Ajouter une semaine\n            </button>\n          </div>\n\n          {/* Weeks List */}\n          <div className=\"space-y-4\">\n            {program.weeks.length === 0 ? (\n              <div className=\"card bg-base-100 shadow-xl\">\n                <div className=\"card-body flex flex-col items-center justify-center py-12\">\n                  <Calendar className=\"h-12 w-12 text-base-content/60 mb-4\" />\n                  <h4 className=\"text-lg font-semibold mb-2\">Aucune semaine créée</h4>\n                  <p className=\"text-base-content/60 text-center max-w-md mb-4\">\n                    Commencez par ajouter la première semaine de votre programme.\n                  </p>\n                  <button className=\"btn btn-primary\" onClick={() => setIsAddWeekModalOpen(true)}>\n                    <Plus className=\"h-4 w-4 mr-2\" />\n                    Ajouter la première semaine\n                  </button>\n                </div>\n              </div>\n            ) : (\n              program.weeks.map((week) => <WeekCard key={week.id} week={week} />)\n            )}\n          </div>\n        </div>\n\n        {/* Analytics Tab Content */}\n        <div className=\"hidden\" id=\"analytics-content\">\n          <div className=\"card bg-base-100 shadow-xl\">\n            <div className=\"card-body\">\n              <h2 className=\"card-title\">Statistiques</h2>\n              <p className=\"text-base-content/60\">Analytics et métriques du programme (à implémenter)</p>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Add Week Modal */}\n      <AddWeekModal\n        nextWeekNumber={program.weeks.length + 1}\n        onOpenChange={setIsAddWeekModalOpen}\n        open={isAddWeekModalOpen}\n        programId={program.id}\n      />\n\n      {/* Edit Program Modal */}\n      <EditProgramModal onOpenChange={setIsEditProgramModalOpen} open={isEditProgramModalOpen} program={program} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/programs-list.tsx",
    "content": "import Link from \"next/link\";\nimport Image from \"next/image\";\nimport { Eye, Edit, Users, Dumbbell } from \"lucide-react\";\n\nimport { getPrograms } from \"../actions/get-programs.action\";\nimport { VisibilityBadge } from \"./visibility-badge\";\nimport { DeleteProgramButton } from \"./delete-program-button\";\n\nexport async function ProgramsList() {\n  const programs = await getPrograms();\n\n  if (programs.length === 0) {\n    return (\n      <div className=\"card bg-base-100 shadow-xl\">\n        <div className=\"card-body flex flex-col items-center justify-center py-12\">\n          <Dumbbell className=\"h-12 w-12 text-base-content/60 mb-4\" />\n          <h3 className=\"text-lg font-semibold mb-2\">Aucun programme</h3>\n          <p className=\"text-base-content/60 text-center max-w-md\">Commencez par créer votre premier programme d&apos;entraînement.</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"overflow-x-auto\">\n      <table className=\"table table-zebra\">\n        <thead>\n          <tr>\n            <th>Programme</th>\n            <th>Statut</th>\n            <th>Durée</th>\n            <th>Contenu</th>\n            <th>Inscrits</th>\n            <th>Actions</th>\n          </tr>\n        </thead>\n        <tbody>\n          {programs.map((program) => (\n            <tr key={program.id}>\n              <td>\n                <div className=\"flex items-center gap-3\">\n                  <div className=\"avatar\">\n                    <div className=\"mask mask-squircle w-12 h-12\">\n                      <Image alt={program.title} height={48} src={program.image} width={48} />\n                    </div>\n                  </div>\n                  <div>\n                    <div className=\"font-bold flex items-center gap-2\">{program.title}</div>\n                    <div className=\"text-sm opacity-50 line-clamp-2 max-w-xs\">{program.description}</div>\n                    <div className=\"flex gap-1 mt-1\">\n                      <div className={`badge badge-xs ${program.isPremium ? \"badge-primary\" : \"badge-secondary\"}`}>\n                        {program.isPremium ? \"Premium\" : \"Gratuit\"}\n                      </div>\n                      <div className=\"badge badge-xs badge-outline\">{program.level}</div>\n                      <div className=\"badge badge-xs badge-ghost\">{program.category}</div>\n                    </div>\n                  </div>\n                </div>\n              </td>\n              <td>\n                <VisibilityBadge currentVisibility={program.visibility} programId={program.id} />\n              </td>\n              <td>\n                <div className=\"text-sm\">\n                  <div className=\"font-semibold\">{program.durationWeeks} semaines</div>\n                  <div className=\"text-xs opacity-50\">{program.sessionsPerWeek} séances/sem</div>\n                </div>\n              </td>\n              <td>\n                <div className=\"text-sm\">\n                  <div>\n                    {program.totalWeeks} sem • {program.totalSessions} séances\n                  </div>\n                  <div className=\"text-xs opacity-50\">{program.totalExercises} exercices</div>\n                </div>\n              </td>\n              <td>\n                <div className=\"flex items-center gap-1\">\n                  <Users className=\"h-4 w-4 opacity-50\" />\n                  <span className=\"font-semibold\">{program.totalEnrollments}</span>\n                </div>\n              </td>\n              <td>\n                <div className=\"flex gap-1\">\n                  <Link className=\"btn btn-ghost btn-xs\" href={`/programs/${program.slug}`} target=\"_blank\" title=\"Voir le programme\">\n                    <Eye className=\"h-4 w-4\" />\n                  </Link>\n                  <Link className=\"btn btn-ghost btn-xs\" href={`/admin/programs/${program.id}/edit`} title=\"Gérer le programme\">\n                    <Edit className=\"h-4 w-4\" />\n                  </Link>\n                  <DeleteProgramButton programId={program.id} programTitle={program.title} />\n                </div>\n              </td>\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/session-card.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Plus, Clock, Dumbbell, Settings, ChevronDown, ChevronRight, Edit } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\n\nimport { SessionWithExercises } from \"../types/program.types\";\nimport { EditSetsModal } from \"./edit-sets-modal\";\nimport { EditSessionModal } from \"./edit-session-modal\";\nimport { AddExerciseModal } from \"./add-exercise-modal\";\n\ninterface SessionCardProps {\n  session: SessionWithExercises;\n}\n\nexport function SessionCard({ session }: SessionCardProps) {\n  const t = useI18n();\n  const [isExpanded, setIsExpanded] = useState(false);\n  const [isAddExerciseModalOpen, setIsAddExerciseModalOpen] = useState(false);\n  const [isEditSessionModalOpen, setIsEditSessionModalOpen] = useState(false);\n  const [selectedExercise, setSelectedExercise] = useState<any>(null);\n\n  return (\n    <div className=\"card bg-base-100 shadow-sm border-l-4 border-l-primary\">\n      {/* Header avec boutons séparés du collapse */}\n      <div className=\"card-body\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-3\">\n            <button className=\"btn btn-ghost btn-sm p-1\" onClick={() => setIsExpanded(!isExpanded)}>\n              {isExpanded ? <ChevronDown className=\"h-4 w-4\" /> : <ChevronRight className=\"h-4 w-4\" />}\n            </button>\n            <div>\n              <div className=\"flex items-center gap-2 mb-1\">\n                <h4 className=\"font-semibold\">\n                  {t(\"programs.session\")} {session.sessionNumber}: {session.title}\n                </h4>\n                <div className={`badge badge-sm ${session.isPremium ? \"badge-primary\" : \"badge-outline\"}`}>\n                  {session.isPremium ? \"Premium\" : \"Gratuit\"}\n                </div>\n              </div>\n              <p className=\"text-sm text-base-content/60\">{session.description}</p>\n              <div className=\"flex items-center gap-4 mt-2 text-sm text-base-content/60\">\n                <div className=\"flex items-center gap-1\">\n                  <Clock className=\"h-3 w-3\" />\n                  <span>{session.estimatedMinutes} min</span>\n                </div>\n                <div className=\"flex items-center gap-1\">\n                  <Dumbbell className=\"h-3 w-3\" />\n                  <span>\n                    {session.exercises.length} exercice{session.exercises.length !== 1 ? \"s\" : \"\"}\n                  </span>\n                </div>\n                {session.equipment.length > 0 && (\n                  <div className=\"flex items-center gap-1\">\n                    <Settings className=\"h-3 w-3\" />\n                    <span>{session.equipment.join(\", \")}</span>\n                  </div>\n                )}\n              </div>\n            </div>\n          </div>\n          <div className=\"flex gap-2\">\n            <button className=\"btn btn-sm btn-outline\" onClick={() => setIsEditSessionModalOpen(true)} title=\"Éditer la séance\">\n              <Edit className=\"h-4 w-4\" />\n            </button>\n            <button className=\"btn btn-sm btn-outline\" onClick={() => setIsAddExerciseModalOpen(true)}>\n              <Plus className=\"h-4 w-4 mr-1\" />\n              Exercice\n            </button>\n          </div>\n        </div>\n      </div>\n\n      {/* Contenu collapsible */}\n      {isExpanded && (\n        <div className=\"card-body pt-0\">\n          <div className=\"divider my-2\"></div>\n          {session.exercises.length === 0 ? (\n            <div className=\"text-center py-6 border-2 border-dashed border-base-300 rounded-lg\">\n              <Dumbbell className=\"h-8 w-8 text-base-content/60 mx-auto mb-2\" />\n              <p className=\"text-base-content/60 mb-3\">Aucun exercice dans cette séance</p>\n              <button className=\"btn btn-sm btn-primary\" onClick={() => setIsAddExerciseModalOpen(true)}>\n                <Plus className=\"h-4 w-4 mr-1\" />\n                Ajouter le premier exercice\n              </button>\n            </div>\n          ) : (\n            <div className=\"space-y-2\">\n              {session.exercises.map((exercise) => (\n                <div className=\"flex items-center justify-between p-3 bg-base-200 rounded-lg\" key={exercise.id}>\n                  <div className=\"flex items-center gap-3\">\n                    <div className=\"w-8 h-8 bg-primary text-primary-content rounded-full flex items-center justify-center text-sm font-semibold\">\n                      {exercise.order + 1}\n                    </div>\n                    <div>\n                      <h5 className=\"font-medium\">{exercise.exercise.name}</h5>\n                      <p className=\"text-sm text-base-content/60\">\n                        {exercise.suggestedSets.length} série{exercise.suggestedSets.length !== 1 ? \"s\" : \"\"}\n                      </p>\n                    </div>\n                  </div>\n                  <button className=\"btn btn-sm btn-ghost\" onClick={() => setSelectedExercise(exercise)} title=\"Éditer les séries\">\n                    <Settings className=\"h-4 w-4\" />\n                  </button>\n                </div>\n              ))}\n            </div>\n          )}\n        </div>\n      )}\n\n      <AddExerciseModal\n        nextOrder={session.exercises.length}\n        onOpenChange={setIsAddExerciseModalOpen}\n        open={isAddExerciseModalOpen}\n        sessionId={session.id}\n      />\n\n      {selectedExercise && (\n        <EditSetsModal exercise={selectedExercise} onOpenChange={(open) => !open && setSelectedExercise(null)} open={!!selectedExercise} />\n      )}\n\n      <EditSessionModal onOpenChange={setIsEditSessionModalOpen} open={isEditSessionModalOpen} session={session} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/visibility-badge.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { Eye, EyeOff, Archive, ChevronDown } from \"lucide-react\";\nimport { ProgramVisibility } from \"@prisma/client\";\n\nimport { updateProgramVisibility } from \"../actions/update-program-visibility.action\";\n\ninterface VisibilityBadgeProps {\n  programId: string;\n  currentVisibility: ProgramVisibility;\n}\n\nconst visibilityConfig = {\n  [ProgramVisibility.DRAFT]: {\n    label: \"Brouillon\",\n    icon: EyeOff,\n    color: \"badge-warning\",\n  },\n  [ProgramVisibility.PUBLISHED]: {\n    label: \"Publié\",\n    icon: Eye,\n    color: \"badge-success\",\n  },\n  [ProgramVisibility.ARCHIVED]: {\n    label: \"Archivé\",\n    icon: Archive,\n    color: \"badge-neutral\",\n  },\n};\n\nexport function VisibilityBadge({ programId, currentVisibility }: VisibilityBadgeProps) {\n  const [isUpdating, setIsUpdating] = useState(false);\n  const router = useRouter();\n\n  const config = visibilityConfig[currentVisibility];\n  const Icon = config.icon;\n\n  const handleVisibilityChange = async (newVisibility: ProgramVisibility) => {\n    if (newVisibility === currentVisibility) {\n      return;\n    }\n\n    setIsUpdating(true);\n    try {\n      await updateProgramVisibility(programId, newVisibility);\n      router.refresh();\n    } catch (error) {\n      console.error(\"Error updating visibility:\", error);\n      alert(error instanceof Error ? error.message : \"Erreur lors de la mise à jour\");\n    } finally {\n      setIsUpdating(false);\n    }\n  };\n\n  return (\n    <div className=\"dropdown dropdown-end\">\n      <div className={`badge ${config.color} gap-1 cursor-pointer hover:opacity-80`} role=\"button\" tabIndex={0}>\n        <Icon className=\"w-3 h-3\" />\n        {config.label}\n        <ChevronDown className=\"w-3 h-3\" />\n      </div>\n\n      <ul className=\"dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52\" tabIndex={0}>\n        {Object.entries(ProgramVisibility).map(([key, value]) => {\n          const itemConfig = visibilityConfig[value];\n          const ItemIcon = itemConfig.icon;\n\n          return (\n            <li key={key}>\n              <a\n                className={`flex items-center gap-2 ${currentVisibility === value ? \"active\" : \"\"}`}\n                onClick={() => handleVisibilityChange(value)}\n              >\n                {isUpdating ? <span className=\"loading loading-spinner loading-xs\"></span> : <ItemIcon className=\"w-4 h-4\" />}\n                {itemConfig.label}\n              </a>\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/programs/ui/week-card.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Plus, Clock, ChevronDown, ChevronRight, Edit } from \"lucide-react\";\n\nimport { WeekWithSessions } from \"../types/program.types\";\nimport { SessionCard } from \"./session-card\";\nimport { EditWeekModal } from \"./edit-week-modal\";\nimport { AddSessionModal } from \"./add-session-modal\";\n\ninterface WeekCardProps {\n  week: WeekWithSessions;\n}\n\nexport function WeekCard({ week }: WeekCardProps) {\n  const [isExpanded, setIsExpanded] = useState(true);\n  const [isAddSessionModalOpen, setIsAddSessionModalOpen] = useState(false);\n  const [isEditWeekModalOpen, setIsEditWeekModalOpen] = useState(false);\n\n  return (\n    <div className=\"card bg-base-100 shadow-xl\">\n      {/* Header avec boutons séparés du collapse */}\n      <div className=\"card-body\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-3\">\n            <button className=\"btn btn-ghost btn-sm p-1\" onClick={() => setIsExpanded(!isExpanded)}>\n              {isExpanded ? <ChevronDown className=\"h-4 w-4\" /> : <ChevronRight className=\"h-4 w-4\" />}\n            </button>\n            <div>\n              <h3 className=\"text-lg font-bold\">\n                Semaine {week.weekNumber}: {week.title}\n              </h3>\n              <p className=\"text-sm text-base-content/60 mt-1\">{week.description}</p>\n            </div>\n          </div>\n          <div className=\"flex items-center gap-2\">\n            <div className=\"badge badge-outline\">\n              {week.sessions.length} séance{week.sessions.length !== 1 ? \"s\" : \"\"}\n            </div>\n            <button className=\"btn btn-sm btn-ghost\" onClick={() => setIsEditWeekModalOpen(true)} title=\"Éditer la semaine\">\n              <Edit className=\"h-4 w-4\" />\n            </button>\n            <button className=\"btn btn-sm btn-primary\" onClick={() => setIsAddSessionModalOpen(true)}>\n              <Plus className=\"h-4 w-4 mr-1\" />\n              Séance\n            </button>\n          </div>\n        </div>\n      </div>\n\n      {/* Contenu collapsible */}\n      {isExpanded && (\n        <div className=\"card-body pt-0\">\n          <div className=\"divider my-2\"></div>\n          {week.sessions.length === 0 ? (\n            <div className=\"text-center py-8 border-2 border-dashed border-base-300 rounded-lg\">\n              <Clock className=\"h-8 w-8 text-base-content/60 mx-auto mb-2\" />\n              <p className=\"text-base-content/60 mb-3\">Aucune séance dans cette semaine</p>\n              <button className=\"btn btn-sm btn-primary\" onClick={() => setIsAddSessionModalOpen(true)}>\n                <Plus className=\"h-4 w-4 mr-1\" />\n                Ajouter la première séance\n              </button>\n            </div>\n          ) : (\n            <div className=\"space-y-3\">\n              {week.sessions.map((session) => (\n                <SessionCard key={session.id} session={session} />\n              ))}\n            </div>\n          )}\n        </div>\n      )}\n\n      <AddSessionModal\n        nextSessionNumber={week.sessions.length + 1}\n        onOpenChange={setIsAddSessionModalOpen}\n        open={isAddSessionModalOpen}\n        weekId={week.id}\n      />\n\n      <EditWeekModal onOpenChange={setIsEditWeekModalOpen} open={isEditWeekModalOpen} week={week} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/admin/users/list/ui/users-table.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useState, useEffect } from \"react\";\nimport { parseAsStringLiteral, useQueryState } from \"nuqs\";\nimport { Eye, ArrowDown, ArrowUp, AlertTriangle } from \"lucide-react\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { authClient } from \"@/features/auth/lib/auth-client\";\nimport { getUsersAction } from \"@/entities/user/model/get-users.actions\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n  Pagination,\n  PaginationContent,\n  PaginationItem,\n  PaginationLink,\n  PaginationNext,\n  PaginationPrevious,\n} from \"@/components/ui/pagination\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\n\n// Define valid sortable columns\nconst sortableColumns = [\"createdAt\", \"email\"] as const;\ntype SortableColumn = (typeof sortableColumns)[number];\n\nconst orderableColumns = [\"asc\", \"desc\"] as const;\ntype OrderableColumn = (typeof orderableColumns)[number];\n\ninterface UsersTableProps {\n  initialUsers: Awaited<ReturnType<typeof getUsersAction>>;\n}\n\nexport function UsersTable({ initialUsers }: UsersTableProps) {\n  const [page, setPage] = useQueryState(\"page\", { defaultValue: \"1\" });\n  const [searchQuery, setSearchQuery] = useQueryState(\"search\", parseAsStringLiteral<string>([]).withDefault(\"\"));\n  const [sortBy, setSortBy] = useQueryState(\"sortBy\", parseAsStringLiteral<SortableColumn>(sortableColumns).withDefault(\"createdAt\"));\n  const [sortOrder, setSortOrder] = useQueryState(\"sortOrder\", parseAsStringLiteral<OrderableColumn>(orderableColumns).withDefault(\"desc\"));\n  const [impersonatingUserId, setImpersonatingUserId] = useState<string | null>(null);\n\n  // Local state for the search input field\n  const [inputValue, setInputValue] = useState(searchQuery);\n\n  const pageNumber = parseInt(page || \"1\", 10);\n\n  // Debounce search effect\n  useEffect(() => {\n    const handler = setTimeout(() => {\n      if (inputValue !== searchQuery) {\n        setSearchQuery(inputValue);\n        setPage(\"1\"); // Reset to first page on new search\n      }\n    }, 500); // 500ms debounce delay\n\n    return () => {\n      clearTimeout(handler);\n    };\n  }, [inputValue, searchQuery, setSearchQuery, setPage]);\n\n  const { data, isLoading, isFetching, isError, error } = useQuery({\n    queryKey: [\"admin-users\", pageNumber, searchQuery, sortBy, sortOrder],\n    queryFn: async () => {\n      try {\n        const result = await getUsersAction({\n          page: pageNumber,\n          limit: 100,\n          search: searchQuery || undefined,\n          sortBy: sortBy as SortableColumn,\n          sortOrder: sortOrder as OrderableColumn,\n        });\n        return result;\n      } catch (error) {\n        console.error(error);\n        return null;\n      }\n    },\n    initialData: initialUsers,\n  });\n\n  const handleSearchInputChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      setInputValue(e.target.value);\n    },\n    [], // No dependencies, setInputValue is stable\n  );\n\n  const handleSort = useCallback(\n    (column: SortableColumn) => {\n      if (sortBy === column) {\n        setSortOrder(sortOrder === \"asc\" ? \"desc\" : \"asc\");\n      } else {\n        setSortBy(column);\n        setSortOrder(\"desc\");\n      }\n      setPage(\"1\");\n    },\n    [sortBy, sortOrder, setSortBy, setSortOrder, setPage],\n  );\n\n  const renderSortIndicator = (column: SortableColumn) => {\n    if (sortBy !== column) return null;\n    return sortOrder === \"asc\" ? <ArrowUp className=\"ml-1 inline h-4 w-4\" /> : <ArrowDown className=\"ml-1 inline h-4 w-4\" />;\n  };\n\n  const handleImpersonate = async (targetUserId: string) => {\n    setImpersonatingUserId(targetUserId);\n    try {\n      const impersonatedSession = await authClient.admin.impersonateUser({\n        userId: targetUserId,\n      });\n\n      if (impersonatedSession && !impersonatedSession.error) {\n        // Success: Reload to apply the new session\n        window.location.reload();\n      } else {\n        console.error(\"Erreur d'impersonnalisation:\", impersonatedSession?.error);\n        alert(`Erreur d'impersonnalisation: ${impersonatedSession?.error?.message || \"Une erreur est survenue.\"}`);\n      }\n    } catch (error) {\n      console.error(\"Exception lors de l'impersonnalisation:\", error);\n      alert(\"Une exception est survenue lors de l'impersonnalisation.\");\n    } finally {\n      setImpersonatingUserId(null);\n    }\n  };\n\n  // Early return for initial loading or critical error before data structure is available\n  if (isLoading) {\n    return (\n      <div className=\"space-y-4\">\n        <div className=\"flex items-center justify-between\">\n          <h2 className=\"text-xl font-semibold\">Tous les utilisateurs</h2>\n          <Skeleton className=\"w-sm h-10 max-w-sm\" />\n        </div>\n        <div className=\"rounded-md border p-4\">\n          <Skeleton className=\"mb-4 h-8 w-full\" />\n          <Skeleton className=\"mb-4 h-8 w-full\" />\n          <Skeleton className=\"h-8 w-full\" />\n        </div>\n      </div>\n    );\n  }\n\n  if (isError && !data?.data) {\n    return (\n      <div className=\"border-destructive bg-destructive/10 space-y-4 rounded-md border p-4\">\n        <div className=\"text-destructive flex items-center\">\n          <AlertTriangle className=\"mr-2 h-5 w-5\" />\n          <h3 className=\"text-lg font-semibold\">Erreur de chargement des utilisateurs</h3>\n        </div>\n        <p className=\"text-destructive/80 text-sm\">\n          Impossible de récupérer la liste des utilisateurs. Veuillez réessayer plus tard. ({error?.message || \"Erreur inconnue\"})\n        </p>\n      </div>\n    );\n  }\n\n  // This check can be removed if the above error/loading states are sufficient\n  // or adjusted if data.data could be null even after successful fetch with no users.\n  if (!data || !data.data) {\n    // This case should ideally be handled by the error state or empty data state below\n    // but kept as a fallback.\n    return <p>Aucune donnée disponible ou erreur de chargement.</p>;\n  }\n\n  const totalPages = data.data.pagination.pages || 1;\n  const tableIsEffectivelyLoading = isFetching; // Use isFetching for background updates\n  const usersToDisplay = data.data.users || [];\n\n  return (\n    <div className=\"space-y-4\">\n      <div className=\"flex items-center justify-between\">\n        <h2 className=\"text-xl font-semibold\">Tous les utilisateurs</h2>\n        <Input className=\"max-w-sm\" onChange={handleSearchInputChange} placeholder=\"Rechercher par ID ou email...\" value={inputValue} />\n      </div>\n\n      <div className=\"rounded-md border\">\n        <Table>\n          <TableHeader>\n            <TableRow>\n              <TableHead>ID</TableHead>\n              <TableHead>Nom</TableHead>\n              <TableHead className=\"hover:bg-muted/50 cursor-pointer\" onClick={() => handleSort(\"email\")}>\n                Email\n                {renderSortIndicator(\"email\")}\n              </TableHead>\n              <TableHead>Rôle</TableHead>\n              <TableHead>Vérifié</TableHead>\n              <TableHead className=\"hover:bg-muted/50 cursor-pointer\" onClick={() => handleSort(\"createdAt\")}>\n                Créé le\n                {renderSortIndicator(\"createdAt\")}\n              </TableHead>\n              <TableHead className=\"text-right\">Actions</TableHead>\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {tableIsEffectivelyLoading && usersToDisplay.length === 0 ? ( // Show skeleton rows if loading and no users yet\n              Array.from({ length: 5 }).map((_, index) => (\n                <TableRow key={`skeleton-${index}`}>\n                  <TableCell colSpan={7}>\n                    <Skeleton className=\"h-6 w-full\" />\n                  </TableCell>\n                </TableRow>\n              ))\n            ) : isError && usersToDisplay.length === 0 ? ( // Show specific error in table if fetch failed\n              <TableRow>\n                <TableCell className=\"text-destructive py-4 text-center\" colSpan={7}>\n                  <div className=\"flex items-center justify-center\">\n                    <AlertTriangle className=\"mr-2 h-5 w-5\" />\n                    <span>Erreur lors du chargement des données.</span>\n                  </div>\n                  <p className=\"text-muted-foreground text-xs\">{error?.message}</p>\n                </TableCell>\n              </TableRow>\n            ) : !tableIsEffectivelyLoading && usersToDisplay.length === 0 ? (\n              <TableRow>\n                <TableCell className=\"py-4 text-center\" colSpan={7}>\n                  Aucun utilisateur trouvé.\n                </TableCell>\n              </TableRow>\n            ) : (\n              usersToDisplay.map((user) => (\n                <TableRow className={tableIsEffectivelyLoading ? \"opacity-50\" : \"\"} key={user.id}>\n                  <TableCell className=\"font-mono text-xs\">{user.id.substring(0, 8)}...</TableCell>\n                  <TableCell>{`${user.firstName || \"\"} ${user.lastName || \"\"}`.trim() || \"-\"}</TableCell>\n                  <TableCell>{user.email}</TableCell>\n                  <TableCell>\n                    <span\n                      className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${\n                        user.role === \"admin\"\n                          ? \"bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200\"\n                          : \"bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200\"\n                      }`}\n                    >\n                      {user.role}\n                    </span>\n                  </TableCell>\n                  <TableCell>\n                    {user.emailVerified ? <span className=\"text-green-600\">✓</span> : <span className=\"text-red-600\">✗</span>}\n                  </TableCell>\n                  <TableCell>{new Date(user.createdAt).toLocaleDateString()}</TableCell>\n                  <TableCell className=\"text-right\">\n                    <Button\n                      disabled={impersonatingUserId === user.id}\n                      onClick={() => handleImpersonate(user.id)}\n                      size=\"small\"\n                      title={`Impersonnaliser ${user.firstName || \"\"} ${user.lastName || \"\"}`}\n                      variant=\"outline\"\n                    >\n                      <Eye className=\"mr-2 h-4 w-4\" />\n                      {impersonatingUserId === user.id ? \"Chargement...\" : \"Impersonnaliser\"}\n                    </Button>\n                  </TableCell>\n                </TableRow>\n              ))\n            )}\n          </TableBody>\n        </Table>\n      </div>\n\n      {totalPages > 1 && (\n        <Pagination>\n          <PaginationContent>\n            <PaginationItem>\n              <PaginationPrevious\n                className={pageNumber <= 1 ? \"pointer-events-none opacity-50\" : \"\"}\n                href=\"#\"\n                onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {\n                  e.preventDefault();\n                  if (pageNumber > 1) setPage((pageNumber - 1).toString());\n                }}\n              />\n            </PaginationItem>\n\n            {Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {\n              const p = i + 1;\n              return (\n                <PaginationItem key={p}>\n                  <PaginationLink\n                    href=\"#\"\n                    isActive={p === pageNumber}\n                    onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {\n                      e.preventDefault();\n                      setPage(p.toString());\n                    }}\n                  >\n                    {p}\n                  </PaginationLink>\n                </PaginationItem>\n              );\n            })}\n\n            <PaginationItem>\n              <PaginationNext\n                className={pageNumber >= totalPages ? \"pointer-events-none opacity-50\" : \"\"}\n                href=\"#\"\n                onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {\n                  e.preventDefault();\n                  if (pageNumber < totalPages) setPage((pageNumber + 1).toString());\n                }}\n              />\n            </PaginationItem>\n          </PaginationContent>\n        </Pagination>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/ads/hooks/useUserSubscription.ts",
    "content": "\"use client\";\n\nimport { useSession } from \"@/features/auth/lib/auth-client\";\n\nexport function useUserSubscription() {\n  const { data: session, ...rest } = useSession();\n  const isPremium = session?.user?.isPremium || false;\n\n  return { isPremium, ...rest };\n}\n"
  },
  {
    "path": "src/features/auth/forgot-password/forgot-password.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const forgotPasswordSchema = z.object({\n  email: z.string().email(\"Adresse e-mail invalide\"),\n});\n\nexport type ForgotPasswordSchema = z.infer<typeof forgotPasswordSchema>;\n"
  },
  {
    "path": "src/features/auth/forgot-password/model/useForgotPassword.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { authClient } from \"@/features/auth/lib/auth-client\";\n\nexport const useForgotPassword = () => {\n  const t = useI18n();\n  const [isLoading, setIsLoading] = useState(false);\n  const [isEmailSent, setIsEmailSent] = useState(false);\n\n  const forgotPassword = async (email: string, setFieldError: (message: string) => void) => {\n    setIsLoading(true);\n    try {\n      const { error } = await authClient.forgetPassword({\n        email,\n        redirectTo: `${getServerUrl()}/${paths.resetPassword}`,\n      });\n\n      if (error) {\n        setFieldError(t(\"error.sending_email\"));\n        return;\n      }\n\n      setIsEmailSent(true);\n    } catch (error) {\n      console.error(error);\n      setFieldError(t(\"error.generic_error\"));\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  return {\n    forgotPassword,\n    isLoading,\n    isEmailSent,\n  };\n};\n"
  },
  {
    "path": "src/features/auth/forgot-password/ui/forgot-password-form.tsx",
    "content": "\"use client\";\n\nimport { useForm } from \"react-hook-form\";\nimport Link from \"next/link\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\nimport { useI18n } from \"locales/client\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { forgotPasswordSchema, ForgotPasswordSchema } from \"@/features/auth/forgot-password/forgot-password.schema\";\nimport { Input } from \"@/components/ui/input\";\nimport { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from \"@/components/ui/form\";\nimport { Button } from \"@/components/ui/button\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\n\nimport { useForgotPassword } from \"../model/useForgotPassword\";\n\nexport const ForgotPasswordForm = () => {\n  const t = useI18n();\n  const { forgotPassword, isLoading, isEmailSent } = useForgotPassword();\n\n  const form = useForm<ForgotPasswordSchema>({\n    resolver: zodResolver(forgotPasswordSchema),\n    defaultValues: {\n      email: \"\",\n    },\n  });\n\n  const onSubmit = async (data: ForgotPasswordSchema) => {\n    await forgotPassword(data.email, (msg) => {\n      form.setError(\"email\", {\n        type: \"manual\",\n        message: msg,\n      });\n    });\n  };\n\n  if (isEmailSent) {\n    return (\n      <Alert className=\"w-full h-full\" variant=\"success\">\n        <AlertDescription>{t(\"success.password_forgot_success\")}</AlertDescription>\n      </Alert>\n    );\n  }\n\n  return (\n    <div className=\"space-y-6\">\n      <div className=\"space-y-2 text-center\">\n        <h1 className=\"text-2xl font-semibold tracking-tight\">{t(\"password_forgot_title\")}</h1>\n        <p className=\"text-muted-foreground text-sm\">{t(\"password_forgot_subtitle\")}</p>\n      </div>\n\n      <Form form={form} onSubmit={onSubmit}>\n        <FormField\n          control={form.control}\n          name=\"email\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>Email</FormLabel>\n              <FormControl>\n                <Input autoComplete=\"email\" placeholder=\"nom@exemple.com\" type=\"email\" {...field} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n\n        <Button className=\"mt-6 w-full\" disabled={isLoading} size=\"large\" type=\"submit\">\n          {isLoading ? t(\"commons.sending\") : t(\"commons.send_me_link\")}\n        </Button>\n      </Form>\n\n      <div className=\"text-center text-sm\">\n        <Link className=\"text-primary hover:underline\" href={`/${paths.signIn}`}>\n          {t(\"commons.back_to_login\")}\n        </Link>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/features/auth/lib/auth-client.ts",
    "content": "import { createAuthClient } from \"better-auth/react\";\nimport { adminClient, customSessionClient, inferAdditionalFields } from \"better-auth/client/plugins\";\n\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nexport const authClient = createAuthClient({\n  /** The base URL of the server (optional if you're using the same domain) */\n  baseURL: getServerUrl(),\n  plugins: [adminClient(), customSessionClient<typeof auth>(), inferAdditionalFields<typeof auth>()],\n});\n\nexport const useIsAdmin = () => {\n  const { data: sessionData, isPending: isSessionLoading } = useSession();\n  return !isSessionLoading && sessionData?.user?.role?.includes(\"admin\");\n};\n\nexport const { useSession } = authClient;\n"
  },
  {
    "path": "src/features/auth/lib/better-auth.ts",
    "content": "import { admin, customSession } from \"better-auth/plugins\";\nimport { nextCookies } from \"better-auth/next-js\";\nimport { prismaAdapter } from \"better-auth/adapters/prisma\";\nimport { betterAuth } from \"better-auth\";\nimport { UserRole } from \"@prisma/client\";\n\nimport { VerifyEmail } from \"@emails/VerifyEmail\";\nimport { ResetPasswordEmail } from \"@emails/ResetPasswordEmail\";\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { sendEmail } from \"@/shared/lib/mail/sendEmail\";\nimport { hashStringWithSalt } from \"@/features/update-password/lib/hash\";\nimport { env } from \"@/env\";\n\nexport const auth = betterAuth({\n  // trustedOrigins: [SiteConfig.prodUrl, \"localhost:3000\", \"https://better-auth.com\", \"http://localhost:3000\"],\n  trustedOrigins: [\"*\", \"workoutcool://\", \"expo://\"],\n  plugins: [\n    admin(),\n    customSession(async ({ user, session }) => {\n      console.log(\"⛏️ customSession executed - fetched from DB - whole user and session data is this ->> \\n\");\n      const userFromDB = await prisma.user.findUnique({\n        where: {\n          id: user.id,\n        },\n        select: {\n          id: true,\n          email: true,\n          emailVerified: true,\n          name: true,\n          firstName: true,\n          lastName: true,\n          image: true,\n          locale: true,\n          role: true,\n          banned: true,\n          banReason: true,\n          banExpires: true,\n          isPremium: true,\n          accounts: {\n            select: { providerId: true },\n          },\n        },\n      });\n\n      return {\n        user: userFromDB,\n        session,\n      };\n    }),\n    nextCookies(),\n  ],\n  user: {\n    additionalFields: {\n      email: {\n        type: \"string\",\n      },\n      name: {\n        type: \"string\",\n      },\n      role: {\n        type: \"string\",\n      },\n      firstName: {\n        type: \"string\",\n      },\n      lastName: {\n        type: \"string\",\n      },\n    },\n  },\n  database: prismaAdapter(prisma, {\n    provider: \"postgresql\",\n  }),\n  session: {\n    cookieCache: {\n      enabled: true,\n      maxAge: 60 * 60 * 24 * 30, // 30 days\n    },\n  },\n  emailVerification: {\n    autoSignInAfterVerification: true,\n    sendOnSignUp: false, // FIXME: TEMPORARY\n    sendVerificationEmail: async ({ user, url }, _req) => {\n      try {\n        const urlObject = new URL(url);\n        const params = new URLSearchParams(urlObject.search);\n        params.set(\"callbackURL\", \"/\");\n\n        // reconstruction\n        urlObject.search = params.toString();\n        const finalUrl = urlObject.toString();\n\n        await sendEmail({\n          to: user.email,\n          subject: \"Verify your email address\",\n          text: `Click the link to verify your email: ${finalUrl}`,\n          react: VerifyEmail({ url: finalUrl }),\n        });\n      } catch (error) {\n        console.error(\"Error sending verification email:\", error);\n      }\n    },\n  },\n  emailAndPassword: {\n    requireEmailVerification: false, // FIXME: TEMPORARY\n    sendResetPassword: async ({ user, url }) => {\n      await sendEmail({\n        to: user.email,\n        subject: \"Reset your password\",\n        text: `Click the link to reset your password: ${url}`,\n        react: ResetPasswordEmail({ url }),\n      });\n    },\n    password: {\n      hash: async (password: string) => {\n        const hashedPassword = hashStringWithSalt(password, env.BETTER_AUTH_SECRET);\n        return hashedPassword;\n      },\n      verify: async ({ password, hash }) => {\n        const hashedPassword = hashStringWithSalt(password, env.BETTER_AUTH_SECRET);\n        return hashedPassword === hash;\n      },\n    },\n    enabled: true,\n  },\n  socialProviders: {\n    google: {\n      enabled: true,\n      clientId: env.GOOGLE_CLIENT_ID,\n      clientSecret: env.GOOGLE_CLIENT_SECRET,\n      mapProfileToUser: async (profile) => {\n        return {\n          ...profile,\n          email: profile.email,\n          firstName: profile.given_name,\n          lastName: profile.family_name,\n          role: UserRole.user,\n        };\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "src/features/auth/model/useLogout.ts",
    "content": "\"use client\";\n\nimport { useRouter } from \"next/navigation\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nimport { authClient } from \"@/features/auth/lib/auth-client\";\n\nexport const useLogout = (redirectUrl: string = \"/\") => {\n  const router = useRouter();\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async () => {\n      await authClient.signOut();\n      router.push(redirectUrl);\n      queryClient.invalidateQueries({ queryKey: [\"session\"] });\n    },\n  });\n};\n"
  },
  {
    "path": "src/features/auth/reset-password/model/useResetPassword.ts",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\n\nimport { useI18n } from \"locales/client\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { authClient } from \"@/features/auth/lib/auth-client\";\nimport { brandedToast } from \"@/components/ui/toast\";\ninterface UseResetPasswordResult {\n  isLoading: boolean;\n  hasToken: boolean;\n  resetPassword: (password: string) => Promise<void>;\n}\n\nexport const useResetPassword = (): UseResetPasswordResult => {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const t = useI18n();\n  const [isLoading, setIsLoading] = useState(false);\n\n  const token = searchParams.get(\"token\") ?? \"\";\n\n  const hasToken = Boolean(token);\n\n  const resetPassword = async (password: string) => {\n    if (!hasToken) return;\n\n    setIsLoading(true);\n\n    try {\n      const { error } = await authClient.resetPassword({ token, newPassword: password });\n\n      if (error) {\n        brandedToast({ title: t(\"error.generic_error\"), variant: \"error\" });\n        return;\n      }\n\n      brandedToast({ title: t(\"success.reset_password_success\"), variant: \"success\" });\n      router.push(`/${paths.signIn}?reset=success`);\n    } catch (e) {\n      console.error(e);\n      brandedToast({ title: t(\"error.generic_error\"), variant: \"error\" });\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  return { isLoading, hasToken, resetPassword };\n};\n"
  },
  {
    "path": "src/features/auth/reset-password/schema/reset-password.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const resetPasswordSchema = z\n  .object({\n    password: z\n      .string()\n      .min(8, \"Le mot de passe doit contenir au moins 8 caractères\")\n      .regex(/[A-Z]/, \"Le mot de passe doit contenir au moins une majuscule\")\n      .regex(/[a-z]/, \"Le mot de passe doit contenir au moins une minuscule\")\n      .regex(/[0-9]/, \"Le mot de passe doit contenir au moins un chiffre\"),\n    confirmPassword: z.string(),\n  })\n  .refine((data) => data.password === data.confirmPassword, {\n    message: \"Les mots de passe ne correspondent pas\",\n    path: [\"confirmPassword\"],\n  });\n\nexport type ResetPasswordFormData = z.infer<typeof resetPasswordSchema>;\n"
  },
  {
    "path": "src/features/auth/reset-password/ui/reset-password-form.tsx",
    "content": "\"use client\";\n\nimport { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\nimport { ResetPasswordFormData, resetPasswordSchema } from \"@/features/auth/reset-password/schema/reset-password.schema\";\nimport { useResetPassword } from \"@/features/auth/reset-password/model/useResetPassword\";\nimport { InputPasswordStrength } from \"@/components/ui/input-password-strength\";\nimport { Input } from \"@/components/ui/input\";\nimport { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from \"@/components/ui/form\";\nimport { Button } from \"@/components/ui/button\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\n\nexport function ResetPasswordForm() {\n  const { isLoading, hasToken, resetPassword } = useResetPassword();\n\n  const form = useForm<ResetPasswordFormData>({\n    resolver: zodResolver(resetPasswordSchema),\n    defaultValues: {\n      password: \"\",\n      confirmPassword: \"\",\n    },\n  });\n\n  const onSubmit = async (formData: ResetPasswordFormData) => {\n    await resetPassword(formData.password);\n  };\n\n  if (!hasToken) {\n    return (\n      <Alert variant=\"error\">\n        <AlertDescription>Le lien de réinitialisation est invalide ou a expiré. Veuillez demander un nouveau lien.</AlertDescription>\n      </Alert>\n    );\n  }\n\n  return (\n    <div className=\"space-y-6 py-10\">\n      <div className=\"space-y-2 text-center\">\n        <h1 className=\"text-2xl font-semibold tracking-tight\">Réinitialisation du mot de passe</h1>\n        <p className=\"text-muted-foreground text-sm\">Veuillez choisir un nouveau mot de passe</p>\n      </div>\n\n      <Form form={form} onSubmit={onSubmit}>\n        <div className=\"space-y-4\">\n          <FormField\n            control={form.control}\n            name=\"password\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Nouveau mot de passe</FormLabel>\n                <FormControl>\n                  <InputPasswordStrength {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n\n          <FormField\n            control={form.control}\n            name=\"confirmPassword\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Confirmer le mot de passe</FormLabel>\n                <FormControl>\n                  <Input autoComplete=\"new-password\" placeholder=\"••••••••\" type=\"password\" {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </div>\n\n        <Button className=\"mt-6 w-full\" disabled={isLoading} type=\"submit\">\n          {isLoading ? \"Réinitialisation en cours...\" : \"Réinitialiser le mot de passe\"}\n        </Button>\n      </Form>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/auth/signin/model/useSignIn.ts",
    "content": "import { useSearchParams } from \"next/navigation\";\n\nimport { useI18n } from \"locales/client\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { LoginSchema } from \"@/features/auth/signin/schema/signin.schema\";\nimport { authClient } from \"@/features/auth/lib/auth-client\";\nimport { brandedToast } from \"@/components/ui/toast\";\n\nexport const useSignIn = () => {\n  const t = useI18n();\n  const searchParams = useSearchParams();\n\n  const signIn = async (values: LoginSchema) => {\n    const redirectUrl = searchParams.get(\"redirect\");\n    const callbackURL = redirectUrl || `${paths.root}?signin=true`;\n    \n    const response = await authClient.signIn.email({\n      email: values.email,\n      password: values.password,\n      callbackURL,\n    });\n\n    if (response?.error) {\n      brandedToast({ title: t(\"error.invalid_credentials\"), variant: \"error\" });\n      return;\n    }\n  };\n\n  return {\n    signIn,\n  };\n};\n"
  },
  {
    "path": "src/features/auth/signin/schema/signin.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const loginSchema = z.object({\n  email: z.string().email(),\n  password: z.string().min(6),\n});\n\nexport type LoginSchema = z.infer<typeof loginSchema>;\n"
  },
  {
    "path": "src/features/auth/signin/ui/CredentialsLoginForm.tsx",
    "content": "\"use client\";\nimport { useForm } from \"react-hook-form\";\nimport * as React from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport Link from \"next/link\";\nimport { Label } from \"@radix-ui/react-label\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { ProviderButton } from \"@/features/auth/ui/ProviderButton\";\nimport { loginSchema, LoginSchema } from \"@/features/auth/signin/schema/signin.schema\";\nimport { useSignIn } from \"@/features/auth/signin/model/useSignIn\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\n\nexport function CredentialsLoginForm({ className, ...props }: React.ComponentPropsWithoutRef<\"form\">) {\n  const t = useI18n();\n  const searchParams = useSearchParams();\n  const isResetSuccess = searchParams.get(\"reset\") === \"success\";\n  const redirectUrl = searchParams.get(\"redirect\");\n\n  const { signIn } = useSignIn();\n\n  const { register, handleSubmit, formState } = useForm({ resolver: zodResolver(loginSchema) });\n  const { errors, isSubmitting } = formState;\n\n  async function onSubmit(values: LoginSchema) {\n    return signIn(values);\n  }\n\n  return (\n    <div className=\"space-y-6\">\n      {isResetSuccess && (\n        <Alert variant=\"success\">\n          <AlertDescription>{t(\"commons.password_reset_success\")}</AlertDescription>\n        </Alert>\n      )}\n\n      <form className={cn(\"flex flex-col gap-6\", className)} onSubmit={handleSubmit(onSubmit)} {...props}>\n        <div className=\"flex flex-col items-center gap-2 text-center\">\n          <h1 className=\"text-2xl font-bold\">{t(\"commons.login_to_your_account_title\")}</h1>\n          <p className=\"text-muted-foreground text-balance text-sm\">{t(\"commons.login_to_your_account_subtitle\")}</p>\n        </div>\n        <div className=\"grid gap-6\">\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"email\">Email</Label>\n            <Input id=\"email\" placeholder=\"m@example.com\" type=\"email\" {...register(\"email\")} aria-invalid={!!errors.email} />\n            {errors.email && <p className=\"text-sm text-red-500\">{errors.email.message}</p>}\n          </div>\n          <div className=\"grid gap-2\">\n            <div className=\"flex items-center\">\n              <Label htmlFor=\"password\">{t(\"commons.password\")}</Label>\n              <a className=\"ml-auto text-sm underline-offset-4 hover:underline\" href={`/${paths.forgotPassword}`}>\n                {t(\"commons.password_forgot\")}\n              </a>\n            </div>\n            <Input id=\"password\" type=\"password\" {...register(\"password\")} aria-invalid={!!errors.password} />\n            {errors.password && <p className=\"text-sm text-red-500\">{errors.password.message}</p>}\n          </div>\n          <Button className=\"w-full\" disabled={isSubmitting} size=\"large\" type=\"submit\">\n            {isSubmitting ? t(\"commons.connecting\") : t(\"commons.login\")}\n          </Button>\n        </div>\n      </form>\n\n      <div className=\"relative\">\n        <div className=\"absolute inset-0 flex items-center\">\n          <span className=\"w-full border-t\" />\n        </div>\n        <div className=\"relative flex justify-center text-xs uppercase\">\n          <span className=\"bg-background text-muted-foreground px-2\">{t(\"commons.or\")}</span>\n        </div>\n      </div>\n\n      <ProviderButton action=\"signin\" className=\"w-full\" providerId=\"google\" variant=\"outline\" />\n\n      <div className=\"text-center text-sm\">\n        {t(\"commons.dont_have_account\")}{\" \"}\n        <Link \n          className=\"underline underline-offset-4\" \n          href={`/${paths.signUp}${redirectUrl ? `?redirect=${encodeURIComponent(redirectUrl)}` : \"\"}`}\n        >\n          {t(\"commons.signup\")}\n        </Link>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/auth/signup/model/signup.action.ts",
    "content": "\"use server\";\n\nimport { UserRole } from \"@prisma/client\";\n\nimport { getI18n } from \"locales/server\";\nimport { setupAnalytics } from \"@/shared/lib/analytics/server\";\nimport { LogEvents } from \"@/shared/lib/analytics/events\";\nimport { ActionError, actionClient } from \"@/shared/api/safe-actions\";\nimport { signUpSchema } from \"@/features/auth/signup/schema/signup.schema\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\nimport { env } from \"@/env\";\n\nexport const signUpAction = actionClient.schema(signUpSchema).action(async ({ parsedInput }) => {\n  const t = await getI18n();\n\n  try {\n    const user = await auth.api.signUpEmail({\n      body: {\n        email: parsedInput.email,\n        password: parsedInput.password,\n        role: UserRole.user,\n        name: parsedInput.firstName,\n        firstName: parsedInput.firstName,\n        lastName: parsedInput.lastName,\n      },\n    });\n\n    if (env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID) {\n      const analytics = await setupAnalytics({\n        userId: user.user.id,\n        fullName: `${parsedInput.firstName} ${parsedInput.lastName}`,\n        email: parsedInput.email,\n      });\n\n      analytics.track({\n        event: LogEvents.Registered.name,\n        channel: LogEvents.Registered.channel,\n      });\n    }\n\n    return user;\n  } catch (error) {\n    console.error(error);\n    throw new ActionError(t(\"backend_errors.EMAIL_ALREADY_EXISTS\"));\n  }\n});\n"
  },
  {
    "path": "src/features/auth/signup/model/useSignUp.ts",
    "content": "\"use client\";\n\nimport { useSearchParams } from \"next/navigation\";\nimport { useMutation } from \"@tanstack/react-query\";\n\nimport { useI18n } from \"locales/client\";\nimport { SignUpSchema } from \"@/features/auth/signup/schema/signup.schema\";\nimport { signUpAction } from \"@/features/auth/signup/model/signup.action\";\nimport { brandedToast } from \"@/components/ui/toast\";\n\nexport const useSignUp = () => {\n  const t = useI18n();\n  const searchParams = useSearchParams();\n\n  const mutation = useMutation({\n    mutationFn: async (values: SignUpSchema) => {\n      if (values.password !== values.verifyPassword) {\n        throw new Error(\"PASSWORD_MISMATCH\");\n      }\n\n      const result = await signUpAction(values);\n\n      if (result?.serverError) {\n        throw new Error(result.serverError);\n      }\n\n      return result;\n    },\n\n    onSuccess: async () => {\n      const redirectUrl = searchParams.get(\"redirect\");\n      const destination = redirectUrl || \"/profile\";\n      window.location.href = destination;\n      // router.push(`/${paths.verifyEmail}?signin=true`);\n    },\n\n    onError: (error: unknown) => {\n      const message = error instanceof Error ? t(error.message as keyof typeof t) : t(\"error.generic_error\");\n\n      brandedToast({ title: message, variant: \"error\" });\n    },\n  });\n\n  return {\n    signUp: mutation.mutateAsync,\n    isLoading: mutation.isPending,\n  };\n};\n"
  },
  {
    "path": "src/features/auth/signup/schema/signup.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const signUpSchema = z.object({\n  firstName: z.string(),\n  lastName: z.string(),\n  email: z.string().email(),\n  password: z.string().min(8),\n  verifyPassword: z.string().min(8),\n});\n\nexport type SignUpSchema = z.infer<typeof signUpSchema>;\n"
  },
  {
    "path": "src/features/auth/signup/ui/signup-form.tsx",
    "content": "\"use client\";\n\nimport { useSearchParams } from \"next/navigation\";\nimport Link from \"next/link\";\n\nimport { useI18n } from \"locales/client\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { ProviderButton } from \"@/features/auth/ui/ProviderButton\";\nimport { useSignUp } from \"@/features/auth/signup/model/useSignUp\";\nimport { Input } from \"@/components/ui/input\";\nimport { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from \"@/components/ui/form\";\nimport { Button } from \"@/components/ui/button\";\n\nimport { signUpSchema } from \"../schema/signup.schema\";\n\nimport type { SignUpSchema } from \"../schema/signup.schema\";\n\nexport const SignUpForm = () => {\n  const t = useI18n();\n  const searchParams = useSearchParams();\n  const redirectUrl = searchParams.get(\"redirect\");\n\n  const form = useZodForm({ schema: signUpSchema });\n\n  const { signUp } = useSignUp();\n\n  async function onSubmit(values: SignUpSchema) {\n    if (values.password !== values.verifyPassword) {\n      form.setError(\"verifyPassword\", {\n        message: \"Password does not match\",\n      });\n      return;\n    }\n\n    return signUp(values);\n  }\n\n  return (\n    <>\n      <Form\n        className=\"max-w-lg space-y-4\"\n        form={form}\n        onSubmit={async (values) => {\n          return onSubmit(values);\n        }}\n      >\n        <div className=\"grid grid-cols-2 gap-4\">\n          <FormField\n            control={form.control}\n            name=\"firstName\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>{t(\"commons.first_name\")}</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"John\" {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n\n          <FormField\n            control={form.control}\n            name=\"lastName\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>{t(\"commons.last_name\")}</FormLabel>\n                <FormControl>\n                  <Input placeholder=\"Doe\" {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n        </div>\n\n        <FormField\n          control={form.control}\n          name=\"email\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>{t(\"commons.email\")}</FormLabel>\n              <FormControl>\n                <Input placeholder=\"john@doe.com\" {...field} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n\n        <FormField\n          control={form.control}\n          name=\"password\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>{t(\"commons.password\")}</FormLabel>\n              <FormControl>\n                <Input type=\"password\" {...field} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n\n        <FormField\n          control={form.control}\n          name=\"verifyPassword\"\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>{t(\"commons.verify_password\")}</FormLabel>\n              <FormControl>\n                <Input type=\"password\" {...field} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n\n        <Button className=\"w-full\" size=\"large\" type=\"submit\">\n          {t(\"commons.submit\")}\n        </Button>\n\n        <div className=\"relative\">\n          <div className=\"absolute inset-0 flex items-center\">\n            <span className=\"w-full border-t\" />\n          </div>\n          <div className=\"relative flex justify-center text-xs uppercase\">\n            <span className=\"bg-background text-muted-foreground px-2\">{t(\"commons.or\")}</span>\n          </div>\n        </div>\n      </Form>\n      <div className=\"mt-2 flex flex-col gap-2\">\n        <ProviderButton action=\"signup\" providerId=\"google\" variant=\"default\" />\n      </div>\n      \n      <div className=\"mt-4 text-center text-sm\">\n        {t(\"commons.already_have_account\")}{\" \"}\n        <Link \n          className=\"underline underline-offset-4\" \n          href={`/${paths.signIn}${redirectUrl ? `?redirect=${encodeURIComponent(redirectUrl)}` : \"\"}`}\n        >\n          {t(\"commons.login\")}\n        </Link>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "src/features/auth/ui/AuthButtonServer.tsx",
    "content": "import { SignUpButton } from \"@/features/auth/ui/SignUpButton\";\nimport { SignInButton } from \"@/features/auth/ui/SignInButton\";\nimport { LoggedInButton } from \"@/features/auth/ui/LoggedInButton\";\nimport { serverAuth } from \"@/entities/user/model/get-server-session-user\";\n\nexport const AuthButtonServer = async () => {\n  const user = await serverAuth();\n\n  if (user) {\n    return <LoggedInButton user={user} />;\n  }\n\n  return (\n    <div className=\"flex items-center gap-2\">\n      <SignInButton />\n      <SignUpButton />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/features/auth/ui/LoggedInButton.tsx",
    "content": "import { UserDropdown } from \"@/features/user/ui/UserDropdown\";\nimport { SessionUser } from \"@/entities/user/types/session-user\";\nimport { displayName } from \"@/entities/user/lib/display-name\";\nimport { Button } from \"@/components/ui/button\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\n\nexport const LoggedInButton = ({ user, showName = true }: { user: SessionUser; showName?: boolean }) => {\n  return (\n    <UserDropdown>\n      <Button size=\"small\" variant=\"outline\">\n        <Avatar className=\"bg-card size-6 hover:cursor-pointer lg:mr-2\">\n          <AvatarFallback className=\"bg-card\">{user.email.slice(0, 1).toUpperCase()}</AvatarFallback>\n          {user.image && <AvatarImage src={user.image} />}\n        </Avatar>\n        {showName && <span className=\"max-lg:hidden\">{displayName(user)}</span>}\n      </Button>\n    </UserDropdown>\n  );\n};\n"
  },
  {
    "path": "src/features/auth/ui/ProviderButton.tsx",
    "content": "\"use client\";\n\nimport { useSearchParams } from \"next/navigation\";\nimport { useMutation } from \"@tanstack/react-query\";\n\nimport { useI18n } from \"locales/client\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { authClient } from \"@/features/auth/lib/auth-client\";\nimport { Loader } from \"@/components/ui/loader\";\nimport { Button, ButtonProps } from \"@/components/ui/button\";\nimport { GoogleSvg } from \"@/components/svg/GoogleSvg\";\n\nimport type { ReactNode } from \"react\";\n\nconst ProviderData: Record<string, { icon: ReactNode; name: string }> = {\n  google: {\n    icon: <GoogleSvg size={16} />,\n    name: \"Google\",\n  },\n};\n\ntype ProviderButtonProps = {\n  providerId: string;\n  variant: ButtonProps[\"variant\"];\n  action: \"signin\" | \"signup\";\n  className?: string;\n};\n\nexport const ProviderButton = (props: ProviderButtonProps) => {\n  const t = useI18n();\n\n  const searchParams = useSearchParams();\n\n  const authMutation = useMutation({\n    mutationFn: async () => {\n      const redirectUrl = searchParams.get(\"redirect\");\n      const callbackUrl = searchParams.get(\"callbackUrl\");\n      const defaultAction = props.action === \"signup\" ? \"signup\" : \"signin\";\n      const defaultCallback = `${getServerUrl()}/?${defaultAction}=true`;\n      \n      await authClient.signIn.social({\n        provider: \"google\",\n        callbackURL: redirectUrl || callbackUrl || defaultCallback,\n      });\n    },\n  });\n\n  const data = ProviderData[props.providerId];\n\n  const traduction =\n    props.action === \"signin\" ? t(\"commons.signin_with\", { provider: data.name }) : t(\"commons.signup_with\", { provider: data.name });\n\n  return (\n    <Button\n      className={props.className}\n      onClick={() => {\n        authMutation.mutate();\n      }}\n      size=\"large\"\n      variant=\"outline\"\n    >\n      {authMutation.isPending ? <Loader size={16} /> : data.icon}\n      <span className=\"ml-2 text-base\">{traduction}</span>\n    </Button>\n  );\n};\n"
  },
  {
    "path": "src/features/auth/ui/SignInButton.tsx",
    "content": "\"use client\";\nimport Link from \"next/link\";\n\nimport { useI18n } from \"locales/client\";\nimport { Button } from \"@/components/ui/button\";\n\nexport const SignInButton = () => {\n  const t = useI18n();\n\n  return (\n    <Button asChild size=\"large\" variant=\"link\">\n      <Link href={\"/auth/signin?callbackUrl=/\"}>{t(\"commons.login\")}</Link>\n    </Button>\n  );\n};\n"
  },
  {
    "path": "src/features/auth/ui/SignUpButton.tsx",
    "content": "import Link from \"next/link\";\n\nimport { getI18n } from \"locales/server\";\nimport { Button } from \"@/components/ui/button\";\n\nexport const SignUpButton = async () => {\n  const t = await getI18n();\n\n  return (\n    <Button asChild variant=\"outline-black\">\n      <Link href={\"/auth/signup\"}>{t(\"commons.register\")}</Link>\n    </Button>\n  );\n};\n"
  },
  {
    "path": "src/features/auth/verify-email/constants.ts",
    "content": "export const COUNTDOWN_TIME = process.env.NODE_ENV === \"development\" ? 3 : 60;\n"
  },
  {
    "path": "src/features/auth/verify-email/model/useResendEmail.ts",
    "content": "import { useEffect, useState } from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { COUNTDOWN_TIME } from \"@/features/auth/verify-email/constants\";\nimport { authClient } from \"@/features/auth/lib/auth-client\";\nimport { brandedToast } from \"@/components/ui/toast\";\n\nexport const useResendEmail = (email: string) => {\n  const t = useI18n();\n  const [countdown, setCountdown] = useState(0);\n  const [isDisabled, setIsDisabled] = useState(false);\n\n  useEffect(() => {\n    if (countdown > 0) {\n      const timer = setInterval(() => setCountdown((prev) => prev - 1), 1000);\n      return () => clearInterval(timer);\n    }\n    setIsDisabled(false);\n  }, [countdown]);\n\n  const resend = async () => {\n    try {\n      setIsDisabled(true);\n      setCountdown(COUNTDOWN_TIME);\n\n      const res = await authClient.sendVerificationEmail({\n        email,\n        callbackURL: `${getServerUrl()}/${paths.root}`,\n      });\n\n      if (res.error) brandedToast({ title: t(res.error.message as keyof typeof t), variant: \"error\" });\n      if (res.data?.status) brandedToast({ title: t(\"email_sent\"), variant: \"success\" });\n    } catch (err) {\n      console.error(err);\n      brandedToast({ title: t(\"cant_send_email\"), variant: \"error\" });\n    } finally {\n      setIsDisabled(false);\n    }\n  };\n\n  return { resend, isDisabled, countdown };\n};\n"
  },
  {
    "path": "src/features/auth/verify-email/ui/verify-email-page.tsx",
    "content": "\"use client\";\n\nimport { Mail } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { useResendEmail } from \"@/features/auth/verify-email/model/useResendEmail\";\nimport { useLogout } from \"@/features/auth/model/useLogout\";\nimport { useCurrentUser } from \"@/entities/user/model/useCurrentUser\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\n\nexport const VerifyEmailPage = () => {\n  const t = useI18n();\n  const user = useCurrentUser();\n  const logout = useLogout(`/${paths.signIn}`);\n\n  const { resend, isDisabled, countdown } = useResendEmail(user?.email ?? \"\");\n\n  const handleResendEmail = async () => {\n    resend();\n  };\n\n  return (\n    <div className=\"bg-background place-items-center px-4 h-full flex items-center justify-center\">\n      <Card className=\"w-full max-w-md p-4 border-none shadow-none\">\n        <CardHeader className=\"text-center\">\n          <div className=\"mb-4 flex justify-center\">\n            <div className=\"rounded-full bg-primary/10 p-3\">\n              <Mail className=\"h-6 w-6 text-primary\" />\n            </div>\n          </div>\n          <CardTitle className=\"text-2xl font-semibold\">{t(\"verify_email\")}</CardTitle>\n        </CardHeader>\n        <CardContent className=\"flex flex-col gap-6\">\n          <CardDescription className=\"text-center text-base\">{t(\"verify_email_subtitle\")}</CardDescription>\n\n          <div className=\"flex flex-col gap-3\">\n            <Button\n              className={cn(\"text-primary hover:text-primary/80\")}\n              disabled={isDisabled}\n              onClick={handleResendEmail}\n              variant=\"outline\"\n            >\n              {isDisabled ? t(\"resend_email_countdown\", { seconds: countdown }) : t(\"resend_email\")}\n            </Button>\n\n            <Button className=\"text-muted-foreground hover:text-foreground\" onClick={() => logout.mutate()} variant={null}>\n              {t(\"logout\")}\n            </Button>\n          </div>\n        </CardContent>\n      </Card>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/features/consent-banner/model/tracking-consent.action.ts",
    "content": "\"use server\";\n\nimport { cookies } from \"next/headers\";\n\nimport { Cookies } from \"@/shared/constants/cookies\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\nimport { trackingConsentSchema } from \"@/features/consent-banner/schema/tracking-consent.schema\";\n\nexport const trackingConsentAction = actionClient.schema(trackingConsentSchema).action(async ({ parsedInput: value }) => {\n  const cookiesStore = await cookies();\n\n  const oneYearFromNow = new Date();\n  oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);\n\n  cookiesStore.set({\n    name: Cookies.TrackingConsent,\n    value: value ? \"1\" : \"0\",\n    expires: oneYearFromNow,\n  });\n\n  return value;\n});\n"
  },
  {
    "path": "src/features/consent-banner/schema/tracking-consent.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const trackingConsentSchema = z.boolean();\n"
  },
  {
    "path": "src/features/consent-banner/ui/consent-banner.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useAction } from \"next-safe-action/hooks\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { trackingConsentAction } from \"@/features/consent-banner/model/tracking-consent.action\";\nimport { Button } from \"@/components/ui/button\";\n\nexport function ConsentBanner() {\n  const t = useI18n();\n\n  const [isOpen, setOpen] = useState(true);\n  const trackingAction = useAction(trackingConsentAction, {\n    onExecute: () => setOpen(false),\n  });\n\n  if (!isOpen) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"bg-background fixed bottom-2 left-2 z-50 flex w-[calc(100vw-16px)] max-w-[420px] flex-col space-y-4 rounded-lg border border-primary p-4 transition-all md:bottom-4 md:left-4\",\n        isOpen && \"animate-in slide-in-from-bottom-full sm:slide-in-from-bottom-full\",\n      )}\n    >\n      <div className=\"text-sm\">{t(\"commons.consent_banner\")}</div>\n      <div className=\"flex justify-end space-x-2\">\n        <Button className=\"h-8 rounded-full opacity-50\" onClick={() => trackingAction.execute(false)}>\n          {t(\"commons.deny\")}\n        </Button>\n        <Button className=\"h-8 rounded-full\" onClick={() => trackingAction.execute(true)}>\n          {t(\"commons.accept\")}\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/contact/support/ContactSupportDialog.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\n\nimport { useI18n } from \"locales/client\";\nimport { SiteConfig } from \"@/shared/config/site-config\";\nimport { useCurrentSession } from \"@/entities/user/model/useCurrentSession\";\nimport { brandedToast } from \"@/components/ui/toast\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Input } from \"@/components/ui/input\";\nimport { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from \"@/components/ui/form\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\n\nimport { ContactSupportSchema } from \"./contact-support.schema\";\nimport { contactSupportAction } from \"./contact-support.action\";\n\nimport type { PropsWithChildren } from \"react\";\nimport type { ContactSupportSchemaType } from \"./contact-support.schema\";\n\nexport type ContactSupportDialogProps = PropsWithChildren<{\n  email?: string;\n  className?: string;\n}>;\n\nexport const ContactSupportDialog = (props: ContactSupportDialogProps) => {\n  const [open, setOpen] = useState(false);\n  const session = useCurrentSession();\n  const t = useI18n();\n  const email = session?.user?.email ?? \"\";\n  const form = useZodForm({\n    schema: ContactSupportSchema,\n    defaultValues: {\n      email: email,\n    },\n  });\n\n  const onSubmit = async (values: ContactSupportSchemaType) => {\n    const action = await contactSupportAction(values);\n\n    if (!action || !action.data) {\n      brandedToast({ title: action?.serverError ?? t(\"error.generic_error\"), variant: \"error\" });\n      return;\n    }\n\n    brandedToast({ title: t(\"email_sent\"), variant: \"success\" });\n    form.reset();\n    setOpen(false);\n  };\n\n  return (\n    <Dialog onOpenChange={(v) => setOpen(v)} open={open}>\n      <DialogTrigger asChild className=\"cursor-pointer\">\n        {props.children ? props.children : <span className={props.className}>{t(\"commons.support\")}</span>}\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{t(\"contact_support\")}</DialogTitle>\n          <DialogDescription>\n            {t(\"contact_support_subtitle\")}{\" \"}\n            <Link className=\"text-primary\" href={`mailto:${SiteConfig.email.contact}`}>\n              {SiteConfig.email.contact}\n            </Link>\n            .\n          </DialogDescription>\n        </DialogHeader>\n        <Form className=\"flex flex-col gap-4\" form={form} onSubmit={async (v) => onSubmit(v)}>\n          {email ? null : (\n            <FormField\n              control={form.control}\n              name=\"email\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Email</FormLabel>\n                  <FormControl>\n                    <Input {...field} />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          )}\n          <FormField\n            control={form.control}\n            name=\"subject\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>{t(\"commons.subject\")}</FormLabel>\n                <FormControl>\n                  <Input {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n          <FormField\n            control={form.control}\n            name=\"message\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>{t(\"commons.message\")}</FormLabel>\n                <FormControl>\n                  <Textarea {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n          <Button type=\"submit\">{t(\"commons.submit\")}</Button>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "src/features/contact/support/contact-support.action.ts",
    "content": "\"use server\";\n\nimport ContactSupportEmail from \"@emails/ContactSupportEmail\";\nimport { sendEmail } from \"@/shared/lib/mail/sendEmail\";\nimport { SiteConfig } from \"@/shared/config/site-config\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\n\nimport { ContactSupportSchema } from \"./contact-support.schema\";\n\nexport const contactSupportAction = actionClient.schema(ContactSupportSchema).action(async ({ parsedInput }) => {\n  await sendEmail({\n    from: SiteConfig.email.from,\n    to: SiteConfig.email.contact,\n    subject: `Support needed from ${parsedInput.email} - ${parsedInput.subject}`,\n    text: parsedInput.message,\n    react: ContactSupportEmail({ email: parsedInput.email, message: parsedInput.message, subject: parsedInput.subject }),\n  });\n  return { message: \"Your message has been sent to support.\" };\n});\n"
  },
  {
    "path": "src/features/contact/support/contact-support.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const ContactSupportSchema = z.object({\n  email: z.string(),\n  subject: z.string(),\n  message: z.string(),\n});\n\nexport type ContactSupportSchemaType = z.infer<typeof ContactSupportSchema>;\n"
  },
  {
    "path": "src/features/contact-feedback/model/contact-feedback.action.ts",
    "content": "\"use server\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { sendEmail } from \"@/shared/lib/mail/sendEmail\";\nimport { SiteConfig } from \"@/shared/config/site-config\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\nimport { serverAuth } from \"@/entities/user/model/get-server-session-user\";\n\nimport { ContactFeedbackSchema } from \"./contact-feedback.schema\";\n\nexport const contactFeedbackAction = actionClient.schema(ContactFeedbackSchema).action(async ({ parsedInput }) => {\n  const user = await serverAuth();\n  const email = user?.email ?? parsedInput.email;\n\n  const feedback = await prisma.feedbacks.create({\n    data: {\n      message: parsedInput.message,\n      review: Number(parsedInput.review) || 0,\n      userId: user?.id,\n      email,\n    },\n  });\n\n  await sendEmail({\n    from: SiteConfig.email.from,\n    to: SiteConfig.email.contact,\n    subject: `New feedback from ${email}`,\n    text: `Review: ${feedback.review}\\n\\nMessage: ${feedback.message}`,\n  });\n\n  return { message: \"Your feedback has been sent to support.\" };\n});\n"
  },
  {
    "path": "src/features/contact-feedback/model/contact-feedback.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const ContactFeedbackSchema = z.object({\n  email: z.string().optional(),\n  review: z.string().optional(),\n  message: z.string(),\n});\n\nexport type ContactFeedbackSchemaType = z.infer<typeof ContactFeedbackSchema>;\n"
  },
  {
    "path": "src/features/contact-feedback/ui/ReviewInput.tsx",
    "content": "import { Angry, Frown, Meh, SmilePlus } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { InlineTooltip } from \"@/components/ui/tooltip\";\n\nexport const ReviewInput = ({ onChange, value }: { onChange: (value: string) => void; value?: string }) => {\n  const t = useI18n();\n\n  const options = [\n    { value: \"1\", icon: Angry, tooltip: t(\"commons.extremely_dissatisfied\") },\n    { value: \"2\", icon: Frown, tooltip: t(\"commons.somewhat_dissatisfied\") },\n    { value: \"3\", icon: Meh, tooltip: t(\"commons.neutral\") },\n    { value: \"4\", icon: SmilePlus, tooltip: t(\"commons.satisfied\") },\n  ];\n\n  return (\n    <>\n      {options.map((item) => (\n        <InlineTooltip key={item.value} title={item.tooltip}>\n          <button\n            className={cn(\"transition hover:rotate-12 hover:scale-110\", {\n              \"-rotate-[16deg] scale-[1.2] text-primary\": value === item.value,\n            })}\n            onClick={() => onChange(item.value)}\n            type=\"button\"\n          >\n            <item.icon size={24} />\n          </button>\n        </InlineTooltip>\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "src/features/contact-feedback/ui/contact-feedback-popover.tsx",
    "content": "\"use client\";\n\nimport { useBoolean } from \"usehooks-ts\";\n\nimport { useI18n } from \"locales/client\";\nimport { ReviewInput } from \"@/features/contact-feedback/ui/ReviewInput\";\nimport { ContactFeedbackSchema, ContactFeedbackSchemaType } from \"@/features/contact-feedback/model/contact-feedback.schema\";\nimport { contactFeedbackAction } from \"@/features/contact-feedback/model/contact-feedback.action\";\nimport { useCurrentSession } from \"@/entities/user/model/useCurrentSession\";\nimport { brandedToast } from \"@/components/ui/toast\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Input } from \"@/components/ui/input\";\nimport { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from \"@/components/ui/form\";\nimport { Button } from \"@/components/ui/button\";\n\nimport type { PropsWithChildren } from \"react\";\n\nexport type ContactFeedbackPopoverProps = PropsWithChildren<{}>;\n\nexport const ContactFeedbackPopover = (props: ContactFeedbackPopoverProps) => {\n  const t = useI18n();\n  const open = useBoolean();\n  const session = useCurrentSession();\n  const email = session?.user?.email ?? \"\";\n  const form = useZodForm({\n    schema: ContactFeedbackSchema,\n    defaultValues: {\n      email: email,\n    },\n  });\n\n  const onSubmit = async (values: ContactFeedbackSchemaType) => {\n    const result = await contactFeedbackAction(values);\n\n    if (!result) {\n      brandedToast({ title: t(\"error.generic_error\"), variant: \"error\" });\n      return;\n    }\n\n    if (result.serverError) {\n      brandedToast({ title: t(`backend_errors.${result.serverError}` as keyof typeof t), variant: \"error\" });\n      return;\n    }\n\n    brandedToast({ title: t(\"success.feedback_sent\"), variant: \"success\" });\n    form.reset();\n    open.setFalse();\n  };\n\n  return (\n    <Popover onOpenChange={open.toggle} open={open.value}>\n      <PopoverTrigger asChild>{props.children ? props.children : <Button variant=\"outline\">Feedback</Button>}</PopoverTrigger>\n      <PopoverContent className=\"p-0\">\n        <Form className=\"flex flex-col gap-4\" form={form} onSubmit={async (v) => onSubmit(v)}>\n          <div className=\"p-2\">\n            {email ? null : (\n              <FormField\n                control={form.control}\n                name=\"email\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Email</FormLabel>\n                    <FormControl>\n                      <Input {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            )}\n\n            <FormField\n              control={form.control}\n              name=\"message\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Message</FormLabel>\n                  <FormControl>\n                    <Textarea {...field} />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </div>\n          <div className=\"border-secondary bg-secondary/50 flex w-full items-center justify-between border-t p-2\">\n            <FormField\n              control={form.control}\n              name=\"review\"\n              render={({ field }) => (\n                <FormItem className=\"flex items-center gap-2 space-y-0\">\n                  <ReviewInput\n                    onChange={(v) => {\n                      field.onChange(v);\n                    }}\n                    value={field.value}\n                  />\n                </FormItem>\n              )}\n            />\n            <Button type=\"submit\" variant=\"outline\">\n              {t(\"commons.submit\")}\n            </Button>\n          </div>\n        </Form>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "src/features/dialogs-provider/DialogProvider.tsx",
    "content": "\"use client\";\n\nimport { create } from \"zustand\";\n\nimport { useI18n } from \"locales/client\";\nimport { logger } from \"@/shared/lib/logger\";\nimport { brandedToast } from \"@/components/ui/toast\";\n\nimport { ProviderConfirmationDialog } from \"./DialogProviderDialog\";\n\nimport type { ConfirmationDialogProps } from \"./DialogProviderDialog\";\n\ntype DialogType = ConfirmationDialogProps & {\n  id: string;\n};\n\ntype DialogStore = {\n  dialogs: DialogType[];\n  addDialog: (dialog: ConfirmationDialogProps) => void;\n  removeDialog: (dialogId: string) => void;\n};\n\nconst useDialogStore = create<DialogStore>((set, get) => ({\n  dialogs: [],\n  addDialog: (dialog) => {\n    const id = Math.random().toString(36).slice(2, 9);\n    const { removeDialog } = get();\n    const newDialog: DialogType = {\n      ...dialog,\n      cancel: {\n        label: dialog.cancel?.label ?? \"Cancel\",\n        onClick: () => {\n          removeDialog(id);\n          dialog.cancel?.onClick();\n        },\n      },\n      action: {\n        label: dialog.action?.label ?? \"\",\n        onClick: () => {\n          // eslint-disable-next-line react-hooks/rules-of-hooks\n          const t = useI18n();\n          // check if it's a promise\n          const onClickReturn = dialog.action?.onClick();\n          if (onClickReturn instanceof Promise) {\n            set((state) => {\n              const dialog = state.dialogs.find((dialog) => dialog.id === id);\n\n              if (dialog) {\n                dialog.loading = true;\n              }\n\n              return { dialogs: [...state.dialogs] };\n            });\n\n            onClickReturn\n              .then(() => {\n                removeDialog(id);\n              })\n              .catch((e) => {\n                logger.error(e);\n                brandedToast({ title: t(\"error.generic_error\"), variant: \"error\" });\n              });\n          } else {\n            dialog.action?.onClick();\n            removeDialog(id);\n          }\n        },\n      },\n      loading: false,\n      id,\n    };\n\n    set((state) => ({ dialogs: [...state.dialogs, newDialog] }));\n\n    return id;\n  },\n  removeDialog: (dialogId) =>\n    set((state) => ({\n      dialogs: state.dialogs.filter((dialog) => dialog.id !== dialogId),\n    })),\n}));\n\nexport const DialogRenderer = () => {\n  const dialogs = useDialogStore((state) => state.dialogs);\n\n  const dialog = dialogs[0] as DialogType | undefined;\n\n  if (dialog) {\n    return <ProviderConfirmationDialog {...dialog} />;\n  }\n\n  return null;\n};\n\nexport const enqueueDialog = (dialog: ConfirmationDialogProps) => useDialogStore.getState().addDialog(dialog);\n"
  },
  {
    "path": "src/features/dialogs-provider/DialogProviderDialog.tsx",
    "content": "\"use client\";\n\nimport { Loader } from \"../../components/ui/loader\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"../../components/ui/alert-dialog\";\n\nimport type { ReactNode } from \"react\";\n\nexport type ConfirmationDialogProps = {\n  title?: string;\n  description?: string;\n  action?: {\n    label: string;\n    onClick: () => void | Promise<void>;\n  };\n  cancel?: {\n    label: string;\n    onClick: () => void | Promise<void>;\n  };\n  loading?: boolean;\n  children?: ReactNode;\n};\n\nexport const ProviderConfirmationDialog = ({ title, description, loading, action, cancel, children }: ConfirmationDialogProps) => {\n  return (\n    <AlertDialog open={true}>\n      <AlertDialogContent>\n        {children ? (\n          children\n        ) : (\n          <>\n            <AlertDialogHeader>\n              <AlertDialogTitle>{title ?? \"\"}</AlertDialogTitle>\n              {description ? <AlertDialogDescription>{description}</AlertDialogDescription> : null}\n            </AlertDialogHeader>\n            <AlertDialogFooter>\n              <AlertDialogCancel disabled={loading} onClick={cancel?.onClick}>\n                {cancel?.label ?? \"Cancel\"}\n              </AlertDialogCancel>\n              {action ? (\n                <AlertDialogAction disabled={loading} onClick={action.onClick}>\n                  {loading ? <Loader /> : action.label}\n                </AlertDialogAction>\n              ) : null}\n            </AlertDialogFooter>\n          </>\n        )}\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n};\n"
  },
  {
    "path": "src/features/email/EmailForm.tsx",
    "content": "\"use client\";\n\nimport { AlertCircle, CheckCircle } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { useMutation } from \"@tanstack/react-query\";\n\nimport { LoadingButton } from \"@/features/form/SubmitButton\";\nimport { Input } from \"@/components/ui/input\";\nimport { Form, FormControl, FormField, FormItem, FormMessage, useZodForm } from \"@/components/ui/form\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\n\nimport { EmailActionSchema } from \"./email.schema\";\nimport { addEmailAction } from \"./email.action\";\n\nimport type { EmailActionSchemaType } from \"./email.schema\";\n\nexport type EmailFormProps = {\n  submitButtonLabel?: string;\n  successMessage?: string;\n};\n\nexport const EmailForm = ({\n  submitButtonLabel = \"Subscribe\",\n  successMessage = \"You have subscribed to our newsletter.\",\n}: EmailFormProps) => {\n  const form = useZodForm({\n    schema: EmailActionSchema,\n  });\n\n  const submit = useMutation({\n    mutationFn: async ({ email }: EmailActionSchemaType) => {\n      const action = await addEmailAction({ email });\n\n      if (action?.data) {\n        return action.data;\n      } else {\n        throw new Error(action?.serverError ?? \"An error occurred while subscribing to the newsletter.\");\n      }\n    },\n  });\n\n  return (\n    <AnimatePresence mode=\"wait\">\n      {submit.isSuccess ? (\n        <motion.div\n          animate={{\n            height: \"auto\",\n            opacity: 1,\n          }}\n          initial={{\n            height: 0,\n            opacity: 0,\n          }}\n          key=\"success\"\n        >\n          <Alert variant=\"success\">\n            <CheckCircle size={20} />\n            <AlertTitle>{successMessage}</AlertTitle>\n          </Alert>\n        </motion.div>\n      ) : (\n        <motion.div\n          animate={{\n            height: \"auto\",\n            opacity: 1,\n          }}\n          exit={{\n            height: 0,\n            opacity: 0,\n          }}\n          key=\"form\"\n        >\n          <Form className=\"flex flex-col gap-4\" disabled={submit.isPending} form={form} onSubmit={async (v) => submit.mutate(v)}>\n            <div className=\"flex items-center gap-4\">\n              <FormField\n                control={form.control}\n                name=\"email\"\n                render={({ field }) => (\n                  <FormItem className=\"relative w-full\">\n                    <FormControl>\n                      <Input\n                        className=\"rounded-lg border-accent-foreground/20 bg-accent px-4 py-6 text-lg focus-visible:ring-foreground\"\n                        placeholder=\"Ton email\"\n                        {...field}\n                      />\n                    </FormControl>\n                    <FormMessage className=\"absolute -bottom-5\" />\n                  </FormItem>\n                )}\n              />\n              <LoadingButton className=\"px-4 py-6 text-lg font-normal\" loading={submit.isPending} variant=\"default\">\n                {submitButtonLabel}\n              </LoadingButton>\n            </div>\n            {submit.isError && (\n              <Alert variant=\"error\">\n                <AlertCircle size={20} />\n                <AlertTitle>{submit.error.message}</AlertTitle>\n                <AlertDescription>Try another email address or contact us.</AlertDescription>\n              </Alert>\n            )}\n          </Form>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n};\n"
  },
  {
    "path": "src/features/email/email.action.ts",
    "content": "\"use server\";\n\nimport { actionClient } from \"@/shared/api/safe-actions\";\n\nimport { EmailActionSchema } from \"./email.schema\";\n\n// export const addEmailAction = action(EmailActionSchema, async ({ email }) => {\n//   try {\n//     const userData = {\n//       email,\n//     };\n\n//     const stripeCustomerId = await setupStripeCustomer(userData);\n//     const resendContactId = await setupResendCustomer(userData);\n\n//     await prisma.user.create({\n//       data: {\n//         ...userData,\n//         stripeCustomerId,\n//         resendContactId,\n//       },\n//     });\n\n//     return { email };\n//   } catch (error) {\n//     throw new ActionError(\"The email is already in use\");\n//   }\n// });\n\nexport const addEmailAction = actionClient.schema(EmailActionSchema).action(async ({ parsedInput: { email } }) => {\n  return { email };\n});\n"
  },
  {
    "path": "src/features/email/email.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const EmailActionSchema = z.object({\n  email: z.string().email(),\n});\n\nexport type EmailActionSchemaType = z.infer<typeof EmailActionSchema>;\n"
  },
  {
    "path": "src/features/form/SubmitButton.tsx",
    "content": "\"use client\";\n\nimport { useFormStatus } from \"react-dom\";\n\nimport { Loader } from \"@/components/ui/loader\";\nimport { Button } from \"@/components/ui/button\";\n\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport type { ButtonProps } from \"@/components/ui/button\";\n\nexport const SubmitButton = (props: ButtonProps) => {\n  const { pending } = useFormStatus();\n\n  return (\n    <LoadingButton loading={pending} {...props}>\n      {props.children}\n    </LoadingButton>\n  );\n};\n\nexport const LoadingButton = ({ loading, ...props }: ButtonProps & { loading?: boolean }) => {\n  return (\n    <Button {...props}>\n      {loading ? (\n        <>\n          <Loader className=\"mr-2\" size={16} /> {props.children}\n        </>\n      ) : (\n        props.children\n      )}\n    </Button>\n  );\n};\n\nexport const SubmitButtonUnstyled = (props: ComponentPropsWithoutRef<\"button\">) => {\n  const { pending } = useFormStatus();\n\n  return <button {...props} disabled={props.disabled ?? pending} type={props.type ?? \"submit\"} />;\n};\n"
  },
  {
    "path": "src/features/layout/BottomNavigation.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport Link from \"next/link\";\nimport { Dumbbell, Grid, Hammer, Crown, Activity, Trophy } from \"lucide-react\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { paths } from \"@/shared/constants/paths\";\n\nexport function BottomNavigation() {\n  const pathname = usePathname();\n  const t = useI18n();\n  const locale = useCurrentLocale();\n\n  const tabs = [\n    {\n      id: \"workout-builder\",\n      label: t(\"bottom_navigation.workouts\"),\n      shortLabel: t(\"bottom_navigation.workouts\"),\n      mobileLabel: t(\"bottom_navigation.workouts\"),\n      href: paths.root,\n      icon: Dumbbell,\n      emoji: \"WorkoutCoolHappy.png\",\n      description: t(\"bottom_navigation.workouts_tooltip\"),\n      isActive: pathname === paths.root || pathname === `/${locale}`,\n    },\n    {\n      id: \"programs\",\n      label: t(\"bottom_navigation.programs\"),\n      shortLabel: t(\"bottom_navigation.programs\"),\n      mobileLabel: t(\"bottom_navigation.programs\"),\n      href: `${paths.programs}`,\n      icon: Grid,\n      emoji: \"WorkoutCoolSwag.png\",\n      description: t(\"bottom_navigation.programs_tooltip\"),\n      isActive: pathname.includes(paths.programs),\n    },\n    {\n      id: \"statistics\",\n      label: t(\"bottom_navigation.statistics\"),\n      shortLabel: t(\"bottom_navigation.statistics\"),\n      mobileLabel: t(\"bottom_navigation.statistics\"),\n      href: `/${locale}/statistics`,\n      icon: Activity,\n      emoji: \"WorkoutCoolBiceps.png\",\n      description: t(\"bottom_navigation.statistics_tooltip\"),\n      isActive: pathname.includes(\"/statistics\"),\n    },\n    {\n      id: \"tools\",\n      label: t(\"bottom_navigation.tools\"),\n      shortLabel: t(\"bottom_navigation.tools\"),\n      mobileLabel: t(\"bottom_navigation.tools\"),\n      href: `/${locale}/tools`,\n      icon: Hammer,\n      description: t(\"bottom_navigation.tools_tooltip\"),\n      isActive: pathname.includes(\"/tools\"),\n    },\n    {\n      id: \"leaderboard\",\n      label: t(\"bottom_navigation.leaderboard\"),\n      shortLabel: t(\"bottom_navigation.leaderboard\"),\n      mobileLabel: t(\"bottom_navigation.leaderboard\"),\n      href: `/${locale}${paths.leaderboard}`,\n      icon: Trophy,\n      description: t(\"bottom_navigation.leaderboard_tooltip\"),\n      isActive: pathname.includes(paths.leaderboard),\n    },\n    {\n      id: \"premium\",\n      label: t(\"bottom_navigation.premium\"),\n      shortLabel: t(\"bottom_navigation.premium\"),\n      mobileLabel: t(\"bottom_navigation.premium\"),\n      href: `/${locale}/premium`,\n      icon: Crown,\n      emoji: \"WorkoutCoolRich.png\",\n      description: t(\"bottom_navigation.premium_tooltip\"),\n      isActive: pathname.includes(\"/premium\"),\n    },\n  ];\n\n  return (\n    <nav className=\"relative bg-white/90 dark:bg-[#232324]/90 backdrop-blur-xl border-t border-[#4F8EF7]/15 dark:border-slate-700/50\">\n      {/* Subtle background gradient */}\n      <div className=\"absolute inset-0 bg-gradient-to-r from-[#4F8EF7]/3 via-transparent to-[#25CB78]/3 pointer-events-none\" />\n\n      <div className=\"relative sm:px-3 py-2\">\n        <div className=\"flex justify-center items-center sm:gap-2 max-w-full mx-auto\">\n          {tabs.map((tab) => {\n            const isActive = tab.isActive;\n            const isPremium = tab.id === \"premium\";\n            const IconComponent = tab.icon;\n\n            return (\n              <Link\n                className={cn(\n                  \"group relative flex flex-col items-center gap-0.5 sm:gap-1 flex-1 min-w-0 px-1 sm:px-4 py-1 rounded-2xl transition-all duration-300 ease-out\",\n                  \"hover:scale-105 active:scale-95\",\n                  isActive && isPremium\n                    ? \"bg-gradient-to-br from-[#FFD93D]/8 to-[#FFA500]/8 border border-[#FFD93D]/20\"\n                    : isActive\n                      ? \"bg-gradient-to-br from-[#4F8EF7]/8 to-[#25CB78]/8 border border-[#4F8EF7]/20\"\n                      : isPremium\n                        ? \"hover:bg-[#FFD93D]/5 dark:hover:bg-[#FFD93D]/10\"\n                        : \"hover:bg-gray-50/80 dark:hover:bg-slate-800/50\",\n                )}\n                href={tab.href}\n                key={tab.id}\n              >\n                {/* Active top indicator */}\n                {isActive && (\n                  <div\n                    className={cn(\n                      \"absolute -top-0.5 left-1/2 transform -translate-x-1/2 w-4 sm:w-6 h-0.5 rounded-full\",\n                      isPremium ? \"bg-gradient-to-r from-[#FFD93D] to-[#FFA500]\" : \"bg-gradient-to-r from-[#4F8EF7] to-[#25CB78]\",\n                    )}\n                  />\n                )}\n\n                {/* Icon container */}\n                <div className=\"relative flex items-center justify-center\">\n                  {/* Active glow effect */}\n                  {isActive && (\n                    <div\n                      className={cn(\n                        \"absolute inset-0 rounded-full blur-sm scale-150\",\n                        isPremium\n                          ? \"bg-gradient-to-br from-[#FFD93D]/15 to-[#FFA500]/15\"\n                          : \"bg-gradient-to-br from-[#4F8EF7]/15 to-[#25CB78]/15\",\n                      )}\n                    />\n                  )}\n\n                  {/* Main icon */}\n                  <div\n                    className={cn(\n                      \"relative flex items-center justify-center w-6 h-6 sm:w-7 sm:h-7 rounded-full transition-all duration-300 ease-out\",\n                      isActive && isPremium\n                        ? \"bg-gradient-to-br from-[#FFD93D] to-[#FFA500] text-white scale-110\"\n                        : isActive\n                          ? \"bg-gradient-to-br from-[#4F8EF7] to-[#25CB78] text-white scale-110\"\n                          : isPremium\n                            ? \"text-[#FFD93D] dark:text-[#FFD93D] group-hover:text-[#FFA500] dark:group-hover:text-[#FFA500]\"\n                            : \"text-gray-500 dark:text-gray-400 group-hover:text-gray-700 dark:group-hover:text-gray-300\",\n                    )}\n                  >\n                    <IconComponent size={isActive ? 15 : 13} strokeWidth={isActive ? 2.5 : 2} />\n\n                    {/* Premium sparkle effect */}\n                    {isPremium && !isActive && (\n                      <div className=\"absolute -top-1 -right-1 w-2 h-2 bg-[#FFD93D] rounded-full opacity-80 animate-pulse\" />\n                    )}\n                  </div>\n                </div>\n\n                {/* Label - responsive text */}\n                <span\n                  className={cn(\n                    \"text-[9px] sm:text-xs font-semibold transition-all duration-300 ease-out leading-tight truncate max-w-full text-center\",\n                    isActive && isPremium\n                      ? \"text-transparent bg-gradient-to-r from-[#FFD93D] to-[#FFA500] bg-clip-text\"\n                      : isActive\n                        ? \"text-transparent bg-gradient-to-r from-[#4F8EF7] to-[#25CB78] bg-clip-text\"\n                        : isPremium\n                          ? \"text-[#FFD93D] dark:text-[#FFD93D] group-hover:text-[#FFA500] dark:group-hover:text-[#FFA500]\"\n                          : \"text-gray-600 dark:text-gray-400 group-hover:text-gray-800 dark:group-hover:text-gray-200\",\n                  )}\n                >\n                  <span className=\"hidden sm:inline\">{tab.shortLabel}</span>\n                  <span className=\"inline sm:hidden\">{tab.mobileLabel}</span>\n                </span>\n\n                {/* Hover tooltip - only on non-touch devices and larger screens */}\n                <div\n                  className={cn(\n                    \"absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-gray-900/90 text-white text-xs rounded-lg opacity-0 pointer-events-none transition-all duration-200 whitespace-nowrap z-50\",\n                    \"after:content-[''] after:absolute after:top-full after:left-1/2 after:transform after:-translate-x-1/2 after:border-4 after:border-transparent after:border-t-gray-900/90\",\n                    \"hidden sm:block sm:group-hover:opacity-100 sm:group-hover:-translate-y-1\",\n                  )}\n                >\n                  {tab.description}\n                </div>\n              </Link>\n            );\n          })}\n        </div>\n      </div>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "src/features/layout/Footer.tsx",
    "content": "import { Github, Mail, Twitter } from \"lucide-react\";\n\nimport { getI18n } from \"locales/server\";\nimport { TFunction } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { paths } from \"@/shared/constants/paths\";\nimport { WorkoutSessionTimer } from \"@/features/workout-session/ui/workout-session-timer\";\nimport UserLeaderboardPosition from \"@/features/leaderboard/ui/user-leaderboard-position\";\nimport { Link } from \"@/components/ui/link\";\nimport { DiscordSvg } from \"@/components/svg/DiscordSvg\";\n\nconst SOCIAL_LINKS = [\n  {\n    href: \"https://github.com/Snouzy/workout-cool\",\n    icon: Github,\n    label: \"GitHub\",\n  },\n  {\n    href: \"https://x.com/snouzy_biceps\",\n    icon: Twitter,\n    label: \"Twitter/X\",\n  },\n  {\n    href: \"mailto:coolworkout6@gmail.com\",\n    icon: Mail,\n    label: \"Email\",\n  },\n  {\n    href: \"https://discord.gg/NtrsUBuHUB\",\n    icon: DiscordSvg,\n    label: \"Discord\",\n  },\n];\n\nconst NAVIGATION = (t: TFunction) => [\n  { name: t(\"commons.donate\"), href: \"https://ko-fi.com/workoutcool\" },\n  { name: t(\"commons.about\"), href: \"/about\" },\n  { name: t(\"commons.privacy\"), href: paths.privacy, hideOnMobile: true },\n];\n\nexport const Footer = async () => {\n  const t = await getI18n();\n  return (\n    <footer className=\"relative border-t border-base-300 dark:border-gray-800 bg-base-100 dark:bg-black px-2 sm:px-6 py-2 rounded-b-lg\">\n      <WorkoutSessionTimer />\n      <UserLeaderboardPosition />\n      <div className=\"flex sm:flex-row justify-between items-center gap-4\">\n        {/* Social Icons */}\n        <div className=\"flex gap-0 sm:gap-2\">\n          {SOCIAL_LINKS.map(({ href, icon: Icon, label }) => (\n            <a\n              aria-label={label}\n              className=\"btn btn-ghost btn-sm btn-circle text-gray-700 dark:text-gray-300 hover:bg-slate-200 dark:hover:bg-gray-800\"\n              href={href}\n              key={label}\n              rel=\"noopener noreferrer\"\n              target=\"_blank\"\n            >\n              <Icon className=\"h-5 w-5\" />\n            </a>\n          ))}\n        </div>\n\n        {/* Navigation Links */}\n        <div className=\"flex sm:flex-row gap-1 sm:gap-3 text-center text-gray-700 dark:text-gray-300\">\n          {NAVIGATION(t).map(({ name, href, hideOnMobile }) => (\n            <Link\n              className={cn(\n                \"hover:underline hover:text-blue-500 dark:hover:text-blue-400 text-xs sm:text-sm\",\n                hideOnMobile && \"hidden sm:block\",\n              )}\n              href={href}\n              key={name}\n              size=\"sm\"\n              variant=\"footer\"\n              {...(href.startsWith(\"http\") && { target: \"_blank\", rel: \"noopener noreferrer\" })}\n            >\n              {name}\n            </Link>\n          ))}\n        </div>\n      </div>\n    </footer>\n  );\n};\n"
  },
  {
    "path": "src/features/layout/Header.tsx",
    "content": "\"use client\";\n\nimport Image from \"next/image\";\nimport { LogIn, UserPlus, LogOut, User, Crown, Sparkles } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport Logo from \"@public/logo.png\";\nimport { LanguageSelector } from \"@/widgets/language-selector/language-selector\";\nimport { usePremiumStatus } from \"@/shared/lib/premium/use-premium\";\nimport { ThemeToggle } from \"@/features/theme/ThemeToggle\";\nimport { ReleaseNotesDialog } from \"@/features/release-notes\";\nimport WorkoutStreakHeader from \"@/features/layout/workout-streak-header\";\nimport { useLogout } from \"@/features/auth/model/useLogout\";\nimport { useSession } from \"@/features/auth/lib/auth-client\";\nimport { env } from \"@/env\";\nimport { Link } from \"@/components/ui/link\";\nimport { RemoveAdsText } from \"@/components/premium/RemoveAdsText\";\n\nexport const Header = () => {\n  const session = useSession();\n  const logout = useLogout();\n  const t = useI18n();\n  const { data: premiumStatus } = usePremiumStatus();\n\n  // Get user initials for avatar\n  const userAvatar = session.data?.user?.email?.substring(0, 2).toUpperCase() || \"\";\n\n  const isPremium = premiumStatus?.isPremium ?? false;\n  const showAds = env.NEXT_PUBLIC_SHOW_ADS === true;\n  const hasAdProvider = env.NEXT_PUBLIC_AD_CLIENT || env.NEXT_PUBLIC_AD_PROVIDER === \"ezoic\";\n\n  const handleSignOut = () => {\n    logout.mutate();\n  };\n\n  const handleCloseDropdown = () => {\n    const element = document.activeElement as HTMLElement;\n    if (!element) return;\n    element.blur();\n  };\n\n  return (\n    <>\n      <div className=\"navbar bg-base-100 dark:bg-black dark:text-gray-200 px-2 sm:px-4 rounded-tl-lg rounded-tr-lg\">\n        {/* Logo and Title */}\n        <div className=\"navbar-start flex items-center gap-2\">\n          <Link\n            className=\"group flex items-center space-x-3 rounded-xl bg-gradient-to-r px-2 sm:px-4 py-2 transition-all duration-200 dark:text-gray-200 dark:bg-gray-800\"\n            href=\"/\"\n          >\n            <div className=\"relative flex-none\">\n              <Image\n                alt=\"workout cool logo\"\n                className=\"h-10 w-10 sm:h-8 sm:w-8 transition-transform duration-200 group-hover:rotate-[20deg] group-hover:scale-110\"\n                height={32}\n                priority\n                src={Logo}\n                width={32}\n              />\n              <div className=\"absolute -top-1 -right-1 h-3 w-3 rounded-full bg-emerald-400 opacity-0 transition-opacity duration-200 group-hover:opacity-100\"></div>\n            </div>\n            <div className=\"flex-col hidden sm:flex\">\n              <span className=\"font-bold transition-colors duration-200 group-hover:text-blue-400\">Workout.cool</span>\n            </div>\n          </Link>\n        </div>\n\n        {/* User Menu */}\n        <div className=\"navbar-end\">\n          {isPremium || !showAds || !hasAdProvider ? <WorkoutStreakHeader /> : <RemoveAdsText />}\n          <ReleaseNotesDialog />\n          <ThemeToggle />\n          <LanguageSelector />\n\n          <div className=\"dropdown dropdown-end ml-1\">\n            <div className=\"tooltip tooltip-bottom\" data-tip={t(\"commons.profile\")}>\n              <div className=\"btn btn-ghost btn-circle avatar relative\" role=\"button\" tabIndex={0}>\n                <div className=\"w-8 rounded-full bg-primary text-primary-content !flex items-center justify-center text-sm font-medium\">\n                  {userAvatar || <User className=\"w-4 h-4\" />}\n                </div>\n                {isPremium && (\n                  <div className=\"absolute -top-1 -right-1 w-4 h-4 bg-amber-400 rounded-full !flex items-center justify-center\">\n                    <Crown className=\"w-2.5 h-2.5 text-amber-900\" />\n                  </div>\n                )}\n              </div>\n            </div>\n\n            <ul\n              className=\"mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 dark:bg-black dark:text-gray-200 rounded-box w-52 border border-slate-200 dark:border-gray-800\"\n              onClick={handleCloseDropdown}\n              tabIndex={0}\n            >\n              <li>\n                <Link className=\"!no-underline\" href=\"/profile\" size=\"base\" variant=\"nav\">\n                  <User className=\"w-4 h-4 text-gray-700 dark:text-gray-300\" />\n                  {t(\"commons.profile\")}\n                </Link>\n              </li>\n\n              {/* Subscription Menu Item */}\n              <li>\n                <Link\n                  className=\"!no-underline\"\n                  href={isPremium ? \"/api/premium/billing-portal\" : \"/premium\"}\n                  size=\"base\"\n                  variant=\"nav\"\n                  {...(isPremium && {\n                    onClick: async (e) => {\n                      e.preventDefault();\n                      try {\n                        const response = await fetch(\"/api/premium/billing-portal\", {\n                          method: \"POST\",\n                          headers: { \"Content-Type\": \"application/json\" },\n                          body: JSON.stringify({ returnUrl: window.location.origin + \"/profile\" }),\n                        });\n                        const data = await response.json();\n                        if (data.success && data.url) {\n                          window.location.href = data.url;\n                        }\n                      } catch (error) {\n                        console.error(\"Error opening billing portal:\", error);\n                      }\n                    },\n                  })}\n                >\n                  {isPremium ? (\n                    <>\n                      <Crown className=\"w-4 h-4 text-amber-500\" />\n                      {t(\"commons.manage_subscription\")}\n                    </>\n                  ) : (\n                    <>\n                      <Sparkles className=\"w-4 h-4 text-purple-500\" />\n                      {t(\"commons.remove_ads\")}\n                    </>\n                  )}\n                </Link>\n              </li>\n\n              <hr className=\"my-1 border-slate-200 dark:border-gray-800\" />\n\n              {!session.data && !session.isPending ? (\n                <>\n                  <li>\n                    <Link className=\"!no-underline\" href=\"/auth/signin\" size=\"base\" variant=\"nav\">\n                      <LogIn className=\"w-4 h-4 text-gray-700 dark:text-gray-300\" />\n                      {t(\"commons.login\")}\n                    </Link>\n                  </li>\n                  <li>\n                    <Link className=\"!no-underline\" href=\"/auth/signup\" size=\"base\" variant=\"nav\">\n                      <UserPlus className=\"w-4 h-4 text-gray-700 dark:text-gray-300\" />\n                      {t(\"commons.register\")}\n                    </Link>\n                  </li>\n                </>\n              ) : (\n                <li>\n                  <button\n                    className=\"flex items-center gap-2 text-base text-gray-700 dark:text-gray-300 hover:bg-slate-200 dark:hover:bg-gray-800 rounded-lg px-3 py-2 transition-colors\"\n                    onClick={handleSignOut}\n                  >\n                    <LogOut className=\"w-4 h-4\" />\n                    {t(\"commons.logout\")}\n                  </button>\n                </li>\n              )}\n            </ul>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "src/features/layout/model/use-sidebar.store.tsx",
    "content": "import { create } from \"zustand\";\n\ninterface SidebarState {\n  currentPageId: string | null;\n  setCurrentPageId: (currentPageId: string | null) => void;\n}\n\nexport const useSidebarStore = create<SidebarState>()((set) => ({\n  currentPageId: null,\n  setCurrentPageId: (currentPageId) => set(() => ({ currentPageId })),\n}));\n"
  },
  {
    "path": "src/features/layout/nav-link.tsx",
    "content": "\"use client\";\n\nimport { usePathname } from \"next/navigation\";\nimport Link from \"next/link\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\ninterface IProp {\n  className?: string;\n  href: string;\n  active?: string;\n  target?: string;\n  targetPath?: string;\n  rel?: string;\n  children: React.ReactNode;\n  onClick?: () => void;\n  isAccordion?: boolean;\n  isSubAccordion?: boolean;\n}\nexport default function NavLink({\n  className,\n  href,\n  active,\n  target,\n  rel,\n  children,\n  onClick,\n  targetPath,\n  isAccordion,\n  isSubAccordion,\n}: IProp) {\n  const pathName = usePathname();\n\n  return (\n    <Link\n      className={cn(\n        \"relative\",\n        {\n          \"sub-menu-active\":\n            (active || (!active && pathName === href) || (targetPath && pathName.startsWith(targetPath))) &&\n            (isAccordion || isSubAccordion),\n\n          active:\n            (active || (!active && pathName === href) || (targetPath && pathName.startsWith(targetPath))) &&\n            !(isAccordion || isSubAccordion),\n        },\n        \"nav-item\",\n        className,\n      )}\n      href={href}\n      onClick={onClick && onClick}\n      rel={rel}\n      target={target}\n    >\n      {children}\n      {isAccordion && (\n        <div className=\"absolute top-3 flex flex-col items-center gap-1 ltr:-left-5 rtl:-right-5\">\n          <div\n            className={cn(\"size-[5px] rounded-full bg-gray-700/50 dark:bg-gray-600\", pathName === href && \"bg-primary dark:bg-[#6683F8]\")}\n          ></div>\n          <div className=\"h-[26px] w-px rounded-full bg-gray-300 dark:bg-gray\"></div>\n        </div>\n      )}\n\n      {isSubAccordion && (\n        <div className=\"absolute top-3 flex flex-col items-center gap-1 ltr:-left-4 rtl:-right-4\">\n          <div\n            className={`size-[5px] rounded-full bg-gray-700/50 dark:bg-gray-600 ${pathName === href && \"bg-primary dark:bg-[#6683F8]\"}`}\n          ></div>\n          <div className=\"h-6 w-px rounded-full bg-gray-300 dark:bg-gray\"></div>\n        </div>\n      )}\n    </Link>\n  );\n}\n"
  },
  {
    "path": "src/features/layout/page-heading.tsx",
    "content": "import { cn } from \"@/shared/lib/utils\";\nimport { Card, CardContent } from \"@/components/ui/card\";\n\ntype PageHeadingProps = {\n  heading: string;\n  className?: string;\n};\n\nconst PageHeading = ({ heading, className }: PageHeadingProps) => {\n  return (\n    <Card className={cn(\"px-5 py-3.5 text-base/5 font-semibold text-black shadow-sm dark:text-white\", className)}>\n      <CardContent>{heading}</CardContent>\n    </Card>\n  );\n};\n\nexport default PageHeading;\n"
  },
  {
    "path": "src/features/layout/useSidebarToggle.ts",
    "content": "\"use client\";\n\nimport { useCallback, useState } from \"react\";\n\nexport const useSidebarToggle = () => {\n  const [isOpen, setIsOpen] = useState(false);\n\n  const toggleSidebar = useCallback(() => {\n    const sidebar = document.getElementById(\"sidebar\");\n    const overlay = document.getElementById(\"overlay\");\n\n    if (sidebar) {\n      sidebar.classList.toggle(\"open\");\n    }\n\n    if (overlay) {\n      overlay.classList.toggle(\"open\");\n    }\n\n    setIsOpen((prev) => !prev);\n  }, []);\n\n  return { isOpen, toggleSidebar };\n};\n"
  },
  {
    "path": "src/features/layout/workout-streak-header.tsx",
    "content": "import { useMemo } from \"react\";\nimport utc from \"dayjs/plugin/utc\";\nimport timezone from \"dayjs/plugin/timezone\";\nimport dayjs from \"dayjs\";\n\nimport { useCurrentLocale } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { formatDate } from \"@/shared/lib/date\";\nimport { useWorkoutSessions } from \"@/features/workout-session/model/use-workout-sessions\";\n\n// Configure dayjs with timezone support\ndayjs.extend(utc);\ndayjs.extend(timezone);\n\nimport type { WorkoutSession } from \"@/shared/lib/workout-session/types/workout-session\";\n\nconst DEFAULT_STREAK_COUNT = 5;\n\n/**\n * Props for WorkoutStreakHeader component\n */\nexport interface WorkoutStreakHeaderProps {\n  className?: string;\n  /** Number of days to display in the streak (default: 5) */\n  streakCount?: number;\n}\n\n/**\n * Represents the streak data for a given day\n */\nexport interface DayStreakData {\n  /** The date for this day */\n  date: string;\n  /** Whether the user worked out on this day */\n  hasWorkout: boolean;\n  /** The workout session for this day (if any, maybe we could redirect to the session page in the future) */\n  session?: WorkoutSession;\n}\n\n/**\n * Complete streak data for the component\n */\nexport interface StreakData {\n  /** Array of daily streak data */\n  days: DayStreakData[];\n  /** Current streak count */\n  currentStreak: number;\n  /** Total workouts in the displayed period */\n  totalWorkouts: number;\n}\n\n/**\n * WorkoutStreakHeader component displays a visual representation of the user's\n * workout streak over the last N days (default 5).\n *\n * @param props - Component props\n * @returns JSX element representing the workout streak\n */\nexport default function WorkoutStreakHeader({ className, streakCount = DEFAULT_STREAK_COUNT }: WorkoutStreakHeaderProps) {\n  const { data: sessions, isLoading: sessionsLoading, error: sessionsError } = useWorkoutSessions();\n  const locale = useCurrentLocale();\n  // Get user's timezone for accurate date calculations (memoized for performance)\n  const userTimezone = useMemo(() => {\n    try {\n      return dayjs.tz.guess();\n    } catch (error) {\n      console.warn(\"Failed to detect timezone, falling back to UTC:\", error);\n      return \"UTC\";\n    }\n  }, []);\n\n  // Calculate recent sessions within the streak period\n  const recentSessions = useMemo(() => {\n    if (!sessions) return [];\n\n    const recentSessions = sessions.filter((s) => {\n      // Only include sessions that have ended (completed workouts)\n      if (!s.endedAt) return false;\n\n      try {\n        // Convert session end time to user's timezone for accurate day comparison\n        const endDate = dayjs(s.endedAt).tz(userTimezone);\n        const cutoffDate = dayjs().tz(userTimezone).subtract(streakCount, \"day\").startOf(\"day\");\n\n        return endDate.isAfter(cutoffDate);\n      } catch (error) {\n        console.warn(\"Error processing session date:\", error, s);\n        return false;\n      }\n    });\n\n    // Sort from oldest to most recent\n    recentSessions.sort((a, b) => dayjs(a.endedAt).diff(dayjs(b.endedAt)));\n    return recentSessions;\n  }, [sessions, streakCount, userTimezone]);\n\n  // Generate streak data for each day in the period\n  const streakData = useMemo<StreakData>(() => {\n    const days: DayStreakData[] = [...Array(streakCount)].map((_, i) => {\n      try {\n        // Calculate target date in user's timezone\n        const targetDate = dayjs()\n          .tz(userTimezone)\n          .subtract(streakCount - 1 - i, \"day\")\n          .startOf(\"day\");\n\n        // Find the most recent session for this day in user's timezone\n        const session = recentSessions.findLast((session) => {\n          try {\n            const sessionDate = dayjs(session.endedAt).tz(userTimezone);\n            return sessionDate.isSame(targetDate, \"day\");\n          } catch (error) {\n            console.warn(\"Error comparing session date:\", error, session);\n            return false;\n          }\n        });\n\n        const date = formatDate(targetDate.toDate(), locale);\n        return {\n          date,\n          hasWorkout: !!session,\n          session: session || undefined,\n        };\n      } catch (error) {\n        console.warn(\"Error calculating streak data for day:\", error, i);\n        // Return fallback data for this day\n        const fallbackDate = dayjs()\n          .subtract(streakCount - 1 - i, \"day\")\n          .format(\"YYYY-MM-DD\");\n        return {\n          date: fallbackDate,\n          hasWorkout: false,\n          session: undefined,\n        };\n      }\n    });\n\n    // Calculate current streak (consecutive days from the end)\n    let currentStreak = 0;\n    for (let i = days.length - 1; i >= 0; i--) {\n      if (days[i].hasWorkout) {\n        currentStreak++;\n      } else {\n        break;\n      }\n    }\n\n    // Calculate total workouts in the period\n    const totalWorkouts = days.filter((day) => day.hasWorkout).length;\n\n    return {\n      days,\n      currentStreak,\n      totalWorkouts,\n    };\n  }, [recentSessions, streakCount, userTimezone]);\n\n  // Handle loading state\n  if (sessionsLoading) {\n    return (\n      <div aria-label=\"Loading workout streak\" className={`flex gap-1 ${className}`} role=\"status\">\n        {[...Array(streakCount)].map((_, i) => (\n          <div\n            aria-hidden=\"true\"\n            className=\"w-4 h-4 sm:w-6 sm:h-6 rounded-sm sm:rounded-md bg-base-300 animate-pulse transition-colors duration-200\"\n            key={i}\n          />\n        ))}\n      </div>\n    );\n  }\n\n  // Handle error state\n  if (sessionsError) {\n    return (\n      <div aria-label=\"Error loading workout streak\" className={`flex gap-1 ${className}`} role=\"alert\">\n        {[...Array(streakCount)].map((_, i) => (\n          <div\n            aria-hidden=\"true\"\n            className=\"w-4 h-4 sm:w-6 sm:h-6 rounded-sm sm:rounded-md bg-error/20 border border-error/30 transition-colors duration-200\"\n            key={i}\n          />\n        ))}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      aria-label={`Workout streak: ${streakData.currentStreak} day${streakData.currentStreak !== 1 ? \"s\" : \"\"}, ${streakData.totalWorkouts} workouts in last ${streakCount} days`}\n      className={cn(\"flex gap-1 sm:mr-2\", className)}\n      role=\"img\"\n    >\n      {streakData.days.map((day) => {\n        const title = `${day.date}: ${day.hasWorkout ? \"✅️\" : \"❌️\"}`;\n\n        return (\n          <div\n            aria-label={`${day.date}: ${day.hasWorkout ? \"Workout completed\" : \"No workout\"}`}\n            className={`w-4 h-4 sm:w-6 sm:h-6 rounded-sm sm:rounded-md transition-all duration-200 ease-in-out tooltip tooltip-bottom hover:scale-110 cursor-pointer focus:ring-2 focus:ring-offset-1 focus:outline-none ${\n              day.hasWorkout\n                ? \"bg-emerald-400 dark:bg-emerald-500 shadow-sm hover:shadow-md hover:brightness-110 focus:ring-emerald-300\"\n                : \"bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 hover:bg-gray-400 dark:hover:bg-gray-500 focus:ring-gray-300 dark:focus:ring-gray-400\"\n            }`}\n            data-tip={title}\n            key={day.date}\n            role=\"button\"\n            tabIndex={0}\n            title={title}\n          />\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/leaderboard/actions/get-top-workout-users.action.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\nimport dayjs from \"dayjs\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\nimport { TopWorkoutUser } from \"@/features/leaderboard/models/types\";\nimport { getDateRangeForPeriod } from \"@/features/leaderboard/lib/utils\";\n\nconst LIMIT_TOP_USERS = 20;\n\nexport type LeaderboardPeriod = \"all-time\" | \"weekly\" | \"monthly\";\n\nconst inputSchema = z.object({\n  period: z.enum([\"all-time\", \"weekly\", \"monthly\"]).default(\"all-time\"),\n});\n\nexport const getTopWorkoutUsersAction = actionClient.schema(inputSchema).action(async ({ parsedInput }) => {\n  const { period } = parsedInput;\n\n  try {\n    const { startDate, endDate } = getDateRangeForPeriod(period);\n\n    const whereClause = {\n      WorkoutSession: {\n        some: startDate\n          ? {\n              startedAt: {\n                gte: startDate,\n                lte: endDate,\n              },\n            }\n          : {},\n      },\n    };\n\n    const topUsers = await prisma.user.findMany({\n      where: whereClause,\n      select: {\n        id: true,\n        name: true,\n        email: true,\n        image: true,\n        createdAt: true,\n        _count: {\n          select: {\n            WorkoutSession: startDate\n              ? {\n                  where: {\n                    startedAt: {\n                      gte: startDate,\n                      lte: endDate,\n                    },\n                  },\n                }\n              : true,\n          },\n        },\n        WorkoutSession: {\n          where: startDate\n            ? {\n                startedAt: {\n                  gte: startDate,\n                  lte: endDate,\n                },\n              }\n            : undefined,\n          select: {\n            endedAt: true,\n            startedAt: true,\n          },\n          orderBy: {\n            startedAt: \"desc\",\n          },\n          take: 1,\n        },\n      },\n      orderBy: {\n        WorkoutSession: {\n          _count: \"desc\",\n        },\n      },\n      take: LIMIT_TOP_USERS,\n    });\n\n    const users: TopWorkoutUser[] = topUsers\n      .map((user) => {\n        const totalWorkouts = user._count.WorkoutSession;\n        const lastWorkout = user.WorkoutSession[0];\n        const lastWorkoutAt = lastWorkout?.endedAt || lastWorkout?.startedAt || null;\n\n        const startDate = user.createdAt;\n        const weeksSinceStart = Math.max(1, Math.ceil(dayjs().diff(dayjs(startDate), \"week\", true)));\n\n        const averageWorkoutsPerWeek = Math.round((totalWorkouts / weeksSinceStart) * 10) / 10;\n\n        return {\n          userId: user.id,\n          userName: user.name,\n          userImage: user.image,\n          totalWorkouts,\n          lastWorkoutAt: lastWorkoutAt,\n          averageWorkoutsPerWeek,\n          memberSince: user.createdAt,\n        };\n      })\n      .sort((a, b) => b.totalWorkouts - a.totalWorkouts);\n\n    return users;\n  } catch (error) {\n    console.error(\"Error fetching top workout users:\", error);\n    throw new Error(\"Failed to fetch top workout users\");\n  }\n});\n"
  },
  {
    "path": "src/features/leaderboard/actions/get-user-position.action.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\nimport { getDateRangeForPeriod } from \"@/features/leaderboard/lib/utils\";\n\nconst inputSchema = z.object({\n  userId: z.string(),\n  period: z.enum([\"all-time\", \"weekly\", \"monthly\"]).default(\"all-time\"),\n});\n\nexport const getUserPositionAction = actionClient.schema(inputSchema).action(async ({ parsedInput }) => {\n  const { userId, period } = parsedInput;\n\n  try {\n    const { startDate, endDate } = getDateRangeForPeriod(period);\n\n    // Get user's workout count\n    const userWorkoutCount = await prisma.workoutSession.count({\n      where: {\n        userId,\n        ...(startDate && {\n          startedAt: {\n            gte: startDate,\n            lte: endDate,\n          },\n        }),\n      },\n    });\n\n    // Calculate real position\n    const totalUsersWithWorkouts = await prisma.user.count({\n      where: {\n        WorkoutSession: {\n          some: startDate\n            ? {\n                startedAt: {\n                  gte: startDate,\n                  lte: endDate,\n                },\n              }\n            : {},\n        },\n      },\n    });\n\n    // Get all users sorted by workout count to find exact position\n    const allUsers = await prisma.user.findMany({\n      where: {\n        WorkoutSession: {\n          some: startDate\n            ? {\n                startedAt: {\n                  gte: startDate,\n                  lte: endDate,\n                },\n              }\n            : {},\n        },\n      },\n      select: {\n        id: true,\n        _count: {\n          select: {\n            WorkoutSession: startDate\n              ? {\n                  where: {\n                    startedAt: {\n                      gte: startDate,\n                      lte: endDate,\n                    },\n                  },\n                }\n              : true,\n          },\n        },\n      },\n      orderBy: {\n        WorkoutSession: {\n          _count: \"desc\",\n        },\n      },\n    });\n\n    const position = allUsers.findIndex((user) => user.id === userId) + 1;\n\n    return {\n      position: position || totalUsersWithWorkouts + 1,\n      totalWorkouts: userWorkoutCount,\n      totalUsers: totalUsersWithWorkouts,\n    };\n  } catch (error) {\n    console.error(\"Error fetching user position:\", error);\n    throw new Error(\"Failed to fetch user position\");\n  }\n});\n"
  },
  {
    "path": "src/features/leaderboard/hooks/use-top-workout-users.ts",
    "content": "\"use client\";\n\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { getTopWorkoutUsersAction, LeaderboardPeriod } from \"../actions/get-top-workout-users.action\";\n\nexport interface UseTopWorkoutUsersOptions {\n  refetchInterval?: number;\n  period?: LeaderboardPeriod;\n}\n\nexport function useTopWorkoutUsers(options: UseTopWorkoutUsersOptions = {}) {\n  const { refetchInterval, period = \"all-time\" } = options;\n\n  return useQuery({\n    queryKey: [\"top-workout-users\", period],\n    queryFn: async () => {\n      const result = await getTopWorkoutUsersAction({ period });\n      return result?.data || [];\n    },\n    staleTime: 5 * 60 * 1000, // 5 minutes\n    refetchInterval,\n    refetchOnWindowFocus: false,\n    retry: 3,\n  });\n}\n"
  },
  {
    "path": "src/features/leaderboard/hooks/use-user-position.ts",
    "content": "\"use client\";\n\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { getUserPositionAction } from \"../actions/get-user-position.action\";\nimport { LeaderboardPeriod } from \"../actions/get-top-workout-users.action\";\n\ninterface UseUserPositionOptions {\n  userId: string | undefined;\n  period: LeaderboardPeriod;\n  enabled?: boolean;\n}\n\nexport function useUserPosition({ userId, period, enabled = true }: UseUserPositionOptions) {\n  return useQuery({\n    queryKey: [\"user-position\", userId, period],\n    queryFn: async () => {\n      if (!userId) return null;\n      const result = await getUserPositionAction({ userId, period });\n      return result?.data || null;\n    },\n    enabled: enabled && !!userId,\n    staleTime: 5 * 60 * 1000, // 5 minutes\n    refetchOnWindowFocus: false,\n  });\n}\n"
  },
  {
    "path": "src/features/leaderboard/lib/utils.ts",
    "content": "import utc from \"dayjs/plugin/utc\";\nimport timezone from \"dayjs/plugin/timezone\";\nimport dayjs from \"dayjs\";\n\n// Initialize dayjs plugins\ndayjs.extend(utc);\ndayjs.extend(timezone);\n\nconst PARIS_TZ = \"Europe/Paris\";\n\nexport type LeaderboardPeriod = \"all-time\" | \"weekly\" | \"monthly\";\n\nexport function getDateRangeForPeriod(period: LeaderboardPeriod): { startDate: Date | undefined; endDate: Date } {\n  const now = dayjs().tz(PARIS_TZ);\n\n  switch (period) {\n    case \"weekly\": {\n      // Start of current week (Monday) in Paris timezone\n      const startOfWeek = now.startOf(\"week\").add(1, \"day\"); // dayjs week starts on Sunday, add 1 for Monday\n      return {\n        startDate: startOfWeek.toDate(),\n        endDate: now.toDate(),\n      };\n    }\n    case \"monthly\": {\n      // Start of current month in Paris timezone\n      const startOfMonth = now.startOf(\"month\");\n      return {\n        startDate: startOfMonth.toDate(),\n        endDate: now.toDate(),\n      };\n    }\n    case \"all-time\":\n    default:\n      return {\n        startDate: undefined,\n        endDate: now.toDate(),\n      };\n  }\n}"
  },
  {
    "path": "src/features/leaderboard/models/types.ts",
    "content": "export interface TopWorkoutUser {\n  userId: string;\n  userName: string;\n  userImage: string | null;\n  totalWorkouts: number;\n  lastWorkoutAt: Date | null;\n  averageWorkoutsPerWeek: number;\n  memberSince: Date;\n}\n"
  },
  {
    "path": "src/features/leaderboard/ui/leaderboard-item.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport Image from \"next/image\";\nimport { ClockIcon } from \"lucide-react\";\n\nimport { useI18n, useCurrentLocale } from \"locales/client\";\nimport { formatDateShort, formatRelativeTime } from \"@/shared/lib/date\";\nimport { TopWorkoutUser } from \"@/features/leaderboard/models/types\";\n\nconst LeaderboardItem: React.FC<{ user: TopWorkoutUser; rank: number }> = ({ user, rank }) => {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n  const [imageError, setImageError] = React.useState(false);\n  const dicebearUrl = `https://api.dicebear.com/7.x/micah/svg?seed=${encodeURIComponent(user.userId)}&backgroundColor=b6e3f4,c0aede,d1d4f9,ffd5dc,ffdfbf`;\n\n  return (\n    <div className=\"flex items-center gap-2 sm:gap-4 p-3 sm:p-4 hover:bg-base-200/50 dark:hover:bg-gray-800/30 transition-colors duration-150\">\n      {/* Rank number */}\n      <div className=\"w-3 sm:w-8 text-center\">\n        <span\n          className={`font-semibold ${rank <= 3 ? \"text-lg\" : \"text-base\"} ${\n            rank === 1\n              ? \"text-yellow-600 dark:text-yellow-500\"\n              : rank === 2\n                ? \"text-gray-500 dark:text-gray-400\"\n                : rank === 3\n                  ? \"text-amber-600 dark:text-amber-500\"\n                  : \"text-gray-600 dark:text-gray-500\"\n          }`}\n        >\n          {rank}\n        </span>\n      </div>\n\n      {/* User Avatar */}\n      <div className=\"relative flex-shrink-0\">\n        <div className=\"avatar\">\n          <div className=\"w-10 h-10 sm:w-12 sm:h-12 rounded-full ring-2 ring-base-200 dark:ring-gray-700\">\n            {user.userImage && !imageError ? (\n              <Image\n                alt={user.userName}\n                className=\"rounded-full object-cover\"\n                height={48}\n                onError={() => setImageError(true)}\n                src={user.userImage}\n                unoptimized={user.userImage.includes(\"googleusercontent\")}\n                width={48}\n              />\n            ) : (\n              <Image alt={user.userName} className=\"rounded-full\" height={48} src={dicebearUrl} width={48} />\n            )}\n          </div>\n        </div>\n      </div>\n\n      {/* User Info */}\n      <div className=\"flex-1 min-w-0\">\n        <h3 className=\"font-medium text-sm sm:text-base text-base-content dark:text-gray-100 truncate\">{user.userName}</h3>\n        <div className=\"flex flex-col sm:flex-row sm:items-center sm:gap-1 text-xs text-gray-600 dark:text-gray-500\">\n          <span className=\"text-base-content/60 dark:text-gray-400 text-[11px]\">\n            {t(\"leaderboard.member_since\")} {formatDateShort(user.memberSince, locale)}\n          </span>\n          {user.lastWorkoutAt && (\n            <>\n              <span className=\"hidden sm:flex\">-</span>\n              <span className=\"gap-1 text-gray-600 dark:text-gray-600\">\n                <span className=\"flex text-[11px] items-center\">\n                  <ClockIcon className=\"w-3 h-3 mr-1\" /> {formatRelativeTime(user.lastWorkoutAt, locale, t(\"commons.just_now\"))}\n                </span>\n              </span>\n            </>\n          )}\n        </div>\n      </div>\n\n      {/* Workout Score - Right aligned on desktop */}\n      <div className=\"flex flex-col items-end\">\n        <div className=\"text-xl sm:text-2xl font-bold text-[#4F8EF7] dark:text-[#4F8EF7]\">{user.totalWorkouts}</div>\n        <div className=\"text-xs text-gray-600 dark:text-gray-500\">{t(\"leaderboard.workouts\").toLowerCase()}</div>\n      </div>\n    </div>\n  );\n};\n\nexport default LeaderboardItem;\n"
  },
  {
    "path": "src/features/leaderboard/ui/leaderboard-page.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { useQueryState } from \"nuqs\";\nimport Image from \"next/image\";\nimport { Trophy, Users, Calendar } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\n\nimport { useTopWorkoutUsers } from \"../hooks/use-top-workout-users\";\nimport { LeaderboardPeriod } from \"../actions/get-top-workout-users.action\";\nimport LeaderboardSkeleton from \"./leaderboard-skeleton\";\nimport LeaderboardItem from \"./leaderboard-item\";\n\nexport default function LeaderboardPage() {\n  const t = useI18n();\n\n  // Use nuqs to manage period in URL\n  const [selectedPeriod, setSelectedPeriod] = useQueryState<LeaderboardPeriod>(\"period\", {\n    defaultValue: \"all-time\",\n    parse: (value) => {\n      if (value === \"weekly\" || value === \"monthly\" || value === \"all-time\") {\n        return value as LeaderboardPeriod;\n      }\n      return \"all-time\";\n    },\n  });\n\n  const { data: topUsers, isLoading, error } = useTopWorkoutUsers({ period: selectedPeriod });\n\n  // Get top 3 for podium display\n  const topThree = topUsers?.slice(0, 3) || [];\n\n  const tabs = [\n    { id: \"all-time\" as LeaderboardPeriod, label: t(\"leaderboard.period_all_time\") },\n    { id: \"monthly\" as LeaderboardPeriod, label: t(\"leaderboard.period_monthly\") },\n    { id: \"weekly\" as LeaderboardPeriod, label: t(\"leaderboard.period_weekly\") },\n  ];\n\n  return (\n    <div className=\"min-h-screen\">\n      <div className=\"container max-w-2xl mx-auto px-2 py-4 sm:px-4 sm:py-8 pb-24\">\n        {/* Header Section */}\n        <div className=\"mb-8\">\n          <div className=\"text-center mb-6\">\n            <div className=\"inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-full mb-4 shadow-lg\">\n              <Trophy className=\"w-8 h-8 text-white\" />\n            </div>\n            <h1 className=\"text-2xl sm:text-3xl font-bold mb-2 bg-gradient-to-r from-yellow-500 to-orange-600 bg-clip-text text-transparent\">\n              {t(\"leaderboard.page_title\")}\n            </h1>\n            <p className=\"text-base-content/70 dark:text-gray-400 max-w-md mx-auto\">\n              {t(\"leaderboard.page_subtitle\")}\n            </p>\n          </div>\n        </div>\n\n        {/* Period Tabs */}\n        <div className=\"flex justify-center mb-6\">\n          <div className=\"tabs tabs-boxed bg-base-200 dark:bg-gray-800\">\n            {tabs.map((tab) => (\n              <button\n                className={`tab ${\n                  selectedPeriod === tab.id ? \"tab-active bg-[#4F8EF7] text-white\" : \"text-base-content/70 dark:text-gray-400\"\n                }`}\n                key={tab.id}\n                onClick={() => setSelectedPeriod(tab.id)}\n              >\n                {tab.label}\n              </button>\n            ))}\n          </div>\n        </div>\n\n        {/* Top 3 Podium - Only on desktop */}\n        {topThree.length === 3 && !isLoading && (\n          <div className=\"flex justify-center items-end gap-4 mb-12 pt-16\">\n            {/* 2nd Place */}\n            <div className=\"text-center\">\n              <div className=\"relative\">\n                <div className=\"avatar mb-3\">\n                  <div className=\"w-20 rounded-full ring-4 ring-gray-400\">\n                    <Image\n                      alt={topThree[1].userName}\n                      height={80}\n                      src={topThree[1].userImage || `https://api.dicebear.com/7.x/micah/svg?seed=${encodeURIComponent(topThree[1].userId)}`}\n                      width={80}\n                    />\n                  </div>\n                </div>\n                <div className=\"absolute -bottom-2 left-1/2 -translate-x-1/2 w-8 h-8 bg-gray-400 rounded-full flex items-center justify-center\">\n                  <span className=\"text-sm font-bold text-white\">2</span>\n                </div>\n              </div>\n              <h3 className=\"font-medium text-sm mt-4\">{topThree[1].userName}</h3>\n              <p className=\"text-xs text-base-content/60\">{topThree[1].totalWorkouts} {t(\"leaderboard.workouts\")}</p>\n            </div>\n\n            {/* 1st Place */}\n            <div className=\"text-center -mt-4\">\n              <div className=\"relative\">\n                <Trophy className=\"absolute -top-10 left-1/2 -translate-x-1/2 w-8 h-8 text-yellow-500\" />\n                <div className=\"avatar mb-3\">\n                  <div className=\"w-24 rounded-full ring-4 ring-yellow-500\">\n                    <Image\n                      alt={topThree[0].userName}\n                      height={96}\n                      src={topThree[0].userImage || `https://api.dicebear.com/7.x/micah/svg?seed=${encodeURIComponent(topThree[0].userId)}`}\n                      width={96}\n                    />\n                  </div>\n                </div>\n                <div className=\"absolute -bottom-2 left-1/2 -translate-x-1/2 w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center\">\n                  <span className=\"text-sm font-bold text-white\">1</span>\n                </div>\n              </div>\n              <h3 className=\"font-medium text-base mt-4\">{topThree[0].userName}</h3>\n              <p className=\"text-sm text-base-content/60\">{topThree[0].totalWorkouts} {t(\"leaderboard.workouts\")}</p>\n            </div>\n\n            {/* 3rd Place */}\n            <div className=\"text-center\">\n              <div className=\"relative\">\n                <div className=\"avatar mb-3\">\n                  <div className=\"w-20 rounded-full ring-4 ring-amber-600\">\n                    <Image\n                      alt={topThree[2].userName}\n                      height={80}\n                      src={topThree[2].userImage || `https://api.dicebear.com/7.x/micah/svg?seed=${encodeURIComponent(topThree[2].userId)}`}\n                      width={80}\n                    />\n                  </div>\n                </div>\n                <div className=\"absolute -bottom-2 left-1/2 -translate-x-1/2 w-8 h-8 bg-amber-600 rounded-full flex items-center justify-center\">\n                  <span className=\"text-sm font-bold text-white\">3</span>\n                </div>\n              </div>\n              <h3 className=\"font-medium text-sm mt-4\">{topThree[2].userName}</h3>\n              <p className=\"text-xs text-base-content/60\">{topThree[2].totalWorkouts} {t(\"leaderboard.workouts\")}</p>\n            </div>\n          </div>\n        )}\n\n        {/* Leaderboard List */}\n        <div className=\"card bg-white dark:bg-[#1A1A1A] border border-base-300 dark:border-gray-800\">\n          {isLoading && <LeaderboardSkeleton />}\n\n          {error && (\n            <div className=\"card-body text-center py-12\">\n              <p className=\"text-base-content/70 dark:text-gray-400\">{t(\"leaderboard.unable_to_load\")}</p>\n              <p className=\"text-sm text-base-content/50 dark:text-gray-500 mt-1\">{t(\"leaderboard.try_again_later\")}</p>\n            </div>\n          )}\n\n          {topUsers && topUsers.length === 0 && !isLoading && (\n            <div className=\"card-body text-center py-12\">\n              <Trophy className=\"w-12 h-12 mx-auto mb-4 text-base-content/20\" />\n              <p className=\"text-base-content/70 dark:text-gray-400\">\n                {selectedPeriod === \"all-time\"\n                  ? t(\"leaderboard.no_champions_yet\")\n                  : selectedPeriod === \"weekly\"\n                    ? t(\"leaderboard.no_sessions_this_week\")\n                    : t(\"leaderboard.no_sessions_this_month\")}\n              </p>\n              <p className=\"text-sm text-base-content/50 dark:text-gray-500 mt-1\">{t(\"leaderboard.complete_first_workout\")}</p>\n            </div>\n          )}\n\n          {topUsers && topUsers.length > 0 && (\n            <div className=\"divide-y divide-base-200 dark:divide-gray-800\">\n              {topUsers.map((user, index) => (\n                <LeaderboardItem key={user.userId} rank={index + 1} user={user} />\n              ))}\n            </div>\n          )}\n        </div>\n\n        <div className=\"grid sm:grid-cols-2 gap-3 max-w-xl mx-auto px-4 my-4 pb-16\">\n          <div className=\"card bg-base-100 dark:bg-gray-800/50 border border-base-200 dark:border-gray-700\">\n            <div className=\"card-body p-4 flex-row items-start gap-3\">\n              <div className=\"flex-shrink-0\">\n                <div className=\"w-10 h-10 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center\">\n                  <Users className=\"w-5 h-5 text-blue-600 dark:text-blue-400\" />\n                </div>\n              </div>\n              <div className=\"flex-1\">\n                <h3 className=\"font-semibold text-sm mb-1\">{t(\"leaderboard.registered_members_only\")}</h3>\n                <p className=\"text-xs text-base-content/60 dark:text-gray-500\">\n                  {t(\"leaderboard.registered_members_description\")}\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"card bg-base-100 dark:bg-gray-800/50 border border-base-200 dark:border-gray-700\">\n            <div className=\"card-body p-4 flex-row items-start gap-3\">\n              <div className=\"flex-shrink-0\">\n                <div className=\"w-10 h-10 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center\">\n                  <Calendar className=\"w-5 h-5 text-purple-600 dark:text-purple-400\" />\n                </div>\n              </div>\n              <div className=\"flex-1\">\n                <h3 className=\"font-semibold text-sm mb-1\">{t(\"leaderboard.reset_timezone\")}</h3>\n                <p className=\"text-xs text-base-content/60 dark:text-gray-500\">\n                  {t(\"leaderboard.reset_timezone_description\")}\n                </p>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/leaderboard/ui/leaderboard-skeleton.tsx",
    "content": "import { Skeleton } from \"@/components/ui/skeleton\";\n\nconst LeaderboardSkeleton: React.FC = () => (\n  <div className=\"divide-y divide-gray-200 dark:divide-gray-700\">\n    {[...Array(5)].map((_, i) => (\n      <div className=\"flex items-center gap-4 p-6\" key={i}>\n        <Skeleton className=\"w-8 h-6\" />\n        <Skeleton className=\"h-12 w-12 rounded-full\" />\n        <div className=\"flex-1 space-y-2\">\n          <Skeleton className=\"h-5 w-32\" />\n          <Skeleton className=\"h-4 w-48\" />\n        </div>\n        <div className=\"text-right space-y-1\">\n          <Skeleton className=\"h-8 w-12 ml-auto\" />\n          <Skeleton className=\"h-3 w-16 ml-auto\" />\n        </div>\n      </div>\n    ))}\n  </div>\n);\n\nexport default LeaderboardSkeleton;\n"
  },
  {
    "path": "src/features/leaderboard/ui/user-leaderboard-position.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { useQueryState } from \"nuqs\";\nimport { usePathname } from \"next/navigation\";\n\nimport { useSession } from \"@/features/auth/lib/auth-client\";\n\nimport { useUserPosition } from \"../hooks/use-user-position\";\nimport { LeaderboardPeriod } from \"../actions/get-top-workout-users.action\";\n\nexport default function UserLeaderboardPosition() {\n  const { data: session } = useSession();\n  const isAuthenticated = !!session?.user;\n  const userId = session?.user?.id;\n\n  // Get period from URL using nuqs\n  const [period] = useQueryState<LeaderboardPeriod>(\"period\", {\n    defaultValue: \"all-time\",\n    parse: (value) => {\n      if (value === \"weekly\" || value === \"monthly\" || value === \"all-time\") {\n        return value as LeaderboardPeriod;\n      }\n      return \"all-time\";\n    },\n  });\n\n  const { data: userPosition, isLoading } = useUserPosition({\n    userId,\n    period,\n    enabled: isAuthenticated,\n  });\n\n  const pathnameIncludesLeaderboard = usePathname().includes(\"/leaderboard\");\n\n  if (!isAuthenticated || isLoading || !userPosition || !pathnameIncludesLeaderboard) {\n    return null;\n  }\n\n  const { position, totalWorkouts, totalUsers } = userPosition;\n\n  // Show motivational message if user has no workouts\n  if (totalWorkouts === 0) {\n    return (\n      <div className=\"absolute bottom-32 left-1/2 -translate-x-1/2 w-[calc(100%-2rem)] max-w-md z-40\">\n        <div className=\"bg-base-100 dark:bg-[#1A1A1A] border border-base-300 dark:border-gray-700 rounded-2xl shadow-lg px-4 py-3\">\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center gap-3\">\n              {/* Motivational Icon */}\n              <div className=\"flex items-center justify-center w-10 h-10 rounded-full bg-base-300 dark:bg-gray-700\">\n                <span className=\"text-2xl\">🫵</span>\n              </div>\n\n              {/* Motivational Message */}\n              <div className=\"flex-1\">\n                <p className=\"text-sm font-medium text-base-content dark:text-gray-100\">Pas encore classé</p>\n                <p className=\"text-xs text-base-content/60 dark:text-gray-400\">Commencez votre première séance !</p>\n              </div>\n            </div>\n\n            {/* Call to Action */}\n            <div className=\"text-right\">\n              <p className=\"text-2xl\">🚀</p>\n            </div>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"absolute bottom-32 left-1/2 -translate-x-1/2 w-[calc(100%-2rem)] max-w-md z-40\">\n      <div className=\"bg-base-100 dark:bg-[#1A1A1A] border border-base-300 dark:border-gray-700 rounded-2xl shadow-lg px-4 py-3\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-3\">\n            {/* Rank Badge */}\n            <div\n              className={`flex items-center justify-center w-10 h-10 rounded-full font-bold text-white ${\n                position === 1\n                  ? \"bg-yellow-500\"\n                  : position === 2\n                    ? \"bg-gray-400\"\n                    : position === 3\n                      ? \"bg-amber-600\"\n                      : position <= 10\n                        ? \"bg-[#4F8EF7]\"\n                        : \"bg-base-300 dark:bg-gray-700 text-base-content dark:text-gray-300\"\n              }`}\n            >\n              <span className=\"text-2xl\">🫵</span>\n            </div>\n\n            {/* User Info */}\n            <div className=\"flex-1\">\n              <p className=\"text-sm font-medium text-base-content dark:text-gray-100\">\n                Position #{position} sur {totalUsers}\n              </p>\n              <p className=\"text-xs text-base-content/60 dark:text-gray-400\">\n                {totalWorkouts} séances {period === \"weekly\" ? \"cette semaine\" : period === \"monthly\" ? \"ce mois\" : \"au total\"}\n              </p>\n            </div>\n          </div>\n\n          {/* Status */}\n          <div className=\"text-right\">\n            <p className=\"text-2xl font-medium text-base-content/70 dark:text-gray-400\">\n              {position === 1 ? \"👑\" : position <= 3 ? \"🏆\" : position <= 10 ? \"💪\" : position <= 20 ? \"🎯\" : \"🚀\"}\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/page/layout.tsx",
    "content": "import { cn } from \"@/shared/lib/utils\";\nimport { Typography } from \"@/components/ui/typography\";\n\nimport type { ComponentPropsWithoutRef } from \"react\";\n\nexport const Layout = (props: ComponentPropsWithoutRef<\"div\">) => {\n  return <div {...props} className={cn(\"\", props.className)} />;\n};\n\nexport const LayoutHeader = (props: ComponentPropsWithoutRef<\"div\">) => {\n  return <div {...props} className={cn(\"flex w-full min-w-[200px] flex-col items-start gap-2 md:flex-1\", props.className)} />;\n};\n\nexport const LayoutTitle = (props: ComponentPropsWithoutRef<\"h1\">) => {\n  return <Typography {...props} className={cn(props.className)} variant=\"h2\" />;\n};\n\nexport const LayoutDescription = (props: ComponentPropsWithoutRef<\"p\">) => {\n  return <Typography {...props} className={cn(props.className)} />;\n};\n\nexport const LayoutActions = (props: ComponentPropsWithoutRef<\"div\">) => {\n  return <div {...props} className={cn(\"flex items-center\", props.className)} />;\n};\n\nexport const LayoutContent = (props: ComponentPropsWithoutRef<\"div\">) => {\n  return <div {...props} className={cn(\"w-full\", props.className)} />;\n};\n"
  },
  {
    "path": "src/features/premium/ui/README.md",
    "content": "# Workout.cool Premium Pricing Components 💪\n\nThis directory contains the complete pricing page implementation following the fitness-focused conversion strategy outlined in the\ndevelopment prompt.\n\n## 🎯 Architecture Overview\n\nThe pricing page follows a **Z-Pattern layout** optimized for conversion with psychological triggers specific to the fitness community.\n\n### Components Structure\n\n```\nsrc/features/premium/ui/\n├── premium-upgrade-card.tsx      # Main orchestrator component\n├── pricing-hero-section.tsx      # Hero with mission statement\n├── feature-comparison-table.tsx  # Detailed feature breakdown\n├── pricing-faq.tsx              # Conversion-oriented FAQ\n├── pricing-testimonials.tsx     # Social proof carousel\n├── mobile-sticky-cta.tsx        # Mobile conversion booster\n└── conversion-flow-notification.tsx # Existing notification\n```\n\n## 🎨 Design System\n\n### Colors (Fitness-focused)\n\n- **Primary**: `#FF6B35` (Orange énergie)\n- **Secondary**: `#00D4AA` (Vert menthe performance)\n- **Success**: `#22C55E` (Vert accomplissement)\n- **Warning**: `#F59E0B` (Orange urgence)\n\n### Typography\n\n- **Primary**: Inter/Poppins (moderne, lisible)\n- **Hierarchy**: H1(3.5rem) → H2(2.5rem) → H3(1.75rem) → Body(1rem)\n\n## 📊 Plan Structure\n\n### 1. FREE (CORE) - €0/forever\n\n- **Badge**: \"Open-Source • Always Free\"\n- **Icon**: Github\n- **Color**: Green (`#22C55E`)\n- **Value Prop**: All essential training functions\n- **CTA**: \"Start Training Free\"\n\n### 2. SUPPORTER - €4.99/month\n\n- **Badge**: \"Support the Mission\"\n- **Icon**: Heart\n- **Color**: Orange (`#FF6B35`)\n- **Value Prop**: Help pay servers + bonus features\n- **CTA**: \"Support for €4.99\"\n\n### 3. PREMIUM ⭐ - €9.99/month\n\n- **Badge**: \"MOST POPULAR • For enthusiasts\"\n- **Icon**: Crown\n- **Color**: Teal (`#00D4AA`)\n- **Value Prop**: All features + early access\n- **CTA**: \"Go Premium €9.99\"\n- **Special**: Scale 105%, shadow effects\n\n## 🧠 Psychological Triggers\n\n### 1. Mission-Driven Urgency\n\n```tsx\n// Transparency banner showing real costs\n\"€450/month server costs to cover\";\n\"234 supporters helping the mission\";\n\"Limited early access spots\";\n```\n\n### 2. Open-Source Trust\n\n- GitHub integration prominent\n- Self-hosting always mentioned\n- Code transparency emphasized\n- No vendor lock-in guarantees\n\n### 3. Community Social Proof\n\n```tsx\n\"12.4K+ Active athletes\";\n\"1.2M+ Visits / mo\";\n\"4.9/5 Community rating\";\n\"+23% Average progression\";\n```\n\n### 4. Fitness-Specific FOMO\n\n- Early access to AI coaching\n- Beta features for enthusiasts\n- Exclusive community access\n- Pro templates (Powerlifting, Bodybuilding)\n\n## 📱 Mobile Optimization\n\n### MobileStickyCard\n\n- Appears after 5 seconds on mobile\n- Dismissible backdrop overlay\n- Quick access to premium upgrade\n- Integrated with auth flow\n\n### Responsive Behavior\n\n- **Mobile**: Stacked plans, sticky CTA\n- **Tablet**: 2+1 column layout\n- **Desktop**: 3-column with emphasis on Premium\n\n## 🔄 Conversion Flow\n\n### Non-authenticated Users\n\n1. Click upgrade → Store pending checkout\n2. Redirect to auth with return URL\n3. After auth → Auto-trigger checkout\n4. Complete payment → Return to app\n\n### Authenticated Users\n\n1. Click upgrade → Direct to Stripe checkout\n2. Payment success → Webhook updates status\n3. Redirect back → Premium features unlocked\n\n## 📊 Analytics & Testing\n\n### Key Metrics to Track\n\n- **Conversion rates** per plan (Free→Supporter, Free→Premium)\n- **Funnel completion** (View→Click→Signup→Payment)\n- **Feature usage** driving upgrades\n- **Mobile vs Desktop** conversion patterns\n\n### A/B Test Opportunities\n\n- Plan order (Free first vs Premium first)\n- Pricing (€4.99 vs €6.99 for Supporter)\n- CTA copy (\"Support Mission\" vs \"Upgrade\")\n- Hero messaging variations\n\n## 🎯 Key Features\n\n### PricingHeroSection\n\n- Mission-driven messaging\n- Community stats\n- Open-source trust indicators\n- Gradient backgrounds with mascot\n\n### FeatureComparisonTable\n\n- Sticky headers for plan visibility\n- Icon-based feature indicators\n- 7 categorized feature groups\n- Mobile-friendly responsive design\n\n### PricingTestimonials\n\n- Auto-rotating carousel (5s interval)\n- Real results with metrics\n- Diverse fitness community representation\n- Mobile/desktop adaptive layout\n\n### PricingFAQ\n\n- Conversion-oriented answers\n- Accordion interaction (one open)\n- Addresses common objections\n- Trust-building responses\n\n## 🚀 Usage\n\n```tsx\nimport { PremiumUpgradeCard } from \"@/features/premium/ui\";\n\nexport default function PremiumPage() {\n  return (\n    <div className=\"min-h-screen bg-white dark:bg-gray-950\">\n      <PremiumUpgradeCard />\n    </div>\n  );\n}\n```\n\n## 📝 Content Strategy\n\n### Tone of Voice\n\n- **Authentic** and transparent\n- **Passionate** about fitness\n- **Community-focused**\n- **Non-aggressive** sales approach\n\n### Key Messages\n\n- \"15 years of passion, redesigned from scratch\"\n- \"Not for money, but to maintain this labor of love\"\n- \"Core features always FREE\"\n- \"Transparent like our code, solid like our PRs\"\n\n## 🔧 Technical Notes\n\n### Dependencies\n\n- React Query for plan fetching\n- Framer Motion for animations (if needed)\n- Lucide React for icons\n- Tailwind CSS for styling\n- TypeScript for type safety\n\n### Performance\n\n- Lazy loading for testimonial images\n- Optimized animations with CSS transforms\n- Minimal JavaScript for interactions\n- Fast loading with server-side rendering\n\n---\n\n**Vision**: This pricing page reflects the authenticity and passion of the fitness community. No aggressive marketing, but a sincere\ninvitation to support a shared mission.\n\n**Principle**: \"Transparent comme notre code, solide comme nos PRs\" 💪\n"
  },
  {
    "path": "src/features/premium/ui/conversion-flow-notification.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Loader2 } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { usePendingCheckout } from \"@/shared/lib/premium/use-pending-checkout\";\n\nexport function ConversionFlowNotification() {\n  const t = useI18n();\n  const { getPendingCheckout } = usePendingCheckout();\n  const [showNotification, setShowNotification] = useState(false);\n  const [planId, setPlanId] = useState<string | null>(null);\n\n  useEffect(() => {\n    const pendingCheckout = getPendingCheckout();\n    if (pendingCheckout) {\n      setPlanId(pendingCheckout.planId);\n      setShowNotification(true);\n\n      // Auto-hide after 5 seconds\n      const timer = setTimeout(() => {\n        setShowNotification(false);\n      }, 5000);\n\n      return () => clearTimeout(timer);\n    }\n  }, [getPendingCheckout]);\n\n  if (!showNotification || !planId) return null;\n\n  return (\n    <div className=\"fixed top-4 right-4 z-50 max-w-sm\">\n      <div className=\"bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-green-200 dark:border-green-800 p-4\">\n        <div className=\"flex items-start gap-3\">\n          <h3 className=\"font-medium text-gray-900 dark:text-white text-sm\">{t(\"commons.loading\")}</h3>\n          <Loader2 className=\"w-5 h-5 text-green-500 mt-0.5 flex-shrink-0\" />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/premium/ui/feature-comparison-table.tsx",
    "content": "\"use client\";\n\nimport { Check, X, Star, Target } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\n\ninterface Feature {\n  name: string;\n  free: boolean | string;\n  premium: boolean | string;\n}\n\ninterface FeatureCategory {\n  name: string;\n  features: Feature[];\n}\n\nexport function FeatureComparisonTable() {\n  const t = useI18n();\n\n  const categories: FeatureCategory[] = [\n    {\n      name: t(\"premium.comparison.categories.equipment\"),\n      features: [\n        {\n          name: t(\"premium.comparison.features.exercise_library\"),\n          free: t(\"premium.comparison.values.basic\"),\n          premium: t(\"premium.comparison.values.complete\"),\n        },\n        {\n          name: t(\"premium.comparison.features.custom_exercise\"),\n          free: false,\n          premium: t(\"premium.comparison.values.unlimited\"),\n        },\n      ],\n    },\n    {\n      name: t(\"premium.comparison.categories.tracking\"),\n      features: [\n        {\n          name: t(\"premium.comparison.features.workout_history\"),\n          free: t(\"premium.comparison.values.six_months\"),\n          premium: t(\"premium.comparison.values.unlimited\"),\n        },\n        {\n          name: t(\"premium.comparison.features.progress_statistics\"),\n          free: false,\n          premium: true,\n        },\n        {\n          name: t(\"premium.comparison.features.personal_records\"),\n          free: false,\n          premium: true,\n        },\n        {\n          name: t(\"premium.comparison.features.volume_analytics\"),\n          free: false,\n          premium: true,\n        },\n      ],\n    },\n    {\n      name: t(\"premium.comparison.categories.programs\"),\n      features: [\n        {\n          name: t(\"premium.comparison.features.predesigned_programs\"),\n          free: t(\"premium.comparison.values.limited\"),\n          premium: t(\"premium.comparison.values.all_programs\"),\n        },\n        {\n          name: t(\"premium.comparison.features.personalized_recommendations\"),\n          free: false,\n          premium: true,\n        },\n        {\n          name: t(\"premium.comparison.features.pro_templates\"),\n          free: false,\n          premium: t(\"premium.comparison.values.soon\"),\n        },\n      ],\n    },\n    {\n      name: t(\"premium.comparison.categories.community\"),\n      features: [\n        {\n          name: t(\"premium.comparison.features.community_access\"),\n          free: t(\"premium.comparison.values.public\"),\n          premium: t(\"premium.comparison.values.vip_access\"),\n        },\n        {\n          name: t(\"premium.comparison.features.private_chat\"),\n          free: false,\n          premium: true,\n        },\n      ],\n    },\n    {\n      name: t(\"premium.comparison.categories.support\"),\n      features: [\n        {\n          name: t(\"premium.comparison.features.community_support\"),\n          free: true,\n          premium: true,\n        },\n        {\n          name: t(\"premium.comparison.features.priority_support\"),\n          free: false,\n          premium: true,\n        },\n        {\n          name: t(\"premium.comparison.features.early_access\"),\n          free: false,\n          premium: true,\n        },\n        {\n          name: t(\"premium.comparison.features.beta_testing\"),\n          free: false,\n          premium: true,\n        },\n      ],\n    },\n  ];\n\n  const renderFeatureValue = (value: boolean | string) => {\n    if (value === true) {\n      return <Check className=\"w-5 h-5 text-[#22C55E] mx-auto\" />;\n    }\n    if (value === false) {\n      return <X className=\"w-5 h-5 text-gray-400 mx-auto\" />;\n    }\n    if (value === t(\"premium.comparison.values.unlimited\")) {\n      return (\n        <div className=\"flex items-center justify-center gap-1\">\n          <span className=\"text-xs font-medium text-[#00D4AA]\">∞ {t(\"premium.comparison.values.unlimited\")}</span>\n        </div>\n      );\n    }\n    if (typeof value === \"string\" && value.includes(\"templates\")) {\n      return (\n        <div className=\"flex items-center justify-center gap-1\">\n          <Star className=\"w-4 h-4 text-[#F59E0B]\" />\n          <span className=\"text-xs font-medium text-gray-700 dark:text-gray-300\">{value}</span>\n        </div>\n      );\n    }\n    if (typeof value === \"string\" && (value.includes(\"Early\") || value.includes(\"Beta\"))) {\n      return (\n        <div className=\"flex items-center justify-center gap-1\">\n          <Target className=\"w-4 h-4 text-[#FF6B35]\" />\n          <span className=\"text-xs font-medium text-[#FF6B35]\">{t(\"premium.comparison.values.early_access\")}</span>\n        </div>\n      );\n    }\n    return <span className=\"text-center text-xs font-medium text-gray-700 dark:text-gray-300\">{value}</span>;\n  };\n\n  return (\n    <section className=\"py-16 bg-gray-50 dark:bg-gray-900/50\">\n      <div className=\"container mx-auto px-2 sm:px-4\">\n        <div className=\"text-center mb-12\">\n          <h2 className=\"text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4\">{t(\"premium.comparison.title\")}</h2>\n          <p className=\"text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto\">{t(\"premium.comparison.subtitle\")}</p>\n        </div>\n\n        <div className=\"max-w-4xl mx-auto\">\n          <div className=\"bg-white dark:bg-gray-900 rounded-2xl shadow-xl overflow-hidden\">\n            {/* Sticky Header */}\n            <div className=\"sticky top-0 z-10 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700\">\n              <div className=\"grid grid-cols-3 gap-4 p-6\">\n                <div className=\"font-semibold text-gray-900 dark:text-white\">{t(\"premium.comparison.features_label\")}</div>\n                <div className=\"text-center\">\n                  <div className=\"font-bold text-gray-900 dark:text-white\">{t(\"premium.plans.free.name\")}</div>\n                  <div className=\"text-sm text-gray-500\">{t(\"premium.plans.free.price_label\")}</div>\n                </div>\n                <div className=\"text-center\">\n                  <div className=\"font-bold text-[#00D4AA]\">{t(\"premium.plans.premium.name\")}</div>\n                  <div className=\"text-sm text-gray-500\">{t(\"premium.plans.premium.price_label\")}</div>\n                </div>\n              </div>\n            </div>\n\n            {/* Categories */}\n            <div className=\"divide-y divide-gray-200 dark:divide-gray-700\">\n              {categories.map((category, categoryIndex) => (\n                <div className=\"p-3 sm:p-6\" key={categoryIndex}>\n                  <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2\">{category.name}</h3>\n\n                  <div className=\"space-y-3\">\n                    {category.features.map((feature, featureIndex) => (\n                      <div className=\"grid grid-cols-3 gap-4 items-center py-2\" key={featureIndex}>\n                        <div className=\"text-xs sm:text-sm text-gray-700 dark:text-gray-300\">{feature.name}</div>\n                        <div className=\"text-center\">{renderFeatureValue(feature.free)}</div>\n                        <div className=\"text-center\">{renderFeatureValue(feature.premium)}</div>\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/features/premium/ui/index.ts",
    "content": "// Premium UI Components\nexport { PremiumUpgradeCard } from \"./premium-upgrade-card\";\nexport { ConversionFlowNotification } from \"./conversion-flow-notification\";\n\n// New Pricing Page Components\nexport { PricingHeroSection } from \"./pricing-hero-section\";\nexport { FeatureComparisonTable } from \"./feature-comparison-table\";\nexport { PricingFAQ } from \"./pricing-faq\";\nexport { PricingTestimonials } from \"./pricing-testimonials\";\n\n// Note: Add other premium components exports here as needed\n"
  },
  {
    "path": "src/features/premium/ui/premium-upgrade-card.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport Image from \"next/image\";\nimport { Crown, Zap, Heart, Check, ArrowRight, LogIn, Github, Users, RefreshCw, Lock, ShieldCheck, GiftIcon } from \"lucide-react\";\nimport { useMutation } from \"@tanstack/react-query\";\n\nimport { useI18n, useCurrentLocale } from \"locales/client\";\nimport { usePremiumRedirect } from \"@/shared/lib/premium/use-premium-redirect\";\nimport { useIsPremium } from \"@/shared/lib/premium/use-premium\";\nimport { usePendingCheckout } from \"@/shared/lib/premium/use-pending-checkout\";\nimport { usePremiumPlans } from \"@/shared/hooks/use-premium-plans\";\nimport { useSession } from \"@/features/auth/lib/auth-client\";\nimport { Button } from \"@/components/ui/button\";\n\nimport { PricingHeroSection } from \"./pricing-hero-section\";\nimport { PricingFAQ } from \"./pricing-faq\";\nimport { FeatureComparisonTable } from \"./feature-comparison-table\";\nimport { ConversionFlowNotification } from \"./conversion-flow-notification\";\n\nimport type { CheckoutResult } from \"@/shared/types/premium.types\";\n\nexport function PremiumUpgradeCard() {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n  const router = useRouter();\n  const isPremium = useIsPremium();\n  const { data: session, isPending: isAuthLoading } = useSession();\n  const isAuthenticated = !!session?.user;\n\n  const { storePendingCheckout, getPendingCheckout, clearPendingCheckout } = usePendingCheckout();\n  const [selectedPlan, setSelectedPlan] = useState<string | null>(null);\n  const [isYearly, setIsYearly] = useState(false);\n\n  // Fetch dynamic pricing\n  const { data: plansData, isLoading: plansLoading } = usePremiumPlans();\n  console.log(\"plansData:\", plansData);\n\n  // Handle premium redirects after successful upgrade\n  usePremiumRedirect();\n\n  // Check for pending checkout after authentication\n  useEffect(() => {\n    if (isAuthenticated && !isAuthLoading) {\n      const pendingCheckout = getPendingCheckout();\n      if (pendingCheckout) {\n        // Auto-trigger checkout for the pending plan\n        clearPendingCheckout();\n        handleCheckout(pendingCheckout.planId);\n      }\n    }\n  }, [isAuthenticated, isAuthLoading, getPendingCheckout, clearPendingCheckout]);\n\n  const checkoutMutation = useMutation({\n    mutationFn: async (planId: string): Promise<CheckoutResult> => {\n      const response = await fetch(\"/api/premium/checkout\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ planId }),\n      });\n\n      if (!response.ok) throw new Error(\"Failed to create checkout\");\n      return response.json();\n    },\n    onSuccess: (result) => {\n      if (result.success && result.checkoutUrl) {\n        window.location.href = result.checkoutUrl;\n      }\n    },\n    onError: (error) => {\n      console.error(\"Checkout error:\", error);\n      alert(t(\"premium.checkout_error\"));\n    },\n  });\n\n  const handleCheckout = (planId: string) => {\n    setSelectedPlan(planId);\n    checkoutMutation.mutate(planId);\n  };\n\n  const handleUpgrade = (planId: string) => {\n    console.log(\"planId:\", planId);\n\n    // Check if user is authenticated\n    if (!isAuthenticated) {\n      // Store the selected plan for after authentication\n      storePendingCheckout(planId);\n\n      // Redirect to sign-in with return URL to premium page\n      const returnUrl = `/${locale}/premium`;\n      router.push(`/auth/signin?redirect=${encodeURIComponent(returnUrl)}`);\n      return;\n    }\n\n    // User is authenticated, proceed with checkout\n    handleCheckout(planId);\n  };\n\n  // Get current pricing based on toggle and API data\n  const monthlyPlan = plansData?.plans.find((p) => p.internalId.startsWith(\"premium-monthly\"));\n  const yearlyPlan = plansData?.plans.find((p) => p.internalId.startsWith(\"premium-yearly\"));\n\n  const monthlyPrice = monthlyPlan?.priceMonthly || 7.9;\n  console.log(\"monthlyPrice:\", monthlyPrice);\n  const yearlyPrice = yearlyPlan?.priceYearly || 49.0;\n  console.log(\"yearlyPrice:\", yearlyPrice);\n  const currency = monthlyPlan?.currency || \"EUR\";\n  console.log(\"currency:\", currency);\n\n  const currentPrice = isYearly ? yearlyPrice : monthlyPrice;\n  const currentPeriod = isYearly ? t(\"premium.pricing.year\") : t(\"premium.pricing.month\");\n  const currentPlanId = isYearly ? yearlyPlan?.id || \"premium-yearly\" : monthlyPlan?.id || \"premium-monthly\";\n\n  // Format price based on locale and currency\n  const formatPrice = (price: number, currency: string) => {\n    return new Intl.NumberFormat(locale === \"zh-CN\" ? \"zh-CN\" : locale, {\n      style: \"currency\",\n      currency: currency,\n      minimumFractionDigits: currency === \"EUR\" ? 2 : 0,\n      maximumFractionDigits: 2,\n    }).format(price);\n  };\n\n  // Log debug info in development\n  useEffect(() => {\n    if (plansData && process.env.NODE_ENV === \"development\") {\n      console.log(\"📊 Plans data:\", plansData);\n      console.log(\"🌍 Detected region:\", plansData.detectedRegion);\n      if (plansData.debug) {\n        console.log(\"🔍 Debug headers:\", plansData.debug.headers);\n      }\n    }\n  }, [plansData]);\n\n  if (isPremium) {\n    return (\n      <div className=\"m-3 relative overflow-hidden bg-gradient-to-b from-[#FF6B35]/5 to-[#00D4AA]/5 dark:from-[#FF6B35]/10 dark:to-[#00D4AA]/10 rounded-3xl p-8 border border-[#FF6B35]/20 dark:border-[#FF6B35]/30\">\n        <div className=\"relative z-10\">\n          <div className=\"flex items-center gap-3\">\n            <div className=\"relative\">\n              <div className=\"w-14 h-14 bg-gradient-to-br from-[#FF6B35] to-[#00D4AA] rounded-2xl flex items-center justify-center\">\n                <Crown className=\"w-7 h-7 text-white\" strokeWidth={2.5} />\n              </div>\n              <div className=\"absolute -bottom-1 -right-1 w-5 h-5 bg-[#22C55E] rounded-full flex items-center justify-center\">\n                <Check className=\"w-3 h-3 text-white\" strokeWidth={3} />\n              </div>\n            </div>\n            <div>\n              <h3 className=\"text-2xl font-bold text-gray-900 dark:text-white\">{t(\"premium.premium_active.title\")}</h3>\n              <div className=\"flex items-center gap-2 text-sm text-[#22C55E] font-medium\">\n                <div className=\"w-2 h-2 bg-[#22C55E] rounded-full animate-pulse\" />\n                {t(\"premium.premium_active.supporting\")}\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <ConversionFlowNotification />\n\n      {/* Hero Section */}\n      <PricingHeroSection />\n\n      {/* Mission-Driven Urgency Banner */}\n      <section className=\"bg-gradient-to-r from-[#FF6B35]/10 to-[#00D4AA]/10 border-y border-[#FF6B35]/20 dark:border-[#FF6B35]/30\">\n        <div className=\"container mx-auto px-4 py-4\">\n          <div className=\"flex items-center justify-center gap-3 text-sm flex-col sm:flex-row\">\n            <div className=\"flex items-center gap-2\">\n              <Heart className=\"w-4 h-4 text-[#22C55E]\" fill=\"currentColor\" />\n              <span className=\"text-gray-700 dark:text-gray-300\">\n                <strong className=\"text-[#22C55E]\">{t(\"premium.mission.supporters_count\")}</strong> {t(\"premium.mission.supporters_text\")}\n              </span>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <Zap className=\"w-4 h-4 text-[#F59E0B]\" />\n              <span className=\"text-gray-700 dark:text-gray-300\">\n                <strong className=\"text-[#F59E0B]\">{t(\"premium.mission.limited\")}</strong> {t(\"premium.mission.early_access\")}\n              </span>\n            </div>\n          </div>\n        </div>\n      </section>\n\n      {/* Plans Section */}\n      <section className=\"py-16\" data-section=\"plans\">\n        <div className=\"container mx-auto px-4\">\n          {/* Pricing Toggle */}\n          <div className=\"flex items-center justify-center mb-12\">\n            <div className=\"bg-gray-100 dark:bg-gray-800 rounded-full p-1 flex items-center\">\n              <button\n                className={`px-6 py-2 rounded-full text-sm font-medium transition-all duration-200 ${\n                  !isYearly\n                    ? \"bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm\"\n                    : \"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white\"\n                }`}\n                onClick={() => setIsYearly(false)}\n              >\n                {t(\"premium.pricing.monthly\")}\n              </button>\n              <button\n                className={`px-6 py-2 rounded-full text-sm font-medium transition-all duration-200 relative ${\n                  isYearly\n                    ? \"bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm\"\n                    : \"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white\"\n                }`}\n                onClick={() => setIsYearly(true)}\n              >\n                {t(\"premium.pricing.yearly\")}\n                <span className=\"absolute -top-2 -right-2 bg-[#22C55E] text-white text-xs px-1.5 py-0.5 rounded-full\">\n                  {t(\"premium.pricing.discount\")}\n                </span>\n              </button>\n            </div>\n          </div>\n\n          {/* Plans Grid */}\n          <div className=\"grid gap-16 sm:gap-8 md:grid-cols-2 max-w-5xl mx-auto\">\n            {/* FREE PLAN */}\n            <div className=\"relative bg-white dark:bg-gray-900 rounded-3xl p-6 md:p-8 border-2 border-gray-200 dark:border-gray-800 transition-all duration-200 ease-out hover:scale-[1.02] hover:-translate-y-1 hover:border-[#22C55E]/30\">\n              <div className=\"absolute -top-4 left-1/2 -translate-x-1/2 w-max\">\n                <div className=\"bg-[#22C55E] text-white text-sm font-bold px-4 py-1.5 rounded-full\">{t(\"premium.plans.free.badge\")}</div>\n              </div>\n\n              <div className=\"text-center space-y-6 mb-8 mt-4\">\n                <div className=\"w-16 h-16 bg-gradient-to-br from-[#22C55E] to-[#16A34A] rounded-2xl flex items-center justify-center mx-auto\">\n                  <Github className=\"w-8 h-8 text-white\" strokeWidth={2.5} />\n                </div>\n\n                <div>\n                  <h3 className=\"text-2xl font-bold text-gray-900 dark:text-white mb-2\">{t(\"premium.plans.free.name\")}</h3>\n                  <div className=\"flex items-baseline justify-center gap-1\">\n                    <span className=\"text-5xl font-bold text-gray-900 dark:text-white\">{formatPrice(0, currency)}</span>\n                    <span className=\"text-lg text-gray-600 dark:text-gray-400\">{t(\"premium.plans.free.period\")}</span>\n                  </div>\n                  <p className=\"text-sm text-gray-600 dark:text-gray-400 mt-2\">{t(\"premium.plans.free.description\")}</p>\n                </div>\n              </div>\n\n              <ul className=\"space-y-3 mb-8\">\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#22C55E] flex-shrink-0\" />\n                  {t(\"premium.plans.free.features.0\")}\n                </li>\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#22C55E] flex-shrink-0\" />\n                  {t(\"premium.plans.free.features.1\")}\n                </li>\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#22C55E] flex-shrink-0\" />\n                  {t(\"premium.plans.free.features.2\")}\n                </li>\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#22C55E] flex-shrink-0\" />\n                  {t(\"premium.plans.free.features.3\")}\n                </li>\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#22C55E] flex-shrink-0\" />\n                  {t(\"premium.plans.free.features.4\")}\n                </li>\n              </ul>\n\n              <Button\n                className=\"w-full h-12 text-base font-semibold bg-white text-[#22C55E] border-2 border-[#22C55E] hover:bg-[#22C55E] hover:text-white transition-all duration-200 rounded-xl\"\n                disabled\n              >\n                {t(\"premium.actions.current_plan\")}\n              </Button>\n              <p className=\"mt-2 text-xs text-center text-gray-600 dark:text-gray-400\">{t(\"premium.plans.free.footer_note\")}</p>\n            </div>\n\n            {/* PREMIUM PLAN */}\n            <div className=\"relative bg-white dark:bg-gray-900 rounded-3xl p-6 md:p-8 border-2 border-[#00D4AA]/30 dark:border-[#00D4AA]/40 shadow-xl shadow-[#00D4AA]/10 transform scale-105 transition-all duration-200 ease-out hover:scale-[1.07] hover:-translate-y-1\">\n              <div className=\"absolute -top-4 left-1/2 -translate-x-1/2 w-max\">\n                <div className=\"bg-gradient-to-r from-[#00D4AA] to-[#0EA5E9] text-white text-sm font-bold px-4 py-1.5 rounded-full flex items-center gap-1.5\">\n                  <Zap className=\"w-4 h-4\" />\n                  {t(\"premium.plans.premium.badge\")}\n                </div>\n              </div>\n\n              <div className=\"text-center space-y-6 mb-8 mt-4\">\n                <div className=\"relative\">\n                  <div className=\"w-16 h-16 bg-gradient-to-br from-[#00D4AA] to-[#0EA5E9] rounded-2xl flex items-center justify-center mx-auto\">\n                    <Crown className=\"w-8 h-8 text-white\" strokeWidth={2.5} />\n                  </div>\n                  {isYearly && (\n                    <div className=\"absolute -bottom-2 -right-2 px-2 py-1 bg-[#F59E0B] rounded-xl flex items-center justify-center rotate-12\">\n                      <span className=\"text-xs font-bold text-white\">{t(\"premium.pricing.discount\")}</span>\n                    </div>\n                  )}\n                </div>\n\n                <div>\n                  <h3 className=\"text-2xl font-bold text-gray-900 dark:text-white mb-2\">{t(\"premium.plans.premium.name\")}</h3>\n                  <div className=\"flex items-baseline justify-center gap-1\">\n                    <span className=\"text-5xl font-bold text-gray-900 dark:text-white\">\n                      {plansLoading ? \"...\" : formatPrice(currentPrice, currency)}\n                    </span>\n                    <span className=\"text-lg text-gray-600 dark:text-gray-400\">/{currentPeriod}</span>\n                  </div>\n                  {isYearly && !plansLoading && (\n                    <div className=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1 bg-[#22C55E]/10 dark:bg-[#22C55E]/20 rounded-full\">\n                      <div className=\"w-2 h-2 bg-[#22C55E] rounded-full\" />\n                      <span className=\"text-sm font-medium text-[#22C55E]\">\n                        {formatPrice(currentPrice / 12, currency)}/{t(\"premium.pricing.month\")}\n                      </span>\n                    </div>\n                  )}\n                  <p className=\"text-sm text-gray-600 dark:text-gray-400 mt-2\">{t(\"premium.plans.premium.description\")}</p>\n                </div>\n              </div>\n\n              <ul className=\"space-y-3 mb-8\">\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#00D4AA] flex-shrink-0\" />\n                  {t(\"premium.plans.premium.features.0\")}\n                </li>\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#00D4AA] flex-shrink-0\" />\n                  {t(\"premium.plans.premium.features.1\")}\n                </li>\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#00D4AA] flex-shrink-0\" />\n                  {t(\"premium.plans.premium.features.2\")}\n                </li>\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#00D4AA] flex-shrink-0\" />\n                  {t(\"premium.plans.premium.features.3\")}\n                </li>\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#00D4AA] flex-shrink-0\" />\n                  {t(\"premium.plans.premium.features.4\")}\n                </li>\n                <li className=\"flex items-center gap-3 text-sm\">\n                  <Check className=\"w-4 h-4 text-[#00D4AA] flex-shrink-0\" />\n                  {t(\"premium.plans.premium.features.5\")}\n                </li>\n              </ul>\n\n              <Button\n                className=\"w-full h-12 text-base font-semibold bg-gradient-to-r from-[#00D4AA] to-[#0EA5E9] hover:from-[#00D4AA]/90 hover:to-[#0EA5E9]/90 text-white shadow-lg shadow-[#00D4AA]/20 transition-all duration-200 rounded-xl hover:scale-[1.02] active:scale-[0.98]\"\n                disabled={checkoutMutation.isPending && selectedPlan === currentPlanId}\n                onClick={() => handleUpgrade(currentPlanId)}\n              >\n                {checkoutMutation.isPending && selectedPlan === currentPlanId ? (\n                  <div className=\"flex items-center justify-center gap-2\">\n                    <div className=\"w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin\" />\n                    <span>{t(\"premium.actions.processing\")}</span>\n                  </div>\n                ) : !isAuthenticated ? (\n                  <div className=\"flex items-center justify-center gap-2\">\n                    <LogIn className=\"w-5 h-5\" />\n                    <span>\n                      {t(\"premium.actions.go_premium\")} {formatPrice(currentPrice, currency)}\n                    </span>\n                  </div>\n                ) : (\n                  <div className=\"flex items-center justify-center gap-2\">\n                    <span>\n                      {t(\"premium.actions.go_premium\")} {formatPrice(currentPrice, currency)}\n                    </span>\n                    <ArrowRight className=\"w-5 h-5\" />\n                  </div>\n                )}\n              </Button>\n              <p className=\"mt-2 text-sm text-center text-gray-600 dark:text-gray-400\">\n                {isYearly ? t(\"premium.plans.premium.footer_yearly\") : t(\"premium.plans.premium.footer_monthly\")}\n              </p>\n            </div>\n          </div>\n\n          {/* Trust Elements */}\n          <div className=\"mt-16 text-center\">\n            <div className=\"items-center justify-items-center gap-6 flex-wrap text-sm text-gray-600 dark:text-gray-400 grid grid-cols-1 sm:grid-cols-2\">\n              <div className=\"flex items-center gap-2\">\n                <Lock className=\"w-4 h-4 text-[#FF6B35]\" />\n                <span>{t(\"premium.trust.gdpr_compliant\")}</span>\n              </div>\n\n              <div className=\"flex items-center gap-2\">\n                <GiftIcon className=\"w-4 h-4 text-[#F59E0B]\" />\n                <span>{t(\"premium.trust.money_back\")}</span>\n              </div>\n              <div className=\"flex items-center gap-2\">\n                <RefreshCw className=\"w-4 h-4 text-[#0EA5E9]\" />\n                <span>{t(\"premium.trust.cancel_anytime\")}</span>\n              </div>\n              <div className=\"flex items-center gap-2\">\n                <ShieldCheck className=\"w-4 h-4 text-[#22C55E]\" />\n                <span>{t(\"premium.trust.secure_payment\")}</span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </section>\n\n      {/* Feature Comparison Table */}\n      <FeatureComparisonTable />\n\n      {/* FAQ */}\n      <PricingFAQ />\n\n      {/* Final CTA Section */}\n      <section className=\"py-16 bg-gradient-to-b from-[#FF6B35]/5 to-[#00D4AA]/5 dark:from-[#FF6B35]/10 dark:to-[#00D4AA]/10\">\n        <div className=\"container mx-auto px-4 text-center\">\n          <div className=\"max-w-3xl mx-auto space-y-8\">\n            <div className=\"relative inline-block\">\n              <Image alt=\"Happy mascot\" className=\"mx-auto\" height={80} src=\"/images/emojis/WorkoutCoolBiceps.png\" width={80} />\n              <div className=\"absolute -top-2 left-1/2 -translate-x-[15%] rotate-2\">\n                <div className=\"w-max px-3 h-7 bg-[#FF6B35] rounded-full flex items-center justify-center\">\n                  <span className=\"text-sm font-bold text-white\">{t(\"premium.final_cta.motivation\")}</span>\n                </div>\n              </div>\n            </div>\n\n            <div className=\"space-y-4\">\n              <h2 className=\"text-3xl md:text-4xl font-bold text-gray-900 dark:text-white\">{t(\"premium.final_cta.title\")}</h2>\n              <p className=\"text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto\">{t(\"premium.final_cta.subtitle\")}</p>\n            </div>\n\n            <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n              <div className=\"text-center space-y-2\">\n                <div className=\"w-12 h-12 bg-[#22C55E]/10 rounded-xl flex items-center justify-center mx-auto\">\n                  <Users className=\"w-6 h-6 text-[#22C55E]\" />\n                </div>\n                <h3 className=\"font-semibold text-gray-900 dark:text-white\">{t(\"premium.final_cta.values.0.title\")}</h3>\n                <p className=\"text-sm text-gray-600 dark:text-gray-400\">{t(\"premium.final_cta.values.0.description\")}</p>\n              </div>\n\n              <div className=\"text-center space-y-2\">\n                <div className=\"w-12 h-12 bg-[#FF6B35]/10 rounded-xl flex items-center justify-center mx-auto\">\n                  <Github className=\"w-6 h-6 text-[#FF6B35]\" />\n                </div>\n                <h3 className=\"font-semibold text-gray-900 dark:text-white\">{t(\"premium.final_cta.values.1.title\")}</h3>\n                <p className=\"text-sm text-gray-600 dark:text-gray-400\">{t(\"premium.final_cta.values.1.description\")}</p>\n              </div>\n\n              <div className=\"text-center space-y-2\">\n                <div className=\"w-12 h-12 bg-[#00D4AA]/10 rounded-xl flex items-center justify-center mx-auto\">\n                  <Heart className=\"w-6 h-6 text-[#00D4AA]\" fill=\"currentColor\" />\n                </div>\n                <h3 className=\"font-semibold text-gray-900 dark:text-white\">{t(\"premium.final_cta.values.2.title\")}</h3>\n                <p className=\"text-sm text-gray-600 dark:text-gray-400\">{t(\"premium.final_cta.values.2.description\")}</p>\n              </div>\n            </div>\n\n            <div className=\"bg-white/50 dark:bg-gray-900/50 backdrop-blur-sm rounded-2xl p-6\">\n              <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-4\">&quot;{t(\"premium.final_cta.quote.text\")}&quot;</p>\n              <p className=\"text-xs text-gray-600 dark:text-gray-600\">{t(\"premium.final_cta.quote.author\")}</p>\n            </div>\n          </div>\n        </div>\n      </section>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/features/premium/ui/pricing-faq.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\n\nexport function PricingFAQ() {\n  const t = useI18n();\n  const [openIndex, setOpenIndex] = useState<number | null>(0);\n\n  const faqItems = [\n    {\n      question: t(\"premium.faq.items.0.question\"),\n      answer: t(\"premium.faq.items.0.answer\"),\n    },\n    {\n      question: t(\"premium.faq.items.1.question\"),\n      answer: t(\"premium.faq.items.1.answer\"),\n    },\n    {\n      question: t(\"premium.faq.items.2.question\"),\n      answer: t(\"premium.faq.items.2.answer\"),\n    },\n    {\n      question: t(\"premium.faq.items.3.question\"),\n      answer: t(\"premium.faq.items.3.answer\"),\n    },\n    {\n      question: t(\"premium.faq.items.4.question\"),\n      answer: t(\"premium.faq.items.4.answer\"),\n    },\n    {\n      question: t(\"premium.faq.items.5.question\"),\n      answer: t(\"premium.faq.items.5.answer\"),\n    },\n    {\n      question: t(\"premium.faq.items.6.question\"),\n      answer: t(\"premium.faq.items.6.answer\"),\n    },\n    {\n      question: t(\"premium.faq.items.7.question\"),\n      answer: t(\"premium.faq.items.7.answer\"),\n    },\n    {\n      question: t(\"premium.faq.items.8.question\"),\n      answer: t(\"premium.faq.items.8.answer\"),\n    },\n    {\n      question: t(\"premium.faq.items.9.question\"),\n      answer: t(\"premium.faq.items.9.answer\"),\n    },\n  ];\n\n  const toggleFAQ = (index: number) => {\n    setOpenIndex(openIndex === index ? null : index);\n  };\n\n  return (\n    <section className=\"py-16\">\n      <div className=\"container mx-auto px-2 sm:px-4\">\n        <div className=\"text-center mb-12\">\n          <h2 className=\"text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4\">{t(\"premium.faq.title\")}</h2>\n          <p className=\"text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto\">{t(\"premium.faq.subtitle\")}</p>\n        </div>\n\n        <div className=\"max-w-4xl mx-auto\">\n          <div className=\"space-y-4\">\n            {faqItems.map((item, index) => (\n              <div\n                className=\"bg-slate-100 dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden\"\n                key={index}\n              >\n                <button\n                  className=\"w-full px-6 py-4 text-left flex items-center justify-between hover:bg-gray-400 dark:hover:bg-gray-800 transition-colors duration-200\"\n                  onClick={() => toggleFAQ(index)}\n                >\n                  <span className=\"text-lg sm:text-xl leading-tight font-semibold text-gray-900 dark:text-white pr-4\">{item.question}</span>\n                  {openIndex === index ? (\n                    <ChevronUp className=\"w-5 h-5 text-gray-500 flex-shrink-0\" />\n                  ) : (\n                    <ChevronDown className=\"w-5 h-5 text-gray-500 flex-shrink-0\" />\n                  )}\n                </button>\n\n                {openIndex === index && (\n                  <div className=\"px-6 pb-4\">\n                    <div className=\"border-t border-gray-200 dark:border-gray-700 pt-4\">\n                      <p className=\"text-gray-700 dark:text-gray-400 leading-relaxed\">{item.answer}</p>\n                    </div>\n                  </div>\n                )}\n              </div>\n            ))}\n          </div>\n\n          {/* Additional Support */}\n          <div className=\"mt-12 text-center\">\n            <div className=\"bg-gradient-to-r from-[#FF6B35]/10 to-[#00D4AA]/10 rounded-2xl p-6\">\n              <h3 className=\"text-xl font-semibold text-gray-900 dark:text-white mb-2\">{t(\"premium.faq.additional_support.title\")}</h3>\n              <p className=\"text-gray-700 dark:text-gray-400 mb-4\">{t(\"premium.faq.additional_support.description\")}</p>\n              <div className=\"flex items-start sm:items-center justify-center gap-4 text-sm text-gray-600 flex-col\">\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"w-2 h-2 bg-[#22C55E] rounded-full\" />\n                  <span className=\"block text-left sm:flex\">{t(\"premium.faq.additional_support.community\")}</span>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"w-2 h-2 bg-[#FF6B35] rounded-full\" />\n                  <span className=\"block text-left sm:flex\">{t(\"premium.faq.additional_support.discussions\")}</span>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"w-2 h-2 bg-[#00D4AA] rounded-full\" />\n                  <span className=\"block text-left sm:flex\">{t(\"premium.faq.additional_support.roadmap\")}</span>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/features/premium/ui/pricing-hero-section.tsx",
    "content": "\"use client\";\n\nimport Image from \"next/image\";\nimport { Sparkles, Heart, Github, Users, TrendingUp, Zap } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\n\nexport function PricingHeroSection() {\n  const t = useI18n();\n\n  return (\n    <section className=\"relative overflow-hidden bg-gradient-to-b from-[#FF6B35]/5 to-[#00D4AA]/5 dark:from-[#FF6B35]/10 dark:to-[#00D4AA]/10 pt-6 pb-16 md:pt-10 md:pb-24\">\n      <div className=\"container mx-auto px-4 text-center\">\n        {/* Main Hero Content */}\n        <div className=\"max-w-4xl mx-auto space-y-8\">\n          {/* Badge */}\n          <div className=\"inline-flex items-center gap-2 px-4 py-2 bg-[#00D4AA]/10 dark:bg-[#00D4AA]/20 rounded-full\">\n            <Github className=\"w-4 h-4 text-[#00D4AA]\" />\n            <span className=\"text-sm font-medium text-[#00D4AA]\">{t(\"premium.hero.badge\")}</span>\n          </div>\n\n          {/* Main Title */}\n          <h1 className=\"text-3xl md:text-6xl font-black text-gray-900 dark:text-white leading-tight\">\n            {t(\"premium.hero.title\")}\n            <span className=\"inline-block ml-2\">💪</span>\n          </h1>\n\n          {/* Subtitle */}\n          <p className=\"text-lg md:text-2xl text-gray-700 dark:text-gray-300 sm:leading-relaxed leading-tight max-w-3xl mx-auto\">\n            {t(\"premium.hero.subtitle\")}\n          </p>\n\n          {/* Social Proof */}\n          <div className=\"relative\">\n            <div className=\"absolute inset-0 bg-gradient-to-r from-[#FF6B35]/20 to-[#00D4AA]/20 blur-3xl opacity-50\" />\n            <div className=\"relative bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm rounded-2xl p-6 md:p-8\">\n              <div className=\"flex items-center justify-center mb-6\">\n                <Image alt=\"Community mascot\" className=\"drop-shadow-2xl\" height={80} src=\"/images/emojis/WorkoutCoolLove.png\" width={80} />\n                <div className=\"absolute -top-2 -right-2\">\n                  <div className=\"relative\">\n                    <Sparkles className=\"w-6 h-6 text-[#FF6B35]\" />\n                    <div className=\"absolute inset-0 animate-ping\">\n                      <Sparkles className=\"w-6 h-6 text-[#FF6B35] opacity-40\" />\n                    </div>\n                  </div>\n                </div>\n              </div>\n\n              <div className=\"grid grid-cols-2 md:grid-cols-4 gap-6 text-center\">\n                <div className=\"space-y-1\">\n                  <div className=\"flex items-center justify-center gap-1\">\n                    <Users className=\"w-4 h-4 text-[#FF6B35]\" />\n                    <span className=\"text-2xl font-bold text-gray-900 dark:text-white\">{t(\"premium.hero.stats.athletes.count\")}</span>\n                  </div>\n                  <p className=\"text-xs text-gray-600 dark:text-gray-400\">{t(\"premium.hero.stats.athletes.label\")}</p>\n                </div>\n\n                <div className=\"space-y-1\">\n                  <div className=\"flex items-center justify-center gap-1\">\n                    <TrendingUp className=\"w-4 h-4 text-[#00D4AA]\" />\n                    <span className=\"text-2xl font-bold text-gray-900 dark:text-white\">{t(\"premium.hero.stats.series.count\")}</span>\n                  </div>\n                  <p className=\"text-xs text-gray-600 dark:text-gray-400\">{t(\"premium.hero.stats.series.label\")}</p>\n                </div>\n\n                <div className=\"space-y-1\">\n                  <div className=\"flex items-center justify-center gap-1\">\n                    <Heart className=\"w-4 h-4 text-[#22C55E]\" fill=\"currentColor\" />\n                    <span className=\"text-2xl font-bold text-gray-900 dark:text-white\">{t(\"premium.hero.stats.rating.count\")}</span>\n                  </div>\n                  <p className=\"text-xs text-gray-600 dark:text-gray-400\">{t(\"premium.hero.stats.rating.label\")}</p>\n                </div>\n\n                <div className=\"space-y-1\">\n                  <div className=\"flex items-center justify-center gap-1\">\n                    <Zap className=\"w-4 h-4 text-[#F59E0B]\" />\n                    <span className=\"text-2xl font-bold text-gray-900 dark:text-white\">{t(\"premium.hero.stats.progression.count\")}</span>\n                  </div>\n                  <p className=\"text-xs text-gray-600 dark:text-gray-400\">{t(\"premium.hero.stats.progression.label\")}</p>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/features/premium/ui/pricing-testimonials.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport Image from \"next/image\";\nimport { ChevronLeft, ChevronRight, Star, TrendingUp, Users } from \"lucide-react\";\n\ninterface Testimonial {\n  quote: string;\n  author: string;\n  role: string;\n  location: string;\n  image: string;\n  result?: string;\n  metric?: string;\n}\n\n// TODO: Add testimonials\nexport function PricingTestimonials() {\n  const [currentIndex, setCurrentIndex] = useState(0);\n  const [isAutoPlaying, setIsAutoPlaying] = useState(true);\n\n  const testimonials: Testimonial[] = [\n    {\n      quote:\n        \"I gained 15kg on bench press in 3 months with Workout.cool's detailed tracking. The GitHub-style history kept me motivated to stay consistent.\",\n      author: \"Marcus T.\",\n      role: \"Powerlifter\",\n      location: \"Berlin, Germany\",\n      image: \"/images/placeholders/coach-avatar.png\",\n      result: \"+15kg bench press\",\n      metric: \"3 months\",\n    },\n    {\n      quote: \"Finally an app that truly understands strength training! The open-source approach gives me confidence in my data privacy.\",\n      author: \"Sarah L.\",\n      role: \"CrossFit Athlete\",\n      location: \"London, UK\",\n      image: \"/images/placeholders/coach-avatar.png\",\n      result: \"40% more consistent\",\n      metric: \"6 months\",\n    },\n    {\n      quote: \"The equipment → muscles → exercises stepper is genius. Perfect for learning proper technique as a beginner.\",\n      author: \"Alex R.\",\n      role: \"Fitness Beginner\",\n      location: \"Paris, France\",\n      image: \"/images/placeholders/coach-avatar.png\",\n      result: \"Perfect form\",\n      metric: \"From day 1\",\n    },\n    {\n      quote: \"Thanks to the detailed progress tracking, I finally broke my deadlift plateau. The analytics are incredibly motivating!\",\n      author: \"Emma K.\",\n      role: \"Bodybuilder\",\n      location: \"Stockholm, Sweden\",\n      image: \"/images/placeholders/coach-avatar.png\",\n      result: \"Broke plateau\",\n      metric: \"2 weeks\",\n    },\n    {\n      quote: \"I can train anywhere - gym, home, park. The video tutorials are crystal clear and the community is amazing.\",\n      author: \"David M.\",\n      role: \"Fitness Enthusiast\",\n      location: \"Madrid, Spain\",\n      image: \"/images/placeholders/coach-avatar.png\",\n      result: \"Train anywhere\",\n      metric: \"Any time\",\n    },\n    {\n      quote: \"The transparency and mission-driven approach convinced me to become a supporter. It's not just an app, it's a movement.\",\n      author: \"Lisa B.\",\n      role: \"Personal Trainer\",\n      location: \"Amsterdam, Netherlands\",\n      image: \"/images/placeholders/coach-avatar.png\",\n      result: \"Supporting mission\",\n      metric: \"1 year\",\n    },\n  ];\n\n  // Auto-play functionality\n  useEffect(() => {\n    if (!isAutoPlaying) return;\n\n    const interval = setInterval(() => {\n      setCurrentIndex((prev) => (prev + 1) % testimonials.length);\n    }, 5000);\n\n    return () => clearInterval(interval);\n  }, [isAutoPlaying, testimonials.length]);\n\n  const goToNext = () => {\n    setCurrentIndex((prev) => (prev + 1) % testimonials.length);\n    setIsAutoPlaying(false);\n  };\n\n  const goToPrev = () => {\n    setCurrentIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length);\n    setIsAutoPlaying(false);\n  };\n\n  const goToSlide = (index: number) => {\n    setCurrentIndex(index);\n    setIsAutoPlaying(false);\n  };\n\n  const getVisibleTestimonials = () => {\n    const result = [];\n    for (let i = 0; i < 3; i++) {\n      const index = (currentIndex + i) % testimonials.length;\n      result.push({ ...testimonials[index], index });\n    }\n    return result;\n  };\n\n  return (\n    <section className=\"py-16 bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800\">\n      <div className=\"container mx-auto px-4\">\n        <div className=\"text-center mb-12\">\n          <h2 className=\"text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4\">Real Results from Real Athletes</h2>\n          <p className=\"text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto\">\n            Join thousands of fitness enthusiasts who&apos;ve transformed their training with Workout.cool\n          </p>\n\n          {/* Rating */}\n          <div className=\"flex items-center justify-center gap-2 mt-6\">\n            <div className=\"flex items-center gap-1\">\n              {[...Array(5)].map((_, i) => (\n                <Star className=\"w-5 h-5 text-[#F59E0B] fill-current\" key={i} />\n              ))}\n            </div>\n            <span className=\"text-lg font-semibold text-gray-900 dark:text-white\">4.9</span>\n            <span className=\"text-gray-600 dark:text-gray-400\">from 2,500+ coolers !</span>\n          </div>\n        </div>\n\n        <div className=\"max-w-6xl mx-auto\">\n          {/* Mobile Carousel */}\n          <div className=\"md:hidden\">\n            <div className=\"relative bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg\">\n              <div className=\"flex items-start gap-4 mb-4\">\n                <Image\n                  alt={testimonials[currentIndex].author}\n                  className=\"w-16 h-16 rounded-full object-cover\"\n                  height={64}\n                  src={testimonials[currentIndex].image}\n                  width={64}\n                />\n                <div className=\"flex-1 flex\">\n                  <div className=\"flex items-center gap-2 mb-1\">\n                    <h4 className=\"font-semibold text-gray-900 dark:text-white\">{testimonials[currentIndex].author}</h4>\n                    {testimonials[currentIndex].result && (\n                      <div className=\"px-2 py-1 bg-[#22C55E]/10 rounded-full\">\n                        <span className=\"text-xs font-medium text-[#22C55E]\">{testimonials[currentIndex].result}</span>\n                      </div>\n                    )}\n                  </div>\n                  <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n                    {testimonials[currentIndex].role} • {testimonials[currentIndex].location}\n                  </p>\n                </div>\n              </div>\n\n              <blockquote className=\"text-gray-700 dark:text-gray-300 leading-relaxed\">\n                &quot;{testimonials[currentIndex].quote}&quot;\n              </blockquote>\n            </div>\n          </div>\n\n          {/* Desktop Grid */}\n          <div className=\"hidden md:grid md:grid-cols-3 gap-6\">\n            {getVisibleTestimonials().map((testimonial, i) => (\n              <div\n                className={`bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg transition-all duration-300 ${\n                  i === 1 ? \"transform scale-105 ring-2 ring-[#FF6B35]/20\" : \"\"\n                }`}\n                key={testimonial.index}\n              >\n                <div className=\"flex items-start gap-4 mb-4\">\n                  <Image\n                    alt={testimonial.author}\n                    className=\"w-12 h-12 rounded-full object-cover\"\n                    height={48}\n                    src={testimonial.image}\n                    width={48}\n                  />\n                  <div className=\"flex-1\">\n                    <div className=\"flex items-center gap-2 mb-1 flex-col\">\n                      <h4 className=\"font-semibold text-gray-900 dark:text-white text-sm\">{testimonial.author}</h4>\n                      {testimonial.result && (\n                        <div className=\"px-2 py-0.5 bg-[#22C55E]/10 rounded-full\">\n                          <span className=\"text-xs font-medium text-[#22C55E]\">{testimonial.result}</span>\n                        </div>\n                      )}\n                    </div>\n                    <p className=\"text-xs text-gray-600 dark:text-gray-400\">{testimonial.role}</p>\n                    <p className=\"text-xs text-gray-500 dark:text-gray-500\">{testimonial.location}</p>\n                  </div>\n                </div>\n\n                <blockquote className=\"text-sm text-gray-700 dark:text-gray-300 leading-relaxed\">\n                  &quot;{testimonial.quote}&quot;\n                </blockquote>\n\n                {testimonial.metric && (\n                  <div className=\"mt-4 flex items-center gap-2\">\n                    <TrendingUp className=\"w-4 h-4 text-[#00D4AA]\" />\n                    <span className=\"text-xs font-medium text-[#00D4AA]\">{testimonial.metric}</span>\n                  </div>\n                )}\n              </div>\n            ))}\n          </div>\n\n          {/* Navigation */}\n          <div className=\"flex items-center justify-center gap-4 mt-8\">\n            <button className=\"p-2 rounded-full bg-white dark:bg-gray-900 shadow-md hover:shadow-lg transition-shadow\" onClick={goToPrev}>\n              <ChevronLeft className=\"w-5 h-5 text-gray-600 dark:text-gray-400\" />\n            </button>\n\n            {/* Dots */}\n            <div className=\"flex items-center gap-2\">\n              {testimonials.map((_, index) => (\n                <button\n                  className={`w-3 h-3 rounded-full transition-colors ${\n                    index === currentIndex ? \"bg-[#FF6B35]\" : \"bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500\"\n                  }`}\n                  key={index}\n                  onClick={() => goToSlide(index)}\n                />\n              ))}\n            </div>\n\n            <button className=\"p-2 rounded-full bg-white dark:bg-gray-900 shadow-md hover:shadow-lg transition-shadow\" onClick={goToNext}>\n              <ChevronRight className=\"w-5 h-5 text-gray-600 dark:text-gray-400\" />\n            </button>\n          </div>\n\n          {/* Community Stats */}\n          <div className=\"mt-12 bg-gradient-to-r from-[#FF6B35]/10 to-[#00D4AA]/10 rounded-2xl p-6\">\n            <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6 text-center\">\n              <div className=\"space-y-2\">\n                <div className=\"flex items-center justify-center gap-2\">\n                  <Users className=\"w-5 h-5 text-[#FF6B35]\" />\n                  <span className=\"text-2xl font-bold text-gray-900 dark:text-white\">2,500+</span>\n                </div>\n                <p className=\"text-sm text-gray-600 dark:text-gray-400\">Happy members</p>\n              </div>\n\n              <div className=\"space-y-2\">\n                <div className=\"flex items-center justify-center gap-2\">\n                  <TrendingUp className=\"w-5 h-5 text-[#00D4AA]\" />\n                  <span className=\"text-2xl font-bold text-gray-900 dark:text-white\">89%</span>\n                </div>\n                <p className=\"text-sm text-gray-600 dark:text-gray-400\">Reach their goals</p>\n              </div>\n\n              <div className=\"space-y-2\">\n                <div className=\"flex items-center justify-center gap-2\">\n                  <Star className=\"w-5 h-5 text-[#F59E0B]\" />\n                  <span className=\"text-2xl font-bold text-gray-900 dark:text-white\">4.9/5</span>\n                </div>\n                <p className=\"text-sm text-gray-600 dark:text-gray-400\">Average rating</p>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/features/programs/actions/complete-program-session.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nexport async function completeProgramSession(sessionProgressId: string, workoutSessionId: string) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  const userId = session.user?.id;\n\n  if (!userId) {\n    throw new Error(\"User not found\");\n  }\n\n  // Get session progress with enrollment\n  const sessionProgress = await prisma.userSessionProgress.findUnique({\n    where: { id: sessionProgressId },\n    include: {\n      enrollment: {\n        include: {\n          program: {\n            include: {\n              weeks: {\n                include: {\n                  sessions: {\n                    orderBy: { sessionNumber: \"asc\" },\n                  },\n                },\n                orderBy: { weekNumber: \"asc\" },\n              },\n            },\n          },\n        },\n      },\n      session: {\n        include: {\n          week: true,\n        },\n      },\n    },\n  });\n\n  if (!sessionProgress || sessionProgress.enrollment.userId !== userId) {\n    throw new Error(\"Session progress not found\");\n  }\n\n  // Update session progress\n  const updatedProgress = await prisma.userSessionProgress.update({\n    where: { id: sessionProgressId },\n    data: {\n      completedAt: new Date(),\n      workoutSessionId,\n    },\n  });\n\n  // Update enrollment stats\n  const enrollment = sessionProgress.enrollment;\n  const completedSessionsCount = await prisma.userSessionProgress.count({\n    where: {\n      enrollmentId: enrollment.id,\n      completedAt: { not: null },\n    },\n  });\n\n  // Find next session\n  const currentWeek = sessionProgress.session.week.weekNumber;\n  const currentSession = sessionProgress.session.sessionNumber;\n\n  let nextWeek = currentWeek;\n  let nextSession = currentSession + 1;\n\n  // Check if we need to move to next week\n  const currentWeekSessions = enrollment.program.weeks.find((w) => w.weekNumber === currentWeek)?.sessions.length || 0;\n\n  if (nextSession > currentWeekSessions) {\n    nextWeek = currentWeek + 1;\n    nextSession = 1;\n  }\n\n  // Check if program is completed\n  const totalSessions = enrollment.program.weeks.reduce((acc, week) => acc + week.sessions.length, 0);\n\n  const isCompleted = completedSessionsCount >= totalSessions;\n\n  // Update enrollment\n  await prisma.userProgramEnrollment.update({\n    where: { id: enrollment.id },\n    data: {\n      completedSessions: completedSessionsCount,\n      currentWeek: isCompleted ? currentWeek : nextWeek,\n      currentSession: isCompleted ? currentSession : nextSession,\n      completedAt: isCompleted ? new Date() : null,\n      isActive: !isCompleted,\n    },\n  });\n\n  revalidatePath(`/programs/${enrollment.program.slug}`);\n\n  return {\n    sessionProgress: updatedProgress,\n    isCompleted,\n    nextWeek: isCompleted ? null : nextWeek,\n    nextSession: isCompleted ? null : nextSession,\n  };\n}\n"
  },
  {
    "path": "src/features/programs/actions/enroll-program.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { setupAnalytics } from \"@/shared/lib/analytics/server\";\nimport { LogEvents } from \"@/shared/lib/analytics/events\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\nimport { env } from \"@/env\";\n\nexport async function enrollInProgram(programId: string) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  const userId = session.user?.id;\n\n  if (!userId) {\n    throw new Error(\"User not found\");\n  }\n\n  // Check if already enrolled\n  const existingEnrollment = await prisma.userProgramEnrollment.findUnique({\n    where: {\n      userId_programId: {\n        userId,\n        programId,\n      },\n    },\n  });\n\n  if (existingEnrollment) {\n    return { enrollment: existingEnrollment, isNew: false };\n  }\n\n  // Create new enrollment\n  const enrollment = await prisma.userProgramEnrollment.create({\n    data: {\n      userId,\n      programId,\n    },\n    include: {\n      program: true,\n    },\n  });\n\n  // Update participant count\n  await prisma.program.update({\n    where: { id: programId },\n    data: {\n      participantCount: {\n        increment: 1,\n      },\n    },\n  });\n\n  revalidatePath(`/programs/${enrollment.program.slug}`);\n\n  if (env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID) {\n    const analytics = await setupAnalytics({\n      userId,\n      fullName: `${session.user?.firstName} ${session.user?.lastName}`,\n      email: session.user?.email,\n    });\n\n    analytics.track({\n      event: LogEvents.EnrolledInProgram.name,\n      channel: LogEvents.EnrolledInProgram.channel,\n      properties: { programId },\n    });\n  }\n\n  return { enrollment, isNew: true };\n}\n"
  },
  {
    "path": "src/features/programs/actions/get-program-by-slug.action.ts",
    "content": "\"use server\";\n\nimport { ExerciseAttributeValueEnum, ProgramLevel, ProgramVisibility } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\n\nexport interface ProgramDetail {\n  id: string;\n  slug: string;\n  slugEn: string;\n  slugEs: string;\n  slugPt: string;\n  slugRu: string;\n  slugZhCn: string;\n  title: string;\n  titleEn: string;\n  titleEs: string;\n  titlePt: string;\n  titleRu: string;\n  titleZhCn: string;\n  description: string;\n  descriptionEn: string;\n  descriptionEs: string;\n  descriptionPt: string;\n  descriptionRu: string;\n  descriptionZhCn: string;\n  category: string;\n  image: string;\n  level: ProgramLevel;\n  type: ExerciseAttributeValueEnum;\n  durationWeeks: number;\n  sessionsPerWeek: number;\n  sessionDurationMin: number;\n  equipment: ExerciseAttributeValueEnum[];\n  isPremium: boolean;\n  emoji?: string;\n  participantCount: number;\n  totalEnrollments: number;\n  coaches: Array<{\n    id: string;\n    name: string;\n    image: string;\n    order: number;\n  }>;\n  weeks: Array<{\n    id: string;\n    weekNumber: number;\n    title: string;\n    description: string;\n    sessions: Array<{\n      id: string;\n      sessionNumber: number;\n      title: string;\n      titleEn: string;\n      titleEs: string;\n      titlePt: string;\n      titleRu: string;\n      titleZhCn: string;\n      description: string;\n      descriptionEn: string;\n      descriptionEs: string;\n      descriptionPt: string;\n      descriptionRu: string;\n      descriptionZhCn: string;\n      slug: string;\n      slugEn: string;\n      slugEs: string;\n      slugPt: string;\n      slugRu: string;\n      slugZhCn: string;\n      equipment: ExerciseAttributeValueEnum[];\n      estimatedMinutes: number;\n      isPremium: boolean;\n      totalExercises: number;\n    }>;\n  }>;\n}\n\nexport async function getProgramBySlug(slug: string): Promise<ProgramDetail | null> {\n  try {\n    const program = await prisma.program.findFirst({\n      where: {\n        OR: [{ slug }, { slugEn: slug }, { slugEs: slug }, { slugPt: slug }, { slugRu: slug }, { slugZhCn: slug }],\n        visibility: ProgramVisibility.PUBLISHED,\n        isActive: true,\n      },\n      include: {\n        coaches: {\n          orderBy: { order: \"asc\" },\n        },\n        weeks: {\n          include: {\n            sessions: {\n              include: {\n                exercises: {\n                  include: {\n                    suggestedSets: true,\n                  },\n                },\n              },\n              orderBy: { sessionNumber: \"asc\" },\n            },\n          },\n          orderBy: { weekNumber: \"asc\" },\n        },\n        enrollments: {\n          select: {\n            id: true,\n          },\n        },\n      },\n    });\n\n    if (!program) {\n      return null;\n    }\n\n    return {\n      id: program.id,\n      slug: program.slug,\n      slugEn: program.slugEn,\n      slugEs: program.slugEs,\n      slugPt: program.slugPt,\n      slugRu: program.slugRu,\n      slugZhCn: program.slugZhCn,\n      title: program.title,\n      titleEn: program.titleEn,\n      titleEs: program.titleEs,\n      titlePt: program.titlePt,\n      titleRu: program.titleRu,\n      titleZhCn: program.titleZhCn,\n      description: program.description,\n      descriptionEn: program.descriptionEn,\n      descriptionEs: program.descriptionEs,\n      descriptionPt: program.descriptionPt,\n      descriptionRu: program.descriptionRu,\n      descriptionZhCn: program.descriptionZhCn,\n      category: program.category,\n      image: program.image,\n      level: program.level,\n      type: program.type,\n      durationWeeks: program.durationWeeks,\n      sessionsPerWeek: program.sessionsPerWeek,\n      sessionDurationMin: program.sessionDurationMin,\n      equipment: program.equipment,\n      isPremium: program.isPremium,\n      participantCount: program.participantCount,\n      totalEnrollments: program.enrollments.length,\n      coaches: program.coaches.map((coach) => ({\n        id: coach.id,\n        name: coach.name,\n        image: coach.image,\n        order: coach.order,\n      })),\n      weeks: program.weeks.map((week) => ({\n        id: week.id,\n        weekNumber: week.weekNumber,\n        title: week.title,\n        titleEn: week.titleEn,\n        titleEs: week.titleEs,\n        titlePt: week.titlePt,\n        titleRu: week.titleRu,\n        titleZhCn: week.titleZhCn,\n        description: week.description,\n        descriptionEn: week.descriptionEn,\n        descriptionEs: week.descriptionEs,\n        descriptionPt: week.descriptionPt,\n        descriptionRu: week.descriptionRu,\n        descriptionZhCn: week.descriptionZhCn,\n        sessions: week.sessions.map((session) => ({\n          id: session.id,\n          sessionNumber: session.sessionNumber,\n          title: session.title,\n          titleEn: session.titleEn,\n          titleEs: session.titleEs,\n          titlePt: session.titlePt,\n          titleRu: session.titleRu,\n          titleZhCn: session.titleZhCn,\n          description: session.description,\n          descriptionEn: session.descriptionEn,\n          descriptionEs: session.descriptionEs,\n          descriptionPt: session.descriptionPt,\n          descriptionRu: session.descriptionRu,\n          descriptionZhCn: session.descriptionZhCn,\n          slug: session.slug,\n          slugEn: session.slugEn,\n          slugEs: session.slugEs,\n          slugPt: session.slugPt,\n          slugRu: session.slugRu,\n          slugZhCn: session.slugZhCn,\n          equipment: session.equipment,\n          estimatedMinutes: session.estimatedMinutes,\n          isPremium: session.isPremium,\n          totalExercises: session.exercises.length,\n        })),\n      })),\n    };\n  } catch (error) {\n    console.error(\"Error fetching program by slug:\", error);\n    return null;\n  }\n}\n"
  },
  {
    "path": "src/features/programs/actions/get-program-progress-by-slug.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { ProgramVisibility } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nexport async function getProgramProgressBySlug(slug: string) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session) {\n    return null;\n  }\n\n  const userId = session.user?.id;\n\n  if (!userId) {\n    throw new Error(\"User not found\");\n  }\n\n  // First, find the program by slug\n  const program = await prisma.program.findFirst({\n    where: {\n      OR: [{ slug }, { slugEn: slug }, { slugEs: slug }, { slugPt: slug }, { slugRu: slug }, { slugZhCn: slug }],\n      visibility: ProgramVisibility.PUBLISHED,\n      isActive: true,\n    },\n    select: {\n      id: true,\n    },\n  });\n\n  if (!program) {\n    return null;\n  }\n\n  const enrollment = await prisma.userProgramEnrollment.findUnique({\n    where: {\n      userId_programId: {\n        userId,\n        programId: program.id,\n      },\n    },\n    include: {\n      sessionProgress: {\n        include: {\n          session: true,\n          workoutSession: true,\n        },\n      },\n      program: {\n        include: {\n          weeks: {\n            include: {\n              sessions: {\n                orderBy: { sessionNumber: \"asc\" },\n              },\n            },\n            orderBy: { weekNumber: \"asc\" },\n          },\n        },\n      },\n    },\n  });\n\n  if (!enrollment) {\n    return null;\n  }\n\n  // Calculate completion stats\n  const totalSessions = enrollment.program.weeks.reduce((acc, week) => acc + week.sessions.length, 0);\n\n  const completedSessions = enrollment.sessionProgress.filter((progress) => progress.completedAt !== null).length;\n\n  const completionPercentage = totalSessions > 0 ? Math.round((completedSessions / totalSessions) * 100) : 0;\n\n  // Check if program is completed (all sessions are done)\n  const isProgramCompleted = totalSessions > 0 && completedSessions === totalSessions;\n\n  return {\n    enrollment,\n    stats: {\n      totalSessions,\n      completedSessions,\n      completionPercentage,\n      currentWeek: enrollment.currentWeek,\n      currentSession: enrollment.currentSession,\n      isProgramCompleted,\n    },\n  };\n}"
  },
  {
    "path": "src/features/programs/actions/get-program-progress.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nexport async function getProgramProgress(programId: string) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session) {\n    return null;\n  }\n\n  const userId = session.user?.id;\n\n  if (!userId) {\n    throw new Error(\"User not found\");\n  }\n\n  const enrollment = await prisma.userProgramEnrollment.findUnique({\n    where: {\n      userId_programId: {\n        userId,\n        programId,\n      },\n    },\n    include: {\n      sessionProgress: {\n        include: {\n          session: true,\n          workoutSession: true,\n        },\n      },\n      program: {\n        include: {\n          weeks: {\n            include: {\n              sessions: {\n                orderBy: { sessionNumber: \"asc\" },\n              },\n            },\n            orderBy: { weekNumber: \"asc\" },\n          },\n        },\n      },\n    },\n  });\n\n  if (!enrollment) {\n    return null;\n  }\n\n  // Calculate completion stats\n  const totalSessions = enrollment.program.weeks.reduce((acc, week) => acc + week.sessions.length, 0);\n\n  const completedSessions = enrollment.sessionProgress.filter((progress) => progress.completedAt !== null).length;\n\n  const completionPercentage = totalSessions > 0 ? Math.round((completedSessions / totalSessions) * 100) : 0;\n\n  // Check if program is completed (all sessions are done)\n  const isProgramCompleted = totalSessions > 0 && completedSessions === totalSessions;\n\n  return {\n    enrollment,\n    stats: {\n      totalSessions,\n      completedSessions,\n      completionPercentage,\n      currentWeek: enrollment.currentWeek,\n      currentSession: enrollment.currentSession,\n      isProgramCompleted,\n    },\n  };\n}\n"
  },
  {
    "path": "src/features/programs/actions/get-public-programs.action.ts",
    "content": "\"use server\";\n\nimport { ProgramVisibility } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\n\nexport interface PublicProgram {\n  id: string;\n\n  slug: string;\n  slugEn: string;\n  slugEs: string;\n  slugPt: string;\n  slugRu: string;\n  slugZhCn: string;\n\n  title: string;\n  titleEn: string;\n  titleEs: string;\n  titlePt: string;\n  titleRu: string;\n  titleZhCn: string;\n\n  description: string;\n  descriptionEn: string;\n  descriptionEs: string;\n  descriptionPt: string;\n  descriptionRu: string;\n  descriptionZhCn: string;\n\n  category: string;\n  image: string;\n  level: string;\n  type: string;\n  durationWeeks: number;\n  sessionsPerWeek: number;\n  sessionDurationMin: number;\n  equipment: string[];\n  isPremium: boolean;\n  participantCount: number;\n  totalWeeks: number;\n  totalSessions: number;\n  totalExercises: number;\n  totalEnrollments: number;\n}\n\nexport async function getPublicPrograms(): Promise<PublicProgram[]> {\n  try {\n    const programs = await prisma.program.findMany({\n      where: {\n        visibility: ProgramVisibility.PUBLISHED,\n        isActive: true,\n      },\n      include: {\n        weeks: {\n          include: {\n            sessions: {\n              include: {\n                exercises: {\n                  include: {\n                    suggestedSets: true,\n                  },\n                },\n              },\n            },\n          },\n        },\n        enrollments: {\n          select: {\n            id: true,\n          },\n        },\n      },\n      orderBy: [\n        { isPremium: \"desc\" }, // Premium d'abord\n        { createdAt: \"desc\" }, // Plus récents ensuite\n      ],\n    });\n\n    return programs.map((program) => ({\n      id: program.id,\n\n      slug: program.slug,\n      slugEn: program.slugEn,\n      slugEs: program.slugEs,\n      slugPt: program.slugPt,\n      slugRu: program.slugRu,\n      slugZhCn: program.slugZhCn,\n\n      title: program.title,\n      titleEn: program.titleEn,\n      titleEs: program.titleEs,\n      titlePt: program.titlePt,\n      titleRu: program.titleRu,\n      titleZhCn: program.titleZhCn,\n\n      description: program.description,\n      descriptionEn: program.descriptionEn,\n      descriptionEs: program.descriptionEs,\n      descriptionPt: program.descriptionPt,\n      descriptionRu: program.descriptionRu,\n      descriptionZhCn: program.descriptionZhCn,\n\n      category: program.category,\n      image: program.image,\n      level: program.level,\n      type: program.type,\n      durationWeeks: program.durationWeeks,\n      sessionsPerWeek: program.sessionsPerWeek,\n      sessionDurationMin: program.sessionDurationMin,\n      equipment: program.equipment,\n      isPremium: program.isPremium,\n      participantCount: program.participantCount,\n      totalWeeks: program.weeks.length,\n      totalSessions: program.weeks.reduce((acc, week) => acc + week.sessions.length, 0),\n      totalExercises: program.weeks.reduce(\n        (acc, week) => acc + week.sessions.reduce((sessAcc, session) => sessAcc + session.exercises.length, 0),\n        0,\n      ),\n      totalEnrollments: program.enrollments.length,\n    }));\n  } catch (error) {\n    console.error(\"Error fetching public programs:\", error);\n    return [];\n  }\n}\n"
  },
  {
    "path": "src/features/programs/actions/get-session-by-slug.action.ts",
    "content": "\"use server\";\n\nimport { ProgramVisibility } from \"@prisma/client\";\n\nimport { Locale } from \"locales/types\";\nimport { getLocaleSuffix } from \"@/shared/types/i18n.types\";\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { SessionDetailResponse } from \"@/entities/program/types/program.types\";\n\n/**\n * Get session details by slug with proper i18n support\n * Uses locale-specific slug fields for search\n */\nexport async function getSessionBySlug(\n  programSlug: string,\n  sessionSlug: string,\n  locale: Locale = \"fr\",\n): Promise<SessionDetailResponse | null> {\n  try {\n    // Determine slug field based on locale\n    const suffix = getLocaleSuffix(locale);\n    const sessionSlugField = suffix ? `slug${suffix}` : \"slug\";\n\n    // Find session with all related data\n    const session = await prisma.programSession.findFirst({\n      where: {\n        [sessionSlugField]: sessionSlug,\n        week: {\n          program: {\n            OR: [\n              { slug: programSlug },\n              { slugEn: programSlug },\n              { slugEs: programSlug },\n              { slugPt: programSlug },\n              { slugRu: programSlug },\n              { slugZhCn: programSlug },\n            ],\n            visibility: ProgramVisibility.PUBLISHED,\n            isActive: true,\n          },\n        },\n      },\n      include: {\n        week: {\n          include: {\n            program: {\n              select: {\n                id: true,\n                description: true,\n                descriptionEn: true,\n                descriptionEs: true,\n                descriptionPt: true,\n                descriptionRu: true,\n                descriptionZhCn: true,\n                title: true,\n                titleEn: true,\n                titleEs: true,\n                titlePt: true,\n                titleRu: true,\n                titleZhCn: true,\n                slug: true,\n                slugEn: true,\n                slugEs: true,\n                slugPt: true,\n                slugRu: true,\n                slugZhCn: true,\n              },\n            },\n          },\n        },\n        exercises: {\n          include: {\n            exercise: {\n              include: {\n                attributes: {\n                  include: {\n                    attributeName: true,\n                    attributeValue: true,\n                  },\n                },\n              },\n            },\n            suggestedSets: {\n              orderBy: { setIndex: \"asc\" },\n            },\n          },\n          orderBy: { order: \"asc\" },\n        },\n      },\n    });\n\n    if (!session) {\n      return null;\n    }\n\n    // adapt to response type\n    return {\n      session: {\n        id: session.id,\n        weekId: session.weekId,\n        sessionNumber: session.sessionNumber,\n        title: session.title,\n        titleEn: session.titleEn,\n        titleEs: session.titleEs,\n        titlePt: session.titlePt,\n        titleRu: session.titleRu,\n        titleZhCn: session.titleZhCn,\n        description: session.description,\n        descriptionEn: session.descriptionEn,\n        descriptionEs: session.descriptionEs,\n        descriptionPt: session.descriptionPt,\n        descriptionRu: session.descriptionRu,\n        descriptionZhCn: session.descriptionZhCn,\n        slug: session.slug,\n        slugEn: session.slugEn,\n        slugEs: session.slugEs,\n        slugPt: session.slugPt,\n        slugRu: session.slugRu,\n        slugZhCn: session.slugZhCn,\n        equipment: session.equipment,\n        estimatedMinutes: session.estimatedMinutes,\n        isPremium: session.isPremium,\n        exercises: session.exercises.map((ex) => ({\n          id: ex.id,\n          sessionId: ex.sessionId,\n          exerciseId: ex.exerciseId,\n          order: ex.order,\n          instructions: ex.instructions,\n          instructionsEn: ex.instructionsEn,\n          instructionsEs: ex.instructionsEs,\n          instructionsPt: ex.instructionsPt,\n          instructionsRu: ex.instructionsRu,\n          instructionsZhCn: ex.instructionsZhCn,\n          exercise: {\n            id: ex.exercise.id,\n            name: ex.exercise.name,\n            nameEn: ex.exercise.nameEn || \"\",\n            nameEs: ex.exercise.nameEn || \"\", // TODO: Fix when DB has proper values\n            namePt: ex.exercise.nameEn || \"\",\n            nameRu: ex.exercise.nameEn || \"\",\n            nameZhCn: ex.exercise.nameEn || \"\",\n            description: ex.exercise.description || \"\",\n            descriptionEn: ex.exercise.descriptionEn || \"\",\n            descriptionEs: ex.exercise.descriptionEn || \"\", // TODO: Fix when DB has proper values\n            descriptionPt: ex.exercise.descriptionEn || \"\",\n            descriptionRu: ex.exercise.descriptionEn || \"\",\n            descriptionZhCn: ex.exercise.descriptionEn || \"\",\n            fullVideoUrl: ex.exercise.fullVideoUrl,\n            fullVideoImageUrl: ex.exercise.fullVideoImageUrl,\n            introduction: null,\n            introductionEn: null,\n            slug: null,\n            slugEn: null,\n            createdAt: ex.exercise.createdAt,\n            updatedAt: ex.exercise.updatedAt,\n            attributes: ex.exercise.attributes.map((attr) => ({\n              id: attr.id,\n              exerciseId: attr.exerciseId,\n              attributeNameId: attr.attributeNameId,\n              attributeValueId: attr.attributeValueId,\n              attributeName: attr.attributeName.name,\n              attributeValue: attr.attributeValue.value,\n            })),\n          },\n          suggestedSets: ex.suggestedSets.map((set) => ({\n            id: set.id,\n            programSessionExerciseId: set.programSessionExerciseId,\n            programExerciseId: set.programSessionExerciseId,\n            setIndex: set.setIndex,\n            types: set.types,\n            valuesInt: set.valuesInt,\n            valuesSec: set.valuesSec,\n            units: set.units,\n          })),\n        })),\n      },\n      program: {\n        id: session.week.program.id,\n        title: session.week.program.title,\n        titleEn: session.week.program.titleEn,\n        titleEs: session.week.program.titleEs,\n        titlePt: session.week.program.titlePt,\n        titleRu: session.week.program.titleRu,\n        titleZhCn: session.week.program.titleZhCn,\n        description: session.week.program.description,\n        descriptionEn: session.week.program.descriptionEn,\n        descriptionEs: session.week.program.descriptionEs,\n        descriptionPt: session.week.program.descriptionPt,\n        descriptionRu: session.week.program.descriptionRu,\n        descriptionZhCn: session.week.program.descriptionZhCn,\n        slug: session.week.program.slug,\n        slugEn: session.week.program.slugEn,\n        slugEs: session.week.program.slugEs,\n        slugPt: session.week.program.slugPt,\n        slugRu: session.week.program.slugRu,\n        slugZhCn: session.week.program.slugZhCn,\n      },\n      week: {\n        id: session.week.id,\n        weekNumber: session.week.weekNumber,\n        title: session.week.title,\n        titleEn: session.week.titleEn,\n        titleEs: session.week.titleEs,\n        titlePt: session.week.titlePt,\n        titleRu: session.week.titleRu,\n        titleZhCn: session.week.titleZhCn,\n        description: session.week.description,\n        descriptionEn: session.week.descriptionEn,\n        descriptionEs: session.week.descriptionEs,\n        descriptionPt: session.week.descriptionPt,\n        descriptionRu: session.week.descriptionRu,\n        descriptionZhCn: session.week.descriptionZhCn,\n        programId: session.week.programId,\n      },\n    };\n  } catch (error) {\n    console.error(\"Error fetching session by slug:\", error);\n    return null;\n  }\n}\n"
  },
  {
    "path": "src/features/programs/actions/get-sitemap-data.action.ts",
    "content": "\"use server\";\n\nimport { ProgramVisibility } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\n\nexport interface SitemapProgramData {\n  slug: string;\n  slugEn: string;\n  slugEs: string;\n  slugPt: string;\n  slugRu: string;\n  slugZhCn: string;\n  updatedAt: Date;\n  weeks: {\n    weekNumber: number;\n    sessions: {\n      slug: string;\n      slugEn: string;\n      slugEs: string;\n      slugPt: string;\n      slugRu: string;\n      slugZhCn: string;\n      // updatedAt: Date; // TODO: add this back in when we have a way to update the sitemap\n    }[];\n  }[];\n}\n\nexport async function getSitemapData(): Promise<SitemapProgramData[]> {\n  try {\n    const programs = await prisma.program.findMany({\n      where: {\n        visibility: ProgramVisibility.PUBLISHED,\n        isActive: true,\n      },\n      select: {\n        slug: true,\n        slugEn: true,\n        slugEs: true,\n        slugPt: true,\n        slugRu: true,\n        slugZhCn: true,\n        updatedAt: true,\n        weeks: {\n          select: {\n            weekNumber: true,\n            sessions: {\n              select: {\n                slug: true,\n                slugEn: true,\n                slugEs: true,\n                slugPt: true,\n                slugRu: true,\n                slugZhCn: true,\n              },\n              orderBy: {\n                sessionNumber: \"asc\",\n              },\n            },\n          },\n          orderBy: {\n            weekNumber: \"asc\",\n          },\n        },\n      },\n      orderBy: {\n        updatedAt: \"desc\",\n      },\n    });\n\n    return programs;\n  } catch (error) {\n    console.error(\"Error fetching sitemap data:\", error);\n    return [];\n  }\n}\n"
  },
  {
    "path": "src/features/programs/actions/start-program-session.action.ts",
    "content": "\"use server\";\n\nimport { headers } from \"next/headers\";\nimport { revalidatePath } from \"next/cache\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nexport async function startProgramSession(enrollmentId: string, sessionId: string) {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session) {\n    throw new Error(\"Unauthorized\");\n  }\n\n  const userId = session.user?.id;\n\n  if (!userId) {\n    throw new Error(\"User not found\");\n  }\n\n  // Verify enrollment belongs to user\n  const enrollment = await prisma.userProgramEnrollment.findFirst({\n    where: {\n      id: enrollmentId,\n      userId,\n    },\n    include: {\n      program: true,\n    },\n  });\n\n  if (!enrollment) {\n    throw new Error(\"Enrollment not found\");\n  }\n\n  // Check if session already started\n  const existingProgress = await prisma.userSessionProgress.findUnique({\n    where: {\n      enrollmentId_sessionId: {\n        enrollmentId,\n        sessionId,\n      },\n    },\n  });\n\n  if (existingProgress) {\n    return { sessionProgress: existingProgress, isNew: false };\n  }\n\n  // Get session details to update current week/session\n  const programSession = await prisma.programSession.findUnique({\n    where: { id: sessionId },\n    include: {\n      week: true,\n      exercises: {\n        include: {\n          exercise: {\n            include: {\n              attributes: {\n                include: {\n                  attributeName: true,\n                  attributeValue: true,\n                },\n              },\n            },\n          },\n          suggestedSets: {\n            orderBy: { setIndex: \"asc\" },\n          },\n        },\n        orderBy: { order: \"asc\" },\n      },\n    },\n  });\n\n  if (!programSession) {\n    throw new Error(\"Session not found\");\n  }\n\n  // Create session progress\n  const sessionProgress = await prisma.userSessionProgress.create({\n    data: {\n      enrollmentId,\n      sessionId,\n    },\n  });\n\n  // Update enrollment current position\n  await prisma.userProgramEnrollment.update({\n    where: { id: enrollmentId },\n    data: {\n      currentWeek: programSession.week.weekNumber,\n      currentSession: programSession.sessionNumber,\n    },\n  });\n\n  revalidatePath(`/programs/${enrollment.program.slug}`);\n\n  return {\n    sessionProgress,\n    isNew: true,\n    sessionData: programSession,\n  };\n}\n"
  },
  {
    "path": "src/features/programs/hooks/use-program-share.ts",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { shareContent, getCurrentPageUrl, type ShareData, type ShareResult } from \"@/shared/lib/web-share\";\n\ninterface UseProgramShareProps {\n  programTitle: string;\n  programDescription?: string;\n}\n\n/**\n * Hook for sharing program content\n * Handles sharing logic and user feedback\n */\nexport function useProgramShare({ programTitle }: UseProgramShareProps) {\n  const t = useI18n();\n  const [isSharing, setIsSharing] = useState(false);\n  const [shareMessage, setShareMessage] = useState<string | null>(null);\n\n  /**\n   * Share the current program page\n   */\n  const handleShare = async (): Promise<void> => {\n    setIsSharing(true);\n    setShareMessage(null);\n\n    try {\n      const shareData: ShareData = {\n        title: t(\"programs.check_out_program\"),\n        text: programTitle,\n        url: getCurrentPageUrl(),\n      };\n\n      const result: ShareResult = await shareContent(shareData);\n\n      if (result.success) {\n        const message = result.method === \"native\" ? t(\"programs.share_success\") : t(\"programs.copied_to_clipboard\");\n\n        setShareMessage(message);\n\n        // Clear message after 3 seconds\n        setTimeout(() => setShareMessage(null), 3000);\n      } else {\n        setShareMessage(t(\"programs.share_failed\"));\n        console.error(\"Share failed:\", result.error);\n      }\n    } catch (error) {\n      setShareMessage(t(\"programs.share_failed\"));\n      console.error(\"Unexpected share error:\", error);\n    } finally {\n      setIsSharing(false);\n    }\n  };\n\n  return {\n    handleShare,\n    isSharing,\n    shareMessage,\n  };\n}\n"
  },
  {
    "path": "src/features/programs/lib/program-metadata.ts",
    "content": "import { Locale } from \"locales/types\";\nimport { TFunction } from \"locales/client\";\nimport { ATTRIBUTE_VALUE_TRANSLATION_KEYS } from \"@/shared/lib/attribute-value-translation\";\nimport { getLocalizedMetadata } from \"@/shared/config/localized-metadata\";\n\nimport { ProgramDetail } from \"../actions/get-program-by-slug.action\";\nimport { getProgramTitle } from \"./translations-mapper\";\n\nexport function generateProgramSEOKeywords(program: ProgramDetail, locale: Locale, t: TFunction): string[] {\n  const baseData = getLocalizedMetadata(locale);\n  const localizedTitle = getProgramTitle(program, locale);\n  const levelTranslation = t(`levels.${program.level}` as keyof typeof t);\n  const equipmentTranslations = program.equipment.map((eq) => ATTRIBUTE_VALUE_TRANSLATION_KEYS[eq]);\n\n  return [\n    ...baseData.keywords,\n    localizedTitle.toLowerCase(),\n    levelTranslation.toLowerCase(),\n    program.category.toLowerCase(),\n    ...equipmentTranslations.map((eq) => eq.toLowerCase()),\n    locale === \"en\"\n      ? \"workout program\"\n      : locale === \"es\"\n        ? \"programa de entrenamiento\"\n        : locale === \"pt\"\n          ? \"programa de treino\"\n          : locale === \"ru\"\n            ? \"программа тренировок\"\n            : locale === \"zh-CN\"\n              ? \"训练计划\"\n              : \"programme d'entraînement\",\n  ];\n}\n"
  },
  {
    "path": "src/features/programs/lib/session-metadata.ts",
    "content": "/* eslint-disable max-len */\nimport { Locale } from \"locales/types\";\nimport { getLocalizedMetadata } from \"@/shared/config/localized-metadata\";\nimport { ProgramSessionWithExercises } from \"@/entities/program-session/types/program-session.types\";\nimport { ProgramI18nReference } from \"@/entities/program/types/program.types\";\n\nimport { getSessionTitle, getSessionDescription, getProgramTitle } from \"./translations-mapper\";\n\nexport function generateSessionSEOKeywords(session: ProgramSessionWithExercises, program: ProgramI18nReference, locale: Locale): string[] {\n  const baseData = getLocalizedMetadata(locale);\n  const sessionTitle = getSessionTitle(session, locale);\n  const programTitle = getProgramTitle(program, locale);\n\n  const exerciseNames = session.exercises.map((ex) => {\n    switch (locale) {\n      case \"en\":\n        return ex.exercise.nameEn || ex.exercise.name;\n      case \"es\":\n        return ex.exercise.nameEn || ex.exercise.name; // Using nameEn as fallback since nameEs doesn't exist\n      case \"pt\":\n        return ex.exercise.nameEn || ex.exercise.name; // Using nameEn as fallback since namePt doesn't exist\n      case \"ru\":\n        return ex.exercise.nameEn || ex.exercise.name; // Using nameEn as fallback since nameRu doesn't exist\n      case \"zh-CN\":\n        return ex.exercise.nameEn || ex.exercise.name; // Using nameEn as fallback since nameZhCn doesn't exist\n      default:\n        return ex.exercise.name;\n    }\n  });\n\n  const localizedSessionType =\n    locale === \"en\"\n      ? \"workout session\"\n      : locale === \"es\"\n        ? \"sesión de entrenamiento\"\n        : locale === \"pt\"\n          ? \"sessão de treino\"\n          : locale === \"ru\"\n            ? \"тренировочная сессия\"\n            : locale === \"zh-CN\"\n              ? \"训练课程\"\n              : \"séance d'entraînement\";\n\n  return [\n    ...baseData.keywords,\n    sessionTitle.toLowerCase(),\n    programTitle.toLowerCase(),\n    localizedSessionType,\n    ...exerciseNames.map((name) => name.toLowerCase()),\n    \"fitness\",\n    \"exercise\",\n    \"training\",\n    \"workout\",\n  ];\n}\n\nexport function generateSessionMetadata(session: ProgramSessionWithExercises, program: ProgramI18nReference, locale: Locale) {\n  const sessionTitle = getSessionTitle(session, locale);\n  const programTitle = getProgramTitle(program, locale);\n  const sessionDescription = getSessionDescription(session, locale);\n  const keywords = generateSessionSEOKeywords(session, program, locale);\n\n  const title = `${sessionTitle} - ${programTitle}`;\n  const description =\n    sessionDescription ||\n    (locale === \"en\"\n      ? `${sessionTitle} workout session from the ${programTitle} program. ${session.exercises.length} exercises, ~${Math.round(session.exercises.length * 3)} minutes.`\n      : locale === \"es\"\n        ? `Sesión de entrenamiento ${sessionTitle} del programa ${programTitle}. ${session.exercises.length} ejercicios, ~${Math.round(session.exercises.length * 3)} minutos.`\n        : locale === \"pt\"\n          ? `Sessão de treino ${sessionTitle} do programa ${programTitle}. ${session.exercises.length} exercícios, ~${Math.round(session.exercises.length * 3)} minutos.`\n          : locale === \"ru\"\n            ? `Тренировочная сессия ${sessionTitle} из программы ${programTitle}. ${session.exercises.length} упражнений, ~${Math.round(session.exercises.length * 3)} минут.`\n            : locale === \"zh-CN\"\n              ? `${programTitle}计划中的${sessionTitle}训练课程。${session.exercises.length}个练习，约${Math.round(session.exercises.length * 3)}分钟。`\n              : `Séance d'entraînement ${sessionTitle} du programme ${programTitle}. ${session.exercises.length} exercices, ~${Math.round(session.exercises.length * 3)} minutes.`);\n\n  return {\n    title,\n    description,\n    keywords: keywords.join(\", \"),\n  };\n}\n"
  },
  {
    "path": "src/features/programs/lib/suggested-sets-helpers.ts",
    "content": "import { WorkoutSetType, WorkoutSetUnit } from \"@prisma/client\";\n\nexport interface CreateSuggestedSetData {\n  setIndex: number;\n  types: WorkoutSetType[];\n  valuesInt?: number[];\n  valuesSec?: number[];\n  units?: WorkoutSetUnit[];\n}\n\n// helpers to create suggested sets\nexport const SUGGESTED_SET_TEMPLATES = {\n  // 3 sets of 10-12 reps with weight\n  strengthTraining: (weight: number = 20): CreateSuggestedSetData[] => [\n    { setIndex: 0, types: [WorkoutSetType.WEIGHT, WorkoutSetType.REPS], valuesInt: [weight, 10], units: [WorkoutSetUnit.kg] },\n    { setIndex: 1, types: [WorkoutSetType.WEIGHT, WorkoutSetType.REPS], valuesInt: [weight, 12], units: [WorkoutSetUnit.kg] },\n    { setIndex: 2, types: [WorkoutSetType.WEIGHT, WorkoutSetType.REPS], valuesInt: [weight, 12], units: [WorkoutSetUnit.kg] },\n  ],\n\n  // 3 sets of bodyweight\n  bodyweight: (reps: number = 10): CreateSuggestedSetData[] => [\n    { setIndex: 0, types: [WorkoutSetType.BODYWEIGHT, WorkoutSetType.REPS], valuesInt: [0, reps] },\n    { setIndex: 1, types: [WorkoutSetType.BODYWEIGHT, WorkoutSetType.REPS], valuesInt: [0, reps] },\n    { setIndex: 2, types: [WorkoutSetType.BODYWEIGHT, WorkoutSetType.REPS], valuesInt: [0, reps] },\n  ],\n\n  // timed exercises\n  timed: (seconds: number = 30): CreateSuggestedSetData[] => [\n    { setIndex: 0, types: [WorkoutSetType.TIME], valuesSec: [seconds] },\n    { setIndex: 1, types: [WorkoutSetType.TIME], valuesSec: [seconds] },\n    { setIndex: 2, types: [WorkoutSetType.TIME], valuesSec: [seconds] },\n  ],\n};\n"
  },
  {
    "path": "src/features/programs/lib/translations-mapper.ts",
    "content": "import { Locale } from \"locales/types\";\nimport { getI18nField } from \"@/shared/lib/i18n-mapper\";\nimport { PublicProgram } from \"@/features/programs/actions/get-public-programs.action\";\nimport { ProgramDetail } from \"@/features/programs/actions/get-program-by-slug.action\";\nimport { ProgramSessionWithExercises } from \"@/entities/program-session/types/program-session.types\";\nimport { ProgramI18nReference } from \"@/entities/program/types/program.types\";\n\n// Re-export the generic mapper for convenience\nexport { getI18nField };\n\nexport const getWeekTitle = (week: ProgramDetail[\"weeks\"][number], locale: Locale): string => {\n  return getI18nField(week, \"title\", locale);\n};\n\nexport const getWeekDescription = (week: ProgramDetail[\"weeks\"][number], locale: Locale): string => {\n  return getI18nField(week, \"description\", locale);\n};\n\n// Specific mappers for program entities\nexport function getProgramTitle(program: ProgramDetail | PublicProgram | ProgramI18nReference, locale: Locale): string {\n  return getI18nField(program, \"title\", locale);\n}\n\nexport function getProgramDescription(program: ProgramDetail | PublicProgram, locale: Locale): string {\n  return getI18nField(program, \"description\", locale);\n}\n\nexport function getProgramSlug(program: ProgramDetail | PublicProgram, locale: Locale): string {\n  return getI18nField(program, \"slug\", locale);\n}\n\nexport function getSessionTitle(\n  session: ProgramDetail[\"weeks\"][number][\"sessions\"][number] | ProgramSessionWithExercises,\n  locale: Locale,\n): string {\n  return getI18nField(session, \"title\", locale);\n}\n\nexport function getSessionDescription(session: any, locale: Locale): string {\n  return getI18nField(session, \"description\", locale);\n}\n\nexport function getSessionSlug(session: any, locale: Locale): string {\n  return getI18nField(session, \"slug\", locale);\n}\n\nexport function getExerciseName(exercise: any, locale: Locale): string {\n  return getI18nField(exercise, \"name\", locale);\n}\n\nexport function getExerciseDescription(exercise: any, locale: Locale): string {\n  return getI18nField(exercise, \"description\", locale);\n}\n\nexport function getExerciseInstructions(exercise: any, locale: Locale): string {\n  return getI18nField(exercise, \"instructions\", locale);\n}\n"
  },
  {
    "path": "src/features/programs/ui/program-card.tsx",
    "content": "import Link from \"next/link\";\nimport Image from \"next/image\";\nimport { Lock, Star, Clock, Calendar, Dumbbell } from \"lucide-react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { Locale } from \"locales/types\";\nimport { getI18n } from \"locales/server\";\nimport { getAttributeValueLabel } from \"@/shared/lib/attribute-value-translation\";\nimport { getProgramDescription, getProgramSlug, getProgramTitle } from \"@/features/programs/lib/translations-mapper\";\n\nimport { PublicProgram } from \"../actions/get-public-programs.action\";\n\ninterface ProgramCardProps {\n  program: PublicProgram;\n  size?: \"small\" | \"medium\" | \"large\";\n  locale: Locale;\n}\n\nexport async function ProgramCard({ program, size = \"medium\", locale }: ProgramCardProps) {\n  const isLocked = program.isPremium;\n  const t = await getI18n();\n  const title = getProgramTitle(program, locale);\n  const description = getProgramDescription(program, locale);\n  const programSlug = getProgramSlug(program, locale);\n\n  const paddingClass = {\n    small: \"p-3 sm:p-4\",\n    medium: \"p-4 sm:p-5\",\n    large: \"p-4 sm:p-6\",\n  }[size];\n\n  const titleClass = {\n    small: \"text-base font-bold\",\n    medium: \"text-lg font-bold\",\n    large: \"text-xl font-bold\",\n  }[size];\n\n  const subtitleClass = {\n    small: \"text-sm\",\n    medium: \"text-sm\",\n    large: \"text-base\",\n  }[size];\n\n  const getLevelConfig = (level: string) => {\n    switch (level) {\n      case \"BEGINNER\":\n        return {\n          gradient: \"from-[#25CB78] to-emerald-500\",\n          emoji: \"/images/emojis/WorkoutCoolHappy.png\",\n          color: \"text-emerald-800 dark:text-emerald-200\",\n          bgColor: \"bg-emerald-100/90 dark:bg-emerald-900/80\",\n          borderColor: \"border-emerald-200/60 dark:border-emerald-700/60\",\n        };\n      case \"INTERMEDIATE\":\n        return {\n          gradient: \"from-[#4F8EF7] to-blue-500\",\n          emoji: \"/images/emojis/WorkoutCoolBiceps.png\",\n          color: \"text-blue-800 dark:text-blue-200\",\n          bgColor: \"bg-blue-100/90 dark:bg-blue-900/80\",\n          borderColor: \"border-blue-200/60 dark:border-blue-700/60\",\n        };\n      case \"ADVANCED\":\n        return {\n          gradient: \"from-orange-500 to-red-500\",\n          emoji: \"/images/emojis/WorkoutCoolSwag.png\",\n          color: \"text-orange-800 dark:text-orange-200\",\n          bgColor: \"bg-orange-100/90 dark:bg-orange-900/80\",\n          borderColor: \"border-orange-200/60 dark:border-orange-700/60\",\n        };\n      default:\n        return {\n          gradient: \"from-slate-500 to-gray-600\",\n          emoji: \"/images/emojis/WorkoutCoolHappy.png\",\n          color: \"text-slate-800 dark:text-slate-200\",\n          bgColor: \"bg-slate-100/90 dark:bg-slate-900/80\",\n          borderColor: \"border-slate-200/60 dark:border-slate-700/60\",\n        };\n    }\n  };\n\n  const levelConfig = getLevelConfig(program.level);\n\n  return (\n    <div className=\"group\">\n      <Link\n        aria-label={`${title} - ${t(`levels.${program.level}` as keyof typeof t)} program`}\n        className={`\n          block rounded-2xl overflow-hidden transition-all duration-300 ease-out\n          bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-900\n          border-2 ${levelConfig.borderColor} hover:border-opacity-60\n          shadow-sm hover:shadow-xl hover:shadow-slate-200/50 dark:hover:shadow-slate-900/50\n          hover:scale-[1.02] hover:-translate-y-1\n        `}\n        href={`/programs/${programSlug}`}\n        itemScope\n        itemType=\"https://schema.org/Course\"\n      >\n        {/* Gradient Accent */}\n        <div className={`h-1 bg-gradient-to-r ${levelConfig.gradient}`} />\n\n        {/* Image Section */}\n        <div className=\"relative h-32 sm:h-36 overflow-hidden\">\n          <Image alt={title} className=\"w-full h-full object-cover\" fill loading=\"lazy\" src={program.image} />\n          {/* Overlay pour lisibilité */}\n          <div className=\"absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent\" />\n\n          {/* Header avec badges sur l'image */}\n          <div className=\"absolute top-3 left-3 right-3 flex items-start justify-between\">\n            <div className=\"flex flex-wrap gap-2\">\n              <span\n                className={`\n                ${levelConfig.bgColor} ${levelConfig.color} \n                px-2 py-1 sm:px-3 rounded-full text-xs font-semibold\n                border ${levelConfig.borderColor}\n                bg-opacity-90 backdrop-blur-sm shadow-sm\n              `}\n              >\n                {t(`levels.${program.level}` as keyof typeof t)}\n              </span>\n              {isLocked && (\n                <span className=\"bg-yellow-400/90 text-yellow-900 px-2 py-1 sm:px-3 rounded-full text-xs font-semibold border border-yellow-400/40 backdrop-blur-sm shadow-sm\">\n                  Premium\n                </span>\n              )}\n            </div>\n\n            {/* Emoji/Lock */}\n            <div className=\"relative\">\n              <div\n                className={`\n                w-8 h-8 \n                bg-white/90 dark:bg-slate-800/90 rounded-full flex items-center justify-center\n                shadow-sm border border-white/20 backdrop-blur-sm\n                group-hover:scale-110 transition-transform duration-200\n              `}\n              >\n                {isLocked ? (\n                  <Lock className=\"text-slate-600 dark:text-slate-400\" size={size === \"large\" ? 16 : 14} />\n                ) : (\n                  <Image\n                    alt=\"Emoji\"\n                    className={\"object-contain w-6 h-6\"}\n                    height={size === \"large\" ? 24 : 20}\n                    loading=\"lazy\"\n                    src={levelConfig.emoji}\n                    width={size === \"large\" ? 24 : 20}\n                  />\n                )}\n              </div>\n              {!isLocked && <div className=\"absolute -top-1 -right-1 w-3 h-3 bg-[#25CB78] rounded-full animate-pulse\" />}\n            </div>\n          </div>\n        </div>\n\n        {/* Content Section */}\n        <div className={`${paddingClass} space-y-3`}>\n          {/* Titre et description */}\n          <div>\n            <h4\n              className={`${titleClass} leading-tight mb-1 text-slate-800 dark:text-slate-200 group-hover:text-slate-900 dark:group-hover:text-slate-100 transition-colors`}\n              itemProp=\"name\"\n            >\n              {title}\n            </h4>\n            <p className={`${subtitleClass} text-slate-600 dark:text-slate-400 leading-relaxed line-clamp-2`} itemProp=\"description\">\n              {description}\n            </p>\n          </div>\n\n          {/* Quick stats */}\n          <div className=\"flex items-center gap-3 sm:gap-4 text-xs text-slate-500 dark:text-slate-400\">\n            <div className=\"flex items-center gap-1\">\n              <Calendar className=\"w-3 h-3\" />\n              <span>\n                {program.durationWeeks} {t(\"programs.week_short\").toLocaleLowerCase()}\n              </span>\n            </div>\n            <div className=\"flex items-center gap-1\">\n              <Clock className=\"w-3 h-3\" />\n              <span>\n                {program.sessionDurationMin} {t(\"programs.min_short\").toLocaleLowerCase()}\n              </span>\n            </div>\n            <div className=\"flex items-center gap-1\">\n              <Star className=\"w-3 h-3\" />\n              <span>\n                {program.sessionsPerWeek}x/{t(\"programs.week_short\").toLocaleLowerCase()}\n              </span>\n            </div>\n          </div>\n\n          {/* Equipment */}\n          {program.equipment && program.equipment.length > 0 && (\n            <div className=\"flex items-start gap-2\">\n              <Dumbbell className=\"w-3 h-3 mt-0.5 text-slate-400 dark:text-slate-500 flex-shrink-0\" />\n              <div className=\"flex flex-wrap gap-1 text-xs text-slate-500 dark:text-slate-400\">\n                {program.equipment.slice(0, 3).map((equipment, index) => (\n                  <span className=\"inline-flex items-center\" key={equipment}>\n                    {getAttributeValueLabel(equipment as ExerciseAttributeValueEnum, t)}\n                    {index < Math.min(program.equipment.length, 3) - 1 && (\n                      <span className=\"text-slate-300 dark:text-slate-600 ml-1\">•</span>\n                    )}\n                  </span>\n                ))}\n                {program.equipment.length > 3 && (\n                  <span className=\"text-slate-400 dark:text-slate-500\">+{program.equipment.length - 3}</span>\n                )}\n              </div>\n            </div>\n          )}\n        </div>\n      </Link>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/programs/ui/program-detail-page.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { useQueryState, parseAsString, parseAsInteger } from \"nuqs\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport Image from \"next/image\";\nimport {\n  BarChart3,\n  Target,\n  Clock,\n  Calendar,\n  Timer,\n  Dumbbell,\n  Lock,\n  Trophy,\n  Users,\n  Zap,\n  CheckCircle2,\n  Unlock,\n  ArrowRight,\n} from \"lucide-react\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport { useIsPremium } from \"@/shared/lib/premium/use-premium\";\nimport { getSlugForLocale } from \"@/shared/lib/locale-slug\";\nimport { ATTRIBUTE_VALUE_TRANSLATION_KEYS, getAttributeValueLabel } from \"@/shared/lib/attribute-value-translation\";\nimport { WelcomeModal } from \"@/features/programs/ui/welcome-modal\";\nimport { ShareButton } from \"@/features/programs/ui/share-button\";\nimport { ProgramProgress } from \"@/features/programs/ui/program-progress\";\nimport {\n  getProgramDescription,\n  getProgramTitle,\n  getSessionDescription,\n  getSessionTitle,\n  getWeekDescription,\n  getWeekTitle,\n} from \"@/features/programs/lib/translations-mapper\";\nimport { env } from \"@/env\";\nimport { Breadcrumbs } from \"@/components/seo/breadcrumbs\";\nimport { HorizontalBottomBanner, HorizontalTopBanner } from \"@/components/ads\";\n\nimport { getProgramProgress } from \"../actions/get-program-progress.action\";\nimport { ProgramDetail } from \"../actions/get-program-by-slug.action\";\n\ninterface ProgramDetailPageProps {\n  program: ProgramDetail;\n  isAuthenticated: boolean;\n}\n\nexport function ProgramDetailPage({ program, isAuthenticated }: ProgramDetailPageProps) {\n  const [tab, setTab] = useQueryState(\"tab\", parseAsString.withDefault(\"about\"));\n  const [selectedWeek, setSelectedWeek] = useQueryState(\"week\", parseAsInteger.withDefault(1));\n  const [showWelcomeModal, setShowWelcomeModal] = useState(false);\n  const [completedSessions, setCompletedSessions] = useState<Set<string>>(new Set());\n  const [_isLoadingProgress, setIsLoadingProgress] = useState(false);\n  const [hasJoinedProgram, setHasJoinedProgram] = useState(false);\n  const [isProgramCompleted, setIsProgramCompleted] = useState(false);\n  const t = useI18n();\n  const searchParams = useSearchParams();\n  const router = useRouter();\n  const currentLocale = useCurrentLocale();\n  const isPremium = useIsPremium();\n  const programTitle = getProgramTitle(program, currentLocale);\n  const programDescription = getProgramDescription(program, currentLocale);\n  const currentWeekFull = program.weeks.find((w) => w.weekNumber === selectedWeek);\n  const currentWeekTitle = currentWeekFull ? getWeekTitle(currentWeekFull, currentLocale) : \"\";\n  const currentWeekDescription = currentWeekFull ? getWeekDescription(currentWeekFull, currentLocale) : \"\";\n\n  const localizedTitle = getProgramTitle(program, currentLocale);\n\n  const breadcrumbItems = [\n    {\n      label: t(\"breadcrumbs.home\"),\n      href: `/${currentLocale}`,\n    },\n    {\n      label: t(\"programs.workout_programs\"),\n      href: `/${currentLocale}/programs`,\n    },\n    {\n      label: localizedTitle,\n      current: true,\n    },\n  ];\n\n  // Load completed sessions when component mounts or when authenticated\n  useEffect(() => {\n    if (isAuthenticated) {\n      loadCompletedSessions();\n    } else {\n      // Reset states for non-authenticated users\n      setHasJoinedProgram(false);\n      setCompletedSessions(new Set());\n      setSelectedWeek(1);\n      setIsProgramCompleted(false);\n    }\n  }, [isAuthenticated]);\n\n  // Reload progress when refresh param changes (indicating session completion)\n  useEffect(() => {\n    const refreshParam = searchParams.get(\"refresh\");\n    if (refreshParam && isAuthenticated) {\n      loadCompletedSessions();\n    }\n  }, [searchParams, isAuthenticated]);\n\n  const loadCompletedSessions = async () => {\n    if (!isAuthenticated) return;\n\n    setIsLoadingProgress(true);\n    try {\n      const progress = await getProgramProgress(program.id);\n      if (progress?.enrollment) {\n        setHasJoinedProgram(true);\n        setSelectedWeek(progress.stats.currentWeek);\n        setIsProgramCompleted(progress.stats.isProgramCompleted);\n        if (progress.enrollment.sessionProgress) {\n          const completed = new Set(\n            progress.enrollment.sessionProgress.filter((sp: any) => sp.completedAt !== null).map((sp: any) => sp.session.id),\n          );\n          setCompletedSessions(completed);\n        }\n      } else {\n        setHasJoinedProgram(false);\n        setCompletedSessions(new Set());\n        setSelectedWeek(1);\n        setIsProgramCompleted(false);\n      }\n    } catch (error) {\n      console.error(\"Failed to load completed sessions:\", error);\n      setHasJoinedProgram(false);\n    } finally {\n      setIsLoadingProgress(false);\n    }\n  };\n\n  const handleCTAClick = () => {\n    if (isAuthenticated && hasJoinedProgram) {\n      router.push(`/programs/${program.slug}/?tab=sessions`);\n    } else {\n      setShowWelcomeModal(true);\n    }\n  };\n\n  const handleJoinProgram = async () => {\n    setShowWelcomeModal(false);\n\n    router.push(`/programs/${program.slug}/?tab=sessions`);\n\n    // if (isAuthenticated && hasJoinedProgram) {\n    //   // Navigate to current session if user has already joined\n    //   const currentWeekData = program.weeks.find((w) => w.weekNumber === selectedWeek);\n    //   const currentSession = currentWeekData?.sessions.find((s) => s.sessionNumber === currentSessionNumber);\n\n    //   if (currentSession) {\n    //     const sessionSlug = getSlugForLocale(currentSession, currentLocale);\n    //     window.location.href = `/${currentLocale}/programs/${program.slug}/session/${sessionSlug}`;\n    //   } else {\n    //     // Fallback to first session if current session not found\n    //     const firstSession = program.weeks[0]?.sessions[0];\n    //     if (firstSession) {\n    //       const sessionSlug = getSlugForLocale(firstSession, currentLocale);\n    //       window.location.href = `/${currentLocale}/programs/${program.slug}/session/${sessionSlug}`;\n    //     }\n    //   }\n    // } else {\n    //   // Navigate to first session for new users or non-authenticated users\n    //   // Enrollment and authentication will be handled on the session page\n    //   const firstSession = program.weeks[0]?.sessions[0];\n    //   if (firstSession) {\n    //     const sessionSlug = getSlugForLocale(firstSession, currentLocale);\n    //     window.location.href = `/${currentLocale}/programs/${program.slug}/session/${sessionSlug}`;\n    //   }\n    // }\n  };\n\n  return (\n    <div className=\"flex-1 flex flex-col overflow-hidden relative\">\n      <div className=\"flex-1 overflow-y-auto overflow-x-hidden pb-20\">\n        <Breadcrumbs items={breadcrumbItems} />\n        {env.NEXT_PUBLIC_TOP_PROGRAM_DETAILS_BANNER_AD_SLOT && (\n          <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_PROGRAM_DETAILS_BANNER_AD_SLOT} />\n        )}\n        {/* Hero Image Section with Gamification */}\n        <div className=\"relative h-40 sm:h-64 bg-gradient-to-br from-[#4F8EF7] to-[#25CB78]\">\n          <Image alt={programTitle} className=\"absolute inset-0 object-cover opacity-30\" fill src={program.image} />\n          <div className=\"absolute inset-0 bg-black/20\"></div>\n\n          {/* Mascot Emoji */}\n          <div className=\"absolute top-4 right-4 w-16 h-16 bg-white/20 backdrop-blur-sm rounded-full border-2 border-white/30 flex items-center justify-center\">\n            <Image\n              alt=\"Mascotte WorkoutCool\"\n              className=\"w-12 h-12 object-contain\"\n              height={48}\n              src=\"/images/emojis/WorkoutCoolSwag.png\"\n              width={48}\n            />\n          </div>\n\n          <div className=\"relative h-full flex items-end p-6\">\n            <div className=\"text-white flex-1\">\n              <div className=\"flex items-center gap-2 mb-3\">\n                <span className=\"bg-[#4F8EF7] text-white px-3 py-1 rounded-full text-xs font-medium flex items-center gap-1\">\n                  <Zap size={12} />\n                  {t(`levels.${program.level}` as keyof typeof t)}\n                </span>\n                <span className=\"bg-[#25CB78] text-white px-3 py-1 rounded-full text-xs font-medium flex items-center gap-1\">\n                  <Users size={12} />\n                  {program.participantCount}+\n                </span>\n                {/* TODO: i18n category */}\n                {/* {program.isPremium && <span className=\"bg-yellow-500 text-white px-3 py-1 rounded-full text-xs font-medium\">Premium</span>} */}\n              </div>\n              <h1 className=\"text-3xl font-bold mb-2\">{programTitle}</h1>\n              {/* TODO: i18n category */}\n              {/* <p className=\"text-white/90 text-sm\">{program.category}</p> */}\n            </div>\n          </div>\n        </div>\n\n        <div className=\"px-0 sm:px-4 py-4\">\n          <div className=\"tabs tabs-lift\" role=\"tablist\">\n            <button\n              aria-label=\"À propos\"\n              className={`tab ${tab === \"about\" ? \"tab-active\" : \"\"}`}\n              onClick={() => setTab(\"about\")}\n              type=\"button\"\n            >\n              {t(\"programs.about\")}\n            </button>\n            <button\n              aria-label={t(\"programs.sessions\")}\n              className={`tab ${tab === \"sessions\" ? \"tab-active\" : \"\"}`}\n              onClick={() => setTab(\"sessions\")}\n              type=\"button\"\n            >\n              {t(\"programs.sessions\")}\n            </button>\n          </div>\n\n          {/* About Tab Content */}\n          {tab === \"about\" && (\n            <div className=\"bg-base-100 border-base-300 rounded-md p-2 sm:p-6\">\n              <div className=\"space-y-6\">\n                {/* User Progress - Only show if authenticated */}\n                {isAuthenticated && <ProgramProgress programId={program.id} />}\n\n                {/* Early Access Teaser Section - Only show if not premium */}\n                {!isPremium && (\n                  <div className=\"relative bg-gradient-to-r from-blue-50 to-green-50 dark:from-blue-900/20 dark:to-green-900/20 border-2 border-dashed border-blue-200 dark:border-blue-700 rounded-xl p-6 overflow-hidden\">\n                    {/* Subtle animation background */}\n                    <div className=\"absolute inset-0 bg-gradient-to-r from-[#4F8EF7]/5 to-[#25CB78]/5 animate-pulse\"></div>\n\n                    <div className=\"relative z-10\">\n                      <div className=\"flex items-start gap-4\">\n                        <div className=\"flex-shrink-0\">\n                          <div className=\"w-12 h-12 bg-gradient-to-r from-[#4F8EF7] to-[#25CB78] rounded-full flex items-center justify-center\">\n                            <Trophy className=\"text-white\" size={24} />\n                          </div>\n                        </div>\n\n                        <div className=\"flex-1\">\n                          <div className=\"flex items-center gap-2 mb-2\">\n                            <h3 className=\"text-lg font-bold text-gray-900 dark:text-white\">{t(\"programs.important_info\")}</h3>\n                          </div>\n\n                          <p className=\"text-sm text-gray-600 dark:text-gray-400  italic\">💡 {t(\"programs.donation_teaser\")}</p>\n                        </div>\n                      </div>\n                    </div>\n                  </div>\n                )}\n\n                {/* Gamified Community Stats */}\n                <div className=\"bg-gradient-to-r from-[#4F8EF7]/10 to-[#25CB78]/10 border-2 border-[#4F8EF7]/20 rounded-xl p-4\">\n                  <div className=\"flex items-center justify-between\">\n                    <div className=\"flex items-center gap-3\">\n                      <div className=\"flex -space-x-2\">\n                        <Image\n                          alt=\"Communauté\"\n                          className=\"w-10 h-10 object-contain\"\n                          height={40}\n                          src=\"/images/emojis/WorkoutCoolHappy.png\"\n                          width={40}\n                        />\n                      </div>\n                      <div>\n                        <div className=\"flex items-center gap-2 mb-1\">\n                          <span className=\"text-sm font-bold text-[#4F8EF7]\">{t(\"programs.community\")}</span>\n                        </div>\n                        <span className=\"text-sm text-gray-600 dark:text-gray-400\">\n                          +{program.participantCount} {t(\"programs.community_count\")}\n                        </span>\n                      </div>\n                    </div>\n                    <div className=\"flex gap-2\">\n                      <div className=\"tooltip tooltip-bottom\" data-tip={t(\"commons.share\")}>\n                        <ShareButton programDescription={programDescription} programTitle={programTitle} />\n                      </div>\n                    </div>\n                  </div>\n                </div>\n\n                <div className=\"bg-white dark:bg-gray-800 border-2 border-[#25CB78]/20 rounded-xl p-4\">\n                  <div className=\"flex items-center gap-2 mb-4\">\n                    <h3 className=\"font-bold text-lg text-[#4F8EF7]\">{t(\"programs.characteristics\")}</h3>\n                  </div>\n\n                  {/* Compact Layout - Mobile: List, Desktop: 2 columns */}\n                  <div className=\"space-y-4 md:grid md:grid-cols-2 md:gap-x-8 md:gap-y-4 md:space-y-0\">\n                    <div className=\"flex items-center gap-4\">\n                      <BarChart3 className=\"text-[#4F8EF7] flex-shrink-0\" size={20} />\n                      <span className=\"text-base font-medium text-gray-900 dark:text-white\">\n                        {t(`levels.${program.level}` as keyof typeof t)}\n                      </span>\n                    </div>\n\n                    <div className=\"flex items-center gap-4\">\n                      <Target className=\"text-[#25CB78] flex-shrink-0\" size={20} />\n                      <span className=\"text-base font-medium text-gray-900 dark:text-white\">{getAttributeValueLabel(program.type, t)}</span>\n                    </div>\n\n                    <div className=\"flex items-center gap-4\">\n                      <Clock className=\"text-[#4F8EF7] flex-shrink-0\" size={20} />\n                      <span className=\"text-base font-medium text-gray-900 dark:text-white\">\n                        {program.durationWeeks} {t(\"programs.weeks\")}\n                      </span>\n                    </div>\n\n                    <div className=\"flex items-center gap-4\">\n                      <Calendar className=\"text-[#25CB78] flex-shrink-0\" size={20} />\n                      <span className=\"text-base font-medium text-gray-900 dark:text-white\">\n                        {program.sessionsPerWeek} {t(\"programs.sessions_per_week\")}\n                      </span>\n                    </div>\n\n                    <div className=\"flex items-center gap-4\">\n                      <Timer className=\"text-[#4F8EF7] flex-shrink-0\" size={20} />\n                      <span className=\"text-base font-medium text-gray-900 dark:text-white\">\n                        ~{program.sessionDurationMin} {t(\"programs.session_duration\")}\n                      </span>\n                    </div>\n\n                    <div className=\"flex items-center gap-4\">\n                      <Dumbbell className=\"text-[#25CB78] flex-shrink-0\" size={20} />\n                      <span className=\"text-base font-medium text-gray-900 dark:text-white\">\n                        {program.equipment.map((equipment, index) => {\n                          const isFirst = index === 0;\n                          const label = t(ATTRIBUTE_VALUE_TRANSLATION_KEYS[equipment] as keyof typeof t);\n\n                          return <span key={equipment}>{isFirst ? label : `, ${label.toLocaleLowerCase()}`}</span>;\n                        })}\n                      </span>\n                    </div>\n                  </div>\n                </div>\n\n                {/* Description */}\n                <div className=\"space-y-4\">\n                  <p className=\"text-gray-700 dark:text-gray-300 text-center\">{programDescription}</p>\n                </div>\n\n                {/* Gamified Coaches Section */}\n                {program.coaches.length > 0 && (\n                  <div className=\"bg-gradient-to-r from-[#25CB78]/10 to-[#4F8EF7]/10 border-2 border-[#25CB78]/20 rounded-xl p-5\">\n                    <div className=\"flex items-center gap-2 mb-4\">\n                      <h3 className=\"text-lg font-bold text-[#25CB78]\">{t(\"programs.your_coach\", { count: program.coaches.length })}</h3>\n                      <div className=\"bg-[#25CB78] text-white px-2 py-1 rounded-full text-xs font-bold\">{program.coaches.length}</div>\n                    </div>\n                    <div className=\"flex gap-6 overflow-x-auto pb-2\">\n                      {program.coaches.map((coach) => (\n                        <div\n                          className=\"flex flex-col items-center gap-3 flex-shrink-0 p-3 bg-white dark:bg-gray-800 rounded-xl border-2 border-[#25CB78]/20 hover:border-[#25CB78] transition-all duration-200 ease-in-out\"\n                          key={coach.id}\n                        >\n                          <div className=\"relative\">\n                            <Image\n                              alt={coach.name}\n                              className=\"w-24 h-24 rounded-full border-3 border-[#25CB78] object-cover\"\n                              height={96}\n                              src={coach.image}\n                              width={96}\n                            />\n                            <div className=\"absolute -bottom-1 -right-1 w-6 h-6 rounded-full flex items-center justify-center\">\n                              <Image\n                                alt=\"Coachs\"\n                                className=\"w-6 h-6 object-contain\"\n                                height={24}\n                                src=\"/images/emojis/WorkoutCoolLove.png\"\n                                width={24}\n                              />\n                            </div>\n                          </div>\n                          <span className=\"text-sm font-bold text-center\">{coach.name}</span>\n                        </div>\n                      ))}\n                    </div>\n                  </div>\n                )}\n              </div>\n            </div>\n          )}\n\n          {/* Sessions Tab Content */}\n          {tab === \"sessions\" && (\n            <div className=\"bg-base-100 border-base-300 rounded-box p-6\">\n              <div className=\"space-y-6\">\n                {/* Week Selector with DaisyUI Tabs */}\n                <div className=\"overflow-x-auto\">\n                  <div className=\"tabs tabs-box w-fit flex-nowrap\">\n                    {program.weeks.map((week) => (\n                      <button\n                        className={`tab flex-shrink-0 ${selectedWeek === week.weekNumber ? \"tab-active\" : \"\"}`}\n                        key={week.id}\n                        onClick={() => setSelectedWeek(week.weekNumber)}\n                      >\n                        {t(\"programs.week_short\")} {week.weekNumber}\n                      </button>\n                    ))}\n                  </div>\n                </div>\n\n                {/* Current Week Title */}\n                <h2 className=\"text-xl font-bold\">{currentWeekTitle}</h2>\n                <p className=\"text-sm text-gray-600 dark:text-gray-400\">{currentWeekDescription}</p>\n\n                {/* Gamified Sessions List */}\n                <div className=\"space-y-3\">\n                  {(() => {\n                    const currentWeekSessions = program.weeks.find((w) => w.weekNumber === selectedWeek)?.sessions || [];\n\n                    if (currentWeekSessions.length === 0) {\n                      return (\n                        <div className=\"bg-white dark:bg-gray-800 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl p-8 text-center\">\n                          <div className=\"flex flex-col items-center gap-3\">\n                            <div className=\"w-16 h-16 bg-gradient-to-r from-[#4F8EF7]/20 to-[#25CB78]/20 rounded-full flex items-center justify-center\">\n                              <Timer className=\"text-[#4F8EF7]\" size={32} />\n                            </div>\n                            <h4 className=\"font-bold text-gray-900 dark:text-white text-lg\">{t(\"programs.sessions_coming_soon\")}</h4>\n                            <p className=\"text-sm text-gray-600 dark:text-gray-400 max-w-md\">{t(\"programs.sessions_in_creation\")}</p>\n                          </div>\n                        </div>\n                      );\n                    }\n\n                    return currentWeekSessions.map((session) => {\n                      const sessionSlug = getSlugForLocale(session, currentLocale);\n                      const sessionName = getSessionTitle(session, currentLocale);\n                      const sessionDescription = getSessionDescription(session, currentLocale);\n\n                      const isCompleted = completedSessions.has(session.id);\n                      return (\n                        <div\n                          className={`bg-white dark:bg-gray-800 rounded-xl p-4 border-2 cursor-pointer transition-all duration-200 ease-in-out flex items-center gap-4 ${\n                            isCompleted\n                              ? \"border-[#25CB78] bg-[#25CB78]/5\"\n                              : session.isPremium\n                                ? \"border-yellow-200 dark:border-yellow-800 bg-yellow-50/50 dark:bg-yellow-900/10 hover:border-yellow-300 hover:scale-[1.02]\"\n                                : \"border-[#25CB78]/20 hover:border-[#25CB78] hover:scale-[1.02]\"\n                          }`}\n                          key={session.id}\n                          onClick={() => {\n                            window.location.href = `/${currentLocale}/programs/${program.slug}/session/${sessionSlug}`;\n                          }}\n                        >\n                          {/* Session Number Badge */}\n                          <div className=\"relative\">\n                            <div\n                              className={`w-8 sm:w-12 h-8 sm:h-12 rounded-xl flex items-center justify-center flex-shrink-0 font-bold text-white ${\n                                isCompleted\n                                  ? \"bg-[#25CB78]\"\n                                  : session.isPremium\n                                    ? isPremium\n                                      ? \"bg-[#4F8EF7]\"\n                                      : \"bg-yellow-500\"\n                                    : \"bg-[#25CB78]\"\n                              }`}\n                            >\n                              {isCompleted ? (\n                                <CheckCircle2 size={18} />\n                              ) : session.isPremium ? (\n                                isPremium ? (\n                                  <Unlock size={18} />\n                                ) : (\n                                  <Lock size={18} />\n                                )\n                              ) : (\n                                <span className=\"text-lg\">{session.sessionNumber}</span>\n                              )}\n                            </div>\n                          </div>\n\n                          {/* Session Info */}\n                          <div className=\"flex-1\">\n                            <div className=\"flex items-center gap-2 mb-1\">\n                              <h4 className={`font-bold ${isCompleted ? \"text-[#25CB78]\" : \"text-gray-900 dark:text-white\"}`}>\n                                {sessionName}\n                              </h4>\n                              {isCompleted && (\n                                <div className=\"bg-[#25CB78] text-white px-2 py-1 rounded-full text-xs font-bold\">\n                                  {t(\"programs.completed\")}\n                                </div>\n                              )}\n                              {!isCompleted && !session.isPremium && (\n                                <div className=\"bg-[#25CB78] text-white px-2 py-1 rounded-full text-xs font-bold\">{t(\"programs.free\")}</div>\n                              )}\n                              {!isCompleted && session.isPremium && (\n                                <div className=\"bg-yellow-500 text-white px-2 py-1 rounded-full text-xs font-bold\">\n                                  {t(\"programs.premium\")}\n                                </div>\n                              )}\n                            </div>\n                            {sessionDescription && (\n                              <p className=\"text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1\">{sessionDescription}</p>\n                            )}\n                            <p className=\"text-xs text-gray-600 mt-1\">\n                              {session.totalExercises} {t(\"programs.exercises\")} • {session.estimatedMinutes} {t(\"programs.min_short\")}\n                            </p>\n                          </div>\n\n                          {/* Status Icon */}\n                          <div className=\"w-10 h-10 flex items-center justify-center\">\n                            {isCompleted ? (\n                              <CheckCircle2 className=\"text-[#25CB78]\" size={24} />\n                            ) : (\n                              <ArrowRight className=\"text-gray-600 dark:text-gray-400\" size={24} />\n                            )}\n                          </div>\n                        </div>\n                      );\n                    });\n                  })()}\n                </div>\n                {env.NEXT_PUBLIC_BOTTOM_PROGRAM_DETAILS_BANNER_AD_SLOT && (\n                  <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_BOTTOM_PROGRAM_DETAILS_BANNER_AD_SLOT} />\n                )}\n              </div>\n            </div>\n          )}\n        </div>\n      </div>\n\n      {/* Enhanced Persuasive Floating CTA - Only show if program is not completed */}\n      {!isProgramCompleted && tab !== \"sessions\" && (\n        <div className=\"absolute bottom-2 right-0 left-0 max-w-sm mx-auto px-4 z-[10]\">\n          <button\n            className={\n              \"w-full bg-gradient-to-r from-[#4F8EF7] to-[#25CB78] hover:from-[#4F8EF7]/90 hover:to-[#25CB78]/90 text-white px-6 py-4 font-bold border-2 border-white/20 hover:scale-[1.02] transition-all duration-200 ease-in-out z-1 flex items-center justify-center gap-2 shadow-xl rounded-full\"\n            }\n            onClick={handleCTAClick}\n          >\n            <Image alt=\"Rejoindre\" className=\"w-6 h-6 object-contain\" height={24} src=\"/images/emojis/WorkoutCoolSwag.png\" width={24} />\n            <div className=\"flex flex-col items-center\">\n              <span className=\"text-base\">{isAuthenticated && hasJoinedProgram ? t(\"programs.continue\") : t(\"programs.join_cta\")}</span>\n            </div>\n            <Trophy className=\"text-white animate-bounce\" size={18} />\n          </button>\n        </div>\n      )}\n\n      {/* Program Completed Message */}\n      {isProgramCompleted && (\n        <div className=\"absolute bottom-2 right-0 left-0 max-w-xs mx-auto bg-gradient-to-r from-[#25CB78] to-[#4F8EF7] text-white px-4 py-3 rounded-full font-bold border-2 border-white/20 z-1 flex items-center justify-center gap-2\">\n          <Trophy className=\"text-white\" size={18} />\n          {t(\"programs.program_completed\")}\n          <CheckCircle2 className=\"text-white\" size={18} />\n        </div>\n      )}\n\n      {/* Welcome Modal */}\n      <WelcomeModal\n        isOpen={showWelcomeModal}\n        onClose={() => setShowWelcomeModal(false)}\n        onJoin={handleJoinProgram}\n        programDuration={`${program.durationWeeks} ${t(\"programs.weeks\")}`}\n        programFrequency={`${program.sessionsPerWeek} ${t(\"programs.sessions_per_week\")}`}\n        programLevel={t(`levels.${program.level}` as keyof typeof t)}\n        programTitle={programTitle}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/programs/ui/program-progress.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { CheckCircle, PlayCircle } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { getProgramProgress } from \"@/features/programs/actions/get-program-progress.action\";\n\ninterface ProgramProgressProps {\n  programId: string;\n}\n\nexport function ProgramProgress({ programId }: ProgramProgressProps) {\n  const [progress, setProgress] = useState<any>(null);\n  const [isLoading, setIsLoading] = useState(true);\n  const searchParams = useSearchParams();\n  const t = useI18n();\n\n  useEffect(() => {\n    loadProgress();\n  }, [programId]);\n\n  // Reload progress when refresh param changes (indicating session completion)\n  useEffect(() => {\n    const refreshParam = searchParams.get(\"refresh\");\n    if (refreshParam) {\n      loadProgress();\n    }\n  }, [searchParams]);\n\n  const loadProgress = async () => {\n    try {\n      const data = await getProgramProgress(programId);\n      setProgress(data);\n    } catch (error) {\n      console.error(\"Failed to load progress:\", error);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  if (isLoading) {\n    return <div className=\"animate-pulse bg-gray-200 dark:bg-gray-700 h-20 rounded-xl\"></div>;\n  }\n\n  if (!progress) {\n    return null;\n  }\n\n  const { stats } = progress;\n\n  return (\n    <div className=\"bg-gradient-to-r from-[#4F8EF7]/10 to-[#25CB78]/10 border-2 border-[#4F8EF7]/20 rounded-xl p-4 mb-6\">\n      <div className=\"flex items-center justify-between mb-3\">\n        <div>\n          <h3 className=\"font-bold text-lg text-[#4F8EF7]\">{t(\"programs.my_progress\")}</h3>\n          <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n            {stats.completedSessions} / {stats.totalSessions} {t(\"programs.completed_sets\")}\n          </p>\n        </div>\n        <div className=\"text-3xl font-bold text-[#25CB78]\">{stats.completionPercentage}%</div>\n      </div>\n\n      <div className=\"flex items-center gap-4 text-sm\">\n        <div className=\"flex items-center gap-2\">\n          <CheckCircle className=\"text-[#25CB78]\" size={16} />\n          <span className=\"text-gray-600 dark:text-gray-400\">{t(\"programs.completed_feminine\")}</span>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <PlayCircle className=\"text-[#4F8EF7]\" size={16} />\n          <span className=\"text-gray-600 dark:text-gray-400\">\n            {t(\"programs.week\")} {stats.currentWeek}, {t(\"programs.session\")} {stats.currentSession}\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/programs/ui/programs-page.tsx",
    "content": "import Image from \"next/image\";\nimport { Crown, TrendingUp } from \"lucide-react\";\n\nimport { Locale } from \"locales/types\";\nimport { getI18n } from \"locales/server\";\nimport { env } from \"@/env\";\nimport { HorizontalBottomBanner, HorizontalTopBanner } from \"@/components/ads\";\n\nimport { getPublicPrograms } from \"../actions/get-public-programs.action\";\nimport { ProgramCard } from \"./program-card\";\n\ninterface ProgramsPageProps {\n  locale: Locale;\n}\n\nexport async function ProgramsPage({ locale }: ProgramsPageProps) {\n  const programs = await getPublicPrograms();\n  const t = await getI18n();\n\n  if (programs.length === 0) {\n    return (\n      <div className=\"flex flex-col h-full flex-1 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800\">\n        <div className=\"flex flex-col items-center justify-center flex-1 p-6\">\n          <div className=\"mb-6\">\n            <Image\n              alt=\"Mascotte WorkoutCool triste\"\n              className=\"object-contain w-20 h-20\"\n              height={80}\n              priority\n              src=\"/images/emojis/WorkoutCoolCry.png\"\n              width={80}\n            />\n          </div>\n          <h1 className=\"text-xl sm:text-2xl font-bold mb-3 text-slate-800 dark:text-slate-200 text-center\">\n            {t(\"programs.no_programs_available\")}\n          </h1>\n          <p className=\"text-slate-600 dark:text-slate-400 text-center max-w-md\">{t(\"programs.no_programs_available_description\")}</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <main className=\"flex flex-col bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800\">\n      {/* Hero Section - Style Apple moderne */}\n      {env.NEXT_PUBLIC_TOP_PROGRAMS_BANNER_AD_SLOT && <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_PROGRAMS_BANNER_AD_SLOT} />}\n      <header className=\"relative overflow-hidden\">\n        <div className=\"absolute inset-0 bg-gradient-to-br from-[#4F8EF7] via-[#4F8EF7] to-[#25CB78]\" />\n        <div className=\"absolute inset-0 opacity-30\">\n          <div\n            className=\"w-full h-full\"\n            style={{\n              backgroundImage:\n                \"url(\\\"data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\\\")\",\n            }}\n          />\n        </div>\n\n        <div className=\"relative p-6 sm:p-8\">\n          <div className=\"flex items-start justify-between\">\n            <div className=\"flex-1\">\n              <div className=\"flex items-center gap-3 mb-3\">\n                <h1 className=\"text-2xl sm:text-3xl font-bold text-white tracking-tight\">{t(\"programs.workout_programs\")}</h1>\n                <div className=\"relative\">\n                  <span className=\"bg-yellow-400 text-black px-3 py-1 rounded-full text-xs font-bold shadow-lg\">{t(\"commons.new\")}</span>\n                  <div className=\"absolute -inset-1 bg-yellow-400/20 rounded-full animate-ping\" />\n                </div>\n              </div>\n              <p className=\"text-white/90 text-base sm:text-lg leading-relaxed max-w-lg\">{t(\"programs.workout_programs_description\")}</p>\n            </div>\n\n            <div className=\"relative ml-4\">\n              <div className=\"w-16 h-16 sm:w-20 sm:h-20 relative\">\n                <Image\n                  alt=\"Mascotte WorkoutCool\"\n                  className=\"object-contain w-full h-full\"\n                  height={80}\n                  priority\n                  src=\"/images/emojis/WorkoutCoolBiceps.png\"\n                  width={80}\n                />\n                <div className=\"absolute -top-2 -right-2 bg-yellow-400 rounded-full p-1.5 shadow-lg\">\n                  <Crown className=\"text-black w-3 h-3\" />\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </header>\n\n      <section className=\"flex-1 p-4 sm:p-6 space-y-8\">\n        {/* Programs Grid */}\n        {programs.length > 0 && (\n          <div>\n            <h2 className=\"sr-only\">{t(\"programs.available_programs\")}</h2>\n\n            {/* Grille asymétrique mobile-first */}\n            <div aria-label={t(\"programs.workout_programs\")} className=\"grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6\" role=\"list\">\n              {programs.map((program, index) => (\n                <article\n                  className={\"relative animate-fade-in-up\"}\n                  key={program.id}\n                  role=\"listitem\"\n                  style={{\n                    animationDelay: `${index * 100}ms`,\n                    animationFillMode: \"both\",\n                  }}\n                >\n                  <ProgramCard locale={locale} program={program} size={index === 0 ? \"medium\" : \"medium\"} />\n                </article>\n              ))}\n            </div>\n\n            {/* Coming Soon Section - Style plus moderne */}\n            <aside className=\"mt-12 relative\">\n              <div className=\"bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-800 dark:to-slate-900 border-2 border-dashed border-slate-300 dark:border-slate-600 rounded-2xl p-3 sm:p-8 text-center relative\">\n                <div className=\"absolute -top-2 -right-2 sm:top-4 sm:right-4\">\n                  <Image\n                    alt=\"Mascotte WorkoutCool excitée\"\n                    className=\"object-contain w-10 h-10 sm:w-12 sm:h-12 opacity-60\"\n                    height={48}\n                    src=\"/images/emojis/WorkoutCoolWooow.png\"\n                    width={48}\n                  />\n                </div>\n\n                {env.NEXT_PUBLIC_BOTTOM_PROGRAMS_BANNER_AD_SLOT && (\n                  <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_BOTTOM_PROGRAMS_BANNER_AD_SLOT} />\n                )}\n\n                <div className=\"flex items-center justify-center gap-3 mb-4\">\n                  <TrendingUp className=\"text-[#4F8EF7] w-6 h-6\" />\n                  <h3 className=\"text-xl font-bold text-slate-800 dark:text-slate-200\">{t(\"programs.more_programs_coming_title\")}</h3>\n                </div>\n\n                <p className=\"text-slate-600 dark:text-slate-400 mb-6 max-w-md mx-auto leading-relaxed\">\n                  {t(\"programs.more_programs_coming_description\")}\n                </p>\n\n                <div className=\"flex flex-wrap items-center justify-center gap-3\">\n                  <span className=\"bg-[#4F8EF7]/10 text-[#4F8EF7] px-4 py-2 rounded-full font-medium border border-[#4F8EF7]/20 transition-all duration-200 hover:bg-[#4F8EF7]/20\">\n                    {t(\"programs.coming_strength\")}\n                  </span>\n                  <span className=\"bg-[#25CB78]/10 text-[#25CB78] px-4 py-2 rounded-full font-medium border border-[#25CB78]/20 transition-all duration-200 hover:bg-[#25CB78]/20\">\n                    {t(\"programs.coming_cardio\")}\n                  </span>\n                  <span className=\"bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 px-4 py-2 rounded-full font-medium border border-purple-200 dark:border-purple-800 transition-all duration-200 hover:bg-purple-200 dark:hover:bg-purple-900/50\">\n                    {t(\"programs.coming_yoga\")}\n                  </span>\n                </div>\n              </div>\n            </aside>\n          </div>\n        )}\n      </section>\n    </main>\n  );\n}\n"
  },
  {
    "path": "src/features/programs/ui/session-access-guard.tsx",
    "content": "\"use client\";\n\nimport { useRouter } from \"next/navigation\";\nimport Image from \"next/image\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport { getSessionAccess, type AccessControlContext } from \"@/shared/lib/access-control\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface SessionAccessGuardProps {\n  context: AccessControlContext;\n  children: React.ReactNode;\n  sessionTitle: string;\n  sessionDescription?: string;\n  programSlug: string;\n  sessionSlug?: string;\n}\n\n/**\n * Guard component that handles session access control\n * Shows appropriate UI based on user authentication and premium status\n */\nexport function SessionAccessGuard({\n  context,\n  children,\n  sessionTitle,\n  sessionDescription,\n  programSlug,\n  sessionSlug,\n}: SessionAccessGuardProps) {\n  const t = useI18n();\n  const router = useRouter();\n  const locale = useCurrentLocale();\n  const accessAction = getSessionAccess(context);\n\n  // User can access the session - show content\n  if (accessAction === \"allow\") {\n    return <>{children}</>;\n  }\n\n  // User needs to authenticate\n  if (accessAction === \"require_auth\") {\n    return (\n      <div className=\"flex flex-col items-center justify-center min-h-[60vh] p-6\">\n        <div className=\"max-w-md mx-auto text-center\">\n          <div className=\"flex items-center justify-center mx-auto mb-6\">\n            <Image alt=\"Login\" height={96} src=\"/images/emojis/WorkoutCoolPolice.png\" width={96} />\n          </div>\n\n          <h2 className=\"text-2xl font-bold mb-4\">{t(\"programs.auth_required\")}</h2>\n          <p className=\"text-gray-600 dark:text-gray-400 mb-6\">{t(\"programs.auth_required_description\")}</p>\n\n          {sessionDescription && (\n            <div className=\"mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg text-left\">\n              <h3 className=\"font-semibold mb-2\">{sessionTitle}</h3>\n              <p className=\"text-gray-700 dark:text-gray-300 text-sm\">{sessionDescription}</p>\n            </div>\n          )}\n\n          <div className=\"space-y-3\">\n            <Button\n              className=\"w-full bg-blue-600 hover:bg-blue-700 text-white\"\n              onClick={() => {\n                const redirectUrl = sessionSlug\n                  ? `/${locale}/programs/${programSlug}/session/${sessionSlug}`\n                  : `/${locale}/programs/${programSlug}`;\n                router.push(`/auth/signin?redirect=${encodeURIComponent(redirectUrl)}`);\n              }}\n              size=\"large\"\n            >\n              {t(\"programs.login_to_continue\")}\n            </Button>\n            <Button\n              className=\"w-full bg-green-600 hover:bg-green-700 text-white\"\n              onClick={() => {\n                const redirectUrl = sessionSlug\n                  ? `/${locale}/programs/${programSlug}/session/${sessionSlug}`\n                  : `/${locale}/programs/${programSlug}`;\n                router.push(`/auth/signup?redirect=${encodeURIComponent(redirectUrl)}`);\n              }}\n              size=\"large\"\n            >\n              {t(\"programs.signup_to_continue\")}\n            </Button>\n            <Button className=\"w-full\" onClick={() => router.push(`/programs/${programSlug}`)} size=\"large\" variant=\"outline\">\n              {t(\"programs.back_to_program\")}\n            </Button>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  // User needs premium subscription\n  if (accessAction === \"require_premium\") {\n    return (\n      <div className=\"flex flex-col items-center justify-center min-h-[60vh] p-6\">\n        <div className=\"max-w-md mx-auto text-center\">\n          <div className=\"flex items-center justify-center mx-auto mb-6\">\n            <Image alt=\"Premium\" height={128} src=\"/images/emojis/WorkoutCoolRich.png\" width={128} />\n          </div>\n\n          <h2 className=\"text-2xl font-bold mb-4\">{t(\"programs.premium_required\")}</h2>\n          <p className=\"text-gray-600 dark:text-gray-400 mb-6\">{t(\"programs.premium_required_description\")}</p>\n\n          {sessionDescription && (\n            <div className=\"mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg text-left\">\n              <h3 className=\"font-semibold mb-2\">{sessionTitle}</h3>\n              <p className=\"text-gray-700 dark:text-gray-300 text-sm\">{sessionDescription}</p>\n            </div>\n          )}\n\n          <div className=\"space-y-3\">\n            <Button\n              className=\"w-full bg-yellow-600 hover:bg-yellow-700 text-white\"\n              onClick={() => {\n                // Redirect to premium page with the current session as return URL\n                const returnUrl = sessionSlug\n                  ? `/${locale}/programs/${programSlug}/session/${sessionSlug}`\n                  : `/${locale}/programs/${programSlug}`;\n                router.push(`/${locale}/premium?return=${encodeURIComponent(returnUrl)}`);\n              }}\n              size=\"large\"\n            >\n              {t(\"programs.upgrade_to_premium\")}\n            </Button>\n            <Button className=\"w-full\" onClick={() => router.push(`/programs/${programSlug}`)} size=\"large\" variant=\"outline\">\n              {t(\"programs.back_to_program\")}\n            </Button>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  // Fallback - should not happen\n  return null;\n}\n"
  },
  {
    "path": "src/features/programs/ui/share-button.tsx",
    "content": "\"use client\";\n\nimport { Share2, Check, Loader2 } from \"lucide-react\";\n\nimport { useProgramShare } from \"../hooks/use-program-share\";\n\ninterface ShareButtonProps {\n  programTitle: string;\n  programDescription?: string;\n  className?: string;\n  size?: \"sm\" | \"md\" | \"lg\";\n  variant?: \"default\" | \"ghost\";\n}\n\n/**\n * Reusable share button component\n * Shows appropriate feedback based on share status\n */\nexport function ShareButton({ \n  programTitle, \n  programDescription,\n  className = \"\",\n  size = \"md\",\n  variant = \"default\"\n}: ShareButtonProps) {\n  const { handleShare, isSharing, shareMessage } = useProgramShare({\n    programTitle,\n    programDescription,\n  });\n\n  // Size classes\n  const sizeClasses = {\n    sm: \"p-2\",\n    md: \"p-3\", \n    lg: \"p-4\",\n  };\n\n  // Variant classes\n  const variantClasses = {\n    default: \"bg-[#4F8EF7] hover:bg-[#4F8EF7]/80 text-white\",\n    ghost: \"bg-transparent hover:bg-white/10 text-current\",\n  };\n\n  // Icon size based on button size\n  const iconSize = {\n    sm: 16,\n    md: 18,\n    lg: 20,\n  }[size];\n\n  return (\n    <div className=\"relative\">\n      <button\n        className={`\n          ${variantClasses[variant]}\n          ${sizeClasses[size]}\n          rounded-xl \n          transition-all \n          duration-200 \n          ease-in-out \n          hover:scale-105\n          disabled:opacity-50\n          disabled:cursor-not-allowed\n          disabled:hover:scale-100\n          ${className}\n        `}\n        disabled={isSharing}\n        onClick={handleShare}\n        type=\"button\"\n      >\n        {isSharing ? (\n          <Loader2 className=\"animate-spin\" size={iconSize} />\n        ) : shareMessage ? (\n          <Check size={iconSize} />\n        ) : (\n          <Share2 size={iconSize} />\n        )}\n      </button>\n\n      {/* Feedback tooltip */}\n      {shareMessage && (\n        <div className=\"absolute -top-12 left-1/2 transform -translate-x-1/2 bg-black text-white text-xs px-2 py-1 rounded whitespace-nowrap z-10\">\n          {shareMessage}\n          <div className=\"absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-black\"></div>\n        </div>\n      )}\n    </div>\n  );\n}"
  },
  {
    "path": "src/features/programs/ui/welcome-modal.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport Image from \"next/image\";\nimport { Trophy, Target, Calendar, Zap, X } from \"lucide-react\";\nimport confetti from \"canvas-confetti\";\n\nimport { useI18n } from \"locales/client\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface WelcomeModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  onJoin: () => void;\n  programTitle: string;\n  programLevel: string;\n  programDuration: string;\n  programFrequency: string;\n}\n\nexport function WelcomeModal({\n  isOpen,\n  onClose,\n  onJoin,\n  programTitle,\n  programLevel,\n  programDuration,\n  programFrequency,\n}: WelcomeModalProps) {\n  const [isAnimating, setIsAnimating] = useState(false);\n  const t = useI18n();\n\n  useEffect(() => {\n    if (isOpen) {\n      setIsAnimating(true);\n      // Trigger confetti animation\n      setTimeout(() => {\n        confetti({\n          particleCount: 100,\n          spread: 70,\n          origin: { y: 0.6 },\n          colors: [\"#4F8EF7\", \"#25CB78\", \"#FFD700\"],\n        });\n      }, 300);\n    }\n  }, [isOpen]);\n\n  if (!isOpen) return null;\n\n  const handleJoin = () => {\n    confetti({\n      particleCount: 200,\n      spread: 120,\n      origin: { y: 0.5 },\n      colors: [\"#4F8EF7\", \"#25CB78\", \"#FFD700\"],\n    });\n    setTimeout(() => {\n      onJoin();\n    }, 800);\n  };\n\n  return (\n    <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm\">\n      <div\n        className={`relative w-full max-w-md bg-white dark:bg-gray-900 rounded-2xl shadow-2xl transform transition-all duration-500 ${\n          isAnimating ? \"scale-100 opacity-100\" : \"scale-95 opacity-0\"\n        }`}\n      >\n        {/* Close button */}\n        <button\n          className=\"absolute top-4 right-4 p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors\"\n          onClick={onClose}\n        >\n          <X className=\"text-gray-500\" size={20} />\n        </button>\n\n        {/* Content */}\n        <div className=\"p-6 space-y-6\">\n          {/* Header with mascot */}\n          <div className=\"text-center\">\n            <div className=\"relative inline-block mb-4\">\n              <div className=\"w-24 h-24 mx-auto relative animate-bounce\">\n                <Image\n                  alt=\"WorkoutCool Mascot\"\n                  className=\"object-contain\"\n                  height={96}\n                  src=\"/images/emojis/WorkoutCoolSwag.png\"\n                  width={96}\n                />\n              </div>\n              <div className=\"absolute -top-2 -right-2 w-8 h-8 bg-[#FFD700] rounded-full flex items-center justify-center animate-pulse\">\n                <Trophy className=\"text-white\" size={16} />\n              </div>\n            </div>\n\n            <h2 className=\"text-2xl font-bold mb-2 bg-gradient-to-r from-[#4F8EF7] to-[#25CB78] bg-clip-text text-transparent\">\n              {t(\"programs.welcome_modal.welcome_title\", { programTitle })}\n            </h2>\n            <p className=\"text-gray-600 dark:text-gray-400\">{t(\"programs.welcome_modal.subtitle\")}</p>\n          </div>\n\n          {/* Program quick info */}\n          <div className=\"bg-gradient-to-r from-[#4F8EF7]/10 to-[#25CB78]/10 rounded-xl p-4 space-y-3\">\n            <div className=\"flex items-center gap-3\">\n              <div className=\"w-8 h-8 bg-[#4F8EF7] rounded-lg flex items-center justify-center\">\n                <Zap className=\"text-white\" size={16} />\n              </div>\n              <div>\n                <p className=\"text-xs text-gray-500 dark:text-gray-400\">{t(\"programs.welcome_modal.level_label\")}</p>\n                <p className=\"font-semibold text-gray-900 dark:text-white\">{programLevel}</p>\n              </div>\n            </div>\n\n            <div className=\"flex items-center gap-3\">\n              <div className=\"w-8 h-8 bg-[#25CB78] rounded-lg flex items-center justify-center\">\n                <Calendar className=\"text-white\" size={16} />\n              </div>\n              <div>\n                <p className=\"text-xs text-gray-500 dark:text-gray-400\">{t(\"programs.welcome_modal.duration_label\")}</p>\n                <p className=\"font-semibold text-gray-900 dark:text-white\">{programDuration}</p>\n              </div>\n            </div>\n\n            <div className=\"flex items-center gap-3\">\n              <div className=\"w-8 h-8 bg-[#4F8EF7] rounded-lg flex items-center justify-center\">\n                <Target className=\"text-white\" size={16} />\n              </div>\n              <div>\n                <p className=\"text-xs text-gray-500 dark:text-gray-400\">{t(\"programs.welcome_modal.frequency_label\")}</p>\n                <p className=\"font-semibold text-gray-900 dark:text-white\">{programFrequency}</p>\n              </div>\n            </div>\n          </div>\n\n          {/* Tips */}\n          {/* <div className=\"bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-xl p-4\">\n            <div className=\"flex gap-3\">\n              <Image alt=\"Tip\" className=\"object-contain\" height={32} src=\"/images/emojis/WorkoutCoolHappy.png\" width={32} />\n              <div className=\"text-sm\">\n                <p className=\"font-semibold text-yellow-800 dark:text-yellow-200 mb-1\">Astuce du coach</p>\n                <p className=\"text-yellow-700 dark:text-yellow-300\">\n                  Commence doucement et augmente l&apos;intensité progressivement. La régularité est la clé du succès !\n                </p>\n              </div>\n            </div>\n          </div> */}\n\n          {/* Actions */}\n          <div className=\"flex gap-3\">\n            <Button className=\"flex-1\" onClick={onClose} size=\"large\" variant=\"outline\">\n              {t(\"programs.welcome_modal.later_button\")}\n            </Button>\n            <Button\n              className=\"flex-1 bg-gradient-to-r from-[#4F8EF7] to-[#25CB78] hover:from-[#4F8EF7]/80 hover:to-[#25CB78]/80 text-white border-0\"\n              onClick={handleJoin}\n              size=\"large\"\n            >\n              {t(\"programs.welcome_modal.start_button\")}\n            </Button>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/release-notes/hooks/index.ts",
    "content": "export { useChangelogNotification } from \"./use-changelog-notification\";\nexport type { UseChangelogNotificationReturn, ChangelogNotificationConfig } from \"./use-changelog-notification\";"
  },
  {
    "path": "src/features/release-notes/hooks/use-changelog-notification.ts",
    "content": "import { useState, useEffect, useCallback } from \"react\";\n\nimport { releaseNotes } from \"../model/notes\";\nimport { changelogNotificationLocal, CHANGELOG_NOTIFICATION_STORAGE_KEY } from \"../model/changelog-notification.local\";\n\nexport interface UseChangelogNotificationReturn {\n  /** Whether to show the notification badge */\n  showBadge: boolean;\n  /** Function to mark the changelog as seen */\n  markAsSeen: () => void;\n  /** Whether the system is currently checking for updates */\n  isChecking: boolean;\n  /** The latest release note date */\n  latestReleaseDate: string | null;\n  /** The last seen timestamp */\n  lastSeenTimestamp: string | null;\n  /** Any error that occurred */\n  error: string | null;\n  /** Function to manually refresh the notification state */\n  refresh: () => void;\n}\n\nexport interface ChangelogNotificationConfig {\n  /** Whether to show badge for new users (default: true) */\n  showBadgeForNewUsers?: boolean;\n  /** Whether to show badge when localStorage is unavailable (default: true) */\n  showBadgeOnStorageError?: boolean;\n  /** Whether to automatically check on mount (default: true) */\n  autoCheck?: boolean;\n}\n\nexport function useChangelogNotification(config: ChangelogNotificationConfig = {}): UseChangelogNotificationReturn {\n  const { showBadgeForNewUsers = true, showBadgeOnStorageError = true, autoCheck = true } = config;\n\n  const [showBadge, setShowBadge] = useState(false);\n  const [isChecking, setIsChecking] = useState(false);\n  const [latestReleaseDate, setLatestReleaseDate] = useState<string | null>(null);\n  const [lastSeenTimestamp, setLastSeenTimestamp] = useState<string | null>(null);\n  const [error, setError] = useState<string | null>(null);\n\n  const checkForUpdates = useCallback(async () => {\n    setIsChecking(true);\n    setError(null);\n\n    try {\n      // Get the latest release date\n      const latest = changelogNotificationLocal.getLatestReleaseDate(releaseNotes);\n      setLatestReleaseDate(latest);\n\n      // Get the last seen timestamp\n      const lastSeen = changelogNotificationLocal.getLastSeenTimestamp();\n      setLastSeenTimestamp(lastSeen);\n\n      // Determine if badge should be shown\n      let shouldShowBadge = false;\n\n      if (!latest) {\n        // No release notes available\n        shouldShowBadge = false;\n      } else if (!lastSeen) {\n        // No timestamp stored (new user or cleared storage)\n        shouldShowBadge = showBadgeForNewUsers;\n      } else {\n        // Check if there are new release notes\n        shouldShowBadge = changelogNotificationLocal.hasNewReleaseNotes(releaseNotes, lastSeen);\n      }\n\n      setShowBadge(shouldShowBadge);\n    } catch (err) {\n      const errorMessage = err instanceof Error ? err.message : \"Unknown error occurred\";\n      setError(errorMessage);\n      console.error(\"Error checking for changelog updates:\", err);\n\n      // Show badge on error if configured to do so\n      setShowBadge(showBadgeOnStorageError);\n    } finally {\n      setIsChecking(false);\n    }\n  }, [showBadgeForNewUsers, showBadgeOnStorageError]);\n\n  const markAsSeen = useCallback(() => {\n    try {\n      changelogNotificationLocal.markChangelogAsSeen();\n      setShowBadge(false);\n      setLastSeenTimestamp(changelogNotificationLocal.getLastSeenTimestamp());\n    } catch (err) {\n      const errorMessage = err instanceof Error ? err.message : \"Failed to mark as seen\";\n      setError(errorMessage);\n      console.error(\"Error marking changelog as seen:\", err);\n    }\n  }, []);\n\n  const refresh = useCallback(() => {\n    checkForUpdates();\n  }, [checkForUpdates]);\n\n  // Auto-check on mount\n  useEffect(() => {\n    if (autoCheck) {\n      checkForUpdates();\n    }\n  }, [checkForUpdates, autoCheck]);\n\n  // Listen for localStorage changes from other tabs\n  useEffect(() => {\n    const handleStorageChange = (event: StorageEvent) => {\n      if (event.key === CHANGELOG_NOTIFICATION_STORAGE_KEY) {\n        checkForUpdates();\n      }\n    };\n\n    window.addEventListener(\"storage\", handleStorageChange);\n    return () => window.removeEventListener(\"storage\", handleStorageChange);\n  }, [checkForUpdates]);\n\n  return {\n    showBadge,\n    markAsSeen,\n    isChecking,\n    latestReleaseDate,\n    lastSeenTimestamp,\n    error,\n    refresh,\n  };\n}\n"
  },
  {
    "path": "src/features/release-notes/index.ts",
    "content": "export { ReleaseNotesDialog } from \"./ui/release-notes-dialog\";\nexport { ChangelogNotificationBadge } from \"./ui/changelog-notification-badge\";\nexport { useChangelogNotification } from \"./hooks/use-changelog-notification\";\nexport type { ReleaseNotesDialogProps } from \"./ui/release-notes-dialog\";\nexport type { ChangelogNotificationBadgeProps } from \"./ui/changelog-notification-badge\";\nexport type { UseChangelogNotificationReturn, ChangelogNotificationConfig } from \"./hooks/use-changelog-notification\";\n"
  },
  {
    "path": "src/features/release-notes/lib/date-utils.ts",
    "content": "/**\n * Utility functions for date handling in the changelog notification system\n */\n\n/**\n * Validates if a given string is a valid ISO timestamp\n * @param timestamp String to validate\n * @returns true if valid ISO timestamp\n */\nexport function isValidISOTimestamp(timestamp: string): boolean {\n  if (!timestamp || typeof timestamp !== \"string\") {\n    return false;\n  }\n\n  try {\n    const date = new Date(timestamp);\n    return !isNaN(date.getTime()) && date.toISOString() === timestamp;\n  } catch {\n    return false;\n  }\n}\n\n/**\n * Validates if a date string is in YYYY-MM-DD format\n * @param dateString Date string to validate\n * @returns true if valid date format\n */\nexport function isValidDateString(dateString: string): boolean {\n  if (!dateString || typeof dateString !== \"string\") {\n    return false;\n  }\n\n  // Check format: YYYY-MM-DD\n  const dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n  if (!dateRegex.test(dateString)) {\n    return false;\n  }\n\n  try {\n    const date = new Date(dateString);\n    return !isNaN(date.getTime());\n  } catch {\n    return false;\n  }\n}\n\n/**\n * Compares two date strings (YYYY-MM-DD format) with timezone awareness\n * @param date1 First date string\n * @param date2 Second date string\n * @returns negative if date1 < date2, positive if date1 > date2, 0 if equal\n */\nexport function compareDateStrings(date1: string, date2: string): number {\n  if (!isValidDateString(date1) || !isValidDateString(date2)) {\n    throw new Error(\"Invalid date format. Expected YYYY-MM-DD.\");\n  }\n\n  const d1 = new Date(date1);\n  const d2 = new Date(date2);\n\n  return d1.getTime() - d2.getTime();\n}\n\n/**\n * Gets the current date as an ISO timestamp\n * @returns Current date as ISO timestamp string\n */\nexport function getCurrentISOTimestamp(): string {\n  return new Date().toISOString();\n}\n"
  },
  {
    "path": "src/features/release-notes/model/changelog-notification.local.ts",
    "content": "import { isValidISOTimestamp, getCurrentISOTimestamp } from \"../lib/date-utils\";\n\nimport type { ReleaseNote } from \"./notes\";\n\nexport const CHANGELOG_NOTIFICATION_STORAGE_KEY = \"lastSeenChangelog\";\n\n// Rate limiting: prevent excessive localStorage operations\nconst RATE_LIMIT_WINDOW = 1000; // 1 second\nlet lastStorageOperation = 0;\n\n/**\n * Checks if rate limiting should be applied\n */\nfunction shouldRateLimit(): boolean {\n  const now = Date.now();\n  const timeSinceLastOperation = now - lastStorageOperation;\n  return timeSinceLastOperation < RATE_LIMIT_WINDOW;\n}\n\n/**\n * Updates the rate limit timestamp\n */\nfunction updateRateLimitTimestamp(): void {\n  lastStorageOperation = Date.now();\n}\n\n/**\n * Sanitizes and validates a timestamp string to prevent XSS and ensure proper format\n */\nfunction sanitizeTimestamp(timestamp: string): string | null {\n  if (!timestamp || typeof timestamp !== \"string\") {\n    return null;\n  }\n\n  // Remove any potential HTML/script tags and trim whitespace\n  const sanitized = timestamp.replace(/<[^>]*>/g, \"\").trim();\n\n  return isValidISOTimestamp(sanitized) ? sanitized : null;\n}\n\n/**\n * Gets the last seen changelog timestamp from localStorage\n * @returns ISO timestamp string or null if not found/invalid\n */\nfunction getLastSeenTimestamp(): string | null {\n  try {\n    const stored = localStorage.getItem(CHANGELOG_NOTIFICATION_STORAGE_KEY);\n    if (!stored) {\n      return null;\n    }\n\n    return sanitizeTimestamp(stored);\n  } catch (error) {\n    console.warn(\"Failed to get last seen changelog timestamp:\", error);\n    // Don't throw in this case - return null to gracefully handle localStorage unavailability\n    return null;\n  }\n}\n\n/**\n * Sets the last seen changelog timestamp in localStorage\n * @param timestamp ISO timestamp string\n */\nfunction setLastSeenTimestamp(timestamp: string): void {\n  if (shouldRateLimit()) {\n    console.warn(\"Rate limit exceeded for localStorage operations\");\n    return;\n  }\n\n  try {\n    const sanitized = sanitizeTimestamp(timestamp);\n    if (!sanitized) {\n      console.warn(\"Invalid timestamp provided to setLastSeenTimestamp:\", timestamp);\n      return;\n    }\n\n    localStorage.setItem(CHANGELOG_NOTIFICATION_STORAGE_KEY, sanitized);\n    updateRateLimitTimestamp();\n  } catch (error) {\n    console.error(\"Failed to save last seen changelog timestamp:\", error);\n    // Don't throw - gracefully handle localStorage unavailability\n  }\n}\n\n/**\n * Gets the latest release note date from the notes array\n * @param notes Array of release notes\n * @returns Latest date string or null if array is empty\n */\nfunction getLatestReleaseDate(notes: ReleaseNote[]): string | null {\n  if (!notes || notes.length === 0) {\n    return null;\n  }\n\n  // Assuming notes are sorted by date (newest first) as per assumptions\n  const latestNote = notes[0];\n  return latestNote?.date || null;\n}\n\n/**\n * Checks if there are new release notes since the last seen timestamp\n * @param notes Array of release notes\n * @param lastSeenTimestamp ISO timestamp of last seen changelog\n * @returns true if there are new notes to show\n */\nfunction hasNewReleaseNotes(notes: ReleaseNote[], lastSeenTimestamp: string | null): boolean {\n  if (!notes || notes.length === 0) {\n    return false;\n  }\n\n  // If no timestamp is stored, show badge (new user or cleared storage)\n  if (!lastSeenTimestamp) {\n    return true;\n  }\n\n  const latestReleaseDate = getLatestReleaseDate(notes);\n  if (!latestReleaseDate) {\n    return false;\n  }\n\n  try {\n    // Convert the timestamp to a date for comparison\n    const lastSeenDate = new Date(lastSeenTimestamp);\n    const latestDate = new Date(latestReleaseDate);\n\n    if (isNaN(lastSeenDate.getTime()) || isNaN(latestDate.getTime())) {\n      return true; // Show badge if we can't compare dates\n    }\n\n    // Check if the latest release is newer than the last seen date\n    return latestDate.getTime() > lastSeenDate.getTime();\n  } catch (error) {\n    console.warn(\"Error checking for new release notes:\", error);\n    return true; // Show badge on error to be safe\n  }\n}\n\n/**\n * Marks the changelog as seen by setting the current timestamp\n */\nfunction markChangelogAsSeen(): void {\n  const now = getCurrentISOTimestamp();\n  setLastSeenTimestamp(now);\n}\n\nexport const changelogNotificationLocal = {\n  getLastSeenTimestamp,\n  setLastSeenTimestamp,\n  getLatestReleaseDate,\n  hasNewReleaseNotes,\n  markChangelogAsSeen,\n};\n"
  },
  {
    "path": "src/features/release-notes/model/notes.ts",
    "content": "export interface ReleaseNote {\n  date: string;\n  titleKey: string;\n  contentKey: string;\n}\n\nexport const releaseNotes: ReleaseNote[] = [\n  {\n    date: \"2025-10-29\",\n    titleKey: \"release_notes.notes.note_2025_10_29.title\",\n    contentKey: \"release_notes.notes.note_2025_10_29.content\",\n  },\n  {\n    date: \"2025-08-18\",\n    titleKey: \"release_notes.notes.note_2025_08_18.title\",\n    contentKey: \"release_notes.notes.note_2025_08_18.content\",\n  },\n  {\n    date: \"2025-07-09\",\n    titleKey: \"release_notes.notes.note_2025_07_09.title\",\n    contentKey: \"release_notes.notes.note_2025_07_09.content\",\n  },\n  {\n    date: \"2025-07-02\",\n    titleKey: \"release_notes.notes.note_2025_07_02.title\",\n    contentKey: \"release_notes.notes.note_2025_07_02.content\",\n  },\n  {\n    date: \"2025-06-23\",\n    titleKey: \"release_notes.notes.note_2025_06_23.title\",\n    contentKey: \"release_notes.notes.note_2025_06_23.content\",\n  },\n  {\n    date: \"2025-06-22\",\n    titleKey: \"release_notes.notes.note_2025_06_22.title\",\n    contentKey: \"release_notes.notes.note_2025_06_22.content\",\n  },\n  {\n    date: \"2025-06-19\",\n    titleKey: \"release_notes.notes.note_2025_06_19.title\",\n    contentKey: \"release_notes.notes.note_2025_06_19.content\",\n  },\n  {\n    date: \"2025-06-18\",\n    titleKey: \"release_notes.notes.note_2025_06_18.title\",\n    contentKey: \"release_notes.notes.note_2025_06_18.content\",\n  },\n  {\n    date: \"2025-06-01\",\n    titleKey: \"release_notes.notes.note_2025_06_01.title\",\n    contentKey: \"release_notes.notes.note_2025_06_01.content\",\n  },\n  {\n    date: \"2025-05-20\",\n    titleKey: \"release_notes.notes.note_2025_05_20.title\",\n    contentKey: \"release_notes.notes.note_2025_05_20.content\",\n  },\n];\n"
  },
  {
    "path": "src/features/release-notes/types/notification.ts",
    "content": "/**\n * Configuration options for the changelog notification system\n */\nexport interface ChangelogNotificationConfig {\n  /** Whether to show badge for new users (default: true) */\n  showBadgeForNewUsers?: boolean;\n  /** Whether to show badge when localStorage is unavailable (default: true) */\n  showBadgeOnStorageError?: boolean;\n  /** Custom storage key (default: \"lastSeenChangelog\") */\n  storageKey?: string;\n}\n\n/**\n * Result of checking for new release notes\n */\nexport interface NotificationCheckResult {\n  /** Whether to show the notification badge */\n  showBadge: boolean;\n  /** The latest release note date */\n  latestReleaseDate: string | null;\n  /** The last seen timestamp */\n  lastSeenTimestamp: string | null;\n  /** Any error that occurred during checking */\n  error?: string;\n}\n\n/**\n * Hook return type for useChangelogNotification\n */\nexport interface UseChangelogNotificationReturn {\n  /** Whether to show the notification badge */\n  showBadge: boolean;\n  /** Function to mark the changelog as seen */\n  markAsSeen: () => void;\n  /** Whether the system is currently checking for updates */\n  isChecking: boolean;\n  /** The latest release note date */\n  latestReleaseDate: string | null;\n  /** The last seen timestamp */\n  lastSeenTimestamp: string | null;\n  /** Any error that occurred */\n  error: string | null;\n}\n\n/**\n * Badge component props\n */\nexport interface ChangelogNotificationBadgeProps {\n  /** Whether to show the badge */\n  show: boolean;\n  /** Custom CSS classes */\n  className?: string;\n  /** ARIA label for accessibility */\n  ariaLabel?: string;\n  /** Custom size (small, medium, large) */\n  size?: \"small\" | \"medium\" | \"large\";\n  /** Custom color theme */\n  variant?: \"primary\" | \"secondary\" | \"success\" | \"warning\" | \"error\";\n}\n\n/**\n * Enhanced ReleaseNotesDialog props\n */\nexport interface EnhancedReleaseNotesDialogProps {\n  /** Function called when dialog is opened */\n  onOpen?: () => void;\n  /** Function called when dialog is closed */\n  onClose?: () => void;\n  /** Whether to show the notification badge */\n  showNotificationBadge?: boolean;\n  /** Custom badge props */\n  badgeProps?: Partial<ChangelogNotificationBadgeProps>;\n}\n\n/**\n * localStorage service interface\n */\nexport interface ChangelogNotificationLocalStorage {\n  /** Get the last seen timestamp */\n  getLastSeenTimestamp: () => string | null;\n  /** Set the last seen timestamp */\n  setLastSeenTimestamp: (timestamp: string) => void;\n  /** Get the latest release date from notes */\n  getLatestReleaseDate: (notes: Array<{ date: string }>) => string | null;\n  /** Check if there are new release notes */\n  hasNewReleaseNotes: (notes: Array<{ date: string }>, lastSeenTimestamp: string | null) => boolean;\n  /** Mark the changelog as seen */\n  markChangelogAsSeen: () => void;\n}\n\n/**\n * Date utility functions interface\n */\nexport interface DateUtils {\n  /** Validate ISO timestamp */\n  isValidISOTimestamp: (timestamp: string) => boolean;\n  /** Compare date strings */\n  compareDateStrings: (date1: string, date2: string) => number;\n  /** Get current ISO timestamp */\n  getCurrentISOTimestamp: () => string;\n  /** Format release date for display */\n  formatReleaseDate: (dateString: string, locale?: string) => string;\n}\n\n/**\n * Error types for the notification system\n */\nexport enum NotificationErrorType {\n  STORAGE_ERROR = \"STORAGE_ERROR\",\n  INVALID_TIMESTAMP = \"INVALID_TIMESTAMP\",\n  INVALID_DATE_FORMAT = \"INVALID_DATE_FORMAT\",\n  PARSING_ERROR = \"PARSING_ERROR\",\n  UNKNOWN_ERROR = \"UNKNOWN_ERROR\",\n}\n\n/**\n * Custom error class for notification system\n */\nexport class NotificationError extends Error {\n  constructor(\n    public type: NotificationErrorType,\n    message: string,\n    public originalError?: Error,\n  ) {\n    super(message);\n    this.name = \"NotificationError\";\n  }\n}\n"
  },
  {
    "path": "src/features/release-notes/ui/changelog-notification-badge.tsx",
    "content": "import React from \"react\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nexport interface ChangelogNotificationBadgeProps {\n  /** ARIA label for accessibility */\n  ariaLabel?: string;\n  /** Custom CSS classes */\n  className?: string;\n  /** Whether to show the badge */\n  show: boolean;\n  /** Custom size (small, medium, large) */\n  size?: \"small\" | \"medium\" | \"large\";\n  /** Custom color variant */\n  variant?: \"primary\" | \"secondary\" | \"success\" | \"warning\" | \"error\";\n}\n\nconst sizeClasses = {\n  small: \"w-2 h-2 sm:w-3 sm:h-3\",\n  medium: \"w-3 h-3\",\n  large: \"w-4 h-4\",\n};\n\nconst variantClasses = {\n  primary: \"bg-blue-500 dark:bg-blue-400\",\n  secondary: \"bg-gray-500 dark:bg-gray-400\",\n  success: \"bg-green-500 dark:bg-green-400\",\n  warning: \"bg-yellow-500 dark:bg-yellow-400\",\n  error: \"bg-red-500 dark:bg-red-400\",\n};\n\nexport function ChangelogNotificationBadge({\n  ariaLabel = \"New updates available\",\n  className,\n  show,\n  size = \"medium\",\n  variant = \"primary\",\n}: ChangelogNotificationBadgeProps) {\n  if (!show) {\n    return null;\n  }\n\n  return (\n    <div\n      aria-label={ariaLabel}\n      aria-live=\"polite\"\n      className={cn(\n        \"absolute top-0.5 right-1 rounded-full transition-all duration-200 ease-in-out\",\n        sizeClasses[size],\n        variantClasses[variant],\n        className,\n      )}\n      role=\"status\"\n    >\n      <span className=\"sr-only\">{ariaLabel}</span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/release-notes/ui/index.ts",
    "content": "export { ChangelogNotificationBadge } from \"./changelog-notification-badge\";\nexport type { ChangelogNotificationBadgeProps } from \"./changelog-notification-badge\";"
  },
  {
    "path": "src/features/release-notes/ui/release-notes-dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Bell } from \"lucide-react\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport { formatDate } from \"@/shared/lib/date\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\n\nimport { releaseNotes } from \"../model/notes\";\nimport { useChangelogNotification } from \"../hooks/use-changelog-notification\";\nimport { ChangelogNotificationBadge } from \"./changelog-notification-badge\";\n\nexport interface ReleaseNotesDialogProps {\n  /** Function called when dialog is opened */\n  onOpen?: () => void;\n  /** Function called when dialog is closed */\n  onClose?: () => void;\n  /** Whether to show the notification badge */\n  showNotificationBadge?: boolean;\n}\n\nexport function ReleaseNotesDialog({ onOpen, onClose, showNotificationBadge = true }: ReleaseNotesDialogProps = {}) {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n\n  // Use the changelog notification hook\n  const { showBadge, markAsSeen } = useChangelogNotification();\n\n  const [open, setOpen] = React.useState(false);\n\n  const handleOpenChange = (newOpen: boolean) => {\n    setOpen(newOpen);\n\n    if (newOpen) {\n      markAsSeen();\n      onOpen?.();\n    } else {\n      onClose?.();\n    }\n  };\n\n  return (\n    <Dialog onOpenChange={handleOpenChange} open={open}>\n      <DialogTrigger asChild>\n        <div className=\"tooltip tooltip-bottom z-10\" data-tip={t(\"commons.changelog\")}>\n          <div className=\"relative\">\n            <Button aria-label={t(\"release_notes.release_notes\")} className=\"rounded-full hover:bg-slate-200\" size=\"small\" variant=\"ghost\">\n              <Bell className=\"text-blue-500 dark:text-blue-400 h-6 w-6\" />\n            </Button>\n            {showNotificationBadge && <ChangelogNotificationBadge show={showBadge} size=\"small\" variant=\"primary\" />}\n          </div>\n        </div>\n      </DialogTrigger>\n      <DialogContent className=\"max-w-md max-h-[60vh] overflow-y-auto\">\n        <DialogHeader>\n          <DialogTitle>{t(\"release_notes.title\")}</DialogTitle>\n        </DialogHeader>\n        <div className=\"space-y-4\">\n          {releaseNotes.map((note) => (\n            <div className=\"border-b pb-2 last:border-b-0 last:pb-0 py-2\" key={note.date}>\n              <div className=\"text-xs text-muted-foreground\">{formatDate(note.date, locale)}</div>\n              <div className=\"font-semibold mb-1\" dangerouslySetInnerHTML={{ __html: t(note.titleKey as keyof typeof t) }} />\n              <div className=\"text-sm mb-4\" dangerouslySetInnerHTML={{ __html: t(note.contentKey as keyof typeof t) }} />\n            </div>\n          ))}\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "src/features/statistics/components/ExerciseSelection.tsx",
    "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport Image from \"next/image\";\nimport { Search, BarChart3 } from \"lucide-react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { ExerciseAttributeNameEnum } from \"@prisma/client\";\n\nimport { ExerciseVideoModal } from \"@/features/workout-builder/ui/exercise-video-modal\";\nimport { ExerciseWithAttributes } from \"@/entities/exercise/types/exercise.types\";\nimport { getExerciseAttributesValueOf, getPrimaryMuscle } from \"@/entities/exercise/shared/muscles\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Input } from \"@/components/ui/input\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\n\nconst exerciseService = {\n  async getAllExercises(params: { page?: number; limit?: number; search?: string; muscle?: string }) {\n    // This would be replaced with actual API call\n    const response = await fetch(`/api/exercises/all?${new URLSearchParams(params as any)}`);\n    return response.json();\n  },\n};\n\ninterface ExerciseSelectionProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  onSelectExercise: (exercise: ExerciseWithAttributes) => void;\n}\n\nconst MUSCLES = [\"CHEST\", \"BACK\", \"SHOULDERS\", \"BICEPS\", \"TRICEPS\", \"LEGS\", \"ABDOMINALS\"];\n\nexport const ExerciseSelection: React.FC<ExerciseSelectionProps> = ({ open, onOpenChange, onSelectExercise }) => {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [selectedMuscle, setSelectedMuscle] = useState<string | undefined>();\n  const [selectedExercise, setSelectedExercise] = useState<ExerciseWithAttributes | null>(null);\n  const [showVideoModal, setShowVideoModal] = useState(false);\n\n  const { data, isLoading, error } = useQuery({\n    queryKey: [\"exercises\", \"all\", searchQuery, selectedMuscle],\n    queryFn: () =>\n      exerciseService.getAllExercises({\n        limit: 50,\n        search: searchQuery || undefined,\n        muscle: selectedMuscle,\n      }),\n    enabled: open,\n  });\n\n  const handleSearch = useCallback((query: string) => {\n    setSearchQuery(query);\n  }, []);\n\n  const handleMuscleSelect = useCallback(\n    (muscle: string) => {\n      setSelectedMuscle(muscle === selectedMuscle ? undefined : muscle);\n    },\n    [selectedMuscle],\n  );\n\n  const handleExercisePress = useCallback((exercise: ExerciseWithAttributes) => {\n    setSelectedExercise(exercise);\n    setShowVideoModal(true);\n  }, []);\n\n  const handleSelectForStats = useCallback(\n    (exercise: ExerciseWithAttributes) => {\n      onSelectExercise(exercise);\n      onOpenChange(false);\n    },\n    [onSelectExercise, onOpenChange],\n  );\n\n  const renderExerciseItem = useCallback(\n    (exercise: ExerciseWithAttributes) => {\n      const primaryMuscle = getPrimaryMuscle(exercise.attributes);\n\n      return (\n        <div\n          className=\"flex items-center gap-4 p-4 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700\"\n          key={exercise.id}\n        >\n          <div className=\"flex-1 flex items-center gap-4 cursor-pointer\" onClick={() => handleExercisePress(exercise)}>\n            {exercise.fullVideoImageUrl && (\n              <div className=\"relative w-16 h-16 rounded-lg overflow-hidden bg-slate-100 dark:bg-slate-700\">\n                <Image alt={exercise.name} className=\"object-cover\" fill src={exercise.fullVideoImageUrl} />\n              </div>\n            )}\n            <div className=\"flex-1\">\n              <h3 className=\"font-medium text-slate-900 dark:text-slate-100 mb-1\">{exercise.name}</h3>\n              {primaryMuscle && (\n                <Badge className=\"text-xs\" variant=\"success\">\n                  {getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.PRIMARY_MUSCLE)}\n                </Badge>\n              )}\n            </div>\n          </div>\n\n          <Button className=\"shrink-0\" onClick={() => handleSelectForStats(exercise)} size=\"small\" variant=\"outline\">\n            <BarChart3 className=\"h-4 w-4 mr-2\" />\n            Voir les stats\n          </Button>\n        </div>\n      );\n    },\n    [handleExercisePress, handleSelectForStats],\n  );\n\n  const renderMuscleFilters = useCallback(\n    () => (\n      <div className=\"flex flex-wrap gap-2 mb-6\">\n        {MUSCLES.map((muscle) => (\n          <button\n            className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${\n              selectedMuscle === muscle\n                ? \"bg-blue-500 text-white\"\n                : \"bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600\"\n            }`}\n            key={muscle}\n            onClick={() => handleMuscleSelect(muscle)}\n          >\n            {muscle}\n          </button>\n        ))}\n      </div>\n    ),\n    [selectedMuscle, handleMuscleSelect],\n  );\n\n  const renderContent = useCallback(() => {\n    if (isLoading) {\n      return (\n        <div className=\"space-y-4\">\n          {Array.from({ length: 6 }).map((_, i) => (\n            <div className=\"flex items-center gap-4\" key={i}>\n              <Skeleton className=\"w-16 h-16 rounded-lg\" />\n              <div className=\"flex-1\">\n                <Skeleton className=\"h-4 w-48 mb-2\" />\n                <Skeleton className=\"h-3 w-24\" />\n              </div>\n              <Skeleton className=\"h-8 w-24\" />\n            </div>\n          ))}\n        </div>\n      );\n    }\n\n    if (error) {\n      return (\n        <div className=\"text-center py-8\">\n          <p className=\"text-red-500\">Erreur lors du chargement des exercices</p>\n        </div>\n      );\n    }\n\n    if (!data?.data || data.data.length === 0) {\n      return (\n        <div className=\"text-center py-8\">\n          <p className=\"text-slate-500\">Aucun exercice trouvé</p>\n        </div>\n      );\n    }\n\n    return <div className=\"space-y-4\">{data.data.map(renderExerciseItem)}</div>;\n  }, [isLoading, error, data, renderExerciseItem]);\n\n  return (\n    <>\n      <Dialog onOpenChange={onOpenChange} open={open}>\n        <DialogContent className=\"max-w-2xl max-h-[80vh] p-0\">\n          <DialogHeader className=\"p-6 pb-4\">\n            <DialogTitle className=\"text-xl font-bold\">Sélectionner un exercice</DialogTitle>\n          </DialogHeader>\n\n          <div className=\"px-6 pb-4\">\n            <div className=\"relative mb-6\">\n              <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-400\" />\n              <Input\n                className=\"pl-10\"\n                onChange={(e) => handleSearch(e.target.value)}\n                placeholder=\"Rechercher un exercice...\"\n                value={searchQuery}\n              />\n            </div>\n\n            {renderMuscleFilters()}\n          </div>\n\n          <ScrollArea className=\"max-h-96 px-6 pb-6\">{renderContent()}</ScrollArea>\n        </DialogContent>\n      </Dialog>\n\n      {selectedExercise && <ExerciseVideoModal exercise={selectedExercise} onOpenChange={setShowVideoModal} open={showVideoModal} />}\n    </>\n  );\n};\n"
  },
  {
    "path": "src/features/statistics/components/ExerciseStatisticsTab.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { AlertCircle } from \"lucide-react\";\n\nimport { useI18n, useCurrentLocale } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { formatDate } from \"@/shared/lib/date\";\nimport { StatisticsTimeframe } from \"@/shared/constants/statistics\";\nimport { PremiumGate } from \"@/components/ui/premium-gate\";\nimport { Loader } from \"@/components/ui/loader\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\n\nimport { useWeightProgression, useOneRepMax, useVolumeData } from \"../hooks/use-exercise-statistics\";\nimport { WeightProgressionChart } from \"./WeightProgressionChart\";\nimport { VolumeChart } from \"./VolumeChart\";\nimport { OneRepMaxChart } from \"./OneRepMaxChart\";\n\ninterface ExerciseStatisticsTabProps {\n  exerciseId: string;\n  unit?: \"kg\" | \"lbs\";\n  className?: string;\n  timeframe: StatisticsTimeframe;\n}\n\nexport function ExerciseCharts({ timeframe, exerciseId, unit = \"kg\", className }: ExerciseStatisticsTabProps) {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n\n  // Fetch data for all charts\n  const weightProgressionQuery = useWeightProgression(exerciseId, timeframe);\n  const oneRepMaxQuery = useOneRepMax(exerciseId, timeframe);\n  const volumeQuery = useVolumeData(exerciseId, timeframe);\n\n  const hasError = weightProgressionQuery.isError || oneRepMaxQuery.isError || volumeQuery.isError;\n\n  return (\n    <PremiumGate className={className} feature=\"exercise-statistics\" upgradeMessage={t(\"statistics.premium_required\")}>\n      <div className={cn(\"space-y-6\", className)}>\n        {/* Error State */}\n        {hasError && (\n          <Alert variant=\"error\">\n            <AlertCircle className=\"h-4 w-4\" />\n            <AlertDescription>{t(\"statistics.error_loading_data\")}</AlertDescription>\n          </Alert>\n        )}\n\n        {/* Charts Grid */}\n        <div className=\"grid gap-6\">\n          {/* Weight Progression Chart */}\n          <div className=\"\">\n            {weightProgressionQuery.isLoading ? (\n              <div className=\"flex h-[300px] items-center justify-center rounded-lg bg-white shadow-sm\">\n                <Loader />\n              </div>\n            ) : weightProgressionQuery.isError ? (\n              <Alert variant=\"error\">\n                <AlertDescription>{t(\"statistics.error_loading_weight_progression\")}</AlertDescription>\n              </Alert>\n            ) : (\n              <WeightProgressionChart data={weightProgressionQuery.data?.data || []} height={300} unit={unit} width={800} />\n            )}\n          </div>\n\n          {/* One Rep Max Chart */}\n          <div>\n            {oneRepMaxQuery.isLoading ? (\n              <div className=\"flex h-[300px] items-center justify-center rounded-lg bg-white shadow-sm\">\n                <Loader />\n              </div>\n            ) : oneRepMaxQuery.isError ? (\n              <Alert variant=\"error\">\n                <AlertDescription>{t(\"statistics.error_loading_1rm\")}</AlertDescription>\n              </Alert>\n            ) : (\n              <OneRepMaxChart\n                data={oneRepMaxQuery.data?.data || []}\n                formula={oneRepMaxQuery.data?.formula || \"Lombardi\"}\n                formulaDescription={oneRepMaxQuery.data?.formulaDescription || \"\"}\n                height={300}\n                unit={unit}\n                width={400}\n              />\n            )}\n          </div>\n\n          {/* Volume Chart */}\n          <div>\n            {volumeQuery.isLoading ? (\n              <div className=\"flex h-[300px] items-center justify-center rounded-lg bg-white shadow-sm\">\n                <Loader />\n              </div>\n            ) : volumeQuery.isError ? (\n              <Alert variant=\"error\">\n                <AlertDescription>{t(\"statistics.error_loading_volume\")}</AlertDescription>\n              </Alert>\n            ) : (\n              <VolumeChart data={volumeQuery.data?.data || []} height={300} width={400} />\n            )}\n          </div>\n        </div>\n\n        {/* Footer */}\n        <div className=\"text-center text-sm text-gray-500\">\n          {t(\"statistics.last_updated\", {\n            date: formatDate(new Date(), locale),\n          })}\n        </div>\n      </div>\n    </PremiumGate>\n  );\n}\n"
  },
  {
    "path": "src/features/statistics/components/ExercisesBrowser.tsx",
    "content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport Image from \"next/image\";\nimport { Search, X } from \"lucide-react\";\nimport debounce from \"lodash.debounce\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { ExerciseAttributeNameEnum, ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { useI18n } from \"locales/client\";\nimport { getAttributeValueLabel } from \"@/shared/lib/attribute-value-translation\";\nimport { StatisticsTimeframe } from \"@/shared/constants/statistics\";\nimport { ExerciseVideoModal } from \"@/features/workout-builder/ui/exercise-video-modal\";\nimport { WorkoutBuilderExerciseWithAttributes } from \"@/features/workout-builder/types\";\nimport { EQUIPMENT_CONFIG } from \"@/features/workout-builder/model/equipment-config\";\nimport { WeightProgressionChart } from \"@/features/statistics/components/WeightProgressionChart\";\nimport { VolumeChart } from \"@/features/statistics/components/VolumeChart\";\nimport { TimeframeSelector } from \"@/features/statistics/components/TimeframeSelector\";\nimport { StatisticsPreviewOverlay } from \"@/features/statistics/components/StatisticsPreviewOverlay\";\nimport { OneRepMaxChart } from \"@/features/statistics/components/OneRepMaxChart\";\nimport { ExerciseCharts } from \"@/features/statistics/components/ExerciseStatisticsTab\";\nimport { useUserSubscription } from \"@/features/ads/hooks/useUserSubscription\";\nimport { ExerciseWithAttributes } from \"@/entities/exercise/types/exercise.types\";\nimport { getExerciseAttributesValueOf } from \"@/entities/exercise/shared/muscles\";\nimport { SimpleSelect, SelectOption } from \"@/components/ui/simple-select\";\n\n// API service for fetching exercises\nconst fetchExercises = async (params: { page?: number; limit?: number; search?: string; muscle?: string; equipment?: string }) => {\n  const searchParams = new URLSearchParams();\n  if (params.page) searchParams.append(\"page\", params.page.toString());\n  if (params.limit) searchParams.append(\"limit\", params.limit.toString());\n  if (params.search) searchParams.append(\"search\", params.search);\n  if (params.muscle && params.muscle !== \"ALL\") searchParams.append(\"muscle\", params.muscle);\n  if (params.equipment && params.equipment !== \"ALL\") searchParams.append(\"equipment\", params.equipment);\n\n  const response = await fetch(`/api/exercises/all?${searchParams}`);\n  if (!response.ok) {\n    throw new Error(\"Failed to fetch exercises\");\n  }\n  return response.json();\n};\n\n// Available muscle groups - will be translated dynamically\nconst MUSCLE_GROUPS = [\n  { value: \"CHEST\", labelKey: \"workout_builder.muscles.chest\" },\n  { value: \"BACK\", labelKey: \"workout_builder.muscles.back\" },\n  { value: \"SHOULDERS\", labelKey: \"workout_builder.muscles.shoulders\" },\n  { value: \"BICEPS\", labelKey: \"workout_builder.muscles.biceps\" },\n  { value: \"TRICEPS\", labelKey: \"workout_builder.muscles.triceps\" },\n  { value: \"QUADRICEPS\", labelKey: \"workout_builder.muscles.quadriceps\" },\n  { value: \"HAMSTRINGS\", labelKey: \"workout_builder.muscles.hamstrings\" },\n  { value: \"ABDOMINALS\", labelKey: \"workout_builder.muscles.abdominals\" },\n  { value: \"OBLIQUES\", labelKey: \"workout_builder.muscles.obliques\" },\n  { value: \"GLUTES\", labelKey: \"workout_builder.muscles.glutes\" },\n  { value: \"CALVES\", labelKey: \"workout_builder.muscles.calves\" },\n  { value: \"FOREARMS\", labelKey: \"workout_builder.muscles.forearms\" },\n  { value: \"TRAPS\", labelKey: \"workout_builder.muscles.traps\" },\n  { value: \"ADDUCTORS\", labelKey: \"workout_builder.muscles.adductors\" },\n  { value: \"ABDUCTORS\", labelKey: \"workout_builder.muscles.abductors\" },\n];\n\n// Exercise Selection Modal Component\nconst ExerciseSelectionModal: React.FC<{\n  isOpen: boolean;\n  onClose: () => void;\n  onSelectExercise: (exercise: ExerciseWithAttributes) => void;\n}> = ({ isOpen, onClose, onSelectExercise }) => {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [selectedEquipment, setSelectedEquipment] = useState<string>(\"ALL\");\n  const [selectedMuscle, setSelectedMuscle] = useState<string>(\"ALL\");\n  const t = useI18n();\n\n  // Prepare equipment options\n  const equipmentOptions: SelectOption[] = [\n    { value: \"ALL\", label: t(\"statistics.all_equipment\") },\n    ...EQUIPMENT_CONFIG.map((equipment) => ({\n      value: equipment.value,\n      label: getAttributeValueLabel(equipment.value as ExerciseAttributeValueEnum, t),\n    })),\n  ];\n\n  // Prepare muscle options\n  const muscleOptions: SelectOption[] = [\n    { value: \"ALL\", label: t(\"statistics.all_muscles\") },\n    ...MUSCLE_GROUPS.map((muscle) => ({\n      value: muscle.value,\n      label: getAttributeValueLabel(muscle.value as ExerciseAttributeValueEnum, t),\n    })),\n  ];\n\n  // Debounced search\n  const debouncedSearch = useMemo(\n    () =>\n      debounce((query: string) => {\n        setSearchQuery(query);\n      }, 300),\n    [],\n  );\n\n  const {\n    data: exercisesData,\n    isLoading,\n    error,\n  } = useQuery({\n    queryKey: [\"exercises\", searchQuery, selectedEquipment, selectedMuscle],\n    queryFn: () =>\n      fetchExercises({\n        page: 1,\n        limit: 50,\n        search: searchQuery || undefined,\n        muscle: selectedMuscle,\n        equipment: selectedEquipment,\n      }),\n    enabled: isOpen,\n  });\n\n  const exercises = exercisesData?.data || [];\n\n  const handleSearch = useCallback(\n    (query: string) => {\n      debouncedSearch(query);\n    },\n    [debouncedSearch],\n  );\n\n  const handleExerciseSelect = (exercise: ExerciseWithAttributes) => {\n    onSelectExercise(exercise);\n    onClose();\n  };\n\n  return (\n    <div className={`modal ${isOpen ? \"modal-open\" : \"\"}`}>\n      <div className=\"modal-box mt-32 w-full max-w-2xl h-full max-h-screen flex flex-col p-4 sm:p-6\">\n        {/* Header */}\n        <div className=\"flex items-center justify-between mb-4\">\n          <h2 className=\"text-2xl font-bold\">{t(\"statistics.select_exercise\")}</h2>\n          <button className=\"btn btn-ghost btn-sm\" onClick={onClose}>\n            <X className=\"h-4 w-4\" />\n          </button>\n        </div>\n        {/* Filters */}\n        <div className=\"space-y-4 mb-4\">\n          {/* Equipment Filter */}\n          <SimpleSelect\n            aria-label=\"Filter by equipment\"\n            className=\"w-full\"\n            onValueChange={setSelectedEquipment}\n            options={equipmentOptions}\n            value={selectedEquipment}\n          />\n\n          {/* Muscle Filter */}\n          <SimpleSelect\n            aria-label=\"Filter by muscle group\"\n            className=\"w-full\"\n            onValueChange={setSelectedMuscle}\n            options={muscleOptions}\n            value={selectedMuscle}\n          />\n\n          {/* Search */}\n          <div className=\"relative\">\n            <Search className=\"absolute left-3 top-3 h-4 w-4 text-gray-400\" />\n            <input\n              className=\"input input-bordered w-ful placeholder:text-gray-500 dark:placeholder:text-gray-600 w-full\"\n              onChange={(e) => handleSearch(e.target.value)}\n              placeholder={t(\"statistics.search_exercises\")}\n            />\n          </div>\n        </div>\n\n        {/* Exercise List */}\n        <div className=\"flex-1 overflow-y-auto\">\n          {isLoading && (\n            <div className=\"flex justify-center py-8\">\n              <span className=\"loading loading-spinner loading-lg\"></span>\n            </div>\n          )}\n\n          {error && (\n            <div className=\"text-center py-8\">\n              <p className=\"text-error\">{t(\"statistics.error_loading_exercises\")}</p>\n            </div>\n          )}\n\n          {!isLoading && exercises.length === 0 && (\n            <div className=\"text-center py-8\">\n              <p className=\"text-gray-500\">{t(\"statistics.no_exercises_found\")}</p>\n            </div>\n          )}\n\n          <div className=\"space-y-2\">\n            {exercises.map((exercise: ExerciseWithAttributes) => {\n              const primaryMuscles = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.PRIMARY_MUSCLE);\n              const primaryMuscleLabel = primaryMuscles.map((muscle) => getAttributeValueLabel(muscle, t)).join(\", \");\n\n              return (\n                <div\n                  className=\"flex items-center gap-4 p-3 hover:bg-base-200 rounded-lg cursor-pointer\"\n                  key={exercise.id}\n                  onClick={() => handleExerciseSelect(exercise)}\n                >\n                  <div className=\"w-16 h-16 bg-base-200 rounded-full flex items-center justify-center overflow-hidden border border-gray-400 dark:border-gray-600\">\n                    {exercise.fullVideoImageUrl && (\n                      <Image\n                        alt={exercise.name || \"\"}\n                        className=\"object-cover h-full w-full scale-150\"\n                        height={64}\n                        src={exercise.fullVideoImageUrl}\n                        width={64}\n                      />\n                    )}\n                  </div>\n                  <div className=\"flex-1\">\n                    <h4 className=\"font-semibold\">{exercise.name}</h4>\n                    <p className=\"text-sm text-gray-500\">{primaryMuscleLabel}</p>\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      </div>\n      <div className=\"modal-backdrop\" onClick={onClose}></div>\n    </div>\n  );\n};\n\nexport const ExercisesBrowser = () => {\n  const [selectedExercise, setSelectedExercise] = useState<ExerciseWithAttributes | null>(null);\n  const [showExerciseModal, setShowExerciseModal] = useState(false);\n  const [showVideoModal, setShowVideoModal] = useState(false);\n  const [selectedTimeframe, setSelectedTimeframe] = useState<StatisticsTimeframe>(\"8weeks\");\n  const { isPremium } = useUserSubscription();\n  const t = useI18n();\n\n  const handleExerciseSelect = (exercise: ExerciseWithAttributes) => {\n    setSelectedExercise(exercise);\n  };\n\n  const openExerciseSelection = () => {\n    setShowExerciseModal(true);\n  };\n\n  const openVideoModal = () => {\n    setShowVideoModal(true);\n  };\n\n  const getExerciseEquipment = (exercise: ExerciseWithAttributes) => {\n    const equipments = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.EQUIPMENT);\n    return equipments.map((equipment) => getAttributeValueLabel(equipment, t));\n  };\n\n  const getExercisePrimaryMuscles = (exercise: ExerciseWithAttributes) => {\n    const primaryMuscles = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.PRIMARY_MUSCLE);\n    return primaryMuscles.map((muscle) => getAttributeValueLabel(muscle, t));\n  };\n\n  // Convert exercise to workout builder format\n  const convertToWorkoutBuilderFormat = (exercise: ExerciseWithAttributes): WorkoutBuilderExerciseWithAttributes => {\n    // Convert attributes to the expected format\n    const convertedAttributes = exercise.attributes.map((attr) => ({\n      id: attr.id || \"\",\n      exerciseId: exercise.id,\n      attributeNameId: \"\",\n      attributeValueId: \"\",\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      attributeName: {\n        id: \"\",\n        name: typeof attr.attributeName === \"string\" ? attr.attributeName : attr.attributeName.name,\n        createdAt: new Date(),\n        updatedAt: new Date(),\n      },\n      attributeValue: {\n        id: \"\",\n        value: typeof attr.attributeValue === \"string\" ? attr.attributeValue : attr.attributeValue.value,\n        createdAt: new Date(),\n        updatedAt: new Date(),\n        attributeNameId: \"\",\n      },\n    }));\n\n    return {\n      id: exercise.id,\n      name: exercise.name,\n      nameEn: exercise.nameEn || null,\n      description: exercise.description,\n      descriptionEn: exercise.descriptionEn || null,\n      fullVideoUrl: exercise.fullVideoUrl || null,\n      fullVideoImageUrl: exercise.fullVideoImageUrl || null,\n      introduction: null,\n      introductionEn: null,\n      order: 0,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      attributes: convertedAttributes,\n    };\n  };\n\n  const router = useRouter();\n\n  const handleUpgrade = () => {\n    router.push(\"/premium\");\n    console.log(\"Upgrade clicked\");\n  };\n\n  return (\n    <>\n      {/* Conversion Banner */}\n\n      <div className=\"min-h-screen\">\n        <div className=\"max-w-2xl mx-auto space-y-6\">\n          {/* Header */}\n          {isPremium && (\n            <div>\n              <button className=\"btn btn-primary w-full mb-6\" onClick={openExerciseSelection}>\n                {t(\"statistics.select_exercise\")}\n              </button>\n            </div>\n          )}\n\n          {/* Selected Exercise Info */}\n          {selectedExercise && (\n            <div className=\"bg-base-100 rounded-lg p-6\">\n              <h2 className=\"text-2xl font-bold mb-4\">{selectedExercise.name}</h2>\n\n              <div className=\"flex flex-col gap-2 mb-4\">\n                <div className=\"flex items-center gap-2\">\n                  <span className=\"text-sm text-gray-500\">{t(\"statistics.equipment_label\")}</span>\n                  <span className=\"text-sm\">{getExerciseEquipment(selectedExercise).join(\", \")}</span>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  <span className=\"text-sm text-gray-500\">{t(\"statistics.primary_muscle_label\")}</span>\n                  <span className=\"text-sm\">{getExercisePrimaryMuscles(selectedExercise).join(\", \")}</span>\n                </div>\n              </div>\n\n              {/* Exercise Image */}\n              <div className=\"bg-base-200 rounded-lg p-4 mb-4\">\n                <div className=\"max-h-48 bg-base-200 rounded-lg flex items-center justify-center overflow-hidden aspect-video border border-gray-400 dark:border-gray-700\">\n                  {selectedExercise.fullVideoImageUrl ? (\n                    <Image\n                      alt={selectedExercise.name}\n                      className=\"object-cover cursor-pointer aspect-video scale-115 justify-center place-self-center\"\n                      height={200}\n                      onClick={openVideoModal}\n                      src={selectedExercise.fullVideoImageUrl}\n                      width={300}\n                    />\n                  ) : (\n                    <div className=\"text-center\">\n                      <p className=\"text-gray-500\">{t(\"statistics.no_image_available\")}</p>\n                    </div>\n                  )}\n                </div>\n              </div>\n            </div>\n          )}\n\n          {/* Statistics Section */}\n          <div className=\"space-y-4\">\n            {/* Time period selector */}\n            <div className=\"flex items-center justify-between bg-base-100 rounded-lg p-4\">\n              <span className=\"hidden sm:block font-semibold\">{t(\"statistics.title\")}</span>\n              <TimeframeSelector className=\"bg-white\" onSelect={setSelectedTimeframe} selected={selectedTimeframe} />\n            </div>\n\n            {/* Stats Charts */}\n            <div className=\"space-y-4\">\n              {selectedExercise ? (\n                <ExerciseCharts exerciseId={selectedExercise.id} timeframe={selectedTimeframe} />\n              ) : (\n                <div className=\"relative gap-y-4 flex flex-col\">\n                  <WeightProgressionChart data={[]} height={250} unit=\"kg\" />\n                  <OneRepMaxChart data={[]} formula=\"Lombardi\" formulaDescription=\"Classic 1RM estimation formula\" height={250} unit=\"kg\" />\n                  <VolumeChart data={[]} height={250} />\n                  <StatisticsPreviewOverlay isVisible={!isPremium} onUpgrade={handleUpgrade} />\n                </div>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Modals - Outside the main container */}\n      <ExerciseSelectionModal\n        isOpen={showExerciseModal}\n        onClose={() => setShowExerciseModal(false)}\n        onSelectExercise={handleExerciseSelect}\n      />\n\n      {selectedExercise && (\n        <ExerciseVideoModal\n          exercise={convertToWorkoutBuilderFormat(selectedExercise)}\n          onOpenChange={setShowVideoModal}\n          open={showVideoModal}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "src/features/statistics/components/OneRepMaxChart.tsx",
    "content": "\"use client\";\n\nimport { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from \"recharts\";\nimport React from \"react\";\nimport { Info } from \"lucide-react\";\n\nimport { useI18n, useCurrentLocale } from \"locales/client\";\nimport { OneRepMaxPoint } from \"@/shared/types/statistics.types\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { formatDate } from \"@/shared/lib/date\";\n\nimport { useChartTheme } from \"../hooks/use-chart-theme\";\n\ninterface OneRepMaxChartProps {\n  data: OneRepMaxPoint[];\n  formula: string;\n  formulaDescription: string;\n  width?: number;\n  height?: number;\n  unit?: \"kg\" | \"lbs\";\n  className?: string;\n}\n\nexport function OneRepMaxChart({\n  data,\n  formula,\n  formulaDescription: _formulaDescription,\n  height = 300,\n  unit = \"kg\",\n  className,\n}: OneRepMaxChartProps) {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n  const { colors } = useChartTheme();\n\n  // Format date for display\n  const formatChartDate = (dateString: string) => {\n    return formatDate(dateString, locale, \"MMM D\");\n  };\n\n  // Generate skeleton data for empty state\n  const generateSkeletonData = () => {\n    const now = new Date();\n    const skeletonData = [];\n    for (let i = 11; i >= 0; i--) {\n      const date = new Date(now);\n      date.setDate(date.getDate() - i * 7);\n      skeletonData.push({\n        date: date.toISOString(),\n        estimatedOneRepMax: 30 + Math.random() * 40, // Random 1RM between 30-70\n        formattedDate: formatChartDate(date.toISOString()),\n      });\n    }\n    return skeletonData;\n  };\n\n  // Use real data or skeleton data\n  const hasData = data.length > 0;\n  const chartData = hasData\n    ? data.map((point) => ({\n        ...point,\n        formattedDate: formatChartDate(point.date),\n      }))\n    : generateSkeletonData();\n\n  // Custom tooltip\n  const CustomTooltip = ({ active, payload, label }: any) => {\n    if (active && payload && payload.length && hasData) {\n      return (\n        <div\n          className=\"p-3 rounded-lg shadow-lg border\"\n          style={{\n            backgroundColor: colors.tooltipBackground,\n            borderColor: colors.tooltipBorder,\n            color: colors.text,\n          }}\n        >\n          <p className=\"font-medium\">{label}</p>\n          <p className=\"text-purple-600\">\n            1RM: {payload[0].value.toFixed(1)} {unit}\n          </p>\n        </div>\n      );\n    }\n    return null;\n  };\n\n  return (\n    <div\n      aria-label={t(\"statistics.one_rep_max_chart\")}\n      className={cn(\"rounded-lg p-4 shadow-sm relative border border-gray-400 dark:border-gray-600\", className)}\n      role=\"img\"\n      style={{ backgroundColor: colors.cardBackground }}\n    >\n      <div className=\"mb-4 flex items-center justify-between\">\n        <h3 className=\"text-lg font-semibold\" style={{ color: colors.text }}>\n          {t(\"statistics.estimated_1rm\")}\n        </h3>\n        <div className=\"tooltip tooltip-left z-50\" data-tip={`${formula} Formula: 1RM = Weight × (1 + (Reps ÷ 30))`}>\n          <button aria-label={t(\"statistics.1rm_formula_info\")} className=\"rounded-full p-1 hover:bg-gray-100 transition-colors\">\n            <Info className=\"h-4 w-4\" style={{ color: colors.textSecondary }} />\n          </button>\n        </div>\n      </div>\n\n      <div style={{ opacity: hasData ? 1 : 0.2 }}>\n        <ResponsiveContainer height={height} width=\"100%\">\n          <LineChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>\n            <CartesianGrid stroke={colors.grid} strokeDasharray=\"3 3\" />\n            <XAxis\n              axisLine={{ stroke: colors.border }}\n              dataKey=\"formattedDate\"\n              tick={{ fontSize: 12, fill: colors.textMuted }}\n              tickLine={{ stroke: colors.border }}\n            />\n            <YAxis\n              axisLine={{ stroke: colors.border }}\n              label={{\n                value: `1RM (${unit})`,\n                angle: -90,\n                position: \"insideLeft\",\n                style: { textAnchor: \"middle\", fill: colors.text },\n              }}\n              tick={{ fontSize: 12, fill: colors.textMuted }}\n              tickLine={{ stroke: colors.border }}\n            />\n            <Tooltip content={<CustomTooltip />} />\n            <Line\n              activeDot={{ r: 6, fill: \"#8B5CF6\" }}\n              dataKey=\"estimatedOneRepMax\"\n              dot={{ fill: \"#8B5CF6\", strokeWidth: 2, r: 4 }}\n              stroke=\"#8B5CF6\"\n              strokeWidth={2}\n              type=\"monotone\"\n            />\n          </LineChart>\n        </ResponsiveContainer>\n      </div>\n\n      {!hasData && (\n        <div className=\"absolute inset-0 flex items-center justify-center\">\n          <div className=\"text-center\">\n            <p className=\"text-lg font-semibold\" style={{ color: colors.text }}>\n              {t(\"statistics.no_1rm_data\")}\n            </p>\n            <p className=\"mt-2 text-sm\" style={{ color: colors.textSecondary }}>\n              {t(\"statistics.complete_sets_with_weight\")}\n            </p>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/statistics/components/StatisticsPreviewOverlay.tsx",
    "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { Lock, Eye, TrendingUp, Zap, Star, Crown, ArrowRight, RotateCcw } from \"lucide-react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\n\nimport { useI18n, useCurrentLocale } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\n\ninterface StatisticsPreviewOverlayProps {\n  className?: string;\n  onUpgrade?: () => void;\n  isVisible?: boolean;\n}\n\n// Composant pour simuler les données qui bougent\nconst AnimatedChart: React.FC = () => {\n  const [data, setData] = useState([\n    { x: 0, y: 30 },\n    { x: 1, y: 45 },\n    { x: 2, y: 35 },\n    { x: 3, y: 60 },\n    { x: 4, y: 50 },\n    { x: 5, y: 75 },\n    { x: 6, y: 85 },\n  ]);\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setData((prev) =>\n        prev.map((point) => ({\n          ...point,\n          y: Math.max(20, Math.min(90, point.y + (Math.random() - 0.5) * 10)),\n        })),\n      );\n    }, 2000);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  const pathData = data.map((point, index) => `${index === 0 ? \"M\" : \"L\"} ${point.x * 40} ${100 - point.y}`).join(\" \");\n\n  return (\n    <div className=\"relative w-full h-32 bg-gradient-to-br from-purple-500/10 to-blue-500/10 rounded-lg overflow-hidden\">\n      <svg className=\"w-full h-full\" viewBox=\"0 0 240 100\">\n        {/* Grid */}\n        <defs>\n          <pattern height=\"20\" id=\"grid\" patternUnits=\"userSpaceOnUse\" width=\"40\">\n            <path d=\"M 40 0 L 0 0 0 20\" fill=\"none\" stroke=\"rgba(139,92,246,0.1)\" strokeWidth=\"1\" />\n          </pattern>\n        </defs>\n        <rect fill=\"url(#grid)\" height=\"100%\" width=\"100%\" />\n\n        {/* Animated line */}\n        <motion.path\n          animate={{ pathLength: 1 }}\n          d={pathData}\n          fill=\"none\"\n          initial={{ pathLength: 0 }}\n          stroke=\"url(#gradient)\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"3\"\n          transition={{ duration: 1.5, ease: \"easeInOut\" }}\n        />\n\n        {/* Gradient definition */}\n        <defs>\n          <linearGradient id=\"gradient\" x1=\"0%\" x2=\"100%\" y1=\"0%\" y2=\"0%\">\n            <stop offset=\"0%\" stopColor=\"#8B5CF6\" />\n            <stop offset=\"100%\" stopColor=\"#3B82F6\" />\n          </linearGradient>\n        </defs>\n\n        {/* Animated dots */}\n        {data.map((point, index) => (\n          <motion.circle\n            animate={{ scale: 1 }}\n            cx={point.x * 40}\n            cy={100 - point.y}\n            fill=\"#8B5CF6\"\n            initial={{ scale: 0 }}\n            key={index}\n            r=\"4\"\n            transition={{ delay: index * 0.1 }}\n          />\n        ))}\n      </svg>\n\n      {/* Overlay stats */}\n      <div className=\"absolute top-2 right-2 bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded px-2 py-1 text-xs\">\n        <div className=\"flex items-center gap-1 text-green-600 dark:text-green-400\">\n          <TrendingUp className=\"w-3 h-3\" />\n          <span className=\"font-semibold\">+12.5%</span>\n        </div>\n      </div>\n    </div>\n  );\n};\n\n// Composant pour les métriques qui s'animent\nconst AnimatedMetrics: React.FC = () => {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n\n  const [metrics, setMetrics] = useState({\n    totalVolume: 2450,\n    prIncrease: 15,\n    weightProgression: 78,\n  });\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setMetrics((prev) => ({\n        totalVolume: prev.totalVolume + Math.floor(Math.random() * 50) - 25,\n        prIncrease: Math.max(0, prev.prIncrease + Math.floor(Math.random() * 6) - 3),\n        weightProgression: Math.max(0, Math.min(100, prev.weightProgression + Math.floor(Math.random() * 10) - 5)),\n      }));\n    }, 3000);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  return (\n    <div className=\"grid grid-cols-3 gap-4\">\n      <motion.div className=\"bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm rounded-lg p-3 text-center\" whileHover={{ scale: 1.05 }}>\n        <p className=\"text-2xl font-bold text-purple-600 dark:text-purple-400\">{metrics.totalVolume.toLocaleString(locale)}</p>\n        <p className=\"text-xs text-black dark:text-gray-400\">{t(\"statistics.total_volume\")}</p>\n      </motion.div>\n\n      <motion.div className=\"bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm rounded-lg p-3 text-center\" whileHover={{ scale: 1.05 }}>\n        <p className=\"text-2xl font-bold text-green-600 dark:text-green-400\">+{metrics.prIncrease}%</p>\n        <p className=\"text-xs text-black dark:text-gray-400\">{t(\"statistics.pr_increase\")}</p>\n      </motion.div>\n\n      <motion.div className=\"bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm rounded-lg p-3 text-center\" whileHover={{ scale: 1.05 }}>\n        <p className=\"text-2xl font-bold text-blue-600 dark:text-blue-400\">{metrics.weightProgression}%</p>\n        <p className=\"text-xs text-black dark:text-gray-400\">{t(\"statistics.weight_progress\")}</p>\n      </motion.div>\n    </div>\n  );\n};\n\nexport const StatisticsPreviewOverlay: React.FC<StatisticsPreviewOverlayProps> = ({ className, onUpgrade, isVisible = true }) => {\n  const t = useI18n();\n  const [isPlaying] = useState(true);\n  const [showTeaserModal, setShowTeaserModal] = useState(false);\n\n  if (!isVisible) return null;\n\n  return (\n    <motion.div\n      animate={{ opacity: 1 }}\n      className={cn(\n        \"absolute inset-0 z-50 bg-gradient-to-br from-purple-900/90 via-blue-900/90 to-purple-900/90 backdrop-blur-sm rounded-lg\",\n        \"flex flex-col items-center justify-center p-8\",\n        className,\n      )}\n      exit={{ opacity: 0 }}\n      initial={{ opacity: 0 }}\n    >\n      {/* Icône principale avec animation */}\n      <motion.div\n        animate={{\n          rotate: isPlaying ? 360 : 0,\n          scale: isPlaying ? 1.1 : 1,\n        }}\n        className=\"mb-6\"\n        transition={{\n          rotate: { duration: 2, repeat: isPlaying ? Infinity : 0, ease: \"linear\" },\n          scale: { duration: 0.3 },\n        }}\n      >\n        <div className=\"relative\">\n          <div className=\"bg-gradient-to-r from-purple-500 to-blue-500 p-6 rounded-full shadow-2xl\">\n            <Lock className=\"w-12 h-12 text-white\" />\n          </div>\n          <motion.div\n            animate={{ opacity: [0, 1, 0] }}\n            className=\"absolute -top-1 -right-1 bg-orange-500 p-2 rounded-full\"\n            transition={{ duration: 2, repeat: Infinity }}\n          >\n            <Crown className=\"w-4 h-4 text-white\" />\n          </motion.div>\n        </div>\n      </motion.div>\n\n      {/* Titre principal */}\n      <motion.h3\n        animate={{ y: 0, opacity: 1 }}\n        className=\"text-3xl font-bold text-white mb-2 text-center\"\n        initial={{ y: 20, opacity: 0 }}\n        transition={{ delay: 0.2 }}\n      >\n        {t(\"statistics.premium_statistics\")}\n      </motion.h3>\n\n      <motion.p\n        animate={{ y: 0, opacity: 1 }}\n        className=\"text-purple-200 text-center mb-6 max-w-md\"\n        initial={{ y: 20, opacity: 0 }}\n        transition={{ delay: 0.3 }}\n      >\n        {t(\"statistics.premium_statistics_description\")}\n      </motion.p>\n\n      {/* Aperçu des données animées */}\n      <motion.div\n        animate={{ scale: 1, opacity: 1 }}\n        className=\"w-full max-w-md mb-6\"\n        initial={{ scale: 0.9, opacity: 0 }}\n        transition={{ delay: 0.4 }}\n      >\n        <AnimatedChart />\n      </motion.div>\n\n      {/* Métriques animées */}\n      <motion.div\n        animate={{ y: 0, opacity: 1 }}\n        className=\"w-full max-w-md mb-8\"\n        initial={{ y: 20, opacity: 0 }}\n        transition={{ delay: 0.5 }}\n      >\n        <AnimatedMetrics />\n      </motion.div>\n\n      {/* Boutons d'action */}\n      <div className=\"flex items-center gap-4\">\n        <motion.button\n          className=\"group relative overflow-hidden bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white font-bold py-3 px-6 rounded-xl shadow-lg transition-all duration-300\"\n          onClick={onUpgrade}\n          whileHover={{ scale: 1.05 }}\n          whileTap={{ scale: 0.95 }}\n        >\n          <div className=\"absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300\" />\n          <div className=\"relative flex items-center gap-2\">\n            <Zap className=\"w-5 h-5\" />\n            <span>{t(\"statistics.upgrade_now\")}</span>\n            <ArrowRight className=\"w-5 h-5 transform group-hover:translate-x-1 transition-transform\" />\n          </div>\n        </motion.button>\n      </div>\n\n      {/* Éléments de confiance */}\n      <motion.div\n        animate={{ y: 0, opacity: 1 }}\n        className=\"mt-6 flex items-center gap-4 text-sm text-purple-200\"\n        initial={{ y: 20, opacity: 0 }}\n        transition={{ delay: 0.6 }}\n      >\n        <div className=\"flex items-center gap-1\">\n          <Star className=\"w-4 h-4 text-yellow-400\" />\n          <span>{t(\"statistics.rating\")}</span>\n        </div>\n        <div className=\"flex items-center gap-1\">\n          <Eye className=\"w-4 h-4\" />\n          <span>{t(\"statistics.no_ads\")}</span>\n        </div>\n        <div className=\"flex items-center gap-1\">\n          <RotateCcw className=\"w-4 h-4\" />\n          <span>{t(\"statistics.cancel_anytime\")}</span>\n        </div>\n      </motion.div>\n\n      {/* Modal de teaser */}\n      <AnimatePresence>\n        {showTeaserModal && (\n          <motion.div\n            animate={{ opacity: 1, scale: 1 }}\n            className=\"absolute inset-0 bg-black/50 flex items-center justify-center\"\n            exit={{ opacity: 0, scale: 0.8 }}\n            initial={{ opacity: 0, scale: 0.8 }}\n          >\n            <div className=\"bg-white dark:bg-gray-800 p-6 rounded-xl shadow-2xl max-w-sm mx-4\">\n              <h4 className=\"text-xl font-bold text-center mb-4\">{t(\"statistics.preview_notice\")}</h4>\n              <p className=\"text-gray-600 dark:text-gray-300 text-center mb-6\">{t(\"statistics.preview_description\")}</p>\n              <button\n                className=\"w-full bg-gradient-to-r from-purple-600 to-blue-600 text-white font-bold py-3 rounded-lg hover:from-purple-700 hover:to-blue-700 transition-all duration-300\"\n                onClick={() => {\n                  setShowTeaserModal(false);\n                  onUpgrade?.();\n                }}\n              >\n                {t(\"statistics.get_premium_access\")}\n              </button>\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </motion.div>\n  );\n};\n"
  },
  {
    "path": "src/features/statistics/components/TimeframeSelector.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { StatisticsTimeframe } from \"@/shared/constants/statistics\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\n\ninterface TimeframeSelectorProps {\n  selected: StatisticsTimeframe;\n  onSelect: (timeframe: StatisticsTimeframe) => void;\n  className?: string;\n}\n\nconst TIMEFRAME_OPTIONS = [\n  { value: \"4weeks\" as StatisticsTimeframe, labelKey: \"statistics.timeframes.4weeks\" },\n  { value: \"8weeks\" as StatisticsTimeframe, labelKey: \"statistics.timeframes.8weeks\" },\n  { value: \"12weeks\" as StatisticsTimeframe, labelKey: \"statistics.timeframes.12weeks\" },\n  { value: \"1year\" as StatisticsTimeframe, labelKey: \"statistics.timeframes.1year\" },\n];\n\nexport function TimeframeSelector({ selected, onSelect, className }: TimeframeSelectorProps) {\n  const t = useI18n();\n\n  return (\n    <Select onValueChange={(value) => onSelect(value as StatisticsTimeframe)} value={selected}>\n      <SelectTrigger className={cn(\"w-32\", className)}>\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {TIMEFRAME_OPTIONS.map((option) => (\n          <SelectItem key={option.value} value={option.value}>\n            {t(option.labelKey as keyof typeof t)}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n"
  },
  {
    "path": "src/features/statistics/components/VolumeChart.tsx",
    "content": "\"use client\";\n\nimport { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from \"recharts\";\nimport React from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { VolumePoint } from \"@/shared/types/statistics.types\";\nimport { cn } from \"@/shared/lib/utils\";\n\nimport { useChartTheme } from \"../hooks/use-chart-theme\";\n\ninterface VolumeChartProps {\n  data: VolumePoint[];\n  width?: number;\n  height?: number;\n  className?: string;\n}\n\nexport function VolumeChart({ data, height = 300, className }: VolumeChartProps) {\n  const t = useI18n();\n  const { colors } = useChartTheme();\n\n  // Format week label\n  const formatWeek = (week: string) => {\n    // Format: \"2024-W12\" -> \"W12\"\n    const parts = week.split(\"-W\");\n    return parts.length > 1 ? `W${parts[1]}` : week;\n  };\n\n  // Format volume for display\n  const formatVolume = (volume: number) => {\n    if (volume >= 1000000) {\n      return `${(volume / 1000000).toFixed(1)}M`;\n    } else if (volume >= 1000) {\n      return `${(volume / 1000).toFixed(1)}K`;\n    }\n    return volume.toString();\n  };\n\n  // Generate skeleton data for empty state\n  const generateSkeletonData = () => {\n    const now = new Date();\n    const skeletonData = [];\n    for (let i = 7; i >= 0; i--) {\n      const date = new Date(now);\n      date.setDate(date.getDate() - i * 7);\n      const year = date.getFullYear();\n      const weekNumber = Math.ceil((date.getTime() - new Date(year, 0, 1).getTime()) / (7 * 24 * 60 * 60 * 1000));\n      const week = `${year}-W${weekNumber.toString().padStart(2, \"0\")}`;\n      const volume = Math.floor(Math.random() * 5000) + 1000; // Random volume between 1000-6000\n\n      skeletonData.push({\n        week,\n        totalVolume: volume,\n        setCount: Math.floor(volume / 100), // Rough estimate of sets\n        formattedWeek: formatWeek(week),\n        formattedVolume: formatVolume(volume),\n      });\n    }\n    return skeletonData;\n  };\n\n  // Use real data or skeleton data\n  const hasData = data.length > 0;\n  const chartData = hasData\n    ? data.map((point) => ({\n        ...point,\n        formattedWeek: formatWeek(point.week),\n        formattedVolume: formatVolume(point.totalVolume),\n      }))\n    : generateSkeletonData();\n\n  // Custom tooltip\n  const CustomTooltip = ({ active, payload, label }: any) => {\n    if (active && payload && payload.length && hasData) {\n      const data = payload[0].payload;\n      return (\n        <div\n          className=\"p-3 rounded-lg shadow-lg border\"\n          style={{\n            backgroundColor: colors.tooltipBackground,\n            borderColor: colors.tooltipBorder,\n            color: colors.text,\n          }}\n        >\n          <p className=\"font-medium\">{label}</p>\n          <p className=\"text-green-600\">\n            {t(\"statistics.volume\")}: {data.formattedVolume}\n          </p>\n          <p className=\"text-sm\" style={{ color: colors.textSecondary }}>\n            {data.setCount} sets\n          </p>\n        </div>\n      );\n    }\n    return null;\n  };\n\n  return (\n    <div\n      aria-label={t(\"statistics.volume_chart\")}\n      className={cn(\"rounded-lg p-4 shadow-sm relative border border-gray-400 dark:border-gray-600\", className)}\n      role=\"img\"\n      style={{ backgroundColor: colors.cardBackground }}\n    >\n      <h3 className=\"mb-4 text-lg font-semibold\" style={{ color: colors.text }}>\n        {t(\"statistics.weekly_volume\")}\n      </h3>\n\n      <div style={{ opacity: hasData ? 1 : 0.2 }}>\n        <ResponsiveContainer height={height} width=\"100%\">\n          <BarChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>\n            <CartesianGrid stroke={colors.grid} strokeDasharray=\"3 3\" />\n            <XAxis\n              axisLine={{ stroke: colors.border }}\n              dataKey=\"formattedWeek\"\n              tick={{ fontSize: 12, fill: colors.textMuted }}\n              tickLine={{ stroke: colors.border }}\n            />\n            <YAxis\n              axisLine={{ stroke: colors.border }}\n              label={{\n                value: t(\"statistics.volume\"),\n                angle: -90,\n                position: \"insideLeft\",\n                style: { textAnchor: \"middle\", fill: colors.text },\n              }}\n              tick={{ fontSize: 12, fill: colors.textMuted }}\n              tickFormatter={formatVolume}\n              tickLine={{ stroke: colors.border }}\n            />\n            <Tooltip content={<CustomTooltip />} />\n            <Bar dataKey=\"totalVolume\" fill=\"#10B981\" radius={[4, 4, 0, 0]} />\n          </BarChart>\n        </ResponsiveContainer>\n      </div>\n\n      <div className=\"mt-4 text-center\">\n        <p className=\"text-xs\" style={{ color: colors.textSecondary }}>\n          {t(\"statistics.volume_calculation\")}\n        </p>\n      </div>\n\n      {!hasData && (\n        <div className=\"absolute inset-0 flex items-center justify-center\">\n          <div className=\"text-center\">\n            <p className=\"text-lg font-semibold\" style={{ color: colors.text }}>\n              {t(\"statistics.no_volume_data\")}\n            </p>\n            <p className=\"mt-2 text-sm\" style={{ color: colors.textSecondary }}>\n              {t(\"statistics.complete_workouts\")}\n            </p>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/statistics/components/WeightProgressionChart.tsx",
    "content": "\"use client\";\n\nimport { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from \"recharts\";\nimport React from \"react\";\n\nimport { useI18n, useCurrentLocale } from \"locales/client\";\nimport { WeightProgressionPoint } from \"@/shared/types/statistics.types\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { formatDate } from \"@/shared/lib/date\";\n\nimport { useChartTheme } from \"../hooks/use-chart-theme\";\n\ninterface WeightProgressionChartProps {\n  data: WeightProgressionPoint[];\n  width?: number;\n  height?: number;\n  unit?: \"kg\" | \"lbs\";\n  className?: string;\n}\n\nexport function WeightProgressionChart({ data, height = 300, unit = \"kg\", className }: WeightProgressionChartProps) {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n  const { colors } = useChartTheme();\n\n  // Format date for display\n  const formatChartDate = (dateString: string) => {\n    return formatDate(dateString, locale, \"MMM D\");\n  };\n\n  // Generate skeleton data for empty state\n  const generateSkeletonData = () => {\n    const now = new Date();\n    const skeletonData = [];\n    for (let i = 11; i >= 0; i--) {\n      const date = new Date(now);\n      date.setDate(date.getDate() - i * 7);\n      skeletonData.push({\n        date: date.toISOString(),\n        weight: 30 + Math.random() * 40, // Random weight between 30-70\n        formattedDate: formatChartDate(date.toISOString()),\n      });\n    }\n    return skeletonData;\n  };\n\n  // Use real data or skeleton data\n  const hasData = data.length > 0;\n  const chartData = hasData\n    ? data.map((point) => ({\n        ...point,\n        formattedDate: formatChartDate(point.date),\n      }))\n    : generateSkeletonData();\n\n  // Custom tooltip\n  const CustomTooltip = ({ active, payload, label }: any) => {\n    if (active && payload && payload.length && hasData) {\n      return (\n        <div\n          className=\"p-3 rounded-lg shadow-lg border\"\n          style={{\n            backgroundColor: colors.tooltipBackground,\n            borderColor: colors.tooltipBorder,\n            color: colors.text,\n          }}\n        >\n          <p className=\"font-medium\">{label}</p>\n          <p className=\"text-blue-600\">\n            {t(\"statistics.weight\")}: {payload[0].value} {unit}\n          </p>\n        </div>\n      );\n    }\n    return null;\n  };\n\n  return (\n    <div\n      aria-label={t(\"statistics.weight_progression_chart\")}\n      className={cn(\"rounded-lg p-4 shadow-sm relative border border-gray-400 dark:border-gray-600\", className)}\n      role=\"img\"\n      style={{ backgroundColor: colors.cardBackground }}\n    >\n      <h3 className=\"mb-4 text-lg font-semibold\" style={{ color: colors.text }}>\n        {t(\"statistics.weight_progression\")}\n      </h3>\n\n      <div style={{ opacity: hasData ? 1 : 0.2 }}>\n        <ResponsiveContainer height={height} width=\"100%\">\n          <LineChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>\n            <CartesianGrid stroke={colors.grid} strokeDasharray=\"3 3\" />\n            <XAxis\n              axisLine={{ stroke: colors.border }}\n              dataKey=\"formattedDate\"\n              tick={{ fontSize: 12, fill: colors.textMuted }}\n              tickLine={{ stroke: colors.border }}\n            />\n            <YAxis\n              axisLine={{ stroke: colors.border }}\n              label={{\n                value: `${t(\"statistics.weight\")} (${unit})`,\n                angle: -90,\n                position: \"insideLeft\",\n                style: { textAnchor: \"middle\", fill: colors.text },\n              }}\n              tick={{ fontSize: 12, fill: colors.textMuted }}\n              tickLine={{ stroke: colors.border }}\n            />\n            <Tooltip content={<CustomTooltip />} />\n            <Line\n              activeDot={{ r: 6, fill: \"#3B82F6\" }}\n              dataKey=\"weight\"\n              dot={{ fill: \"#3B82F6\", strokeWidth: 2, r: 4 }}\n              stroke=\"#3B82F6\"\n              strokeWidth={2}\n              type=\"monotone\"\n            />\n          </LineChart>\n        </ResponsiveContainer>\n      </div>\n\n      {!hasData && (\n        <div className=\"absolute inset-0 flex items-center justify-center\">\n          <div className=\"text-center\">\n            <p className=\"text-lg font-semibold\" style={{ color: colors.text }}>\n              {t(\"statistics.no_data_yet\")}\n            </p>\n            <p className=\"mt-2 text-sm\" style={{ color: colors.textSecondary }}>\n              {t(\"statistics.start_tracking\")}\n            </p>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/statistics/components/index.ts",
    "content": "export { ExerciseCharts } from \"./ExerciseStatisticsTab\";\nexport { WeightProgressionChart } from \"./WeightProgressionChart\";\nexport { OneRepMaxChart } from \"./OneRepMaxChart\";\nexport { VolumeChart } from \"./VolumeChart\";\nexport { TimeframeSelector } from \"./TimeframeSelector\";\nexport { ExerciseSelection } from \"./ExerciseSelection\";\n"
  },
  {
    "path": "src/features/statistics/hooks/use-chart-theme.ts",
    "content": "import { useTheme } from \"next-themes\";\n\nexport function useChartTheme() {\n  const { theme, systemTheme } = useTheme();\n  const currentTheme = theme === \"system\" ? systemTheme : theme;\n  const isDark = currentTheme === \"dark\";\n\n  return {\n    isDark,\n    colors: {\n      background: isDark ? \"#1f2937\" : \"#ffffff\",\n      cardBackground: isDark ? \"#374151\" : \"#ffffff\",\n      text: isDark ? \"#f9fafb\" : \"#374151\",\n      textSecondary: isDark ? \"#d1d5db\" : \"#6b7280\",\n      textMuted: isDark ? \"#9ca3af\" : \"#6b7280\",\n      border: isDark ? \"#4b5563\" : \"#e5e7eb\",\n      grid: isDark ? \"#4b5563\" : \"#e5e7eb\",\n      tooltipBackground: isDark ? \"#374151\" : \"#ffffff\",\n      tooltipBorder: isDark ? \"#4b5563\" : \"#e5e7eb\",\n    },\n  };\n}\n"
  },
  {
    "path": "src/features/statistics/hooks/use-exercise-statistics.ts",
    "content": "\"use client\";\n\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { StatisticsTimeframe } from \"@/shared/constants/statistics\";\n\n// Query keys\nexport const STATISTICS_QUERY_KEYS = {\n  all: [\"exercise-statistics\"] as const,\n  byExercise: (exerciseId: string) => [...STATISTICS_QUERY_KEYS.all, exerciseId] as const,\n  weightProgression: (exerciseId: string, timeframe: StatisticsTimeframe) =>\n    [...STATISTICS_QUERY_KEYS.byExercise(exerciseId), \"weight-progression\", timeframe] as const,\n  oneRepMax: (exerciseId: string, timeframe: StatisticsTimeframe) =>\n    [...STATISTICS_QUERY_KEYS.byExercise(exerciseId), \"one-rep-max\", timeframe] as const,\n  volume: (exerciseId: string, timeframe: StatisticsTimeframe) =>\n    [...STATISTICS_QUERY_KEYS.byExercise(exerciseId), \"volume\", timeframe] as const,\n};\n\n// Fetch functions\nasync function fetchWeightProgression(exerciseId: string, timeframe: StatisticsTimeframe) {\n  const response = await fetch(`/api/exercises/${exerciseId}/statistics/weight-progression?timeframe=${timeframe}`, {\n    credentials: \"include\",\n  });\n\n  if (!response.ok) {\n    const error = await response.json();\n    throw new Error(error.message || \"Failed to fetch weight progression\");\n  }\n\n  return response.json();\n}\n\nasync function fetchOneRepMax(exerciseId: string, timeframe: StatisticsTimeframe) {\n  const response = await fetch(`/api/exercises/${exerciseId}/statistics/one-rep-max?timeframe=${timeframe}`, { credentials: \"include\" });\n\n  if (!response.ok) {\n    const error = await response.json();\n    throw new Error(error.message || \"Failed to fetch one-rep max data\");\n  }\n\n  return response.json();\n}\n\nasync function fetchVolume(exerciseId: string, timeframe: StatisticsTimeframe) {\n  const response = await fetch(`/api/exercises/${exerciseId}/statistics/volume?timeframe=${timeframe}`, { credentials: \"include\" });\n\n  if (!response.ok) {\n    const error = await response.json();\n    throw new Error(error.message || \"Failed to fetch volume data\");\n  }\n\n  return response.json();\n}\n\n// Hook for weight progression data\nexport function useWeightProgression(exerciseId: string, timeframe: StatisticsTimeframe = \"8weeks\", enabled = true) {\n  return useQuery({\n    queryKey: STATISTICS_QUERY_KEYS.weightProgression(exerciseId, timeframe),\n    queryFn: () => fetchWeightProgression(exerciseId, timeframe),\n    enabled: enabled && !!exerciseId,\n    staleTime: 60 * 60 * 1000, // 1 hour\n    gcTime: 2 * 60 * 60 * 1000, // 2 hours\n  });\n}\n\n// Hook for one-rep max data\nexport function useOneRepMax(exerciseId: string, timeframe: StatisticsTimeframe = \"8weeks\", enabled = true) {\n  return useQuery({\n    queryKey: STATISTICS_QUERY_KEYS.oneRepMax(exerciseId, timeframe),\n    queryFn: () => fetchOneRepMax(exerciseId, timeframe),\n    enabled: enabled && !!exerciseId,\n    staleTime: 60 * 60 * 1000, // 1 hour\n    gcTime: 2 * 60 * 60 * 1000, // 2 hours\n  });\n}\n\n// Hook for volume data\nexport function useVolumeData(exerciseId: string, timeframe: StatisticsTimeframe = \"8weeks\", enabled = true) {\n  return useQuery({\n    queryKey: STATISTICS_QUERY_KEYS.volume(exerciseId, timeframe),\n    queryFn: () => fetchVolume(exerciseId, timeframe),\n    enabled: enabled && !!exerciseId,\n    staleTime: 60 * 60 * 1000, // 1 hour\n    gcTime: 2 * 60 * 60 * 1000, // 2 hours\n  });\n}\n"
  },
  {
    "path": "src/features/statistics/types/index.ts",
    "content": "// Re-export types from shared\nexport type {\n  WeightProgressionPoint,\n  WeightProgressionResponse,\n  OneRepMaxPoint,\n  OneRepMaxResponse,\n  VolumePoint,\n  VolumeResponse,\n  ExerciseStatisticsResponse,\n  StatisticsErrorResponse,\n  StatisticsRequestParams,\n} from \"@/shared/types/statistics.types\";\n\n// Client-side specific types\nexport interface ChartDimensions {\n  width: number;\n  height: number;\n  padding: {\n    top: number;\n    right: number;\n    bottom: number;\n    left: number;\n  };\n}\n"
  },
  {
    "path": "src/features/theme/ThemeProviders.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider, ThemeProviderProps } from \"next-themes\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "src/features/theme/ThemeToggle.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { MoonIcon, SunIcon } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\n\nexport function ThemeToggle() {\n  const { setTheme, resolvedTheme } = useTheme();\n\n  return (\n    <div className=\"tooltip tooltip-bottom\" data-tip={resolvedTheme === \"light\" ? \"Dark mode\" : \"Light mode\"}>\n      <Button\n        className=\"hover:bg-slate-200 rounded-full p-1 pr-1 sm:p-2 sm:pr-2\"\n        onClick={() => setTheme(resolvedTheme === \"light\" ? \"dark\" : \"light\")}\n        variant=\"ghost\"\n      >\n        {resolvedTheme === \"light\" ? (\n          <MoonIcon className=\"text-blue-500 dark:text-blue-400 h-6 w-6\" />\n        ) : (\n          <SunIcon className=\"text-blue-500 dark:text-blue-400 h-6 w-6\" />\n        )}\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/theme/ui/ThemeSynchronizer.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useTheme } from \"next-themes\";\n\n/**\n * Synchronizes the <meta name=\"theme-color\"> tag with the current theme (light/dark).\n * Ensures the browser UI (mobile address bar, etc.) matches the user's selected theme.\n */\nexport function ThemeSynchronizer() {\n  const { resolvedTheme } = useTheme();\n\n  useEffect(() => {\n    const themeColor = resolvedTheme === \"dark\" ? \"#18181b\" : \"#f3f4f6\";\n    let meta = document.querySelector(\"meta[name=theme-color]\");\n    if (!meta) {\n      meta = document.createElement(\"meta\");\n      meta.setAttribute(\"name\", \"theme-color\");\n      document.head.appendChild(meta);\n    }\n    meta.setAttribute(\"content\", themeColor);\n  }, [resolvedTheme]);\n\n  return null;\n}\n"
  },
  {
    "path": "src/features/update-password/lib/hash.ts",
    "content": "import crypto from \"crypto\";\nexport const hashStringWithSalt = (string: string, salt: string) => {\n  const hash = crypto.createHash(\"sha256\");\n\n  const saltedString = salt + string;\n\n  hash.update(saltedString);\n\n  const hashedString = hash.digest(\"hex\");\n\n  return hashedString;\n};\n"
  },
  {
    "path": "src/features/update-password/lib/validate-password.ts",
    "content": "import { PASSWORD_REGEX } from \"@/shared/constants/regexs\";\n\nexport const validatePassword = (password: string) => {\n  return PASSWORD_REGEX.test(password);\n};\n"
  },
  {
    "path": "src/features/update-password/model/update-password.action.ts",
    "content": "\"use server\";\n\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { ERROR_MESSAGES } from \"@/shared/constants/errors\";\nimport { ActionError } from \"@/shared/api/safe-actions\";\nimport { mobileAuthenticatedActionClient } from \"@/shared/api/mobile-safe-actions\";\nimport { UpdatePasswordSchema } from \"@/features/update-password/model/update-password.schema\";\nimport { validatePassword } from \"@/features/update-password/lib/validate-password\";\nimport { hashStringWithSalt } from \"@/features/update-password/lib/hash\";\nimport { env } from \"@/env\";\n\n/**\n * Core password update logic that can be used by both the action and API route\n */\nexport async function updateUserPassword(\n  userId: string,\n  currentPassword: string,\n  newPassword: string,\n  confirmPassword: string\n) {\n  if (newPassword !== confirmPassword) {\n    throw new ActionError(ERROR_MESSAGES.PASSWORDS_DO_NOT_MATCH);\n  }\n\n  const { password, id } = await prisma.account.findFirstOrThrow({\n    where: { userId },\n    select: { password: true, id: true },\n  });\n\n  const hashedCurrentPassword = hashStringWithSalt(currentPassword, env.BETTER_AUTH_SECRET);\n\n  if (hashedCurrentPassword !== password) {\n    throw new ActionError(ERROR_MESSAGES.INVALID_CURRENT_PASSWORD);\n  }\n\n  if (!validatePassword(newPassword)) {\n    throw new ActionError(ERROR_MESSAGES.INVALID_NEW_PASSWORD);\n  }\n\n  const updatedUser = await prisma.user.update({\n    where: {\n      id: userId,\n    },\n    data: {\n      accounts: {\n        update: {\n          where: {\n            id,\n            userId,\n          },\n          data: {\n            password: hashStringWithSalt(newPassword, env.BETTER_AUTH_SECRET),\n          },\n        },\n      },\n    },\n  });\n\n  return updatedUser;\n}\n\nexport const updatePasswordAction = mobileAuthenticatedActionClient\n  .schema(UpdatePasswordSchema)\n  .action(async ({ parsedInput, ctx }) => {\n      const { confirmPassword, currentPassword, newPassword } = parsedInput;\n      const { user } = ctx as { user: any };\n\n      return updateUserPassword(user.id, currentPassword, newPassword, confirmPassword);\n    });\n"
  },
  {
    "path": "src/features/update-password/model/update-password.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const UpdatePasswordSchema = z.object({\n  currentPassword: z.string().min(8),\n  newPassword: z.string().min(8),\n  confirmPassword: z.string().min(8),\n});\n"
  },
  {
    "path": "src/features/update-password/ui/password-form.tsx",
    "content": "\"use client\";\nimport { z } from \"zod\";\nimport { useForm } from \"react-hook-form\";\nimport { LockKeyhole, LockKeyholeOpen } from \"lucide-react\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\nimport { useI18n } from \"locales/client\";\nimport { Input } from \"@/workoutcool/components/ui/input\";\nimport { Button } from \"@/workoutcool/components/ui/button\";\nimport { updatePasswordAction } from \"@/features/update-password/model/update-password.action\";\nimport { brandedToast } from \"@/components/ui/toast\";\nimport { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from \"@/components/ui/form\";\n\nconst passwordFormSchema = z\n  .object({\n    currentPassword: z.string().min(1, \"Current password is required\"),\n    newPassword: z.string().min(8, \"Password must be at least 8 characters\"),\n    confirmPassword: z.string().min(1, \"Please confirm your password\"),\n  })\n  .refine((data) => data.newPassword === data.confirmPassword, {\n    message: \"Passwords don't match\",\n    path: [\"confirmPassword\"],\n  });\n\ntype PasswordFormValues = z.infer<typeof passwordFormSchema>;\n\nexport function PasswordForm() {\n  const t = useI18n();\n\n  const form = useForm<PasswordFormValues>({\n    resolver: zodResolver(passwordFormSchema),\n    defaultValues: {\n      currentPassword: \"\",\n      newPassword: \"\",\n      confirmPassword: \"\",\n    },\n  });\n\n  const handleSubmit = async (values: PasswordFormValues) => {\n    try {\n      const result = await updatePasswordAction(values);\n\n      if (result?.serverError) {\n        brandedToast({ title: t(result?.serverError as keyof typeof t), variant: \"error\" });\n        return;\n      }\n\n      brandedToast({ title: t(\"success.password_updated_successfully\"), variant: \"success\" });\n      form.reset();\n    } catch (error) {\n      brandedToast({ title: t(\"error.generic_error\"), variant: \"error\" });\n      console.error(error);\n    }\n  };\n\n  return (\n    <Form form={form} onSubmit={handleSubmit}>\n      <div className=\"space-y-5 p-4\">\n        <FormField\n          control={form.control}\n          name=\"currentPassword\"\n          render={({ field }) => (\n            <FormItem className=\"space-y-2.5\">\n              <FormLabel className=\"font-semibold leading-tight\">{t(\"current_password\")}</FormLabel>\n              <div className=\"relative\">\n                <FormControl>\n                  <Input {...field} className=\"ltr:pl-9 rtl:pr-9\" placeholder={t(\"current_password_placeholder\")} type=\"password\" />\n                </FormControl>\n                <LockKeyhole className=\"absolute top-3 size-4 ltr:left-3 rtl:right-3\" />\n              </div>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n\n        <FormField\n          control={form.control}\n          name=\"newPassword\"\n          render={({ field }) => (\n            <FormItem className=\"space-y-2.5\">\n              <FormLabel className=\"font-semibold leading-tight\">{t(\"new_password\")}</FormLabel>\n              <div className=\"relative\">\n                <FormControl>\n                  <Input {...field} className=\"ltr:pl-9 rtl:pr-9\" placeholder={t(\"new_password_placeholder\")} type=\"password\" />\n                </FormControl>\n                <LockKeyholeOpen className=\"absolute top-3 size-4 ltr:left-3 rtl:right-3\" />\n              </div>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n\n        <FormField\n          control={form.control}\n          name=\"confirmPassword\"\n          render={({ field }) => (\n            <FormItem className=\"space-y-2.5\">\n              <FormLabel className=\"font-semibold leading-tight\">{t(\"confirm_password\")}</FormLabel>\n              <div className=\"relative\">\n                <FormControl>\n                  <Input {...field} className=\"ltr:pl-9 rtl:pr-9\" placeholder={t(\"confirm_password_placeholder\")} type=\"password\" />\n                </FormControl>\n                <LockKeyholeOpen className=\"absolute top-3 size-4 ltr:left-3 rtl:right-3\" />\n              </div>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n\n        <div className=\"flex items-center justify-end gap-4\">\n          <Button className=\"text-danger\" onClick={() => form.reset()} size=\"large\" type=\"button\" variant=\"outline-general\">\n            Cancel\n          </Button>\n          <Button disabled={form.formState.isSubmitting} size=\"large\" type=\"submit\" variant=\"black\">\n            {form.formState.isSubmitting ? \"Updating...\" : \"Update password\"}\n          </Button>\n        </div>\n      </div>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "src/features/user/ui/UserDropdown.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { LayoutDashboard, LogOut, User2 } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { useLogout } from \"@/features/auth/model/useLogout\";\nimport { Loader } from \"@/components/ui/loader\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\nimport type { PropsWithChildren } from \"react\";\n\nexport const UserDropdown = ({ children }: PropsWithChildren) => {\n  const logout = useLogout();\n\n  const t = useI18n();\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>\n      <DropdownMenuContent className=\"w-56\">\n        <DropdownMenuLabel>{t(\"commons.profile\")}</DropdownMenuLabel>\n        <DropdownMenuItem asChild>\n          <Link href=\"/account\">\n            <User2 className=\"mr-2 size-4\" />\n            {t(\"commons.my_account\")}\n          </Link>\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem asChild>\n          <Link href=\"/dashboard\">\n            <LayoutDashboard className=\"mr-2 size-4\" />\n            {t(\"commons.dashboard\")}\n          </Link>\n        </DropdownMenuItem>\n\n        <DropdownMenuSeparator />\n\n        <DropdownMenuGroup>\n          <DropdownMenuItem\n            onClick={(e) => {\n              e.stopPropagation();\n              e.preventDefault();\n              logout.mutate();\n            }}\n          >\n            {logout.isPending ? <Loader className=\"mr-2 size-4\" /> : <LogOut className=\"mr-2 size-4\" />}\n            <span>{t(\"commons.logout\")}</span>\n          </DropdownMenuItem>\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/actions/get-exercises-by-muscle.action.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\nimport { ExerciseAttributeNameEnum, ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\n\n\nconst getExercisesByMuscleSchema = z.object({\n  equipment: z.array(z.nativeEnum(ExerciseAttributeValueEnum)),\n});\n\nexport const getExercisesByMuscleAction = actionClient\n  .schema(getExercisesByMuscleSchema)\n  .action(async ({ parsedInput }) => {\n    const { equipment } = parsedInput;\n\n    try {\n      const [primaryMuscleAttributeName, equipmentAttributeName] = await Promise.all([\n        prisma.exerciseAttributeName.findUnique({\n          where: { name: ExerciseAttributeNameEnum.PRIMARY_MUSCLE },\n        }),\n        prisma.exerciseAttributeName.findUnique({\n          where: { name: ExerciseAttributeNameEnum.EQUIPMENT },\n        }),\n      ]);\n\n      if (!primaryMuscleAttributeName || !equipmentAttributeName) {\n        throw new Error(\"Missing attributes in database\");\n      }\n\n      const muscleGroups = [\n        ExerciseAttributeValueEnum.CHEST,\n        ExerciseAttributeValueEnum.BACK,\n        ExerciseAttributeValueEnum.SHOULDERS,\n        ExerciseAttributeValueEnum.BICEPS,\n        ExerciseAttributeValueEnum.TRICEPS,\n        ExerciseAttributeValueEnum.QUADRICEPS,\n        ExerciseAttributeValueEnum.HAMSTRINGS,\n        ExerciseAttributeValueEnum.GLUTES,\n        ExerciseAttributeValueEnum.CALVES,\n        ExerciseAttributeValueEnum.ABDOMINALS,\n        ExerciseAttributeValueEnum.FOREARMS,\n        ExerciseAttributeValueEnum.TRAPS,\n        ExerciseAttributeValueEnum.LATS,\n      ];\n\n      const exercisesByMuscle = await Promise.all(\n        muscleGroups.map(async (muscle) => {\n          const exercises = await prisma.exercise.findMany({\n            where: {\n              AND: [\n                {\n                  attributes: {\n                    some: {\n                      attributeNameId: primaryMuscleAttributeName.id,\n                      attributeValue: {\n                        value: muscle,\n                      },\n                    },\n                  },\n                },\n                {\n                  attributes: {\n                    some: {\n                      attributeNameId: equipmentAttributeName.id,\n                      attributeValue: {\n                        value: {\n                          in: equipment,\n                        },\n                      },\n                    },\n                  },\n                },\n                // Exclude stretching exercises\n                {\n                  NOT: {\n                    attributes: {\n                      some: {\n                        attributeValue: {\n                          value: \"STRETCHING\",\n                        },\n                      },\n                    },\n                  },\n                },\n              ],\n            },\n            include: {\n              attributes: {\n                include: {\n                  attributeName: true,\n                  attributeValue: true,\n                },\n              },\n            },\n            orderBy: {\n              nameEn: \"asc\",\n            },\n          });\n\n          return {\n            muscle,\n            exercises,\n          };\n        })\n      );\n\n      // Filter out muscle groups with no exercises\n      return exercisesByMuscle.filter(group => group.exercises.length > 0);\n    } catch (error) {\n      console.error(\"Error fetching exercises by muscle:\", error);\n      throw new Error(\"Error fetching exercises by muscle\");\n    }\n  });"
  },
  {
    "path": "src/features/workout-builder/actions/get-exercises.action.ts",
    "content": "\"use server\";\n\nimport { ExerciseAttributeNameEnum } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\n\nimport { getExercisesSchema } from \"../schema/get-exercises.schema\";\n\n// Utility function to shuffle an array (Fisher-Yates shuffle)\nfunction shuffleArray<T>(array: T[]): T[] {\n  const shuffled = [...array];\n  for (let i = shuffled.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1));\n    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];\n  }\n  return shuffled;\n}\n\nexport const getExercisesAction = actionClient.schema(getExercisesSchema).action(async ({ parsedInput }) => {\n  const { equipment, muscles, limit } = parsedInput;\n\n  try {\n    // First, get the attribute name IDs once\n    const [primaryMuscleAttributeName, secondaryMuscleAttributeName, equipmentAttributeName] = await Promise.all([\n      prisma.exerciseAttributeName.findUnique({\n        where: { name: ExerciseAttributeNameEnum.PRIMARY_MUSCLE },\n      }),\n      prisma.exerciseAttributeName.findUnique({\n        where: { name: ExerciseAttributeNameEnum.SECONDARY_MUSCLE },\n      }),\n      prisma.exerciseAttributeName.findUnique({\n        where: { name: ExerciseAttributeNameEnum.EQUIPMENT },\n      }),\n    ]);\n\n    if (!primaryMuscleAttributeName || !secondaryMuscleAttributeName || !equipmentAttributeName) {\n      throw new Error(\"Missing attributes in database\");\n    }\n\n    // Get exercises for each selected muscle using Hybrid Algorithm\n    const exercisesByMuscle = await Promise.all(\n      muscles.map(async (muscle) => {\n        const MINIMUM_THRESHOLD = 20;\n        const TARGET_POOL_SIZE = Math.max(limit * 4, 30); // Larger pool for better randomization\n\n        // Step 1: Get exercises where muscle is PRIMARY\n        const primaryExercises = await prisma.exercise.findMany({\n          where: {\n            AND: [\n              {\n                attributes: {\n                  some: {\n                    attributeNameId: primaryMuscleAttributeName.id,\n                    attributeValue: {\n                      value: muscle,\n                    },\n                  },\n                },\n              },\n              {\n                attributes: {\n                  some: {\n                    attributeNameId: equipmentAttributeName.id,\n                    attributeValue: {\n                      value: {\n                        in: equipment,\n                      },\n                    },\n                  },\n                },\n              },\n              // Exclude stretching exercises\n              {\n                NOT: {\n                  attributes: {\n                    some: {\n                      attributeValue: {\n                        value: \"STRETCHING\",\n                      },\n                    },\n                  },\n                },\n              },\n            ],\n          },\n          include: {\n            attributes: {\n              include: {\n                attributeName: true,\n                attributeValue: true,\n              },\n            },\n          },\n          take: TARGET_POOL_SIZE,\n        });\n\n        let allExercises = [...primaryExercises];\n\n        // Step 2: If we don't have enough exercises, add SECONDARY muscle exercises\n        if (allExercises.length < MINIMUM_THRESHOLD) {\n          const secondaryExercises = await prisma.exercise.findMany({\n            where: {\n              AND: [\n                {\n                  attributes: {\n                    some: {\n                      attributeNameId: secondaryMuscleAttributeName.id,\n                      attributeValue: {\n                        value: muscle,\n                      },\n                    },\n                  },\n                },\n                {\n                  attributes: {\n                    some: {\n                      attributeNameId: equipmentAttributeName.id,\n                      attributeValue: {\n                        value: {\n                          in: equipment,\n                        },\n                      },\n                    },\n                  },\n                },\n                // Exclude exercises already found as primary\n                {\n                  id: {\n                    notIn: primaryExercises.map((ex) => ex.id),\n                  },\n                },\n                // Exclude stretching exercises\n                {\n                  NOT: {\n                    attributes: {\n                      some: {\n                        attributeValue: {\n                          value: \"STRETCHING\",\n                        },\n                      },\n                    },\n                  },\n                },\n              ],\n            },\n            include: {\n              attributes: {\n                include: {\n                  attributeName: true,\n                  attributeValue: true,\n                },\n              },\n            },\n            take: TARGET_POOL_SIZE - primaryExercises.length,\n          });\n\n          allExercises = [...allExercises, ...secondaryExercises];\n        }\n\n        // Step 3: Weighted randomization (favor primary muscle exercises)\n        const shuffledPrimary = shuffleArray(primaryExercises);\n        const shuffledSecondary = shuffleArray(allExercises.filter((ex) => !primaryExercises.some((primary) => primary.id === ex.id)));\n\n        // Step 4: Create final selection with weighted distribution\n        const selectedExercises = [];\n        const primaryRatio = 0.7; // 70% primary muscles when possible\n        const targetPrimary = Math.ceil(limit * primaryRatio);\n        const targetSecondary = limit - targetPrimary;\n\n        // Add primary muscle exercises first\n        selectedExercises.push(...shuffledPrimary.slice(0, Math.min(targetPrimary, shuffledPrimary.length)));\n\n        // Fill remaining slots with secondary or more primary exercises\n        const remainingSlots = limit - selectedExercises.length;\n        if (remainingSlots > 0) {\n          if (shuffledSecondary.length > 0) {\n            selectedExercises.push(...shuffledSecondary.slice(0, Math.min(targetSecondary, shuffledSecondary.length)));\n          }\n\n          // If still need more exercises, add more primary ones\n          const stillNeedMore = limit - selectedExercises.length;\n          if (stillNeedMore > 0 && shuffledPrimary.length > targetPrimary) {\n            selectedExercises.push(...shuffledPrimary.slice(targetPrimary, targetPrimary + stillNeedMore));\n          }\n        }\n\n        // Final shuffle to avoid predictable patterns\n        const finalExercises = shuffleArray(selectedExercises).slice(0, limit);\n\n        return {\n          muscle,\n          exercises: finalExercises,\n        };\n      }),\n    );\n\n    // Filter muscles that have no exercises\n    const filteredResults = exercisesByMuscle.filter((group) => group.exercises.length > 0);\n\n    return filteredResults;\n  } catch (error) {\n    console.error(\"Error fetching exercises:\", error);\n    throw new Error(\"Error fetching exercises\");\n  }\n});\n"
  },
  {
    "path": "src/features/workout-builder/actions/get-favorite-exercises.action.ts",
    "content": "\"use server\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { authenticatedActionClient } from \"@/shared/api/safe-actions\";\n\nexport const getFavoriteExercises = authenticatedActionClient.action(async ({ ctx }) => {\n  const { user } = ctx;\n\n  try {\n    const favorites = await prisma.userFavoriteExercise.findMany({\n      where: {\n        userId: user.id,\n      },\n      select: {\n        exerciseId: true,\n        updatedAt: true,\n      },\n      orderBy: {\n        updatedAt: \"desc\",\n      },\n    });\n\n    return {\n      success: true,\n      favorites: favorites.map((f) => ({\n        exerciseId: f.exerciseId,\n        updatedAt: f.updatedAt.toISOString(),\n      })),\n    };\n  } catch (error) {\n    console.error(\"Error getting favorite exercises:\", error);\n    throw new Error(`Failed to get favorites: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n  }\n});\n"
  },
  {
    "path": "src/features/workout-builder/actions/pick-exercise.action.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\n\nimport { actionClient } from \"@/shared/api/safe-actions\";\n\nconst pickExerciseSchema = z.object({\n  exerciseId: z.string(),\n});\n\nexport const pickExerciseAction = actionClient.schema(pickExerciseSchema).action(async ({ parsedInput }) => {\n  try {\n    const { exerciseId } = parsedInput;\n\n    // Pour l'instant, on retourne juste l'ID de l'exercice\n    // Plus tard, on pourra ajouter de la logique pour marquer l'exercice comme \"picked\"\n    // dans une base de données ou un système de préférences utilisateur\n\n    return {\n      success: true,\n      exerciseId,\n      message: \"Exercise picked successfully\",\n    };\n  } catch (error) {\n    console.error(\"Error picking exercise:\", error);\n    return { serverError: \"Failed to pick exercise\" };\n  }\n});\n"
  },
  {
    "path": "src/features/workout-builder/actions/shuffle-exercise.action.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\nimport { ExerciseAttributeNameEnum, ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\n\nconst shuffleExerciseSchema = z.object({\n  muscle: z.nativeEnum(ExerciseAttributeValueEnum),\n  equipment: z.array(z.nativeEnum(ExerciseAttributeValueEnum)),\n  excludeExerciseIds: z.array(z.string()),\n});\n\nexport const shuffleExerciseAction = actionClient.schema(shuffleExerciseSchema).action(async ({ parsedInput }) => {\n  const { muscle, equipment, excludeExerciseIds } = parsedInput;\n\n  try {\n    const [primaryMuscleAttributeName, secondaryMuscleAttributeName, equipmentAttributeName] = await Promise.all([\n      prisma.exerciseAttributeName.findUnique({\n        where: { name: ExerciseAttributeNameEnum.PRIMARY_MUSCLE },\n      }),\n      prisma.exerciseAttributeName.findUnique({\n        where: { name: ExerciseAttributeNameEnum.SECONDARY_MUSCLE },\n      }),\n      prisma.exerciseAttributeName.findUnique({\n        where: { name: ExerciseAttributeNameEnum.EQUIPMENT },\n      }),\n    ]);\n\n    if (!primaryMuscleAttributeName || !secondaryMuscleAttributeName || !equipmentAttributeName) {\n      throw new Error(\"Missing attributes in database\");\n    }\n\n    const primaryExercises = await prisma.exercise.findMany({\n      where: {\n        AND: [\n          {\n            id: {\n              notIn: excludeExerciseIds,\n            },\n          },\n          {\n            attributes: {\n              some: {\n                attributeNameId: primaryMuscleAttributeName.id,\n                attributeValue: {\n                  value: muscle,\n                },\n              },\n            },\n          },\n          {\n            attributes: {\n              some: {\n                attributeNameId: equipmentAttributeName.id,\n                attributeValue: {\n                  value: {\n                    in: equipment,\n                  },\n                },\n              },\n            },\n          },\n          {\n            NOT: {\n              attributes: {\n                some: {\n                  attributeValue: {\n                    value: ExerciseAttributeValueEnum.STRETCHING,\n                  },\n                },\n              },\n            },\n          },\n        ],\n      },\n      include: {\n        attributes: {\n          include: {\n            attributeName: true,\n            attributeValue: true,\n          },\n        },\n      },\n      take: 50,\n    });\n\n    let allExercises = [...primaryExercises];\n\n    if (allExercises.length < 3) {\n      const secondaryExercises = await prisma.exercise.findMany({\n        where: {\n          AND: [\n            {\n              id: {\n                notIn: [...excludeExerciseIds, ...primaryExercises.map((ex) => ex.id)],\n              },\n            },\n            {\n              attributes: {\n                some: {\n                  attributeNameId: secondaryMuscleAttributeName.id,\n                  attributeValue: {\n                    value: muscle,\n                  },\n                },\n              },\n            },\n            {\n              attributes: {\n                some: {\n                  attributeNameId: equipmentAttributeName.id,\n                  attributeValue: {\n                    value: {\n                      in: equipment,\n                    },\n                  },\n                },\n              },\n            },\n            {\n              NOT: {\n                attributes: {\n                  some: {\n                    attributeValue: {\n                      value: ExerciseAttributeValueEnum.STRETCHING,\n                    },\n                  },\n                },\n              },\n            },\n          ],\n        },\n        include: {\n          attributes: {\n            include: {\n              attributeName: true,\n              attributeValue: true,\n            },\n          },\n        },\n        take: 50 - primaryExercises.length,\n      });\n\n      allExercises = [...allExercises, ...secondaryExercises];\n    }\n\n    if (allExercises.length === 0) {\n      return { serverError: \"No alternative exercises found\" };\n    }\n\n    const randomIndex = Math.floor(Math.random() * allExercises.length);\n    const selectedExercise = allExercises[randomIndex];\n\n    return { exercise: selectedExercise };\n  } catch (error) {\n    console.error(\"Error shuffling exercise:\", error);\n    return { serverError: \"Failed to shuffle exercise\" };\n  }\n});\n"
  },
  {
    "path": "src/features/workout-builder/actions/sync-favorite-exercises.action.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { authenticatedActionClient } from \"@/shared/api/safe-actions\";\n\nconst syncFavoriteExercisesSchema = z.object({\n  exerciseIds: z.array(z.string()),\n});\n\nexport const syncFavoriteExercisesAction = authenticatedActionClient\n  .schema(syncFavoriteExercisesSchema)\n  .action(async ({ parsedInput, ctx }) => {\n    const { user } = ctx;\n    const { exerciseIds } = parsedInput;\n\n    try {\n      await prisma.$transaction(async (tx) => {\n        // Delete all current favorites\n        await tx.userFavoriteExercise.deleteMany({\n          where: { userId: user.id },\n        });\n\n        // Create new favorites\n        if (exerciseIds.length > 0) {\n          await tx.userFavoriteExercise.createMany({\n            data: exerciseIds.map((exerciseId) => ({\n              userId: user.id,\n              exerciseId,\n            })),\n          });\n        }\n      });\n\n      return { success: true };\n    } catch (error) {\n      console.error(\"Error syncing favorite exercises:\", error);\n      throw new Error(`Failed to sync favorites: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n    }\n  });\n"
  },
  {
    "path": "src/features/workout-builder/hooks/use-exercises.ts",
    "content": "\"use client\";\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { getExercisesAction } from \"../actions/get-exercises.action\";\n\ninterface UseExercisesProps {\n  equipment: ExerciseAttributeValueEnum[];\n  muscles: ExerciseAttributeValueEnum[];\n  enabled?: boolean;\n}\n\nexport function useExercises({ equipment, muscles, enabled = true }: UseExercisesProps) {\n  return useQuery({\n    queryKey: [\"exercises\", equipment.sort(), muscles.sort()],\n    queryFn: async () => {\n      if (equipment.length === 0 || muscles.length === 0) {\n        return [];\n      }\n\n      const result = await getExercisesAction({\n        equipment,\n        muscles,\n        limit: 3,\n      });\n\n      if (result?.serverError) {\n        throw new Error(result.serverError);\n      }\n\n      return result?.data || [];\n    },\n    enabled: enabled && equipment.length > 0 && muscles.length > 0,\n    staleTime: 5 * 60 * 1000, // 5 minutes\n  });\n}\n"
  },
  {
    "path": "src/features/workout-builder/hooks/use-favorite-exercises.service.ts",
    "content": "import { useSession } from \"@/features/auth/lib/auth-client\";\n\nimport { favoriteExercisesLocal } from \"../model/favorite-exercises.local\";\nimport { syncFavoriteExercisesAction } from \"../actions/sync-favorite-exercises.action\";\nimport { getFavoriteExercises } from \"../actions/get-favorite-exercises.action\";\n\nexport const useFavoriteExercisesService = () => {\n  const { data: session } = useSession();\n  const userId = session?.user?.id;\n\n  // Optimized: Only read local storage, no API calls for immediate operations\n  const getAll = (): string[] => {\n    return favoriteExercisesLocal\n      .getAll()\n      .filter((f) => f.status !== \"deleteOnSync\")\n      .map((f) => f.exerciseId);\n  };\n\n  const isFavorite = (exerciseId: string): boolean => {\n    const favorites = getAll();\n    return favorites.includes(exerciseId);\n  };\n\n  const add = (exerciseId: string): void => {\n    favoriteExercisesLocal.add(exerciseId);\n\n    // Background sync - don't await to avoid blocking\n    if (userId) {\n      syncInBackground();\n    }\n  };\n\n  const remove = (exerciseId: string): void => {\n    favoriteExercisesLocal.removeById(exerciseId);\n\n    // Background sync - don't await to avoid blocking\n    if (userId) {\n      syncInBackground();\n    }\n  };\n\n  const toggle = (exerciseId: string): void => {\n    const isCurrentlyFavorite = isFavorite(exerciseId);\n\n    if (isCurrentlyFavorite) {\n      remove(exerciseId);\n    } else {\n      add(exerciseId);\n    }\n  };\n\n  // Background sync function that doesn't block UI\n  const syncInBackground = async (): Promise<void> => {\n    try {\n      const favorites = getAll();\n      const result = await syncFavoriteExercisesAction({ exerciseIds: favorites });\n      if (result?.serverError) {\n        console.error(\"Background sync failed:\", result.serverError);\n      }\n    } catch (error) {\n      console.error(\"Background sync error:\", error);\n    }\n  };\n\n  const fetchServerFavorites = async (): Promise<string[]> => {\n    if (!userId) return [];\n\n    try {\n      const result = await getFavoriteExercises();\n      if (result?.serverError) throw new Error(result.serverError);\n      return (result?.data?.favorites || []).map((f) => f.exerciseId);\n    } catch (error) {\n      console.error(\"Failed to fetch server favorites:\", error);\n      return [];\n    }\n  };\n\n  return {\n    getAll,\n    isFavorite,\n    add,\n    remove,\n    toggle,\n    fetchServerFavorites,\n    syncInBackground,\n  };\n};\n"
  },
  {
    "path": "src/features/workout-builder/hooks/use-favorites-modal.ts",
    "content": "import { useState, useEffect, useMemo, useCallback } from \"react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { useSession } from \"@/features/auth/lib/auth-client\";\n\nimport { useFavoriteExercisesService } from \"./use-favorite-exercises.service\";\n\ninterface ExerciseWithAttributes {\n  id: string;\n  name: string;\n  nameEn: string;\n  fullVideoImageUrl: string | null;\n  attributes: Array<{\n    attributeName: { name: string };\n    attributeValue: { value: string };\n  }>;\n}\n\ninterface MuscleGroup {\n  muscle: ExerciseAttributeValueEnum;\n  exercises: ExerciseWithAttributes[];\n}\n\ninterface UseFavoritesModalProps {\n  isOpen: boolean;\n  muscleGroups?: MuscleGroup[];\n}\n\nexport const useFavoritesModal = ({ isOpen, muscleGroups }: UseFavoritesModalProps) => {\n  const { data: session } = useSession();\n  const favoriteService = useFavoriteExercisesService();\n  const [favoriteExercises, setFavoriteExercises] = useState<Set<string>>(new Set());\n  const [isInitialized, setIsInitialized] = useState(false);\n\n  // Initialize favorites when modal opens\n  useEffect(() => {\n    if (isOpen && !isInitialized) {\n      const favorites = favoriteService.getAll();\n      setFavoriteExercises(new Set(favorites));\n      setIsInitialized(true);\n\n      // Fetch server favorites in background if user is logged in\n      if (session?.user) {\n        favoriteService.fetchServerFavorites().then((serverFavorites) => {\n          const localFavorites = favoriteService.getAll();\n          const allFavorites = [...new Set([...localFavorites, ...serverFavorites])];\n          setFavoriteExercises(new Set(allFavorites));\n        });\n      }\n    } else if (!isOpen && isInitialized) {\n      // Reset when modal closes\n      setIsInitialized(false);\n    }\n  }, [isOpen, isInitialized, favoriteService, session?.user]);\n\n  // Handle favorite toggle\n  const handleToggleFavorite = useCallback(\n    (exerciseId: string) => {\n      favoriteService.toggle(exerciseId);\n\n      // Update local state optimistically\n      setFavoriteExercises((prev) => {\n        const newSet = new Set(prev);\n        if (newSet.has(exerciseId)) {\n          newSet.delete(exerciseId);\n        } else {\n          newSet.add(exerciseId);\n        }\n        return newSet;\n      });\n    },\n    [favoriteService],\n  );\n\n  // Get favorite exercises with their muscle groups\n  const favoriteExercisesList = useMemo(() => {\n    if (!muscleGroups) return [];\n\n    const favorites: Array<ExerciseWithAttributes & { muscle: ExerciseAttributeValueEnum }> = [];\n    const seenExercises = new Set<string>();\n\n    muscleGroups.forEach((group) => {\n      group.exercises.forEach((exercise) => {\n        if (favoriteExercises.has(exercise.id) && !seenExercises.has(exercise.id)) {\n          favorites.push({ ...exercise, muscle: group.muscle });\n          seenExercises.add(exercise.id);\n        }\n      });\n    });\n\n    return favorites;\n  }, [muscleGroups, favoriteExercises]);\n\n  // Check if exercise is favorite\n  const isFavorite = useCallback(\n    (exerciseId: string) => {\n      return favoriteExercises.has(exerciseId);\n    },\n    [favoriteExercises],\n  );\n\n  return {\n    favoriteExercises: favoriteExercisesList,\n    isFavorite,\n    handleToggleFavorite,\n  };\n};\n"
  },
  {
    "path": "src/features/workout-builder/hooks/use-sync-favorite-exercises.ts",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { favoriteExercisesLocal } from \"@/features/workout-builder/model/favorite-exercises.local\";\nimport { syncFavoriteExercisesAction } from \"@/features/workout-builder/actions/sync-favorite-exercises.action\";\nimport { getFavoriteExercises } from \"@/features/workout-builder/actions/get-favorite-exercises.action\";\nimport { useSession } from \"@/features/auth/lib/auth-client\";\n\ninterface SyncState {\n  isSyncing: boolean;\n  error: Error | null;\n}\n\nexport function useSyncFavoriteExercises() {\n  const { data: session, isPending: isSessionLoading } = useSession();\n\n  const [syncState, setSyncState] = useState<SyncState>({\n    isSyncing: false,\n    error: null,\n  });\n\n  useEffect(() => {\n    if (!isSessionLoading && session?.user) {\n      syncFavoriteExercises();\n    }\n  }, [session, isSessionLoading]);\n\n  const syncFavoriteExercises = async () => {\n    if (!session?.user || syncState.isSyncing) return;\n    setSyncState((prev) => ({ ...prev, isSyncing: true, error: null }));\n\n    try {\n      const res = await getFavoriteExercises();\n      if (res?.serverError) {\n        throw new Error(res?.serverError);\n      }\n      const serverFavorites = res?.data?.favorites ?? [];\n      const localFavorites = favoriteExercisesLocal.getAll();\n      let updatedLocalFavorites = localFavorites.map((f) => f.exerciseId);\n\n      const unsyncedDeletes = localFavorites.filter((l) => l.status === \"deleteOnSync\");\n      for (const unsyncedDelete of unsyncedDeletes) {\n        const serverMatch = serverFavorites.find((s) => s.exerciseId === unsyncedDelete.exerciseId);\n        if (!serverMatch || unsyncedDelete.updatedAt > serverMatch.updatedAt) {\n          updatedLocalFavorites = updatedLocalFavorites.filter((favorite) => favorite !== unsyncedDelete.exerciseId);\n        }\n      }\n\n      // Has a synced status but not persisted anymore\n      const syncedStatus = localFavorites.filter(({ status }) => status === \"synced\");\n      for (const l of syncedStatus) {\n        const serverMatch = serverFavorites.find((s) => s.exerciseId === l.exerciseId);\n        if (!serverMatch) {\n          updatedLocalFavorites = updatedLocalFavorites.filter((favorite) => favorite !== l.exerciseId);\n        }\n      }\n\n      // Add server favorites that don't exist locally\n      for (const s of serverFavorites) {\n        const localMatch = localFavorites.find((l) => l.exerciseId === s.exerciseId);\n        if (!localMatch) {\n          updatedLocalFavorites = [...updatedLocalFavorites, s.exerciseId];\n        }\n      }\n\n      // Remove duplicates\n      updatedLocalFavorites = [...new Set(updatedLocalFavorites)];\n\n      favoriteExercisesLocal.saveAll(\n        updatedLocalFavorites.map((id) => {\n          return { exerciseId: id, updatedAt: new Date().toISOString(), status: \"synced\" };\n        }),\n      );\n\n      const syncResult = await syncFavoriteExercisesAction({ exerciseIds: updatedLocalFavorites });\n      if (syncResult?.serverError) {\n        throw new Error(syncResult?.serverError);\n      }\n    } catch (error) {\n      console.error(\"Failed to sync favorites:\", error);\n      setSyncState((prev) => ({ ...prev, error: error as Error }));\n    } finally {\n      setSyncState((prev) => ({ ...prev, isSyncing: false }));\n    }\n  };\n\n  return {\n    syncState,\n    syncFavoriteExercises,\n  };\n}\n"
  },
  {
    "path": "src/features/workout-builder/hooks/use-workout-session.ts",
    "content": "\"use client\";\n\nimport { useWorkoutSessionStore } from \"@/features/workout-session/model/workout-session.store\";\n\nexport function useWorkoutSession() {\n  // Le paramètre sessionId n'est plus utilisé ici, la logique de persistance reste dans workoutSessionLocal\n  // (si besoin, on peut l'utiliser pour charger une session spécifique dans le store)\n  return useWorkoutSessionStore();\n}\n"
  },
  {
    "path": "src/features/workout-builder/hooks/use-workout-stepper.ts",
    "content": "\"use client\";\n\nimport { useWorkoutBuilderStore } from \"../model/workout-builder.store\";\n\nexport function useWorkoutStepper() {\n  const {\n    currentStep,\n    selectedEquipment,\n    selectedMuscles,\n    exercisesByMuscle,\n    isLoadingExercises,\n    exercisesError,\n    exercisesOrder,\n    shufflingExerciseId,\n    setStep,\n    nextStep,\n    prevStep,\n    toggleEquipment,\n    clearEquipment,\n    toggleMuscle,\n    clearMuscles,\n    fetchExercises,\n    setExercisesOrder,\n    shuffleExercise,\n    pickExercise,\n    deleteExercise,\n    loadFromSession,\n  } = useWorkoutBuilderStore();\n\n  const canProceedToStep2 = selectedEquipment.length > 0;\n  const canProceedToStep3 = selectedMuscles.length > 0;\n\n  return {\n    // state\n    currentStep,\n    selectedEquipment,\n    selectedMuscles,\n\n    // exercises\n    exercisesByMuscle,\n    isLoadingExercises,\n    exercisesError,\n\n    // navigation\n    goToStep: setStep,\n    nextStep,\n    prevStep,\n\n    // equipment\n    toggleEquipment,\n    clearEquipment,\n\n    // muscles\n    toggleMuscle,\n    clearMuscles,\n\n    // validation\n    canProceedToStep2,\n    canProceedToStep3,\n\n    // fetch\n    fetchExercises,\n\n    // order\n    exercisesOrder,\n    setExercisesOrder,\n\n    // shuffle\n    shuffleExercise,\n\n    // additional\n    shufflingExerciseId,\n\n    // pick\n    pickExercise,\n\n    // delete\n    deleteExercise,\n\n    // load\n    loadFromSession,\n  };\n}\n"
  },
  {
    "path": "src/features/workout-builder/index.ts",
    "content": "export { WorkoutStepper } from \"./ui/workout-stepper\";\nexport { useWorkoutStepper } from \"./hooks/use-workout-stepper\";\nexport { useWorkoutSession } from \"../workout-session/model/use-workout-session\";\nexport { EQUIPMENT_CONFIG, getEquipmentByValue, getEquipmentLabel } from \"./model/equipment-config\";\nexport type { WorkoutBuilderState, WorkoutBuilderStep, EquipmentItem } from \"./types\";\n"
  },
  {
    "path": "src/features/workout-builder/model/equipment-config.ts",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport PullupBar from \"@public/images/equipment/pull-up-bar.png\";\nimport Plate from \"@public/images/equipment/plate.png\";\nimport Kettlebell from \"@public/images/equipment/kettlebell.png\";\nimport Dumbbell from \"@public/images/equipment/dumbbell.png\";\nimport Bodyweight from \"@public/images/equipment/bodyweight.png\";\nimport Bench from \"@public/images/equipment/bench.png\";\nimport Barbell from \"@public/images/equipment/barbell.png\";\nimport Band from \"@public/images/equipment/band.png\";\n\nimport { EquipmentItem } from \"../types\";\n\nexport const EQUIPMENT_CONFIG: EquipmentItem[] = [\n  {\n    value: ExerciseAttributeValueEnum.BODY_ONLY,\n    label: \"Bodyweight\",\n    icon: Bodyweight,\n    description: \"Exercises using only your body weight\",\n    className: \"h-12 w-12\",\n  },\n  {\n    value: ExerciseAttributeValueEnum.DUMBBELL,\n    label: \"Dumbbell\",\n    icon: Dumbbell,\n    description: \"Free weight exercises with dumbbells\",\n    className: \"h-12 w-12\",\n  },\n  {\n    value: ExerciseAttributeValueEnum.BARBELL,\n    label: \"Barbell\",\n    icon: Barbell,\n    description: \"Compound movements with a barbell\",\n    className: \"h-12 w-12\",\n  },\n  {\n    value: ExerciseAttributeValueEnum.KETTLEBELLS,\n    label: \"Kettlebell\",\n    icon: Kettlebell,\n    description: \"Dynamic exercises with kettlebells\",\n    className: \"h-12 w-12\",\n  },\n  {\n    value: ExerciseAttributeValueEnum.BANDS,\n    label: \"Band\",\n    icon: Band,\n    description: \"Resistance band exercises\",\n    className: \"h-12 w-12\",\n  },\n  {\n    value: ExerciseAttributeValueEnum.WEIGHT_PLATE,\n    label: \"Plate\",\n    icon: Plate,\n    description: \"Exercises using weight plates\",\n    className: \"h-12 w-12\",\n  },\n  {\n    value: ExerciseAttributeValueEnum.PULLUP_BAR,\n    label: \"Pull-up bar\",\n    icon: PullupBar,\n    description: \"Upper body exercises with a pull-up bar\",\n    className: \"h-12 w-12\",\n  },\n  {\n    value: ExerciseAttributeValueEnum.BENCH,\n    label: \"Bench\",\n    icon: Bench,\n    description: \"Bench exercises and support\",\n    className: \"h-12 w-12\",\n  },\n];\n\nexport function getEquipmentByValue(value: ExerciseAttributeValueEnum): EquipmentItem | undefined {\n  return EQUIPMENT_CONFIG.find((equipment) => equipment.value === value);\n}\n\nexport function getEquipmentLabel(value: ExerciseAttributeValueEnum): string {\n  return getEquipmentByValue(value)?.label || value;\n}\n"
  },
  {
    "path": "src/features/workout-builder/model/favorite-exercises-synchronizer.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\n\nimport { useSession } from \"@/features/auth/lib/auth-client\";\n\nimport { useSyncFavoriteExercises } from \"../hooks/use-sync-favorite-exercises\";\n\nexport function FavoriteExercisesSynchronizer() {\n  const { syncFavoriteExercises } = useSyncFavoriteExercises();\n  const { data: session } = useSession();\n\n  useEffect(() => {\n    if (session?.user) {\n      syncFavoriteExercises();\n    }\n  }, [session?.user]);\n\n  return null;\n}\n"
  },
  {
    "path": "src/features/workout-builder/model/favorite-exercises.local.ts",
    "content": "export type SyncStatus = \"local\" | \"synced\" | \"deleteOnSync\";\n\nexport interface LocalFavoriteExercise {\n  exerciseId: string;\n  status: SyncStatus;\n  updatedAt: string; // ISO date string\n}\nexport const FAVORITE_EXERICSES_STORAGE_KEY = \"favoriteExercises\";\n\nfunction getAll(): LocalFavoriteExercise[] {\n  try {\n    const stored = localStorage.getItem(FAVORITE_EXERICSES_STORAGE_KEY);\n    if (!stored) return [];\n    return JSON.parse(stored);\n  } catch {\n    return [];\n  }\n}\n\nfunction saveAll(favorites: LocalFavoriteExercise[]) {\n  try {\n    localStorage.setItem(FAVORITE_EXERICSES_STORAGE_KEY, JSON.stringify(favorites));\n  } catch (error) {\n    console.error(\"Failed to save favorites:\", error);\n  }\n}\n\nfunction removeById(exerciseId: string) {\n  const favorites = favoriteExercisesLocal.getAll();\n  const existing = favorites.find((f) => f.exerciseId === exerciseId);\n\n  if (existing) {\n    if (existing.status === \"local\") {\n      // If it was never synced, just remove it completely\n      const filtered = favorites.filter((f) => f.exerciseId !== exerciseId);\n      favoriteExercisesLocal.saveAll(filtered);\n    } else {\n      // If it was synced, mark as deleteOnSync so we can remove from server later\n      existing.status = \"deleteOnSync\";\n      existing.updatedAt = new Date().toISOString();\n      favoriteExercisesLocal.saveAll(favorites);\n    }\n  }\n}\n\nfunction add(exerciseId: string) {\n  const favorites = getAll();\n  const existing = favorites.find((f) => f.exerciseId === exerciseId);\n  const now = new Date().toISOString();\n\n  if (!existing) {\n    // Add new favorite\n    favorites.push({ exerciseId, status: \"local\", updatedAt: now });\n  } else if (existing.status === \"deleteOnSync\") {\n    // Re-add a previously deleted favorite\n    existing.status = \"local\";\n    existing.updatedAt = now;\n  }\n\n  saveAll(favorites);\n}\n\nexport const favoriteExercisesLocal = {\n  getAll,\n  saveAll,\n  removeById,\n  add,\n};\n"
  },
  {
    "path": "src/features/workout-builder/model/workout-builder.store.ts",
    "content": "import { create } from \"zustand\";\nimport { ExerciseAttributeValueEnum, WorkoutSessionExercise } from \"@prisma/client\";\n\nimport { WorkoutBuilderStep } from \"../types\";\nimport { shuffleExerciseAction } from \"../actions/shuffle-exercise.action\";\nimport { pickExerciseAction } from \"../actions/pick-exercise.action\";\nimport { getExercisesAction } from \"../actions/get-exercises.action\";\n\ninterface WorkoutBuilderState {\n  currentStep: WorkoutBuilderStep;\n  selectedEquipment: ExerciseAttributeValueEnum[];\n  selectedMuscles: ExerciseAttributeValueEnum[];\n\n  exercisesByMuscle: any[]; //TODO: type this\n  isLoadingExercises: boolean;\n  exercisesError: any; //TODO: type this\n  exercisesOrder: string[];\n  shufflingExerciseId: string | null;\n\n  // Actions\n  setStep: (step: WorkoutBuilderStep) => void;\n  nextStep: () => void;\n  prevStep: () => void;\n  toggleEquipment: (equipment: ExerciseAttributeValueEnum) => void;\n  clearEquipment: () => void;\n  toggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  clearMuscles: () => void;\n  fetchExercises: () => Promise<void>;\n  setExercisesOrder: (order: string[]) => void;\n  setExercisesByMuscle: (exercisesByMuscle: any[]) => void;\n  shuffleExercise: (exerciseId: string, muscle: ExerciseAttributeValueEnum) => Promise<void>;\n  pickExercise: (exerciseId: string) => Promise<void>;\n  deleteExercise: (exerciseId: string) => void;\n  loadFromSession: (params: {\n    equipment: ExerciseAttributeValueEnum[];\n    muscles: ExerciseAttributeValueEnum[];\n    exercisesByMuscle: {\n      muscle: ExerciseAttributeValueEnum;\n      exercises: WorkoutSessionExercise[];\n    }[];\n    exercisesOrder: string[];\n  }) => void;\n}\n\nexport const useWorkoutBuilderStore = create<WorkoutBuilderState>((set, get) => ({\n  currentStep: 1 as WorkoutBuilderStep,\n  selectedEquipment: [],\n  selectedMuscles: [],\n  exercisesByMuscle: [],\n  isLoadingExercises: false,\n  exercisesError: null,\n  exercisesOrder: [],\n  shufflingExerciseId: null,\n\n  setStep: (step) => set({ currentStep: step }),\n  nextStep: () => set((state) => ({ currentStep: Math.min(state.currentStep + 1, 3) as WorkoutBuilderStep })),\n  prevStep: () => set((state) => ({ currentStep: Math.max(state.currentStep - 1, 1) as WorkoutBuilderStep })),\n\n  toggleEquipment: (equipment) =>\n    set((state) => ({\n      selectedEquipment: state.selectedEquipment.includes(equipment)\n        ? state.selectedEquipment.filter((e) => e !== equipment)\n        : [...state.selectedEquipment, equipment],\n    })),\n  clearEquipment: () => set({ selectedEquipment: [] }),\n\n  toggleMuscle: (muscle) =>\n    set((state) => ({\n      selectedMuscles: state.selectedMuscles.includes(muscle)\n        ? state.selectedMuscles.filter((m) => m !== muscle)\n        : [...state.selectedMuscles, muscle],\n    })),\n  clearMuscles: () => set({ selectedMuscles: [] }),\n\n  fetchExercises: async () => {\n    set({ isLoadingExercises: true, exercisesError: null });\n    try {\n      const { selectedEquipment, selectedMuscles } = get();\n      const result = await getExercisesAction({\n        equipment: selectedEquipment,\n        muscles: selectedMuscles,\n        limit: 3,\n      });\n      if (result?.serverError) {\n        throw new Error(result.serverError);\n      }\n      set({ exercisesByMuscle: result?.data || [], isLoadingExercises: false });\n    } catch (error) {\n      set({ exercisesError: error, isLoadingExercises: false });\n    }\n  },\n\n  setExercisesOrder: (order) => set({ exercisesOrder: order }),\n\n  setExercisesByMuscle: (exercisesByMuscle) => set({ exercisesByMuscle }),\n\n  deleteExercise: (exerciseId) =>\n    set((state) => ({\n      exercisesByMuscle: state.exercisesByMuscle\n        .map((group) => {\n          const filteredExercises = group.exercises.filter((ex: any) => ex.id !== exerciseId);\n\n          if (filteredExercises.length === group.exercises.length) {\n            return group;\n          }\n\n          return { ...group, exercises: filteredExercises };\n        })\n        .filter((group) => group.exercises.length > 0),\n      exercisesOrder: state.exercisesOrder.filter((id) => id !== exerciseId),\n    })),\n\n  shuffleExercise: async (exerciseId, muscle) => {\n    set({ shufflingExerciseId: exerciseId });\n    try {\n      const { selectedEquipment, exercisesByMuscle } = get();\n\n      const allExerciseIds = exercisesByMuscle.flatMap((group) => group.exercises.map((ex: any) => ex.id));\n\n      const result = await shuffleExerciseAction({\n        muscle: muscle,\n        equipment: selectedEquipment,\n        excludeExerciseIds: allExerciseIds,\n      });\n\n      if (result?.serverError) {\n        throw new Error(result.serverError);\n      }\n\n      if (result?.data?.exercise) {\n        const newExercise = result.data.exercise;\n\n        set((state) => ({\n          exercisesByMuscle: state.exercisesByMuscle.map((group) => {\n            if (group.muscle === muscle) {\n              return {\n                ...group,\n                exercises: group.exercises.map((ex: any) => (ex.id === exerciseId ? { ...newExercise, order: ex.order } : ex)),\n              };\n            }\n            return group;\n          }),\n          exercisesOrder: state.exercisesOrder.map((id) => (id === exerciseId ? newExercise.id : id)),\n        }));\n      }\n    } catch (error) {\n      console.error(\"Error shuffling exercise:\", error);\n      throw error;\n    } finally {\n      set({ shufflingExerciseId: null });\n    }\n  },\n\n  pickExercise: async (exerciseId) => {\n    try {\n      const result = await pickExerciseAction({ exerciseId });\n\n      if (result?.serverError) {\n        throw new Error(result.serverError);\n      }\n\n      if (result?.data?.success) {\n        // Pour l'instant, on affiche juste un message de succès\n        // Plus tard, on pourra ajouter de la logique pour marquer visuellement l'exercice\n        console.log(\"Exercise picked successfully:\", exerciseId);\n\n        // Optionnel: on pourrait ajouter une propriété \"picked\" aux exercices\n        // ou maintenir une liste des exercices \"picked\"\n      }\n    } catch (error) {\n      console.error(\"Error picking exercise:\", error);\n      throw error;\n    }\n  },\n\n  loadFromSession: ({ equipment, muscles, exercisesByMuscle, exercisesOrder }) => {\n    set({\n      selectedEquipment: equipment,\n      selectedMuscles: muscles,\n      exercisesByMuscle,\n      exercisesOrder,\n      currentStep: 3,\n      isLoadingExercises: false,\n      exercisesError: null,\n    });\n  },\n}));\n"
  },
  {
    "path": "src/features/workout-builder/schema/get-exercises.schema.ts",
    "content": "import { z } from \"zod\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const getExercisesSchema = z.object({\n  equipment: z.array(z.nativeEnum(ExerciseAttributeValueEnum)).min(1, \"Au moins un équipement est requis\"),\n  muscles: z.array(z.nativeEnum(ExerciseAttributeValueEnum)).min(1, \"Au moins un muscle est requis\"),\n  limit: z.number().int().min(1).max(10).default(3),\n});\n\nexport type GetExercisesInput = z.infer<typeof getExercisesSchema>;\n"
  },
  {
    "path": "src/features/workout-builder/types/index.ts",
    "content": "import { StaticImageData } from \"next/image\";\nimport { ExerciseAttributeValueEnum, WorkoutSet } from \"@prisma/client\";\n\nimport { ExerciseWithAttributes } from \"@/entities/exercise/types/exercise.types\";\n\n// Re-export the type for consistency\nexport type { ExerciseWithAttributes };\n\nexport interface WorkoutBuilderState {\n  currentStep: number;\n  selectedEquipment: ExerciseAttributeValueEnum[];\n  selectedMuscles: ExerciseAttributeValueEnum[];\n  selectedExercises: string[];\n}\n\nexport type WorkoutBuilderStep = 1 | 2 | 3;\n\nexport interface StepperStepProps {\n  stepNumber: number;\n  title: string;\n  description: string;\n  isActive: boolean;\n  isCompleted: boolean;\n}\n\nexport interface EquipmentItem {\n  value: ExerciseAttributeValueEnum;\n  label: string;\n  icon: StaticImageData;\n  description?: string;\n  className?: string;\n}\n\n// Types pour les exercices avec leurs attributs\nexport type WorkoutBuilderExerciseWithAttributes = ExerciseWithAttributes & {\n  order: number;\n};\n\nexport type ExerciseWithAttributesAndSets = ExerciseWithAttributes & {\n  sets: WorkoutSet[];\n};\n\nexport interface ExercisesByMuscle {\n  muscle: ExerciseAttributeValueEnum;\n  exercises: ExerciseWithAttributes[];\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/add-exercise-modal.tsx",
    "content": "\"use client\";\n\nimport { useBoolean } from \"usehooks-ts\";\nimport { useState, useEffect } from \"react\";\nimport Image from \"next/image\";\nimport { Plus, Loader2, X, ChevronDown, ChevronUp } from \"lucide-react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport { FavoriteButton } from \"@/features/workout-builder/ui/favorite-button\";\nimport { useFavoritesModal } from \"@/features/workout-builder/hooks/use-favorites-modal\";\n\nimport { useWorkoutBuilderStore } from \"../model/workout-builder.store\";\nimport { getExercisesByMuscleAction } from \"../actions/get-exercises-by-muscle.action\";\n\ninterface AddExerciseModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  selectedEquipment: ExerciseAttributeValueEnum[];\n}\n\ninterface ExerciseWithAttributes {\n  id: string;\n  name: string;\n  nameEn: string;\n  fullVideoImageUrl: string | null;\n  attributes: Array<{\n    attributeName: { name: string };\n    attributeValue: { value: string };\n  }>;\n}\n\ninterface MuscleGroup {\n  muscle: ExerciseAttributeValueEnum;\n  exercises: ExerciseWithAttributes[];\n}\n\nexport const AddExerciseModal = ({ isOpen, onClose, selectedEquipment }: AddExerciseModalProps) => {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n  const [expandedMuscle, setExpandedMuscle] = useState<string | null>(null);\n  const { value: isFavoritesExpanded, setTrue: openFavorites, setFalse: closeFavorites } = useBoolean(false);\n\n  const { exercisesByMuscle, setExercisesByMuscle, setExercisesOrder, exercisesOrder } = useWorkoutBuilderStore();\n  const { data: muscleGroups, isLoading } = useQuery({\n    queryKey: [\"exercises-by-muscle\", selectedEquipment],\n    queryFn: async () => {\n      const result = await getExercisesByMuscleAction({ equipment: selectedEquipment });\n      if (result?.serverError) {\n        throw new Error(result.serverError);\n      }\n      return result?.data as MuscleGroup[];\n    },\n    enabled: isOpen && selectedEquipment.length > 0,\n  });\n\n  // Use the favorites hook\n  const { favoriteExercises, isFavorite, handleToggleFavorite } = useFavoritesModal({\n    isOpen,\n    muscleGroups: muscleGroups || [],\n  });\n\n  useEffect(() => {\n    const handleEsc = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") onClose();\n    };\n    if (isOpen) {\n      document.addEventListener(\"keydown\", handleEsc);\n      return () => document.removeEventListener(\"keydown\", handleEsc);\n    }\n  }, [isOpen, onClose]);\n\n  const handleAddExercise = (exercise: ExerciseWithAttributes, muscle: ExerciseAttributeValueEnum) => {\n    // If we're in the stepper, add to the workout builder store\n    const muscleGroupIndex = exercisesByMuscle.findIndex((group) => group.muscle === muscle);\n\n    if (muscleGroupIndex === -1) {\n      const newExercisesByMuscle = [...exercisesByMuscle, { muscle, exercises: [exercise] }];\n      setExercisesByMuscle(newExercisesByMuscle);\n    } else {\n      // Check if exercise already exists in this muscle group to avoid duplicates\n      const existingExercises = exercisesByMuscle[muscleGroupIndex].exercises;\n      const exerciseExists = existingExercises.some((ex: ExerciseWithAttributes) => ex.id === exercise.id);\n\n      if (!exerciseExists) {\n        const newExercisesByMuscle = [...exercisesByMuscle];\n        newExercisesByMuscle[muscleGroupIndex] = {\n          ...newExercisesByMuscle[muscleGroupIndex],\n          exercises: [...newExercisesByMuscle[muscleGroupIndex].exercises, exercise],\n        };\n        setExercisesByMuscle(newExercisesByMuscle);\n      }\n    }\n\n    // Only add to exercisesOrder if not already present to avoid duplicates\n    const newExercisesOrder = exercisesOrder.includes(exercise.id) ? exercisesOrder : [...exercisesOrder, exercise.id];\n    setExercisesOrder(newExercisesOrder);\n\n    onClose();\n  };\n\n  const getMuscleLabel = (muscle: string) => {\n    const muscleKey = muscle.toLowerCase();\n    return t((\"workout_builder.muscles.\" + muscleKey) as keyof typeof t);\n  };\n\n  if (!isOpen) return null;\n\n  return (\n    <div aria-labelledby=\"modal-title\" aria-modal=\"true\" className=\"modal modal-open\" role=\"dialog\">\n      <div className=\"modal-box max-w-4xl max-h-[95vh] overflow-hidden flex flex-col p-0 w-full bg-white dark:bg-gray-900\">\n        {/* Header moderne avec mascotte */}\n        <div className=\"bg-gradient-to-r from-blue-500 to-purple-600 dark:from-blue-600 dark:to-purple-700 p-6 flex items-center justify-between\">\n          <div className=\"flex items-center gap-4\">\n            <div className=\"relative\">\n              <Image\n                alt=\"Workout Cool mascotte\"\n                className=\"rounded-full\"\n                height={40}\n                src=\"/images/emojis/WorkoutCoolHappy.png\"\n                width={40}\n              />\n            </div>\n            <h1 className=\"text-2xl font-bold text-white\" id=\"modal-title\">\n              {t(\"workout_builder.addExercise\")}\n            </h1>\n          </div>\n          <button\n            aria-label={t(\"commons.close\")}\n            className=\"btn btn-circle btn-sm bg-white/20 hover:bg-white/30 text-white border-0 transition-all duration-200 ease-in-out\"\n            onClick={onClose}\n          >\n            <X className=\"h-4 w-4\" />\n          </button>\n        </div>\n\n        {/* Contenu principal */}\n        <div className=\"flex-1 overflow-y-auto p-2 sm:p-4 bg-gray-50 dark:bg-gray-800\">\n          {isLoading ? (\n            <div className=\"flex flex-col items-center justify-center py-16 space-y-4\">\n              <Loader2 className=\"h-12 w-12 animate-spin text-blue-500\" />\n              <p className=\"text-xl text-gray-600 dark:text-gray-300 font-medium\">{t(\"commons.loading\")}...</p>\n            </div>\n          ) : (\n            <div className=\"space-y-3\">\n              {/* Favorites Section */}\n              {favoriteExercises.length > 0 && (\n                <div className=\"bg-white dark:bg-gray-900 rounded-xl border border-yellow-200 dark:border-yellow-700 overflow-hidden\">\n                  {/* Favorites Header (Accordion Button) */}\n                  <button\n                    aria-controls=\"favorites-section\"\n                    aria-expanded={isFavoritesExpanded}\n                    className=\"w-full p-4 bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/30 dark:to-orange-900/30 hover:from-yellow-100 hover:to-orange-100 dark:hover:from-yellow-800/40 dark:hover:to-orange-800/40 transition-all duration-200 ease-in-out flex items-center justify-between focus:outline-none focus:ring-2 focus:ring-yellow-500\"\n                    onClick={() => (isFavoritesExpanded ? closeFavorites() : openFavorites())}\n                  >\n                    <div className=\"flex items-center space-x-3\">\n                      <div className=\"w-3 h-3 bg-gradient-to-r from-yellow-500 to-orange-500 rounded-full\"></div>\n                      <span className=\"text-lg font-bold text-gray-900 dark:text-white\">{t(\"commons.favorites\")}</span>\n                      <span className=\"text-sm font-medium text-yellow-600 dark:text-yellow-400 bg-yellow-100 dark:bg-yellow-800/50 px-2 py-1 rounded-full\">\n                        {favoriteExercises.length}\n                      </span>\n                    </div>\n                    {isFavoritesExpanded ? (\n                      <ChevronUp className=\"h-5 w-5 text-gray-600 dark:text-gray-300\" />\n                    ) : (\n                      <ChevronDown className=\"h-5 w-5 text-gray-600 dark:text-gray-300\" />\n                    )}\n                  </button>\n\n                  {/* Favorites Content */}\n                  {isFavoritesExpanded && (\n                    <div className=\"divide-y divide-gray-100 dark:divide-gray-800\" id=\"favorites-section\">\n                      {favoriteExercises.map((exercise) => (\n                        <div\n                          aria-label={`Ajouter ${locale === \"en\" ? exercise.nameEn || exercise.name : exercise.name}`}\n                          className=\"p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 ease-in-out cursor-pointer group\"\n                          key={exercise.id}\n                          onClick={() => {\n                            const { muscle, ...exerciseWithoutMuscle } = exercise;\n                            handleAddExercise(exerciseWithoutMuscle as ExerciseWithAttributes, muscle);\n                          }}\n                          onKeyDown={(e) => {\n                            if (e.key === \"Enter\" || e.key === \" \") {\n                              e.preventDefault();\n                              const { muscle, ...exerciseWithoutMuscle } = exercise;\n                              handleAddExercise(exerciseWithoutMuscle as ExerciseWithAttributes, muscle);\n                            }\n                          }}\n                          role=\"button\"\n                          tabIndex={0}\n                        >\n                          <div className=\"flex items-center gap-2 sm:gap-4\">\n                            <div className=\"flex flex-col sm:flex-row\">\n                              {/* Image de l'exercice avec bordure colorée */}\n                              <div className=\"relative\">\n                                {exercise.fullVideoImageUrl && (\n                                  <div className=\"relative h-16 w-16 rounded-xl overflow-hidden shrink-0 bg-gray-100 dark:bg-gray-700 border-2 border-yellow-200 dark:border-yellow-600 group-hover:border-yellow-400 group-hover:shadow-lg transition-all duration-200\">\n                                    <Image\n                                      alt={exercise.nameEn || \"\"}\n                                      className=\"w-full h-full object-cover scale-[1.5]\"\n                                      height={64}\n                                      loading=\"lazy\"\n                                      src={exercise.fullVideoImageUrl}\n                                      width={64}\n                                    />\n                                  </div>\n                                )}\n                              </div>\n\n                              {/* Favorite Button */}\n                              <div className=\"flex items-center justify-center\">\n                                <FavoriteButton\n                                  exerciseId={exercise.id}\n                                  isFavorite={isFavorite(exercise.id)}\n                                  onToggle={handleToggleFavorite}\n                                />\n                              </div>\n                            </div>\n\n                            <div className=\"flex-1 flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4\">\n                              {/* Nom de l'exercice */}\n                              <div className=\"flex-1\">\n                                <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white group-hover:text-yellow-600 dark:group-hover:text-yellow-400 transition-colors leading-tight\">\n                                  {locale === \"fr\" ? exercise.name : exercise.nameEn || exercise.name}\n                                </h3>\n                              </div>\n\n                              {/* Bouton d'ajout moderne */}\n                              <button\n                                aria-label={`Ajouter ${locale === \"en\" ? exercise.nameEn || exercise.name : exercise.name}`}\n                                className=\"btn btn-sm sm:btn-md bg-green-500 hover:bg-green-600 text-white border-0 transition-all duration-200 ease-in-out group-hover:scale-105 shadow-sm hover:shadow-md\"\n                                onClick={(e) => {\n                                  e.stopPropagation();\n                                  const { muscle, ...exerciseWithoutMuscle } = exercise;\n                                  handleAddExercise(exerciseWithoutMuscle as ExerciseWithAttributes, muscle);\n                                }}\n                              >\n                                <Plus className=\"h-4 w-4 sm:h-5 sm:w-5\" />\n                                <span className=\"ml-1 font-medium\">{t(\"commons.add\")}</span>\n                              </button>\n                            </div>\n                          </div>\n                        </div>\n                      ))}\n                    </div>\n                  )}\n                </div>\n              )}\n\n              {/* Muscle Groups */}\n              {muscleGroups?.map((group) => (\n                <div\n                  className=\"bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden\"\n                  key={group.muscle}\n                >\n                  {/* Bouton de groupe musculaire */}\n                  <button\n                    aria-controls={`muscle-${group.muscle}`}\n                    aria-expanded={expandedMuscle === group.muscle}\n                    className=\"w-full p-4 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/30 dark:to-purple-900/30 hover:from-blue-100 hover:to-purple-100 dark:hover:from-blue-800/40 dark:hover:to-purple-800/40 transition-all duration-200 ease-in-out flex items-center justify-between focus:outline-none focus:ring-2 focus:ring-blue-500\"\n                    onClick={() => setExpandedMuscle(expandedMuscle === group.muscle ? null : group.muscle)}\n                  >\n                    <div className=\"flex items-center space-x-3\">\n                      <div className=\"w-3 h-3 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full\"></div>\n                      <span className=\"text-lg font-bold text-gray-900 dark:text-white\">{getMuscleLabel(group.muscle)}</span>\n                    </div>\n                    <div className=\"flex items-center space-x-3\">\n                      <span className=\"text-sm font-medium text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-800/50 px-2 py-1 rounded-full\">\n                        {group.exercises.length}\n                      </span>\n                      {expandedMuscle === group.muscle ? (\n                        <ChevronUp className=\"h-5 w-5 text-gray-600 dark:text-gray-300\" />\n                      ) : (\n                        <ChevronDown className=\"h-5 w-5 text-gray-600 dark:text-gray-300\" />\n                      )}\n                    </div>\n                  </button>\n\n                  {/* Liste des exercices */}\n                  {expandedMuscle === group.muscle && (\n                    <div className=\"divide-y divide-gray-100 dark:divide-gray-800\" id={`muscle-${group.muscle}`}>\n                      {group.exercises.map((exercise) => (\n                        <div\n                          aria-label={`Ajouter ${locale === \"en\" ? exercise.nameEn || exercise.name : exercise.name}`}\n                          className=\"p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 ease-in-out cursor-pointer group\"\n                          key={exercise.id}\n                          onClick={() => handleAddExercise(exercise, group.muscle)}\n                          onKeyDown={(e) => {\n                            if (e.key === \"Enter\" || e.key === \" \") {\n                              e.preventDefault();\n                              handleAddExercise(exercise, group.muscle);\n                            }\n                          }}\n                          role=\"button\"\n                          tabIndex={0}\n                        >\n                          <div className=\"flex items-center gap-4\">\n                            <div className=\"flex flex-col sm:flex-row\">\n                              {/* Image de l'exercice avec bordure colorée */}\n                              <div className=\"relative\">\n                                {exercise.fullVideoImageUrl && (\n                                  <div className=\"relative h-16 w-16 rounded-xl overflow-hidden shrink-0 bg-gray-100 dark:bg-gray-700 border-2 border-gray-400 dark:border-gray-600 group-hover:border-green-400 group-hover:shadow-lg transition-all duration-200\">\n                                    <Image\n                                      alt={exercise.nameEn}\n                                      className=\"w-full h-full object-cover scale-[1.5]\"\n                                      height={64}\n                                      loading=\"lazy\"\n                                      src={exercise.fullVideoImageUrl}\n                                      width={64}\n                                    />\n                                  </div>\n                                )}\n                                {/* Badge de réussite */}\n                                <div className=\"absolute -top-1 -right-1 w-6 h-6 bg-yellow-400 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n                                  <span className=\"text-xs font-bold text-yellow-900\">B</span>\n                                </div>\n                              </div>\n\n                              {/* Favorite Button */}\n                              <div className=\"flex items-center justify-center\">\n                                <FavoriteButton\n                                  exerciseId={exercise.id}\n                                  isFavorite={isFavorite(exercise.id)}\n                                  onToggle={handleToggleFavorite}\n                                />\n                              </div>\n                            </div>\n\n                            <div className=\"flex-1 flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4\">\n                              {/* Nom de l'exercice */}\n                              <div className=\"flex-1\">\n                                <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white group-hover:text-green-600 dark:group-hover:text-green-400 transition-colors leading-tight\">\n                                  {locale === \"fr\" ? exercise.name : exercise.nameEn || exercise.name}\n                                </h3>\n                              </div>\n\n                              {/* Bouton d'ajout moderne */}\n                              <button\n                                aria-label={`Ajouter ${locale === \"en\" ? exercise.nameEn || exercise.name : exercise.name}`}\n                                className=\"btn btn-sm sm:btn-md bg-green-500 hover:bg-green-600 text-white border-0 transition-all duration-200 ease-in-out group-hover:scale-105 shadow-sm hover:shadow-md\"\n                                onClick={(e) => {\n                                  e.stopPropagation();\n                                  handleAddExercise(exercise, group.muscle);\n                                }}\n                              >\n                                <Plus className=\"h-4 w-4 sm:h-5 sm:w-5\" />\n                                <span className=\"ml-1 font-medium\">{t(\"commons.add\")}</span>\n                              </button>\n                            </div>\n                          </div>\n                        </div>\n                      ))}\n                    </div>\n                  )}\n                </div>\n              ))}\n            </div>\n          )}\n        </div>\n\n        <div className=\"bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 p-4\">\n          <button\n            aria-label={t(\"commons.close\")}\n            className=\"btn btn-md bg-red-500 hover:bg-red-600 w-full text-white border-0 transition-all duration-200 ease-in-out\"\n            onClick={onClose}\n          >\n            <X className=\"h-4 w-4\" />\n            <span className=\"ml-2 font-medium\">{t(\"commons.close\")}</span>\n          </button>\n        </div>\n      </div>\n      <div className=\"modal-backdrop bg-black/60 backdrop-blur-sm\" onClick={onClose} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/equipment-selection.tsx",
    "content": "\"use client\";\nimport Image from \"next/image\";\nimport { Check } from \"lucide-react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { useI18n, useCurrentLocale } from \"locales/client\";\nimport { getEquipmentTranslation } from \"@/shared/lib/workout-session/equipments\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { env } from \"@/env\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { HorizontalBottomBanner } from \"@/components/ads\";\n\nimport { EQUIPMENT_CONFIG } from \"../model/equipment-config\";\n\ninterface EquipmentSelectionProps {\n  onClearEquipment: VoidFunction;\n  onToggleEquipment: (equipment: ExerciseAttributeValueEnum) => void;\n  selectedEquipment: ExerciseAttributeValueEnum[];\n}\n\ninterface EquipmentCardProps {\n  equipment: (typeof EQUIPMENT_CONFIG)[0];\n  isSelected: boolean;\n  onToggle: () => void;\n}\n\nfunction EquipmentCard({ equipment, isSelected, onToggle }: EquipmentCardProps) {\n  const t = useI18n();\n\n  const translation = getEquipmentTranslation(equipment.value, t);\n\n  return (\n    <Card\n      className={cn(\n        // Base styles - Chess.com inspiration\n        \"group relative overflow-hidden cursor-pointer\",\n        \"bg-gradient-to-br from-slate-50 to-slate-200 dark:from-slate-800 dark:to-slate-900\",\n        \"border-2 border-slate-200 dark:border-slate-700\",\n        \"rounded-xl shadow-sm hover:shadow-xl\",\n        // Transitions smooth\n        \"transition-all duration-300 ease-out\",\n        \"hover:scale-[1.02] hover:-translate-y-1\",\n        // Selected state\n        isSelected && [\n          \"border-emerald-400 dark:border-emerald-500\",\n          \"bg-gradient-to-br from-emerald-50 to-emerald-100 dark:from-emerald-900/20 dark:to-emerald-800/20\",\n          \"shadow-emerald-200/50 dark:shadow-emerald-900/50 shadow-lg\",\n        ],\n        // Hover effects\n        !isSelected && \"hover:border-slate-300 dark:hover:border-slate-600\",\n      )}\n      onClick={onToggle}\n    >\n      <CardContent className=\"p-2 sm:p-4 h-auto flex flex-col justify-center items-center relative\">\n        <div\n          className={cn(\n            \"absolute top-3 left-3 w-2 h-2 rounded-full transition-colors duration-200\",\n            isSelected ? \"bg-emerald-400\" : \"bg-slate-300 dark:bg-slate-600\",\n          )}\n        />\n\n        {isSelected && (\n          <div className=\"absolute top-2 right-2\">\n            <div className=\"absolute inset-0 bg-emerald-400 rounded-full animate-ping opacity-25\" />\n            <Badge\n              className=\"relative bg-emerald-500 hover:bg-emerald-600 text-white border-0 h-6 w-6 p-0 flex items-center justify-center rounded-full\"\n              variant=\"default\"\n            >\n              <Check className=\"h-3 w-3\" />\n            </Badge>\n          </div>\n        )}\n\n        <div className=\"flex items-center justify-center mb-3\">\n          <div className={cn(\"relative transition-transform duration-200 group-hover:scale-110\", isSelected && \"scale-105\")}>\n            <Image\n              alt={`${translation.label} illustration`}\n              className=\"object-contain filter transition-all duration-200 group-hover:brightness-110\"\n              height={48}\n              src={equipment.icon}\n              style={{\n                width: \"6.25rem\",\n                height: \"5rem\",\n              }}\n              width={64}\n            />\n          </div>\n        </div>\n\n        {/* Label centré - PLUS VISIBLE MAINTENANT */}\n        <div className=\"text-center\">\n          <h3\n            className={cn(\n              \"font-semibold text-sm transition-all duration-200\",\n              \"tracking-wide leading-tight\",\n              isSelected\n                ? \"text-emerald-700 dark:text-emerald-300\"\n                : \"text-slate-700 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-slate-200\",\n            )}\n          >\n            {translation.label}\n          </h3>\n        </div>\n\n        {/* Progress bar subtile en bas */}\n        <div className=\"absolute bottom-0 left-0 right-0 h-1 bg-slate-200 dark:bg-slate-700 overflow-hidden rounded-b-xl\">\n          <div\n            className={cn(\n              \"h-full transition-all duration-500 ease-out\",\n              isSelected ? \"w-full bg-gradient-to-r from-emerald-400 to-emerald-500\" : \"w-0 group-hover:w-1/3 bg-slate-400\",\n            )}\n          />\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\nexport function EquipmentSelection({ onToggleEquipment, selectedEquipment }: EquipmentSelectionProps) {\n  const locale = useCurrentLocale();\n\n  return (\n    <div className=\"space-y-6\">\n      <div className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4\">\n        {EQUIPMENT_CONFIG.map((equipment, index) => (\n          <div\n            className=\"animate-fade-in-up\"\n            key={equipment.value}\n            style={{\n              animationDelay: `${index * 50}ms`,\n              animationFillMode: \"both\",\n            }}\n          >\n            <EquipmentCard\n              equipment={equipment}\n              isSelected={selectedEquipment.includes(equipment.value)}\n              onToggle={() => onToggleEquipment(equipment.value)}\n            />\n          </div>\n        ))}\n      </div>\n      {(env.NEXT_PUBLIC_EQUIPMENT_SELECTION_BANNER_AD_SLOT || env.NEXT_PUBLIC_EZOIC_EQUIPMENT_SELECTION_PLACEMENT_ID) && (\n        <HorizontalBottomBanner\n          adSlot={env.NEXT_PUBLIC_EQUIPMENT_SELECTION_BANNER_AD_SLOT}\n          ezoicPlacementId={env.NEXT_PUBLIC_EZOIC_EQUIPMENT_SELECTION_PLACEMENT_ID}\n        />\n      )}\n\n      {/* {locale === \"fr\" ? (\n        <NutripureAffiliateBanner />\n      ) : (\n        (env.NEXT_PUBLIC_EQUIPMENT_SELECTION_BANNER_AD_SLOT || env.NEXT_PUBLIC_EZOIC_EQUIPMENT_SELECTION_PLACEMENT_ID) && (\n          <HorizontalBottomBanner\n            adSlot={env.NEXT_PUBLIC_EQUIPMENT_SELECTION_BANNER_AD_SLOT}\n            ezoicPlacementId={env.NEXT_PUBLIC_EZOIC_EQUIPMENT_SELECTION_PLACEMENT_ID}\n          />\n        )\n      )} */}\n      {/* <ActionBar onClearEquipment={onClearEquipment} selectedCount={selectedEquipment.length} /> */}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/exercise-card.tsx",
    "content": "import { useState } from \"react\";\nimport Image from \"next/image\";\nimport { Play, Shuffle, MoreVertical, Trash2, Info, Target } from \"lucide-react\";\nimport { ExerciseAttributeNameEnum } from \"@prisma/client\";\n\nimport { useI18n } from \"locales/client\";\nimport { getExerciseAttributesValueOf } from \"@/entities/exercise/shared/muscles\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"@/components/ui/dropdown-menu\";\nimport { Card, CardContent, CardHeader } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\n\nimport { ExerciseVideoModal } from \"./exercise-video-modal\";\n\nimport type { ExerciseWithAttributes } from \"../types\";\n\ninterface ExerciseCardProps {\n  exercise: ExerciseWithAttributes;\n  muscle: string;\n  onShuffle: (exerciseId: string, muscle: string) => void;\n  onPick: (exerciseId: string) => void;\n  onDelete: (exerciseId: string, muscle: string) => void;\n}\n\nexport function ExerciseCard({ exercise, muscle, onShuffle, onPick, onDelete }: ExerciseCardProps) {\n  const t = useI18n();\n  const [imageError, setImageError] = useState(false);\n  const [showVideo, setShowVideo] = useState(false);\n\n  // Extraire les attributs utiles\n  const equipmentAttributes = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.EQUIPMENT);\n  const typeAttributes = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.TYPE);\n  const mechanicsTypeValue = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.MECHANICS_TYPE);\n\n  const handlePlayVideo = () => {\n    setShowVideo(true);\n  };\n\n  return (\n    <TooltipProvider>\n      <Card className=\"group relative overflow-hidden bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 hover:shadow-lg transition-all duration-200 hover:border-blue-200 dark:hover:border-blue-800\">\n        <CardHeader className=\"relative p-0\">\n          {/* Image/Vidéo thumbnail */}\n          <div className=\"relative h-48 bg-gradient-to-br from-slate-200 to-slate-200 dark:from-slate-700 dark:to-slate-800\">\n            {exercise.fullVideoImageUrl && !imageError ? (\n              <>\n                <Image\n                  alt={exercise.name}\n                  className=\"object-cover transition-transform group-hover:scale-105\"\n                  fill\n                  loading=\"lazy\"\n                  onError={() => setImageError(true)}\n                  sizes=\"(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw\"\n                  src={exercise.fullVideoImageUrl}\n                />\n                <div className=\"absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center\">\n                  <Button className=\"bg-white/90 text-slate-900\" onClick={handlePlayVideo} size=\"small\" variant=\"secondary\">\n                    <Play className=\"h-4 w-4 mr-2\" />\n                    {t(\"workout_builder.exercise.watch_video\")}\n                  </Button>\n                </div>\n              </>\n            ) : (\n              <div className=\"h-full flex items-center justify-center\">\n                <div className=\"text-slate-400 dark:text-slate-500\">\n                  <Target className=\"h-12 w-12\" />\n                </div>\n              </div>\n            )}\n\n            {/* Badge du muscle en haut à gauche */}\n            <div className=\"absolute top-3 left-3\">\n              <Badge className=\"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100\" variant=\"outline\">\n                {t(`workout_builder.muscles.${muscle.toLowerCase()}` as keyof typeof t)}\n              </Badge>\n            </div>\n\n            {/* Menu d'actions en haut à droite */}\n            <div className=\"absolute top-3 right-3\">\n              <DropdownMenu>\n                <DropdownMenuTrigger asChild>\n                  <Button className=\"h-8 w-8 bg-white/90 hover:bg-white\" size=\"small\" variant=\"ghost\">\n                    <MoreVertical className=\"h-4 w-4\" />\n                  </Button>\n                </DropdownMenuTrigger>\n                <DropdownMenuContent align=\"end\">\n                  <DropdownMenuItem onClick={() => onShuffle(exercise.id, muscle)}>\n                    <Shuffle className=\"h-4 w-4 mr-2\" />\n                    {t(\"workout_builder.exercise.shuffle\")}\n                  </DropdownMenuItem>\n                  <DropdownMenuItem onClick={() => onDelete(exercise.id, muscle)}>\n                    <Trash2 className=\"h-4 w-4 mr-2\" />\n                    {t(\"workout_builder.exercise.remove\")}\n                  </DropdownMenuItem>\n                </DropdownMenuContent>\n              </DropdownMenu>\n            </div>\n          </div>\n        </CardHeader>\n\n        <CardContent className=\"p-4\">\n          {/* Titre de l'exercice */}\n          <div className=\"flex items-start justify-between mb-3\">\n            <h4 className=\"font-semibold text-slate-900 dark:text-slate-200 text-sm leading-tight line-clamp-2\">{exercise.name}</h4>\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <Button className=\"h-8 w-8 ml-2 flex-shrink-0\" size=\"small\" variant=\"ghost\">\n                  <Info className=\"h-4 w-4\" />\n                </Button>\n              </TooltipTrigger>\n              <TooltipContent className=\"max-w-xs\" side=\"left\">\n                <div className=\"space-y-2\">\n                  <p className=\"text-sm\">{exercise.introduction}</p>\n                  {mechanicsTypeValue && (\n                    <p className=\"text-xs text-slate-500\">\n                      <strong>Type:</strong> {mechanicsTypeValue.map((mt) => mt.replace(\"_\", \" \")).join(\", \")}\n                    </p>\n                  )}\n                </div>\n              </TooltipContent>\n            </Tooltip>\n          </div>\n\n          {/* Tags des équipements */}\n          {equipmentAttributes.length > 0 && (\n            <div className=\"flex flex-wrap gap-1 mb-3\">\n              {equipmentAttributes.slice(0, 2).map((equipment, index) => (\n                <Badge className=\"text-xs px-2 py-0.5\" key={index} variant=\"outline\">\n                  {equipment.replace(\"_\", \" \")}\n                </Badge>\n              ))}\n              {equipmentAttributes.length > 2 && (\n                <Badge className=\"text-xs px-2 py-0.5\" variant=\"outline\">\n                  +{equipmentAttributes.length - 2}\n                </Badge>\n              )}\n            </div>\n          )}\n\n          {/* Types d'entraînement */}\n          {typeAttributes.length > 0 && (\n            <div className=\"flex flex-wrap gap-1 mb-4\">\n              {typeAttributes.slice(0, 2).map((type, index) => (\n                <Badge\n                  className=\"text-xs px-2 py-0.5 bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100\"\n                  key={index}\n                  variant=\"default\"\n                >\n                  {type}\n                </Badge>\n              ))}\n            </div>\n          )}\n\n          {/* Actions */}\n          <div className=\"flex items-center gap-2\">\n            <Button\n              className=\"flex-1 text-blue-600 border-blue-200 hover:bg-blue-50 dark:text-blue-400 dark:border-blue-800 dark:hover:bg-blue-950\"\n              onClick={() => onShuffle(exercise.id, muscle)}\n              size=\"small\"\n              variant=\"outline\"\n            >\n              <Shuffle className=\"h-4 w-4 mr-1\" />\n              {t(\"workout_builder.exercise.shuffle\")}\n            </Button>\n            <Button className=\"flex-1 bg-blue-600 hover:bg-blue-700 text-white\" onClick={() => onPick(exercise.id)} size=\"small\">\n              ⭐ {t(\"workout_builder.exercise.pick\")}\n            </Button>\n          </div>\n        </CardContent>\n      </Card>\n      {/* Video Modal */}\n      {exercise.fullVideoUrl && <ExerciseVideoModal exercise={exercise} onOpenChange={setShowVideo} open={showVideo} />}\n    </TooltipProvider>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/exercise-list-item.tsx",
    "content": "import React, { useCallback } from \"react\";\nimport Image from \"next/image\";\nimport { Play, Shuffle, Trash2, GripVertical, Loader2, BarChart3 } from \"lucide-react\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { useSortable } from \"@dnd-kit/sortable\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport useBoolean from \"@/shared/hooks/useBoolean\";\nimport { Button } from \"@/components/ui/button\";\n\nimport { ExerciseVideoModal } from \"./exercise-video-modal\";\n\nimport type { ExerciseWithAttributes } from \"../types\";\n\nconst MUSCLE_CONFIGS: Record<string, string> = {\n  ABDOMINALS: \"bg-red-500\",\n  BICEPS: \"bg-purple-500\",\n  BACK: \"bg-blue-500\",\n  CHEST: \"bg-green-500\",\n  SHOULDERS: \"bg-orange-500\",\n  OBLIQUES: \"bg-pink-500\",\n};\n\ninterface ExerciseListItemProps {\n  exercise: ExerciseWithAttributes;\n  muscle: string;\n  onShuffle: (exerciseId: string, muscle: string) => void;\n  onPick: (exerciseId: string) => void;\n  onDelete: (exerciseId: string, muscle: string) => void;\n  isShuffling?: boolean;\n}\n\nexport const ExerciseListItem = React.memo(function ExerciseListItem({\n  exercise,\n  muscle,\n  onShuffle,\n  onDelete,\n  isShuffling,\n}: Omit<ExerciseListItemProps, \"onPick\">) {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n  const playVideo = useBoolean();\n\n  const { attributes, listeners, setNodeRef, transform, isDragging } = useSortable({ id: exercise.id });\n\n  const exerciseName = locale === \"fr\" ? exercise.name : exercise.nameEn;\n  const muscleColor = MUSCLE_CONFIGS[muscle] || \"bg-gray-500\";\n  const muscleTitle = t((\"workout_builder.muscles.\" + muscle.toLowerCase()) as keyof typeof t);\n\n  const handleShuffle = useCallback(() => {\n    onShuffle(exercise.id, muscle);\n  }, [onShuffle, exercise.id, muscle]);\n\n  return (\n    <div\n      className={`flex items-center gap-3 p-3 bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700 select-none ${isDragging ? \"shadow-lg\" : \"\"}`}\n      ref={setNodeRef}\n      style={{\n        transform: CSS.Transform.toString(transform),\n        zIndex: isDragging ? 1000 : 1,\n        position: isDragging ? \"relative\" : \"static\",\n        userSelect: \"none\",\n        WebkitUserSelect: \"none\",\n        MozUserSelect: \"none\",\n        msUserSelect: \"none\",\n      }}\n    >\n      <div\n        className=\"cursor-grab active:cursor-grabbing touch-none select-none p-1 -m-1\"\n        style={{ touchAction: \"none\" }}\n        {...attributes}\n        {...listeners}\n      >\n        <GripVertical className=\"h-5 w-5 text-slate-400\" />\n      </div>\n\n      {exercise.fullVideoImageUrl && (\n        <div\n          className=\"relative h-10 w-10 rounded overflow-hidden shrink-0 bg-slate-200 dark:bg-slate-800 cursor-pointer border border-slate-200 dark:border-slate-700/50\"\n          onClick={playVideo.setTrue}\n        >\n          <Image\n            alt={exerciseName ?? \"\"}\n            className=\"w-full h-full object-cover scale-[1.5]\"\n            height={32}\n            loading=\"lazy\"\n            src={exercise.fullVideoImageUrl}\n            width={32}\n          />\n          <div className=\"absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity\">\n            <Play className=\"h-3 w-3 text-white fill-current\" />\n          </div>\n        </div>\n      )}\n\n      <div\n         \n        className={`tooltip tooltip-bottom w-5 h-5 rounded text-white text-xs font-bold flex items-center justify-center shrink-0 cursor-pointer ${muscleColor}`}\n        data-tip={muscleTitle}\n      >\n        {muscle.charAt(0)}\n      </div>\n\n      <div className=\"flex-1 min-w-0\">\n        <span className=\"text-sm font-medium text-slate-900 dark:text-slate-100 md:truncate\">{exerciseName}</span>\n      </div>\n\n      <Button\n        className=\"p-2 sm:p-2 min-h-[44px] min-w-[44px] sm:min-h-min sm:min-w-min touch-manipulation\"\n        disabled={isShuffling}\n        onClick={handleShuffle}\n        size=\"small\"\n        variant=\"outline\"\n      >\n        {isShuffling ? <Loader2 className=\"h-4 w-4 sm:h-3.5 sm:w-3.5 animate-spin\" /> : <Shuffle className=\"h-4 w-4 sm:h-3.5 sm:w-3.5\" />}\n        <span className=\"hidden sm:inline ml-1\">{t(\"workout_builder.exercise.shuffle\")}</span>\n      </Button>\n\n      <button\n        className=\"p-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300\"\n        onClick={playVideo.setTrue}\n      >\n        <BarChart3 className=\"h-4 w-4\" />\n      </button>\n\n      <button\n        className=\"p-2 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300\"\n        onClick={() => onDelete(exercise.id, muscle)}\n      >\n        <Trash2 className=\"h-4 w-4\" />\n      </button>\n\n      {exercise.fullVideoUrl && <ExerciseVideoModal exercise={exercise} onOpenChange={playVideo.toggle} open={playVideo.value} />}\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/features/workout-builder/ui/exercise-pick-modal.tsx",
    "content": "import { useEffect, useRef } from \"react\";\nimport Image from \"next/image\";\nimport { X, Play } from \"lucide-react\";\nimport { ExerciseAttributeNameEnum } from \"@prisma/client\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport { getExerciseAttributesValueOf } from \"@/entities/exercise/shared/muscles\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\n\nimport type { ExerciseWithAttributes } from \"../types\";\n\ninterface ExercisePickModalProps {\n  exercise: ExerciseWithAttributes | null;\n  muscle: string;\n  isOpen: boolean;\n  onClose: () => void;\n  onConfirmPick: () => void;\n}\n\nexport function ExercisePickModal({ exercise, muscle, isOpen, onClose, onConfirmPick }: ExercisePickModalProps) {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n  const modalRef = useRef<HTMLDialogElement>(null);\n\n  useEffect(() => {\n    const modal = modalRef.current;\n    if (!modal) return;\n\n    if (isOpen) {\n      modal.showModal();\n    } else {\n      modal.close();\n    }\n  }, [isOpen]);\n\n  useEffect(() => {\n    const modal = modalRef.current;\n    if (!modal) return;\n\n    const handleClose = () => {\n      onClose();\n    };\n\n    modal.addEventListener(\"close\", handleClose);\n    return () => modal.removeEventListener(\"close\", handleClose);\n  }, [onClose]);\n\n  if (!exercise) return null;\n\n  const exerciseName = locale === \"fr\" ? exercise.name : exercise.nameEn;\n  const exerciseDescription = locale === \"fr\" ? exercise.description : exercise.descriptionEn;\n\n  // Extraire les attributs utiles\n  const equipmentAttributes = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.EQUIPMENT);\n  const typeAttributes = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.TYPE);\n  const mechanicsTypeValue = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.MECHANICS_TYPE);\n\n  const handleConfirm = (e: React.MouseEvent) => {\n    e.preventDefault();\n    onConfirmPick();\n    onClose();\n  };\n\n  return (\n    <dialog className=\"modal modal-bottom sm:modal-middle\" ref={modalRef}>\n      <div className=\"modal-box max-w-2xl\">\n        {/* Header */}\n        <div className=\"flex items-start justify-between mb-4\">\n          <div className=\"flex-1\">\n            <h3 className=\"font-bold text-lg text-slate-900 dark:text-slate-100\">{exerciseName}</h3>\n            <div className=\"flex items-center gap-2 mt-2\">\n              <Badge className=\"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100\" variant=\"outline\">\n                {t(`workout_builder.muscles.${muscle.toLowerCase()}` as keyof typeof t)}\n              </Badge>\n              {mechanicsTypeValue && (\n                <Badge className=\"text-xs\" variant=\"outline\">\n                  {mechanicsTypeValue.map((value) => value.replace(\"_\", \" \"))}\n                </Badge>\n              )}\n            </div>\n          </div>\n          <form method=\"dialog\">\n            <Button className=\"p-1\" size=\"small\" variant=\"ghost\">\n              <X className=\"h-4 w-4\" />\n            </Button>\n          </form>\n        </div>\n\n        {/* Image/Video */}\n        {exercise.fullVideoImageUrl && (\n          <div className=\"relative h-48 bg-gradient-to-br from-slate-200 to-slate-200 dark:from-slate-700 dark:to-slate-800 rounded-lg overflow-hidden mb-4\">\n            <Image\n              alt={exerciseName || \"Exercise\"}\n              className=\"object-cover\"\n              fill\n              sizes=\"(max-width: 768px) 100vw, 50vw\"\n              src={exercise.fullVideoImageUrl}\n            />\n            {exercise.fullVideoUrl && (\n              <div className=\"absolute inset-0 bg-black/20 flex items-center justify-center\">\n                <Button className=\"bg-white/90 text-slate-900\" size=\"small\" variant=\"secondary\">\n                  <Play className=\"h-4 w-4 mr-2\" />\n                  {t(\"workout_builder.exercise.watch_video\")}\n                </Button>\n              </div>\n            )}\n          </div>\n        )}\n\n        {/* Description */}\n        {exerciseDescription && (\n          <div className=\"mb-4\">\n            <h4 className=\"font-semibold text-sm text-slate-900 dark:text-slate-100 mb-2\">Description</h4>\n            <p className=\"text-sm text-slate-600 dark:text-slate-400 leading-relaxed\">{exerciseDescription}</p>\n          </div>\n        )}\n\n        {/* Attributes */}\n        <div className=\"space-y-3 mb-6\">\n          {/* Equipment */}\n          {equipmentAttributes.length > 0 && (\n            <div>\n              <h4 className=\"font-semibold text-sm text-slate-900 dark:text-slate-100 mb-2\">Equipment</h4>\n              <div className=\"flex flex-wrap gap-1\">\n                {equipmentAttributes.map((equipment, index) => (\n                  <Badge className=\"text-xs\" key={index} variant=\"outline\">\n                    {equipment.replace(\"_\", \" \")}\n                  </Badge>\n                ))}\n              </div>\n            </div>\n          )}\n\n          {/* Exercise Types */}\n          {typeAttributes.length > 0 && (\n            <div>\n              <h4 className=\"font-semibold text-sm text-slate-900 dark:text-slate-100 mb-2\">Exercise Types</h4>\n              <div className=\"flex flex-wrap gap-1\">\n                {typeAttributes.map((type, index) => (\n                  <Badge\n                    className=\"text-xs bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100\"\n                    key={index}\n                    variant=\"default\"\n                  >\n                    {type}\n                  </Badge>\n                ))}\n              </div>\n            </div>\n          )}\n        </div>\n\n        {/* Actions */}\n        <div className=\"modal-action\">\n          <form className=\"flex gap-2\" method=\"dialog\">\n            <Button size=\"small\" variant=\"outline\">\n              Cancel\n            </Button>\n            <Button className=\"bg-blue-600 hover:bg-blue-700 text-white\" onClick={handleConfirm} size=\"small\">\n              ⭐ Confirm Pick\n            </Button>\n          </form>\n        </div>\n      </div>\n    </dialog>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/exercise-video-modal.tsx",
    "content": "import { useState } from \"react\";\nimport { BarChart3, Play } from \"lucide-react\";\nimport { ExerciseAttributeNameEnum, ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport { getYouTubeEmbedUrl } from \"@/shared/lib/youtube\";\nimport { getAttributeValueLabel } from \"@/shared/lib/attribute-value-translation\";\nimport { StatisticsTimeframe } from \"@/shared/constants/statistics\";\nimport { ExerciseCharts } from \"@/features/statistics/components/ExerciseStatisticsTab\";\nimport { TimeframeSelector } from \"@/features/statistics/components\";\nimport { getExerciseAttributesValueOf } from \"@/entities/exercise/shared/muscles\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\n\nimport type { ExerciseWithAttributes } from \"@/entities/exercise/types/exercise.types\";\n\ninterface ExerciseVideoModalProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  exercise: ExerciseWithAttributes;\n}\n\nexport function ExerciseVideoModal({ open, onOpenChange, exercise }: ExerciseVideoModalProps) {\n  const t = useI18n();\n  const locale = useCurrentLocale();\n  const [activeTab, setActiveTab] = useState(\"video\");\n  const [selectedTimeframe, setSelectedTimeframe] = useState<StatisticsTimeframe>(\"8weeks\");\n\n  const title = locale === \"fr\" ? exercise.name : exercise.nameEn || exercise.name;\n  const introduction = locale === \"fr\" ? exercise.introduction : exercise.introductionEn || exercise.introduction;\n  const description = locale === \"fr\" ? exercise.description : exercise.descriptionEn || exercise.description;\n  const videoUrl = exercise.fullVideoUrl;\n  const youTubeEmbedUrl = getYouTubeEmbedUrl(videoUrl ?? \"\");\n\n  const type = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.TYPE);\n  const pMuscles = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.PRIMARY_MUSCLE);\n  const sMuscles = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.SECONDARY_MUSCLE);\n  const equipment = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.EQUIPMENT);\n  const mechanics = getExerciseAttributesValueOf(exercise, ExerciseAttributeNameEnum.MECHANICS_TYPE);\n\n  // Couleurs pour les badges\n  const badgeColors: Record<string, string> = {\n    TYPE: \"bg-violet-100 text-violet-800 dark:bg-violet-900 dark:text-violet-100\",\n    PRIMARY_MUSCLE: \"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100\",\n    SECONDARY_MUSCLE: \"bg-sky-100 text-sky-800 dark:bg-sky-900 dark:text-sky-100\",\n    EQUIPMENT: \"bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-100\",\n    MECHANICS_TYPE: \"bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100\",\n  };\n\n  const renderBadge = (value: ExerciseAttributeValueEnum, color: string) => {\n    return (\n      <span className={`px-2 py-0.5 rounded text-xs font-medium ${color}`} key={value}>\n        {getAttributeValueLabel(value, t)}\n      </span>\n    );\n  };\n\n  return (\n    <Dialog onOpenChange={onOpenChange} open={open}>\n      <DialogContent className=\"max-w-2xl p-0 max-h-[80vh]\">\n        <DialogHeader className=\"flex flex-row items-center justify-between px-4 pt-4 pb-2\">\n          <DialogTitle className=\"text-lg md:text-xl font-bold flex flex-col gap-2\">\n            <span className=\"text-slate-700 dark:text-slate-200 pr-10 text-left\">{title}</span>\n            <div className=\"flex flex-wrap gap-2 mt-2\">\n              {type.map((type) => renderBadge(type, badgeColors.TYPE))}\n              {pMuscles.map((pMuscle) => renderBadge(pMuscle, badgeColors.PRIMARY_MUSCLE))}\n              {sMuscles.map((sMuscle) => renderBadge(sMuscle, badgeColors.SECONDARY_MUSCLE))}\n              {equipment.map((eq) => renderBadge(eq, badgeColors.EQUIPMENT))}\n              {mechanics.map((mechanic) => renderBadge(mechanic, badgeColors.MECHANICS_TYPE))}\n            </div>\n          </DialogTitle>\n        </DialogHeader>\n\n        <Tabs className=\"flex-1\" onValueChange={setActiveTab} value={activeTab}>\n          <TabsList className=\"grid w-full grid-cols-2 mx-4\" style={{ width: \"calc(100% - 2rem)\" }}>\n            <TabsTrigger className=\"flex items-center gap-2\" value=\"video\">\n              <Play size={16} />\n              {t(\"statistics.tabs.video\")}\n            </TabsTrigger>\n            <TabsTrigger className=\"flex items-center gap-2\" value=\"statistics\">\n              <BarChart3 size={16} />\n              {t(\"statistics.title\") || \"Statistics\"}\n            </TabsTrigger>\n          </TabsList>\n\n          <TabsContent className=\"mt-0\" value=\"video\">\n            {/* Introduction */}\n            {introduction && (\n              <div\n                className=\"px-6 pt-2 pb-2 text-slate-700 dark:text-slate-200 text-sm md:text-base prose dark:prose-invert max-w-none\"\n                dangerouslySetInnerHTML={{ __html: introduction }}\n              />\n            )}\n\n            {/* Vidéo */}\n            <div className=\"w-full aspect-video bg-black flex items-center justify-center\">\n              {videoUrl ? (\n                youTubeEmbedUrl ? (\n                  <iframe\n                    allow=\"autoplay; encrypted-media\"\n                    allowFullScreen\n                    className=\"w-full h-full border-0\"\n                    referrerPolicy=\"strict-origin-when-cross-origin\"\n                    src={youTubeEmbedUrl}\n                    title={title ?? \"\"}\n                  />\n                ) : (\n                  <video autoPlay className=\"w-full h-full object-contain bg-black\" controls poster=\"\" src={videoUrl} />\n                )\n              ) : (\n                <div className=\"text-white text-center p-8\">{t(\"workout_builder.exercise.no_video_available\")}</div>\n              )}\n            </div>\n\n            {/* Instructions (description) */}\n            {description && (\n              <div\n                className=\"px-6 pt-4 pb-6 text-slate-700 dark:text-slate-200 text-sm md:text-base prose dark:prose-invert max-w-none border-t border-slate-200 dark:border-slate-800 mt-2\"\n                dangerouslySetInnerHTML={{ __html: description }}\n              />\n            )}\n          </TabsContent>\n\n          <TabsContent className=\"mt-0 px-2 md:px-6 pt-4 pb-6\" value=\"statistics\">\n            <div className=\"space-y-4\">\n              {/* Timeframe selector */}\n              <div className=\"flex items-center justify-between flex-col sm:flex-row\">\n                <h3 className=\"text-lg font-semibold text-slate-700 dark:text-slate-200\">{t(\"statistics.performance_over_time\")}</h3>\n                <TimeframeSelector onSelect={setSelectedTimeframe} selected={selectedTimeframe} />\n              </div>\n\n              {/* Charts */}\n              <ExerciseCharts exerciseId={exercise.id} timeframe={selectedTimeframe} />\n            </div>\n          </TabsContent>\n        </Tabs>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/exercises-selection.tsx",
    "content": "import { useState, useEffect, useCallback, useMemo } from \"react\";\nimport { Loader2, Plus } from \"lucide-react\";\nimport { arrayMove, SortableContext, verticalListSortingStrategy } from \"@dnd-kit/sortable\";\nimport { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, MouseSensor } from \"@dnd-kit/core\";\n\nimport { useI18n } from \"locales/client\";\nimport { env } from \"@/env\";\nimport { HorizontalBottomBanner } from \"@/components/ads\";\n\nimport { useWorkoutStepper } from \"../hooks/use-workout-stepper\";\nimport { ExerciseListItem } from \"./exercise-list-item\";\n\nimport type { ExerciseWithAttributes } from \"../types\";\n\ninterface ExercisesSelectionProps {\n  isLoading: boolean;\n  exercisesByMuscle: { muscle: string; exercises: ExerciseWithAttributes[] }[];\n  error: any;\n  onShuffle: (exerciseId: string, muscle: string) => void;\n  onPick: (exerciseId: string) => void;\n  onDelete: (exerciseId: string, muscle: string) => void;\n  onAdd: () => void;\n  shufflingExerciseId?: string | null;\n}\n\nexport const ExercisesSelection = ({\n  isLoading,\n  exercisesByMuscle,\n  error,\n  onShuffle,\n  onPick: _todo,\n  onDelete,\n  onAdd,\n  shufflingExerciseId,\n}: ExercisesSelectionProps) => {\n  const t = useI18n();\n  const [flatExercises, setFlatExercises] = useState<{ id: string; muscle: string; exercise: ExerciseWithAttributes }[]>([]);\n  const { setExercisesOrder, exercisesOrder } = useWorkoutStepper();\n\n  const sensors = useSensors(\n    useSensor(PointerSensor, {\n      activationConstraint: {\n        delay: 100,\n        tolerance: 5,\n      },\n    }),\n    useSensor(MouseSensor, {\n      activationConstraint: {\n        delay: 0,\n        distance: 0,\n      },\n    }),\n  );\n\n  const sortableItems = useMemo(() => flatExercises.map((item) => item.id), [flatExercises]);\n\n  const flatExercisesComputed = useMemo(() => {\n    if (exercisesByMuscle.length === 0) return [];\n\n    const flat = exercisesByMuscle.flatMap((group) =>\n      group.exercises.map((exercise) => ({\n        id: exercise.id,\n        muscle: group.muscle,\n        exercise,\n      })),\n    );\n\n    if (exercisesOrder.length === 0) return flat;\n\n    const exerciseMap = new Map(flat.map((item) => [item.id, item]));\n    const orderedFlat = exercisesOrder.map((id) => exerciseMap.get(id)).filter(Boolean) as typeof flat;\n    const newExercises = flat.filter((item) => !exercisesOrder.includes(item.id));\n\n    return [...orderedFlat, ...newExercises];\n  }, [exercisesByMuscle, exercisesOrder]);\n\n  useEffect(() => {\n    setFlatExercises(flatExercisesComputed);\n  }, [flatExercisesComputed]);\n\n  const handleDragEnd = useCallback(\n    (event: DragEndEvent) => {\n      const { active, over } = event;\n      if (active.id !== over?.id) {\n        setFlatExercises((items) => {\n          const oldIndex = items.findIndex((item) => item.id === active.id);\n          const newIndex = items.findIndex((item) => item.id === over?.id);\n          const newOrder = arrayMove(items, oldIndex, newIndex);\n          setExercisesOrder(newOrder.map((item) => item.id));\n          return newOrder;\n        });\n      }\n    },\n    [setExercisesOrder],\n  );\n\n  if (isLoading) {\n    return (\n      <div className=\"space-y-6\">\n        <div className=\"text-center\">\n          <Loader2 className=\"h-8 w-8 animate-spin mx-auto text-blue-600\" />\n          <p className=\"mt-4 text-slate-600 dark:text-slate-400\">{t(\"workout_builder.loading.exercises\")}</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"space-y-6\">\n      {flatExercises.length > 0 ? (\n        <div className=\"max-w-4xl mx-auto\">\n          {/* Liste des exercices drag and drop */}\n          <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd} sensors={sensors}>\n            <SortableContext items={sortableItems} strategy={verticalListSortingStrategy}>\n              <div className=\"bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 overflow-hidden\">\n                {flatExercises.map((item) => (\n                  <ExerciseListItem\n                    exercise={item.exercise}\n                    isShuffling={shufflingExerciseId === item.exercise.id}\n                    key={item.id}\n                    muscle={item.muscle}\n                    onDelete={onDelete}\n                    onShuffle={onShuffle}\n                  />\n                ))}\n                <div className=\"border-t border-slate-200 dark:border-slate-800\">\n                  <button\n                    className=\"w-full flex items-center gap-3 py-4 px-4 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-950 transition-colors\"\n                    onClick={onAdd}\n                  >\n                    <div className=\"h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center\">\n                      <Plus className=\"h-4 w-4 text-white\" />\n                    </div>\n                    <span className=\"font-medium\">{t(\"commons.add\")}</span>\n                  </button>\n                </div>\n              </div>\n            </SortableContext>\n          </DndContext>\n        </div>\n      ) : error ? (\n        <div className=\"text-center py-20\">\n          <p className=\"text-red-600 dark:text-red-400\">{t(\"workout_builder.error.loading_exercises\")}</p>\n        </div>\n      ) : (\n        <div className=\"text-center py-20\">\n          <p className=\"text-slate-600 dark:text-slate-400\">{t(\"workout_builder.no_exercises_found\")}</p>\n        </div>\n      )}\n\n      {env.NEXT_PUBLIC_EXERCISE_SELECTION_BANNER_AD_SLOT && (\n        <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_EXERCISE_SELECTION_BANNER_AD_SLOT} />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/favorite-button.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/shared/lib/utils\";\nimport { StarButton } from \"@/components/ui/star-button\";\n\ninterface FavoriteButtonProps {\n  exerciseId: string;\n  isFavorite: boolean;\n  onToggle: (exerciseId: string) => void;\n  className?: string;\n}\n\nexport const FavoriteButton = ({ exerciseId, isFavorite, onToggle, className = \"\" }: FavoriteButtonProps) => {\n  const handleToggle = (e: React.MouseEvent) => {\n    e.stopPropagation();\n\n    try {\n      onToggle(exerciseId);\n    } catch (error) {\n      console.error(\"Failed to toggle favorite:\", error);\n    }\n  };\n\n  return (\n    <StarButton className={cn(className, \"btn-sm btn-link\")} isActive={isFavorite} isLoading={false} onClick={handleToggle}></StarButton>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/favorite-exercise-button.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport {\n  FAVORITE_EXERICSES_STORAGE_KEY,\n  favoriteExercisesLocal,\n  LocalFavoriteExercise,\n} from \"@/features/workout-builder/model/favorite-exercises.local\";\nimport { brandedToast } from \"@/components/ui/toast\";\nimport { StarButton } from \"@/components/ui/star-button\";\n\nimport { useSyncFavoriteExercises } from \"../hooks/use-sync-favorite-exercises\";\n\ninterface FavoriteExerciseButtonProps {\n  exerciseId: string;\n  className?: string;\n}\n\nexport function FavoriteExerciseButton({ exerciseId, className }: FavoriteExerciseButtonProps) {\n  const [isLoading, setIsLoading] = useState(false);\n  const [isFavorite, setIsFavorite] = useState(false);\n  const { syncFavoriteExercises } = useSyncFavoriteExercises();\n  const t = useI18n();\n  const text = isFavorite ? t(\"commons.remove_from_favorites\") : t(\"commons.add_to_favorites\");\n\n  useEffect(() => {\n    try {\n      const stored = localStorage.getItem(FAVORITE_EXERICSES_STORAGE_KEY);\n      if (!stored) {\n        setIsFavorite(false);\n        return;\n      }\n\n      const favorites: LocalFavoriteExercise[] = JSON.parse(stored);\n      setIsFavorite(favorites.some((f) => f.exerciseId === exerciseId && f.status !== \"deleteOnSync\"));\n    } catch {\n      setIsFavorite(false);\n    }\n  }, [exerciseId]);\n\n  function handleToggleFavorite() {\n    if (isLoading) return;\n\n    setIsLoading(true);\n    const newFavoriteState = !isFavorite;\n    // Update localStorage first\n    if (newFavoriteState) {\n      favoriteExercisesLocal.add(exerciseId); // status: \"local\"\n      setIsFavorite(true);\n      brandedToast({ title: t(\"commons.added_to_favorites\"), variant: \"success\" });\n    } else {\n      favoriteExercisesLocal.removeById(exerciseId);\n      setIsFavorite(false);\n    }\n    try {\n      syncFavoriteExercises();\n    } catch (error) {\n      console.error(\"Failed to favorite exercise:\", error);\n    } finally {\n      setIsLoading(false);\n    }\n  }\n\n  return (\n    <StarButton className={cn(\"mb-2\", className)} isActive={isFavorite} isLoading={isLoading} onClick={handleToggleFavorite}>\n      <span className=\"text-sm text-slate-500 dark:text-slate-400\">{text}</span>\n    </StarButton>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscle-selection.tsx",
    "content": "import React from \"react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { useI18n } from \"locales/client\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { TricepsGroup } from \"@/features/workout-builder/ui/muscles/triceps-group\";\nimport { TrapsGroup } from \"@/features/workout-builder/ui/muscles/traps-group\";\nimport { ShouldersGroup } from \"@/features/workout-builder/ui/muscles/shoulders-group\";\nimport { QuadricepsGroup } from \"@/features/workout-builder/ui/muscles/quadriceps-group\";\nimport { ObliquesGroup } from \"@/features/workout-builder/ui/muscles/obliques-group\";\nimport { HamstringsGroup } from \"@/features/workout-builder/ui/muscles/hamstrings-group\";\nimport { GlutesGroup } from \"@/features/workout-builder/ui/muscles/glutes-group\";\nimport { ForearmsGroup } from \"@/features/workout-builder/ui/muscles/forearms-group\";\nimport { ChestGroup } from \"@/features/workout-builder/ui/muscles/chest-group\";\nimport { CalvesGroup } from \"@/features/workout-builder/ui/muscles/calves-group\";\nimport { BicepsGroup } from \"@/features/workout-builder/ui/muscles/biceps-group\";\nimport { BackGroup } from \"@/features/workout-builder/ui/muscles/back-group\";\nimport { AbdominalsGroup } from \"@/features/workout-builder/ui/muscles/abdominals-group\";\nimport { env } from \"@/env\";\nimport { HorizontalBottomBanner } from \"@/components/ads\";\n\ninterface MuscleSelectionProps {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  selectedMuscles: ExerciseAttributeValueEnum[];\n  selectedEquipment: ExerciseAttributeValueEnum[];\n}\n\nconst MuscleIllustration = ({\n  selectedMuscles,\n  onToggleMuscle,\n}: {\n  selectedMuscles: ExerciseAttributeValueEnum[];\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  isLoading?: boolean;\n}) => {\n  const getMuscleClasses = (muscle: ExerciseAttributeValueEnum) => {\n    const isSelected = selectedMuscles.includes(muscle);\n    return cn(\n      \"cursor-pointer transition-all duration-100 ease-out\",\n      isSelected ? \"fill-blue-500 \" : \"fill-slate-400 group-hover:fill-blue-400\",\n    );\n  };\n\n  return (\n    <svg className=\"h-auto w-full\" id=\"muscle-illustration\" viewBox=\"0 0 535 462\" xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        d=\"M 440.43,458.85\n           C 437.78,458.84 435.54,458.42 433.79,457.57\n             433.79,457.57 432.77,457.08 432.77,457.08\n             432.77,457.08 432.29,456.06 432.29,456.06\n             429.18,449.45 429.63,444.89 430.30,439.24\n             429.97,436.78 429.59,433.93 429.27,431.10\n             429.27,431.10 429.25,430.98 429.25,430.98\n             428.99,428.69 428.70,426.16 428.97,423.54\n             428.97,423.54 430.15,416.10 430.15,416.10\n             430.20,415.87 430.23,415.67 430.27,415.49\n             430.34,415.04 430.43,414.58 430.58,414.08\n             430.76,413.50 430.96,412.92 431.15,412.34\n             431.38,411.68 431.61,411.02 431.80,410.35\n             432.46,408.08 432.58,404.08 432.05,401.63\n             431.46,398.91 431.03,396.56 430.61,394.29\n             430.13,391.71 429.68,389.26 429.02,386.31\n             429.02,386.31 427.59,379.94 427.59,379.94\n             427.24,379.48 426.97,379.08 426.82,378.82\n             424.91,375.48 423.82,372.30 423.47,369.10\n             422.88,363.54 423.44,358.04 425.21,352.31\n             425.63,350.94 426.02,349.76 426.38,348.67\n             427.27,345.99 427.90,344.08 428.23,341.57\n             428.23,341.57 424.73,337.14 424.73,337.14\n             421.29,329.16 420.04,324.01 418.59,318.04\n             418.18,316.36 417.75,314.60 417.25,312.68\n             415.13,304.55 413.57,295.76 413.26,290.28\n             412.99,285.26 412.82,279.27 412.65,273.48\n             412.49,267.48 412.31,261.26 412.02,256.13\n             411.96,255.21 412.21,251.29 412.83,242.29\n             413.03,239.31 413.26,235.94 413.27,235.46\n             413.27,235.46 413.26,232.25 413.26,232.25\n             413.24,231.90 413.24,231.59 413.27,231.28\n             413.37,230.35 413.75,229.52 414.31,228.86\n             413.21,227.71 412.25,226.53 411.42,225.28\n             410.59,226.53 409.63,227.71 408.53,228.86\n             409.09,229.51 409.47,230.34 409.56,231.27\n             409.60,231.65 409.59,231.99 409.58,232.24\n             409.58,232.24 409.57,235.54 409.57,235.54\n             409.58,235.94 409.81,239.31 410.01,242.29\n             410.63,251.30 410.88,255.21 410.82,256.13\n             410.53,261.26 410.35,267.48 410.19,273.49\n             410.02,279.28 409.85,285.26 409.58,290.28\n             409.27,295.76 407.71,304.55 405.59,312.68\n             405.09,314.60 404.66,316.36 404.25,318.04\n             402.80,324.01 401.55,329.16 398.11,337.14\n             398.11,337.14 394.61,341.57 394.61,341.57\n             394.94,344.08 395.57,345.99 396.46,348.67\n             396.82,349.76 397.21,350.94 397.63,352.31\n             399.40,358.05 399.96,363.54 399.37,369.10\n             399.02,372.30 397.93,375.47 396.02,378.82\n             395.87,379.08 395.60,379.48 395.25,379.94\n             395.25,379.94 393.82,386.31 393.82,386.31\n             393.16,389.26 392.71,391.71 392.23,394.29\n             391.81,396.56 391.38,398.91 390.79,401.63\n             390.26,404.08 390.38,408.08 391.04,410.35\n             391.24,411.03 391.46,411.69 391.69,412.36\n             391.89,412.93 392.08,413.51 392.26,414.08\n             392.41,414.58 392.50,415.04 392.57,415.49\n             392.60,415.67 392.64,415.87 392.69,416.10\n             392.69,416.10 393.87,423.54 393.87,423.54\n             394.14,426.16 393.85,428.69 393.58,430.98\n             393.58,430.98 393.57,431.10 393.57,431.10\n             393.25,433.93 392.87,436.78 392.54,439.24\n             393.21,444.89 393.66,449.45 390.55,456.06\n             390.55,456.06 390.07,457.08 390.07,457.08\n             390.07,457.08 389.05,457.57 389.05,457.57\n             387.29,458.42 385.06,458.85 382.41,458.85\n             378.89,458.85 372.81,457.91 370.46,455.35\n             369.83,454.66 369.30,453.95 368.80,453.27\n             368.33,452.63 367.89,452.04 367.45,451.58\n             366.24,450.33 364.39,449.83 362.25,449.25\n             359.42,448.48 356.20,447.61 353.69,444.74\n             351.08,441.77 350.35,438.06 351.70,434.56\n             353.12,430.87 356.77,427.74 360.79,426.76\n             361.75,426.53 363.03,426.42 364.82,426.42\n             367.03,426.42 369.59,426.60 371.42,426.74\n             371.42,426.74 371.35,425.99 371.35,425.99\n             371.35,425.92 371.34,425.84 371.32,425.76\n             371.27,425.37 371.19,424.78 371.27,424.10\n             371.33,423.55 371.39,423.01 371.45,422.47\n             371.64,420.71 371.84,418.89 372.19,417.07\n             372.82,413.78 372.65,410.28 371.66,406.05\n             369.54,396.98 367.35,387.61 364.43,378.60\n             364.28,378.13 364.12,377.66 363.96,377.18\n             362.95,374.19 361.81,370.81 361.83,367.13\n             361.83,367.13 361.84,365.60 361.84,365.60\n             361.46,364.54 361.29,363.55 361.22,362.73\n             361.05,360.72 360.98,355.97 361.25,353.51\n             361.58,350.53 362.49,347.56 363.37,344.68\n             363.66,343.75 363.95,342.81 364.22,341.85\n             365.91,335.96 367.31,333.12 369.24,329.19\n             369.38,328.90 369.53,328.60 369.68,328.30\n             369.68,328.30 369.55,327.91 369.55,327.91\n             369.46,327.66 369.36,327.42 369.27,327.17\n             368.97,326.43 368.60,325.50 368.47,324.44\n             368.47,324.44 368.40,323.94 368.40,323.94\n             368.11,321.74 367.81,319.46 367.88,317.11\n             367.94,314.90 368.09,312.70 368.24,310.57\n             368.41,308.11 368.57,305.79 368.60,303.45\n             368.60,303.45 368.60,303.12 368.60,303.12\n             368.60,303.12 366.65,292.27 366.65,292.27\n             366.65,292.27 366.52,291.72 366.52,291.72\n             366.36,290.97 366.19,290.19 366.07,289.37\n             365.69,286.78 365.29,284.19 364.89,281.60\n             363.91,275.26 362.90,268.71 362.23,262.20\n             361.47,254.97 361.31,247.65 361.15,240.57\n             361.15,240.57 361.12,239.63 361.12,239.63\n             361.03,235.34 361.16,230.64 361.54,224.82\n             361.82,220.62 362.42,216.44 362.98,212.89\n             363.33,210.59 364.59,208.94 366.42,208.24\n             366.35,207.37 365.99,201.68 365.99,201.68\n             365.99,201.68 365.94,201.52 365.94,201.52\n             365.76,201.00 365.42,200.05 365.65,198.91\n             365.65,198.91 365.90,197.61 365.90,197.61\n             366.52,194.48 367.17,191.24 367.95,188.06\n             368.59,185.51 369.36,182.87 370.67,180.40\n             369.83,179.41 369.40,178.03 369.65,176.77\n             370.45,172.93 371.22,169.45 372.02,166.13\n             372.37,164.66 373.41,163.58 374.74,163.22\n             373.81,161.21 373.08,159.25 372.44,157.42\n             370.79,152.66 370.06,147.80 369.34,143.11\n             369.17,141.97 368.89,137.01 368.89,137.01\n             368.89,137.01 366.80,141.65 366.18,142.79\n             364.50,145.87 362.77,149.06 360.86,152.17\n             360.20,153.25 359.32,154.18 358.41,155.02\n             358.66,155.60 358.79,156.22 358.77,156.80\n             358.76,157.15 358.76,157.51 358.76,157.87\n             358.77,159.20 358.78,160.70 358.28,162.23\n             357.71,163.99 356.91,165.56 356.12,167.08\n             355.87,167.57 355.62,168.06 355.38,168.55\n             355.11,169.09 354.81,169.63 354.51,170.16\n             354.09,170.90 353.70,171.59 353.50,172.20\n             351.03,179.67 347.88,187.22 344.13,194.63\n             341.73,199.36 339.22,204.13 336.79,208.73\n             335.29,211.59 333.79,214.44 332.30,217.30\n             331.59,218.67 330.61,219.30 329.78,219.59\n             329.78,221.61 329.74,223.64 329.69,225.61\n             329.69,225.61 329.65,227.42 329.65,227.42\n             330.01,230.24 328.97,232.77 328.13,234.81\n             327.99,235.16 327.85,235.50 327.71,235.84\n             326.87,238.00 325.92,240.26 324.35,242.28\n             324.28,242.47 324.20,242.66 324.09,242.86\n             324.09,242.86 323.32,244.31 323.32,244.31\n             322.53,245.81 321.71,247.37 320.83,248.88\n             319.78,250.69 318.78,252.19 317.75,253.49\n             316.77,254.74 315.13,255.19 313.93,255.19\n             313.65,255.19 313.37,255.16 313.10,255.11\n             311.82,257.69 309.89,259.06 307.37,259.20\n             307.27,259.20 307.16,259.21 307.07,259.21\n             305.84,259.21 304.95,258.81 304.34,258.36\n             304.27,258.40 304.21,258.44 304.15,258.47\n             303.50,258.86 302.70,259.07 301.85,259.07\n             301.23,259.07 299.67,258.95 298.52,257.84\n             297.34,256.69 297.07,255.13 296.91,254.20\n             296.79,253.51 296.87,252.90 297.02,252.40\n             296.12,251.80 295.24,250.23 295.19,250.15\n             295.19,250.15 294.82,247.24 294.82,247.24\n             294.97,246.83 295.10,246.45 295.23,246.07\n             295.52,245.19 295.83,244.27 296.29,243.36\n             297.38,241.20 298.53,239.05 299.64,236.96\n             299.64,236.96 300.19,235.94 300.19,235.94\n             299.24,236.16 296.85,236.66 296.85,236.66\n             296.29,236.78 295.74,236.84 295.20,236.84\n             292.96,236.84 290.96,235.81 289.56,233.96\n             289.19,233.47 288.07,232.00 288.56,230.13\n             289.04,228.27 290.68,227.54 291.30,227.27\n             294.10,226.03 296.75,224.86 298.35,222.67\n             298.92,221.88 299.53,221.15 300.11,220.45\n             300.11,220.45 300.65,219.79 300.65,219.79\n             302.63,217.39 305.43,214.40 309.66,212.65\n             310.13,212.45 310.57,212.33 310.95,212.23\n             311.01,211.85 311.12,211.46 311.27,211.04\n             311.86,209.45 312.45,207.85 313.04,206.25\n             314.68,201.80 316.39,197.20 317.97,192.66\n             318.59,190.88 319.08,189.06 319.61,187.14\n             319.91,186.03 321.31,182.33 321.61,181.97\n             321.61,181.97 323.95,178.73 323.95,178.73\n             323.95,178.73 324.73,171.29 324.73,171.29\n             324.89,161.95 329.32,154.48 334.67,146.37\n             334.67,146.37 336.38,144.91 336.38,144.91\n             336.46,140.43 337.28,136.47 338.88,132.80\n             339.06,132.39 339.27,132.01 339.51,131.68\n             339.30,131.26 339.12,130.82 338.98,130.34\n             338.74,129.48 338.51,128.44 338.69,127.29\n             338.77,126.78 338.85,126.27 338.92,125.77\n             339.20,123.89 339.48,121.94 340.11,119.98\n             340.84,117.67 341.82,115.48 342.76,113.35\n             343.01,112.78 343.27,112.20 343.52,111.63\n             343.95,110.63 344.52,109.96 345.12,109.50\n             345.10,109.21 345.10,108.90 345.14,108.57\n             345.22,107.88 345.29,107.19 345.36,106.49\n             345.51,104.90 345.67,103.26 345.97,101.60\n             347.09,95.33 349.20,90.71 352.60,87.09\n             356.99,82.42 362.56,79.00 369.63,76.62\n             370.42,76.35 372.80,75.76 372.80,75.76\n             372.80,75.76 375.20,75.16 375.20,75.16\n             376.28,74.89 377.36,74.60 378.43,74.32\n             379.52,74.02 380.63,73.73 381.75,73.45\n             381.75,73.45 386.64,70.83 386.64,70.83\n             386.87,70.70 387.06,70.61 387.24,70.50\n             387.24,70.50 388.87,69.53 388.87,69.53\n             390.27,68.70 391.72,67.84 393.09,66.96\n             393.88,66.46 394.82,65.55 395.08,63.05\n             395.23,61.62 395.35,60.16 395.46,58.71\n             394.14,57.92 392.38,56.48 392.07,53.83\n             391.95,52.84 391.86,51.86 391.76,50.87\n             391.66,49.76 391.55,48.64 391.41,47.53\n             391.41,47.53 391.29,46.62 391.29,46.62\n             390.27,46.40 389.18,45.89 388.47,44.92\n             387.72,43.91 387.21,42.83 386.76,41.88\n             386.76,41.88 386.66,41.66 386.66,41.66\n             385.89,40.01 385.15,38.36 384.41,36.70\n             384.41,36.70 384.30,36.43 384.30,36.43\n             383.56,34.78 383.81,32.70 384.96,30.88\n             385.90,29.39 387.22,28.42 388.66,28.13\n             388.62,27.18 388.62,26.26 388.66,25.36\n             388.96,17.39 391.51,11.24 396.44,6.58\n             399.81,3.39 404.04,1.45 409.00,0.83\n             409.92,0.72 410.86,0.66 411.79,0.66\n             422.46,0.66 431.53,8.09 433.84,18.74\n             434.48,21.68 434.75,24.76 434.65,28.09\n             434.65,28.09 434.83,28.16 434.83,28.16\n             437.41,28.85 439.72,32.08 439.66,34.39\n             439.66,34.39 439.65,34.86 439.65,34.86\n             439.65,34.86 439.50,35.30 439.50,35.30\n             439.42,35.60 439.30,35.99 439.10,36.41\n             437.99,38.79 436.87,41.16 435.74,43.52\n             435.50,44.07 435.08,45.05 433.99,45.69\n             433.39,46.03 432.81,46.24 432.35,46.40\n             432.27,46.43 432.19,46.46 432.11,46.49\n             432.11,46.49 431.86,48.26 431.86,48.26\n             431.75,49.06 431.67,49.86 431.58,50.66\n             431.45,51.89 431.31,53.16 431.08,54.44\n             430.64,56.91 428.82,58.21 427.53,58.96\n             427.61,60.09 427.69,61.22 427.75,62.35\n             427.84,64.50 428.62,65.92 430.26,66.94\n             431.65,67.81 433.07,68.64 434.49,69.48\n             435.14,69.86 435.79,70.24 436.45,70.63\n             436.45,70.63 440.83,73.29 440.83,73.29\n             442.35,73.68 443.85,74.12 445.31,74.55\n             446.16,74.80 447.00,75.04 447.85,75.28\n             447.85,75.28 450.38,76.00 450.38,76.00\n             450.38,76.00 452.00,76.37 452.73,76.62\n             459.80,79.00 465.37,82.42 469.76,87.09\n             473.16,90.71 475.27,95.33 476.39,101.60\n             476.69,103.26 476.85,104.89 477.00,106.48\n             477.07,107.18 477.26,109.21 477.24,109.50\n             477.84,109.96 478.41,110.63 478.84,111.63\n             479.09,112.21 479.35,112.78 479.61,113.36\n             480.55,115.48 481.52,117.68 482.25,119.98\n             482.88,121.94 483.16,123.88 483.44,125.76\n             483.51,126.27 483.59,126.78 483.67,127.29\n             483.85,128.44 483.62,129.48 483.38,130.33\n             483.24,130.82 483.06,131.26 482.85,131.68\n             483.09,132.02 483.30,132.39 483.48,132.80\n             485.08,136.47 485.90,140.43 485.98,144.91\n             485.98,144.91 487.69,146.37 487.69,146.37\n             493.04,154.48 497.47,161.95 497.63,171.29\n             497.63,171.29 497.66,173.18 497.66,173.18\n             497.66,173.18 498.41,178.75 498.41,178.75\n             498.41,178.75 502.45,186.03 502.75,187.15\n             503.28,189.07 503.77,190.88 504.39,192.66\n             505.98,197.20 507.68,201.81 509.33,206.27\n             509.92,207.86 510.50,209.45 511.09,211.04\n             511.24,211.46 511.35,211.85 511.41,212.23\n             511.79,212.33 512.23,212.45 512.70,212.65\n             516.93,214.40 519.74,217.39 521.71,219.79\n             521.71,219.79 522.25,220.45 522.25,220.45\n             522.84,221.15 523.44,221.88 524.01,222.67\n             525.61,224.86 528.26,226.03 531.06,227.27\n             531.68,227.54 533.32,228.27 533.80,230.13\n             534.29,232.00 533.17,233.47 532.80,233.96\n             531.40,235.81 529.40,236.84 527.16,236.84\n             527.16,236.84 527.16,236.84 527.16,236.84\n             526.62,236.84 526.07,236.78 525.51,236.66\n             525.51,236.66 525.00,236.55 525.00,236.55\n             524.07,236.36 523.13,236.16 522.17,235.94\n             522.17,235.94 522.75,237.02 522.75,237.02\n             523.85,239.08 524.99,241.22 526.07,243.36\n             526.53,244.27 526.84,245.19 527.14,246.08\n             527.27,246.45 527.39,246.84 527.54,247.24\n             527.54,247.24 527.07,250.32 527.02,250.41\n             526.71,250.95 526.24,251.79 525.35,252.40\n             525.49,252.89 525.57,253.49 525.46,254.15\n             525.29,255.12 525.03,256.69 523.83,257.84\n             522.70,258.95 521.13,259.07 520.51,259.07\n             519.66,259.07 518.86,258.86 518.21,258.48\n             518.15,258.44 518.09,258.40 518.02,258.36\n             517.41,258.81 516.52,259.21 515.29,259.21\n             515.20,259.21 515.09,259.20 514.99,259.20\n             512.47,259.06 510.54,257.69 509.26,255.11\n             508.99,255.16 508.71,255.19 508.43,255.19\n             507.23,255.19 505.60,254.74 504.61,253.49\n             503.58,252.19 502.58,250.69 501.53,248.88\n             500.64,247.36 499.82,245.80 499.03,244.29\n             499.03,244.29 498.28,242.87 498.28,242.87\n             498.16,242.66 498.08,242.46 498.01,242.28\n             496.44,240.26 495.49,238.00 494.64,235.84\n             494.51,235.50 494.37,235.16 494.23,234.81\n             493.39,232.77 492.35,230.24 492.71,227.42\n             492.71,227.42 492.67,225.67 492.67,225.67\n             492.63,223.68 492.58,221.63 492.58,219.59\n             491.75,219.30 490.77,218.67 490.06,217.31\n             488.56,214.42 487.05,211.55 485.54,208.68\n             483.12,204.09 480.62,199.34 478.24,194.63\n             474.48,187.22 471.33,179.67 468.86,172.20\n             468.66,171.59 468.27,170.90 467.85,170.16\n             467.55,169.63 467.25,169.09 466.98,168.55\n             466.74,168.06 466.49,167.57 466.24,167.09\n             465.46,165.56 464.65,163.99 464.08,162.23\n             463.59,160.69 463.59,159.20 463.60,157.87\n             463.60,157.51 463.60,157.15 463.59,156.80\n             463.57,156.22 463.70,155.60 463.95,155.02\n             463.04,154.18 462.16,153.25 461.50,152.17\n             459.59,149.05 457.85,145.86 456.17,142.77\n             455.55,141.63 454.23,139.22 454.20,139.15\n             454.20,139.15 453.91,141.76 453.80,142.61\n             452.65,151.14 450.93,157.54 448.25,163.26\n             449.50,163.66 450.48,164.72 450.82,166.13\n             451.62,169.44 452.39,172.92 453.18,176.77\n             453.44,178.02 453.01,179.41 452.17,180.40\n             453.48,182.87 454.25,185.51 454.88,188.06\n             455.67,191.24 456.32,194.48 456.94,197.61\n             456.94,197.61 457.19,198.91 457.19,198.91\n             457.42,200.05 457.08,201.01 456.90,201.52\n             456.90,201.52 456.84,201.69 456.84,201.69\n             456.84,201.69 456.49,207.37 456.42,208.24\n             458.25,208.94 459.51,210.59 459.86,212.89\n             460.42,216.44 461.02,220.63 461.30,224.83\n             461.68,230.64 461.81,235.34 461.72,239.63\n             461.72,239.63 461.69,240.56 461.69,240.56\n             461.53,247.65 461.37,254.97 460.61,262.20\n             459.94,268.71 458.93,275.26 457.95,281.60\n             457.55,284.19 457.15,286.78 456.77,289.37\n             456.65,290.20 456.48,290.97 456.31,291.72\n             456.31,291.72 456.19,292.27 456.19,292.27\n             456.19,292.27 454.24,303.12 454.24,303.12\n             454.24,303.12 454.24,303.44 454.24,303.44\n             454.27,305.80 454.43,308.12 454.60,310.58\n             454.75,312.71 454.90,314.91 454.96,317.11\n             455.03,319.46 454.73,321.73 454.44,323.92\n             454.44,323.92 454.37,324.44 454.37,324.44\n             454.24,325.50 453.87,326.43 453.57,327.17\n             453.47,327.42 453.38,327.66 453.29,327.91\n             453.29,327.91 453.16,328.30 453.16,328.30\n             453.31,328.60 453.45,328.90 453.60,329.19\n             455.53,333.12 456.92,335.96 458.61,341.85\n             458.89,342.81 459.18,343.75 459.47,344.68\n             460.35,347.56 461.26,350.53 461.59,353.51\n             461.86,355.97 461.79,360.73 461.62,362.73\n             461.55,363.55 461.37,364.55 461.00,365.60\n             461.00,365.60 461.01,367.13 461.01,367.13\n             461.03,370.80 459.89,374.18 458.89,377.17\n             458.72,377.65 458.56,378.13 458.41,378.60\n             455.49,387.61 453.29,396.98 451.18,406.05\n             450.19,410.28 450.02,413.78 450.65,417.07\n             451.00,418.89 451.20,420.71 451.39,422.47\n             451.45,423.01 451.51,423.55 451.57,424.09\n             451.65,424.78 451.57,425.37 451.51,425.76\n             451.50,425.84 451.49,425.92 451.49,425.99\n             451.49,425.99 451.42,426.74 451.42,426.74\n             453.25,426.60 455.81,426.42 458.02,426.42\n             459.81,426.42 461.09,426.53 462.05,426.77\n             466.07,427.74 469.72,430.87 471.14,434.56\n             472.48,438.06 471.76,441.77 469.15,444.74\n             466.64,447.61 463.42,448.48 460.59,449.25\n             458.45,449.83 456.60,450.33 455.39,451.58\n             454.95,452.04 454.51,452.63 454.04,453.27\n             453.53,453.95 453.01,454.66 452.38,455.35\n             450.03,457.91 443.94,458.85 440.43,458.85\n             440.43,458.85 440.43,458.85 440.43,458.85\n             440.43,458.85 440.43,458.85 440.43,458.85 Z\"\n        fill=\"#f5f5f5\"\n        id=\"path14\"\n        stroke=\"black\"\n        strokeWidth=\"1\"\n      />\n\n      <path\n        d=\"M 389.54,40.30\n           C 389.99,41.24 390.42,42.20 391.03,43.03\n             391.30,43.39 392.00,43.57 392.50,43.55\n             392.50,43.55 394.11,43.55 394.11,43.55\n             394.27,44.74 394.42,45.94 394.57,47.13\n             394.84,49.23 394.98,51.35 395.23,53.45\n             395.41,54.97 396.50,55.70 397.88,56.40\n             397.97,55.89 398.04,55.52 398.10,55.16\n             398.48,52.88 398.72,50.57 399.25,48.32\n             399.97,45.30 401.03,42.32 403.65,40.38\n             408.17,37.03 413.00,36.85 418.05,39.24\n             420.66,40.48 422.15,42.66 423.07,45.18\n             423.91,47.51 424.36,50.00 424.90,52.44\n             425.17,53.70 425.25,55.00 425.44,56.48\n             426.69,55.79 427.72,55.12 427.95,53.88\n             428.31,51.88 428.42,49.83 428.71,47.81\n             428.91,46.40 429.10,44.98 429.31,43.56\n             429.31,43.56 430.82,43.57 430.82,43.57\n             431.35,43.36 431.91,43.21 432.39,42.93\n             432.62,42.80 432.73,42.43 432.86,42.15\n             433.99,39.80 435.11,37.43 436.22,35.07\n             436.35,34.79 436.41,34.49 436.47,34.30\n             436.50,33.26 434.84,31.27 433.88,31.22\n             433.88,31.22 431.34,30.16 431.34,30.16\n             431.34,30.16 431.34,30.19 431.34,30.19\n             431.62,26.61 431.52,23.03 430.73,19.42\n             428.56,9.44 419.37,2.74 409.39,3.99\n             405.31,4.51 401.65,6.04 398.62,8.90\n             393.87,13.39 392.08,19.15 391.84,25.48\n             391.78,27.17 391.85,28.85 392.01,30.52\n             392.01,30.52 389.55,31.22 389.55,31.22\n             388.06,31.26 386.60,33.77 387.21,35.14\n             387.98,36.86 388.74,38.59 389.54,40.30\"\n        fill=\"#757575\"\n        id=\"path34\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        d=\"M 386.48,416.75\n           C 384.13,415.88 379.40,417.50 378.57,419.80\n             378.09,421.14 377.53,422.44 376.94,423.74\n             376.94,423.74 376.92,423.73 376.92,423.73\n             375.29,427.30 375.14,427.42 373.23,430.09\n             371.54,429.96 364.14,429.23 361.54,429.86\n             358.67,430.56 355.77,432.86 354.68,435.70\n             353.85,437.86 354.05,440.32 356.08,442.64\n             359.75,446.82 366.01,445.50 369.74,449.37\n             370.85,450.52 371.72,452.01 372.81,453.19\n             374.55,455.09 383.34,456.78 387.67,454.70\n             390.42,448.85 390.02,445.04 389.33,439.21\n             389.71,436.39 390.08,433.57 390.41,430.74\n             390.74,427.81 391.09,424.87 390.37,421.93\n             389.82,419.70 388.93,417.66 386.48,416.75\"\n        fill=\"#757575\"\n        id=\"path52\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        d=\"M 461.30,429.86\n           C 458.70,429.23 451.30,429.96 449.61,430.09\n             447.70,427.42 447.55,427.30 445.92,423.73\n             445.92,423.73 445.90,423.74 445.90,423.74\n             445.31,422.44 444.75,421.14 444.27,419.80\n             443.44,417.50 438.71,415.88 436.36,416.75\n             433.91,417.66 433.02,419.70 432.47,421.93\n             431.75,424.87 432.10,427.81 432.43,430.74\n             432.76,433.57 433.13,436.39 433.51,439.21\n             432.82,445.04 432.42,448.85 435.17,454.70\n             439.50,456.78 448.29,455.09 450.03,453.19\n             451.12,452.01 451.98,450.52 453.10,449.37\n             456.83,445.50 463.09,446.82 466.76,442.64\n             468.79,440.32 468.99,437.86 468.16,435.70\n             467.07,432.86 464.17,430.56 461.30,429.86\"\n        fill=\"#757575\"\n        id=\"path72\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        d=\"M 529.77,230.19\n           C 526.63,228.79 523.50,227.38 521.44,224.54\n             520.75,223.60 519.98,222.71 519.24,221.81\n             517.12,219.22 514.71,216.93 511.48,215.59\n             511.01,215.40 510.50,215.30 510.01,215.16\n             510.01,215.16 505.16,214.72 505.16,214.72\n             505.16,214.72 505.17,214.75 505.20,214.80\n             504.43,214.75 503.63,214.85 502.85,215.14\n             500.67,215.97 498.57,217.00 496.48,218.03\n             496.13,218.20 495.77,218.78 495.77,219.18\n             495.76,221.99 495.84,224.80 495.90,227.61\n             495.50,230.06 496.73,232.42 497.62,234.68\n             498.51,236.98 499.40,239.01 500.84,240.70\n             500.93,240.98 500.99,241.19 501.09,241.37\n             502.14,243.35 503.16,245.35 504.28,247.28\n             505.14,248.75 506.06,250.19 507.11,251.52\n             507.41,251.90 508.34,252.11 508.83,251.94\n             509.61,251.68 509.27,250.91 509.01,250.35\n             508.14,248.50 507.14,246.70 506.37,244.81\n             506.25,244.50 506.09,244.21 505.93,243.94\n             506.29,243.72 506.68,243.36 507.10,242.77\n             507.55,243.74 508.03,244.70 508.47,245.68\n             509.64,248.24 510.81,250.80 511.96,253.37\n             512.59,254.78 513.43,255.92 515.16,256.02\n             516.13,256.07 516.63,255.59 516.61,254.47\n             516.54,254.27 516.43,253.90 516.27,253.55\n             514.90,250.53 513.57,247.49 512.13,244.50\n             511.52,243.23 510.83,242.00 510.12,240.78\n             510.35,240.51 510.62,240.23 510.91,239.95\n             511.52,241.10 512.12,242.25 512.70,243.42\n             514.42,246.87 516.06,250.36 517.86,253.77\n             518.27,254.57 519.06,255.27 519.84,255.74\n             520.27,255.99 521.27,255.89 521.62,255.55\n             522.07,255.11 522.20,254.29 522.32,253.62\n             522.37,253.29 522.12,252.88 521.94,252.54\n             519.32,247.38 516.71,242.22 514.06,237.08\n             514.29,236.85 514.49,236.61 514.66,236.38\n             517.06,240.60 519.47,244.81 521.91,249.00\n             522.18,249.45 523.08,249.98 523.41,249.84\n             523.92,249.63 524.19,248.86 524.55,248.34\n             524.06,247.01 523.75,245.85 523.22,244.79\n             521.78,241.93 520.25,239.12 518.75,236.29\n             517.96,234.80 517.17,233.31 516.37,231.83\n             516.34,231.72 516.30,231.60 516.25,231.47\n             516.72,230.53 517.73,230.23 517.73,230.23\n             518.19,230.97 519.03,231.73 519.86,232.00\n             521.91,232.67 524.05,233.10 526.17,233.54\n             527.85,233.90 529.25,233.38 530.26,232.04\n             531.00,231.06 530.90,230.69 529.77,230.19\"\n        fill=\"#757575\"\n        id=\"path76\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        d=\"M 325.88,218.03\n           C 323.79,217.00 321.69,215.97 319.51,215.14\n             318.73,214.85 317.94,214.75 317.16,214.80\n             317.19,214.75 317.20,214.72 317.20,214.72\n             317.20,214.72 312.35,215.16 312.35,215.16\n             311.86,215.30 311.35,215.40 310.88,215.59\n             307.65,216.93 305.24,219.22 303.12,221.81\n             302.38,222.71 301.61,223.60 300.92,224.54\n             298.86,227.38 295.73,228.79 292.59,230.19\n             291.46,230.69 291.36,231.06 292.10,232.04\n             293.11,233.38 294.52,233.90 296.19,233.54\n             298.31,233.10 300.45,232.67 302.50,232.00\n             303.33,231.73 304.17,230.97 304.63,230.23\n             304.63,230.23 305.64,230.53 306.11,231.47\n             306.06,231.60 306.02,231.72 306.00,231.83\n             305.19,233.31 304.40,234.80 303.61,236.29\n             302.11,239.12 300.58,241.93 299.14,244.79\n             298.61,245.85 298.30,247.01 297.81,248.34\n             298.17,248.86 298.44,249.63 298.95,249.84\n             299.28,249.98 300.18,249.45 300.45,249.00\n             302.90,244.81 305.30,240.60 307.70,236.38\n             307.87,236.61 308.08,236.85 308.30,237.08\n             305.65,242.22 303.04,247.38 300.42,252.54\n             300.25,252.88 299.99,253.29 300.04,253.62\n             300.16,254.29 300.29,255.11 300.74,255.55\n             301.09,255.89 302.09,255.99 302.52,255.74\n             303.30,255.27 304.09,254.57 304.51,253.77\n             306.30,250.36 307.94,246.87 309.66,243.42\n             310.24,242.25 310.84,241.10 311.45,239.95\n             311.74,240.23 312.01,240.51 312.24,240.78\n             311.53,242.00 310.84,243.23 310.23,244.50\n             308.79,247.49 307.46,250.53 306.09,253.55\n             305.93,253.90 305.82,254.27 305.75,254.47\n             305.73,255.59 306.23,256.07 307.20,256.02\n             308.93,255.92 309.77,254.78 310.40,253.37\n             311.55,250.80 312.72,248.24 313.89,245.68\n             314.33,244.70 314.81,243.74 315.26,242.77\n             315.68,243.36 316.07,243.72 316.43,243.94\n             316.27,244.21 316.11,244.50 315.99,244.81\n             315.23,246.70 314.22,248.50 313.35,250.35\n             313.09,250.91 312.75,251.68 313.53,251.94\n             314.02,252.11 314.95,251.90 315.25,251.52\n             316.30,250.19 317.23,248.75 318.08,247.28\n             319.20,245.35 320.22,243.35 321.27,241.37\n             321.37,241.19 321.43,240.98 321.52,240.70\n             322.96,239.01 323.85,236.98 324.75,234.68\n             325.63,232.42 326.86,230.06 326.46,227.61\n             326.52,224.80 326.60,221.99 326.60,219.18\n             326.59,218.78 326.23,218.20 325.88,218.03\"\n        fill=\"#757575\"\n        id=\"path90\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        d=\"M 163.05,461.45\n           C 158.90,461.45 155.08,459.99 151.36,456.99\n             151.06,456.75 150.77,456.51 150.48,456.27\n             149.03,455.08 147.89,454.14 146.36,453.53\n             145.99,453.39 145.59,453.25 145.18,453.11\n             142.74,452.27 139.05,450.99 138.04,446.97\n             138.04,446.97 136.56,441.10 136.56,441.10\n             136.56,441.10 136.56,441.04 136.56,441.04\n             136.43,440.61 136.30,440.09 136.25,439.49\n             136.05,437.10 135.88,434.69 135.71,432.29\n             135.71,432.29 135.58,430.46 135.58,430.46\n             135.55,430.00 135.55,429.54 135.57,429.10\n             135.57,429.10 135.05,425.79 135.05,425.79\n             135.02,425.61 134.99,425.46 134.96,425.33\n             134.85,424.84 134.70,424.16 134.84,423.34\n             135.01,422.35 135.15,421.36 135.29,420.37\n             135.66,417.75 136.04,415.04 136.87,412.38\n             137.93,408.99 138.14,403.76 137.37,399.93\n             136.36,394.90 135.10,391.41 133.76,387.71\n             132.45,384.07 131.09,380.30 129.97,375.00\n             128.82,369.56 128.75,362.93 129.78,358.12\n             130.39,355.25 130.84,352.69 131.31,349.99\n             131.55,348.61 131.79,347.20 132.07,345.71\n             132.08,345.44 132.09,344.90 132.11,344.22\n             132.30,338.18 132.39,337.65 132.65,336.99\n             132.65,336.99 132.50,337.00 132.50,337.00\n             132.50,337.00 131.56,334.88 131.56,334.88\n             127.35,325.02 124.94,314.33 124.41,303.10\n             124.05,295.50 124.41,288.12 125.49,281.04\n             125.49,281.04 125.38,280.94 125.38,280.94\n             124.57,280.23 124.08,279.32 123.90,278.25\n             123.70,277.05 123.23,275.13 122.77,273.28\n             122.20,270.98 121.62,268.61 121.37,266.92\n             119.41,257.83 119.13,255.58 118.30,248.97\n             118.30,248.97 118.04,246.88 118.04,246.88\n             118.04,246.88 117.91,245.80 117.91,245.80\n             117.05,238.57 116.84,236.85 116.84,232.26\n             116.84,232.26 116.84,231.75 116.84,231.75\n             116.84,229.80 117.00,227.86 117.57,225.52\n             117.57,225.52 113.70,225.61 112.64,225.59\n             113.12,227.17 113.12,229.16 113.12,231.75\n             113.12,231.75 113.12,232.26 113.12,232.26\n             113.12,236.86 112.92,238.58 112.05,245.83\n             112.05,245.83 111.93,246.85 111.93,246.85\n             111.93,246.85 111.66,248.97 111.66,248.97\n             110.83,255.58 110.55,257.83 108.59,266.92\n             108.35,268.61 107.76,270.98 107.19,273.28\n             106.74,275.13 106.26,277.04 106.07,278.25\n             105.89,279.32 105.39,280.23 104.59,280.94\n             104.59,280.94 104.47,281.04 104.47,281.04\n             105.55,288.12 105.92,295.50 105.56,303.10\n             105.02,314.33 102.62,325.02 98.41,334.88\n             98.41,334.88 97.49,337.01 97.49,337.01\n             97.49,337.01 97.32,336.99 97.32,336.99\n             97.58,337.65 97.66,338.19 97.85,344.23\n             97.87,344.90 97.89,345.44 97.90,345.71\n             98.17,347.20 98.42,348.60 98.66,349.98\n             99.13,352.69 99.57,355.24 100.19,358.12\n             101.22,362.93 101.14,369.56 99.99,375.00\n             98.88,380.30 97.52,384.07 96.20,387.72\n             94.87,391.41 93.61,394.90 92.59,399.93\n             91.82,403.76 92.03,408.99 93.09,412.38\n             93.92,415.04 94.31,417.75 94.68,420.37\n             94.82,421.36 94.96,422.35 95.12,423.34\n             95.26,424.16 95.11,424.84 95.01,425.33\n             94.98,425.46 94.94,425.61 94.92,425.79\n             94.92,425.79 94.39,429.10 94.39,429.10\n             94.42,429.54 94.41,430.00 94.38,430.46\n             94.38,430.46 94.25,432.26 94.25,432.26\n             94.09,434.67 93.92,437.09 93.72,439.49\n             93.67,440.09 93.53,440.62 93.41,441.04\n             93.41,441.04 93.40,441.10 93.40,441.10\n             93.40,441.10 91.93,446.97 91.93,446.97\n             90.92,450.99 87.23,452.27 84.78,453.11\n             84.37,453.25 83.98,453.39 83.60,453.53\n             82.07,454.14 80.93,455.08 79.49,456.26\n             79.20,456.50 78.90,456.75 78.60,456.99\n             74.89,459.99 71.06,461.45 66.91,461.45\n             66.91,461.45 66.91,461.45 66.91,461.45\n             62.30,461.45 57.87,459.65 53.36,457.65\n             51.97,457.03 51.03,455.92 50.70,454.54\n             50.07,451.95 51.81,449.36 53.18,447.64\n             55.51,444.70 59.13,441.56 60.10,440.96\n             64.38,438.31 67.08,435.83 70.42,431.46\n             70.42,431.46 70.53,431.31 70.53,431.31\n             70.53,431.31 73.05,428.71 73.05,428.71\n             73.05,428.71 74.82,418.14 74.82,418.14\n             74.89,416.51 75.10,414.93 75.30,413.39\n             75.30,413.39 75.44,412.31 75.44,412.31\n             75.54,411.50 75.96,410.61 76.65,409.95\n             75.30,407.35 74.14,404.74 73.50,402.85\n             70.62,394.24 69.85,389.19 68.69,381.56\n             68.47,380.09 68.23,378.51 67.96,376.80\n             66.68,368.76 67.11,358.60 67.94,351.77\n             68.90,343.90 71.05,336.53 74.36,329.77\n             74.36,329.77 74.34,325.66 74.34,325.66\n             74.33,324.38 74.33,323.13 74.33,321.89\n             74.33,319.17 74.32,316.61 74.26,314.03\n             74.19,311.01 74.09,307.96 73.97,304.93\n             73.97,304.93 65.22,275.79 65.22,275.79\n             64.91,273.62 64.60,271.72 64.32,269.97\n             63.56,265.16 62.95,261.37 62.66,256.01\n             62.43,251.78 63.10,246.73 63.75,241.85\n             64.10,239.27 64.42,236.83 64.62,234.52\n             65.47,224.55 68.64,213.50 71.44,203.75\n             71.44,203.75 71.97,201.87 71.97,201.87\n             72.52,199.97 73.52,197.54 74.00,195.83\n             74.27,194.89 76.33,189.87 76.33,189.87\n             75.47,186.51 75.36,182.81 75.97,178.48\n             76.31,176.06 76.82,173.67 77.31,171.37\n             77.46,170.65 77.61,169.93 77.76,169.21\n             77.90,168.51 78.14,167.94 78.34,167.48\n             78.40,167.35 78.45,167.22 78.50,167.08\n             78.50,167.08 78.62,166.77 78.62,166.77\n             78.62,166.77 77.68,165.62 77.68,165.62\n             76.05,163.64 75.24,161.28 75.19,158.41\n             75.19,158.41 75.19,158.28 75.19,158.28\n             75.19,158.28 74.49,150.88 74.49,150.88\n             74.49,150.88 74.49,150.84 74.49,150.84\n             74.40,149.94 74.29,148.97 74.18,147.99\n             73.93,145.80 73.68,143.53 73.58,141.65\n             73.53,140.56 73.48,139.47 73.46,138.37\n             72.65,140.70 71.62,142.92 70.00,146.33\n             69.53,147.32 68.58,148.85 67.48,150.61\n             67.24,151.00 67.02,151.35 66.86,151.61\n             66.63,151.99 66.35,152.39 66.01,152.77\n             66.22,153.20 66.33,153.58 66.39,153.78\n             67.35,157.36 66.89,161.09 65.06,164.57\n             63.93,166.71 63.47,168.82 62.94,171.26\n             62.55,173.00 62.16,174.80 61.49,176.74\n             59.21,183.31 56.18,190.91 53.24,198.14\n             51.63,202.11 49.70,205.84 47.84,209.46\n             46.07,212.88 44.25,216.42 42.76,220.05\n             42.58,220.47 42.40,220.89 42.22,221.31\n             42.22,221.31 40.97,224.18 40.97,224.18\n             40.97,224.18 40.86,224.15 40.86,224.15\n             42.64,229.65 41.85,233.96 38.26,238.47\n             38.26,238.47 33.71,244.53 33.71,244.53\n             32.23,247.47 30.43,250.80 27.94,253.73\n             27.61,254.13 26.81,255.07 25.42,255.23\n             25.03,255.28 24.64,255.32 24.26,255.32\n             23.95,255.32 23.66,255.29 23.38,255.25\n             23.17,255.69 22.95,256.14 22.72,256.58\n             21.69,258.60 19.72,259.76 17.33,259.76\n             17.33,259.76 17.32,259.76 17.32,259.76\n             16.19,259.76 15.18,259.44 14.41,258.84\n             14.41,258.84 13.65,259.44 13.65,259.44\n             13.65,259.44 12.50,259.44 12.50,259.44\n             10.61,259.44 8.93,258.55 8.00,257.06\n             7.20,255.78 7.04,254.21 7.52,252.75\n             6.04,251.73 5.18,250.30 5.08,248.72\n             4.93,246.18 6.25,244.26 7.21,242.86\n             7.51,242.44 7.78,242.04 7.97,241.70\n             9.20,239.43 10.49,237.15 11.75,234.94\n             11.18,235.07 7.80,235.81 7.80,235.81\n             7.80,235.81 7.22,235.72 7.22,235.72\n             4.47,235.29 2.54,233.97 1.46,231.82\n             0.76,230.40 1.22,228.88 1.38,228.45\n             1.83,227.21 2.66,226.64 3.27,226.38\n             3.27,226.38 4.35,225.91 4.35,225.91\n             5.50,225.41 6.65,224.91 7.81,224.43\n             7.81,224.43 9.93,221.82 9.93,221.82\n             9.93,221.82 11.93,219.82 11.93,219.82\n             13.89,217.86 15.92,215.82 17.99,213.87\n             18.93,212.99 20.04,212.28 21.27,211.75\n             21.27,211.75 22.88,207.80 23.04,207.47\n             27.66,197.87 30.28,188.42 31.05,178.57\n             31.91,167.54 35.39,157.88 41.40,149.86\n             41.68,149.49 42.30,148.65 43.26,148.05\n             41.37,144.72 40.66,140.47 40.83,137.53\n             41.15,132.05 41.68,129.20 44.04,124.12\n             45.49,120.99 47.11,118.63 48.82,116.80\n             48.77,116.56 48.73,116.27 48.74,115.95\n             48.79,114.60 48.81,113.21 48.82,111.82\n             48.87,106.25 48.93,100.49 50.97,95.17\n             52.48,91.27 56.98,86.16 60.29,83.67\n             65.57,79.70 69.80,78.82 76.13,77.85\n             76.39,77.81 76.63,77.78 76.88,77.76\n             76.88,77.76 82.81,75.15 82.81,75.15\n             82.81,75.15 83.88,74.70 83.88,74.70\n             84.50,74.44 85.04,74.22 85.58,73.97\n             88.89,72.43 92.20,70.88 95.50,69.32\n             95.76,69.20 96.08,69.06 96.46,68.95\n             96.54,67.29 96.68,65.65 96.83,64.06\n             96.83,64.06 96.99,62.21 96.99,62.21\n             96.99,62.21 97.76,59.95 97.76,59.95\n             96.26,58.80 95.37,57.09 95.20,55.04\n             95.03,52.88 94.83,50.71 94.62,48.60\n             93.71,48.45 92.89,48.06 92.28,47.48\n             91.45,46.69 90.97,45.75 90.58,44.93\n             89.33,42.29 88.40,40.25 87.54,38.31\n             86.91,36.89 87.08,35.13 88.00,33.49\n             88.87,31.95 90.18,30.85 91.59,30.43\n             91.59,30.43 91.93,30.25 91.93,30.25\n             91.92,29.34 91.94,28.38 91.99,27.36\n             92.19,23.40 93.00,19.71 94.40,16.40\n             96.73,10.90 102.83,5.59 108.57,4.06\n             110.57,3.53 112.62,3.26 114.66,3.26\n             123.91,3.26 131.92,8.60 135.56,17.21\n             136.65,19.78 137.12,22.66 137.38,24.60\n             137.44,25.07 137.44,25.72 137.43,27.45\n             137.42,28.14 137.42,29.20 137.43,30.02\n             137.43,30.02 138.61,30.81 138.61,30.81\n             142.41,33.34 143.15,36.08 141.18,40.27\n             141.09,40.47 140.97,40.80 140.82,41.19\n             139.77,43.96 138.22,48.04 134.99,48.60\n             134.81,50.41 134.64,52.26 134.55,54.06\n             134.41,56.54 133.44,58.60 131.77,60.01\n             131.77,60.01 132.49,61.18 132.49,61.18\n             132.49,61.18 132.56,61.88 132.56,61.88\n             132.63,62.49 132.68,63.08 132.74,63.65\n             132.84,64.79 132.95,65.86 133.10,66.87\n             133.23,67.68 133.26,68.44 133.20,69.17\n             133.33,69.22 133.44,69.27 133.54,69.32\n             136.84,70.88 140.15,72.43 143.46,73.97\n             143.99,74.21 144.54,74.44 145.15,74.70\n             145.15,74.70 146.20,75.14 146.20,75.14\n             146.20,75.14 152.10,77.73 152.10,77.73\n             152.51,77.74 152.95,77.78 153.40,77.85\n             159.74,78.82 163.96,79.70 169.24,83.67\n             172.55,86.16 177.06,91.27 178.56,95.17\n             180.61,100.49 180.66,106.25 180.72,111.81\n             180.73,113.21 180.74,114.60 180.79,115.95\n             180.80,116.27 180.77,116.56 180.72,116.80\n             182.43,118.63 184.04,120.99 185.50,124.12\n             187.85,129.20 188.39,132.05 188.70,137.53\n             188.87,140.47 188.16,144.72 186.28,148.05\n             187.23,148.65 187.86,149.49 188.14,149.86\n             194.14,157.88 197.62,167.54 198.48,178.57\n             199.25,188.42 201.87,197.87 206.49,207.47\n             206.65,207.80 206.87,208.17 207.11,208.56\n             207.24,208.78 207.37,209.00 207.50,209.22\n             207.50,209.22 208.71,211.33 208.71,211.33\n             208.71,211.33 208.26,211.76 208.26,211.76\n             209.47,212.29 210.57,212.99 211.50,213.87\n             213.58,215.82 215.61,217.86 217.57,219.83\n             217.57,219.83 219.57,221.82 219.57,221.82\n             219.57,221.82 221.68,224.43 221.68,224.43\n             222.85,224.92 224.01,225.42 225.17,225.92\n             225.17,225.92 226.24,226.38 226.24,226.38\n             226.84,226.64 227.67,227.21 228.12,228.45\n             228.28,228.88 228.74,230.40 228.04,231.82\n             226.96,233.97 225.03,235.29 222.28,235.72\n             222.28,235.72 221.70,235.81 221.70,235.81\n             221.70,235.81 218.32,235.07 217.75,234.94\n             219.01,237.15 220.30,239.43 221.53,241.70\n             221.72,242.04 221.99,242.44 222.28,242.86\n             223.25,244.26 224.57,246.18 224.42,248.72\n             224.32,250.30 223.46,251.72 221.98,252.75\n             222.46,254.21 222.30,255.78 221.50,257.06\n             220.55,258.57 218.91,259.44 217.00,259.44\n             217.00,259.44 215.82,259.41 215.82,259.41\n             215.82,259.41 215.09,258.84 215.09,258.84\n             214.31,259.44 213.31,259.76 212.18,259.76\n             212.18,259.76 212.16,259.76 212.16,259.76\n             209.78,259.76 207.81,258.60 206.77,256.58\n             206.55,256.14 206.33,255.69 206.12,255.25\n             205.84,255.29 205.54,255.32 205.23,255.32\n             204.86,255.32 204.47,255.28 204.10,255.23\n             202.68,255.07 201.89,254.13 201.59,253.77\n             199.07,250.81 197.27,247.47 195.79,244.53\n             195.79,244.53 191.19,238.40 191.19,238.40\n             187.65,233.96 186.86,229.65 188.64,224.16\n             188.64,224.16 188.57,224.18 188.57,224.18\n             188.57,224.18 187.32,221.31 187.32,221.31\n             187.13,220.89 186.95,220.47 186.77,220.05\n             185.29,216.42 183.46,212.88 181.69,209.46\n             179.83,205.84 177.90,202.11 176.29,198.14\n             173.35,190.91 170.32,183.31 168.05,176.74\n             167.38,174.80 166.98,173.00 166.60,171.26\n             166.06,168.82 165.60,166.71 164.47,164.57\n             162.64,161.10 162.18,157.36 163.15,153.78\n             163.20,153.57 163.32,153.20 163.52,152.77\n             163.19,152.39 162.90,151.99 162.67,151.61\n             162.51,151.35 162.29,150.99 162.05,150.60\n             160.95,148.85 160.01,147.31 159.53,146.33\n             158.01,143.13 157.01,140.98 156.23,138.80\n             156.17,142.13 155.94,145.36 155.56,148.55\n             155.48,149.22 155.36,149.87 155.25,150.53\n             155.25,150.53 155.13,151.24 155.13,151.24\n             155.13,151.24 154.38,159.12 154.38,159.12\n             154.19,161.70 153.37,163.83 151.81,165.77\n             151.81,165.77 150.92,166.88 150.92,166.88\n             150.96,166.95 151.00,167.03 151.03,167.10\n             151.26,167.54 151.49,168.00 151.65,168.59\n             153.51,175.28 154.22,180.87 153.90,186.19\n             153.83,187.46 153.61,188.68 153.30,189.86\n             153.30,189.86 155.22,194.51 156.10,196.53\n             156.92,198.44 157.45,199.98 157.99,201.87\n             157.99,201.87 158.52,203.74 158.52,203.74\n             161.32,213.49 164.49,224.55 165.35,234.52\n             165.54,236.83 165.87,239.27 166.21,241.85\n             166.86,246.73 167.53,251.78 167.30,256.01\n             167.01,261.37 166.41,265.16 165.64,269.96\n             165.36,271.72 164.56,274.38 164.24,276.55\n             164.24,276.55 155.99,304.93 155.99,304.93\n             155.87,307.96 155.77,311.01 155.70,314.03\n             155.64,316.62 155.64,319.19 155.64,321.91\n             155.64,323.14 155.63,324.39 155.63,325.66\n             155.63,325.66 155.61,329.77 155.61,329.77\n             158.92,336.53 161.07,343.89 162.02,351.77\n             162.85,358.60 163.28,368.77 162.01,376.80\n             161.73,378.51 161.49,380.09 161.27,381.56\n             160.11,389.20 159.34,394.24 156.46,402.85\n             155.83,404.74 154.67,407.35 153.31,409.95\n             154.00,410.61 154.42,411.50 154.52,412.30\n             154.52,412.30 154.66,413.39 154.66,413.39\n             154.86,414.92 155.07,416.51 155.14,418.14\n             155.14,418.14 156.91,428.71 156.91,428.71\n             156.91,428.71 159.44,431.31 159.44,431.31\n             159.44,431.31 159.55,431.46 159.55,431.46\n             162.88,435.83 165.58,438.31 169.86,440.96\n             170.83,441.56 174.46,444.71 176.79,447.64\n             178.15,449.36 179.90,451.95 179.27,454.54\n             178.93,455.92 177.99,457.03 176.62,457.64\n             172.09,459.65 167.66,461.45 163.05,461.45\n             163.05,461.45 163.05,461.45 163.05,461.45 Z\"\n        fill=\"#f5f5f5\"\n        id=\"path104\"\n        stroke=\"black\"\n        strokeWidth=\"1\"\n      />\n\n      <BicepsGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <ForearmsGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <ChestGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <TricepsGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <AbdominalsGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <ObliquesGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <QuadricepsGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <ShouldersGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <CalvesGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <TrapsGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <BackGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <HamstringsGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <GlutesGroup getMuscleClasses={getMuscleClasses} onToggleMuscle={onToggleMuscle} />\n      <path\n        d=\"M 104.03,61.80\n           C 103.06,61.49 102.16,60.94 100.91,60.35\n             100.91,60.35 100.05,62.86 100.05,62.86\n             99.87,65.02 99.64,67.18 99.55,69.35\n             99.53,69.99 99.77,70.71 100.08,71.29\n             101.93,74.70 104.16,77.84 106.87,80.60\n             108.32,82.07 110.05,83.30 111.76,84.48\n             112.76,85.17 113.26,84.85 113.30,83.65\n             113.35,82.13 113.38,80.59 113.19,79.09\n             112.70,75.08 111.25,71.36 109.79,67.61\n             108.67,64.75 106.98,62.76 104.03,61.80\"\n        fill=\"#757575\"\n        id=\"path132\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        d=\"M 116.30,83.66\n           C 116.30,84.79 116.73,85.05 117.64,84.53\n             118.89,83.82 120.25,83.13 121.20,82.09\n             123.48,79.60 125.62,76.96 127.68,74.28\n             129.24,72.28 130.46,70.10 130.03,67.35\n             129.78,65.73 129.67,64.08 129.47,62.21\n             129.47,62.21 128.49,60.62 128.49,60.62\n             126.72,61.24 125.11,61.94 123.59,62.79\n             122.75,63.25 121.75,63.79 121.34,64.56\n             119.17,68.65 117.42,72.93 116.64,77.52\n             116.30,79.53 116.30,81.61 116.30,83.66\"\n        fill=\"#757575\"\n        id=\"path134\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        d=\"M 109.84,65.48\n           C 111.79,70.01 113.40,74.63 113.80,79.59\n             113.83,79.97 113.93,80.36 114.10,80.69\n             114.24,80.96 114.53,81.15 114.76,81.38\n             114.99,81.15 115.31,80.95 115.42,80.67\n             115.61,80.18 115.72,79.65 115.77,79.12\n             116.19,74.83 117.46,70.78 119.13,66.84\n             119.45,66.09 119.73,65.33 120.03,64.57\n             120.03,64.57 109.53,64.57 109.53,64.57\n             109.63,64.87 109.71,65.19 109.84,65.48\"\n        fill=\"#757575\"\n        id=\"path136\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        d=\"M 93.39,43.60\n           C 93.66,44.18 93.97,44.80 94.42,45.23\n             94.72,45.51 95.32,45.61 95.75,45.54\n             95.75,45.54 97.45,45.59 97.45,45.59\n             97.75,48.65 98.05,51.72 98.30,54.79\n             98.41,56.17 99.03,57.22 100.21,57.84\n             104.24,59.99 108.93,63.14 113.77,63.15\n             119.09,63.16 124.33,60.64 128.91,58.21\n             130.57,57.33 131.34,55.82 131.45,53.89\n             131.59,51.15 131.89,48.42 132.16,45.69\n             132.16,45.69 133.86,45.49 133.86,45.49\n             136.04,46.22 137.73,40.31 138.37,38.95\n             139.67,36.19 139.40,35.07 136.88,33.39\n             136.88,33.39 134.49,31.79 134.49,31.79\n             134.17,31.57 134.41,25.80 134.30,25.01\n             134.01,22.83 133.56,20.45 132.70,18.42\n             128.77,9.14 118.99,4.50 109.37,7.06\n             104.57,8.34 99.23,12.97 97.26,17.62\n             95.92,20.77 95.26,24.12 95.10,27.51\n             95.02,29.05 95.01,30.57 95.11,32.06\n             95.11,32.06 92.71,33.36 92.71,33.36\n             91.34,33.53 89.84,35.83 90.38,37.06\n             91.35,39.25 92.36,41.43 93.39,43.60\"\n        fill=\"#757575\"\n        id=\"path162\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        d=\"M 79.71,412.49\n           C 79.61,412.26 79.22,411.99 79.04,412.03\n             78.82,412.09 78.56,412.44 78.53,412.69\n             78.28,414.61 77.99,416.53 77.92,418.46\n             77.92,418.46 75.96,430.18 75.96,430.18\n             75.96,430.18 72.89,433.34 72.89,433.34\n             69.49,437.79 66.53,440.64 61.74,443.60\n             61.22,443.92 57.83,446.78 55.61,449.57\n             53.84,451.81 52.82,454.00 54.62,454.81\n             62.63,458.36 69.13,460.64 76.65,454.58\n             78.57,453.03 80.11,451.57 82.47,450.64\n             84.83,449.71 88.19,449.10 88.91,446.21\n             89.00,445.88 90.33,440.59 90.33,440.59\n             90.33,440.59 90.34,440.44 90.34,440.44\n             90.47,440.04 90.59,439.64 90.62,439.24\n             90.87,436.24 91.07,433.24 91.28,430.24\n             91.44,427.99 90.67,426.17 89.11,424.48\n             85.68,420.73 81.85,417.26 79.71,412.49\"\n        fill=\"#757575\"\n        id=\"path164\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        d=\"M 168.23,443.60\n           C 163.44,440.64 160.47,437.79 157.08,433.34\n             157.08,433.34 154.01,430.18 154.01,430.18\n             154.01,430.18 152.04,418.46 152.04,418.46\n             151.98,416.53 151.68,414.61 151.44,412.69\n             151.41,412.44 151.14,412.09 150.93,412.03\n             150.74,411.99 150.35,412.26 150.25,412.49\n             148.11,417.26 144.29,420.73 140.85,424.48\n             139.30,426.17 138.53,427.99 138.68,430.24\n             138.89,433.24 139.09,436.24 139.34,439.24\n             139.38,439.64 139.50,440.04 139.63,440.44\n             139.63,440.44 139.64,440.59 139.64,440.59\n             139.64,440.59 140.97,445.88 141.05,446.21\n             141.78,449.10 145.13,449.71 147.50,450.64\n             149.85,451.57 151.40,453.03 153.31,454.58\n             160.83,460.64 167.33,458.36 175.35,454.81\n             177.15,454.00 176.13,451.81 174.35,449.57\n             172.14,446.78 168.74,443.92 168.23,443.60\"\n        fill=\"#757575\"\n        id=\"path190\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        d=\"M 36.97,219.69\n           C 37.21,215.21 37.71,210.75 38.11,206.27\n             38.17,205.62 38.23,204.98 38.29,204.33\n             38.29,204.33 39.72,196.96 39.72,196.96\n             39.72,196.96 39.72,196.96 39.72,196.96\n             39.84,196.42 39.95,195.88 40.05,195.34\n             40.13,194.89 40.06,194.41 40.06,193.70\n             35.84,199.72 31.79,205.49 27.74,211.25\n             27.74,211.25 24.95,213.94 24.95,213.94\n             23.14,214.20 21.45,214.89 20.12,216.14\n             17.43,218.66 14.85,221.30 12.24,223.91\n             12.24,223.91 9.73,227.00 9.73,227.00\n             9.73,227.00 9.74,227.00 9.74,227.00\n             7.98,227.71 6.24,228.48 4.49,229.23\n             4.26,229.33 4.09,230.13 4.25,230.44\n             4.93,231.80 6.17,232.41 7.70,232.65\n             10.10,232.12 12.54,231.59 14.98,231.02\n             14.98,231.02 17.11,231.85 17.11,231.85\n             17.05,231.95 16.98,232.04 16.91,232.16\n             16.81,232.32 16.72,232.49 16.62,232.65\n             14.64,236.16 12.62,239.64 10.69,243.18\n             9.78,244.87 8.06,246.48 8.18,248.53\n             8.23,249.32 8.84,249.89 9.32,250.22\n             9.54,250.36 10.30,249.82 10.72,249.47\n             11.10,249.17 11.38,248.73 11.64,248.31\n             13.51,245.22 15.38,242.13 17.23,239.03\n             18.08,237.60 18.91,236.15 19.70,234.69\n             19.70,234.69 20.33,235.44 20.33,235.44\n             18.33,238.89 16.31,242.34 14.40,245.84\n             13.05,248.31 11.84,250.85 10.60,253.38\n             9.91,254.81 10.72,256.37 12.58,256.33\n             12.96,256.03 13.84,255.61 14.31,254.92\n             15.18,253.64 15.81,252.19 16.55,250.82\n             18.58,247.03 20.60,243.23 22.65,239.44\n             22.65,239.44 23.34,240.65 23.34,240.65\n             21.85,243.33 20.36,246.02 18.96,248.74\n             17.95,250.70 17.05,252.72 16.17,254.74\n             15.64,255.95 16.11,256.66 17.32,256.66\n             18.49,256.66 19.42,256.21 19.96,255.16\n             20.65,253.81 21.26,252.41 21.93,251.05\n             23.17,248.56 24.41,246.06 25.69,243.60\n             25.69,243.60 26.67,244.66 26.67,244.66\n             26.53,244.89 26.40,245.13 26.29,245.39\n             25.47,247.26 24.42,249.03 23.54,250.88\n             23.38,251.23 23.42,252.03 23.59,252.10\n             24.02,252.28 24.57,252.20 25.07,252.15\n             25.25,252.12 25.43,251.88 25.58,251.71\n             27.87,249.02 29.52,245.94 31.09,242.82\n             31.09,242.82 31.12,242.80 31.12,242.80\n             31.12,242.80 35.83,236.53 35.83,236.53\n             38.95,232.60 39.35,229.23 37.74,224.61\n             37.19,223.07 36.88,221.32 36.97,219.69\"\n        fill=\"#757575\"\n        id=\"path202\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        d=\"M 225.01,229.23\n           C 223.26,228.48 221.52,227.71 219.76,227.00\n             219.76,227.00 219.76,227.00 219.76,227.00\n             219.76,227.00 217.26,223.91 217.26,223.91\n             214.64,221.30 212.06,218.66 209.38,216.14\n             208.05,214.89 206.36,214.20 204.55,213.94\n             204.55,213.94 201.75,211.25 201.75,211.25\n             197.71,205.49 193.66,199.72 189.44,193.70\n             189.44,194.41 189.37,194.89 189.45,195.34\n             189.55,195.88 189.66,196.42 189.78,196.96\n             189.78,196.96 189.78,196.96 189.78,196.96\n             189.78,196.96 191.21,204.33 191.21,204.33\n             191.27,204.98 191.33,205.62 191.39,206.27\n             191.79,210.75 192.29,215.21 192.53,219.69\n             192.62,221.32 192.30,223.07 191.76,224.61\n             190.14,229.23 190.54,232.60 193.67,236.53\n             193.67,236.53 198.38,242.80 198.38,242.80\n             198.38,242.80 198.41,242.82 198.41,242.82\n             199.98,245.94 201.63,249.02 203.92,251.71\n             204.07,251.88 204.25,252.12 204.43,252.15\n             204.93,252.20 205.48,252.28 205.91,252.10\n             206.08,252.03 206.12,251.23 205.96,250.88\n             205.08,249.03 204.03,247.26 203.21,245.39\n             203.10,245.13 202.97,244.89 202.83,244.66\n             202.83,244.66 203.81,243.60 203.81,243.60\n             205.09,246.06 206.33,248.56 207.56,251.05\n             208.24,252.41 208.84,253.81 209.54,255.16\n             210.07,256.21 211.01,256.66 212.17,256.66\n             213.38,256.66 213.86,255.95 213.33,254.74\n             212.45,252.72 211.54,250.70 210.54,248.74\n             209.14,246.02 207.65,243.33 206.16,240.65\n             206.16,240.65 206.85,239.44 206.85,239.44\n             208.89,243.23 210.92,247.03 212.95,250.82\n             213.68,252.19 214.32,253.64 215.19,254.92\n             215.66,255.61 216.53,256.03 216.92,256.33\n             218.78,256.37 219.59,254.81 218.90,253.38\n             217.66,250.85 216.45,248.31 215.10,245.84\n             213.18,242.34 211.17,238.89 209.17,235.44\n             209.17,235.44 209.80,234.69 209.80,234.69\n             210.59,236.15 211.42,237.60 212.27,239.03\n             214.12,242.13 215.99,245.22 217.86,248.31\n             218.12,248.73 218.40,249.17 218.77,249.47\n             219.20,249.82 219.96,250.36 220.17,250.22\n             220.66,249.89 221.27,249.32 221.32,248.53\n             221.44,246.48 219.72,244.87 218.80,243.18\n             216.88,239.64 214.86,236.16 212.87,232.65\n             212.78,232.49 212.69,232.32 212.59,232.16\n             212.51,232.04 212.45,231.95 212.39,231.85\n             212.39,231.85 214.52,231.02 214.52,231.02\n             216.95,231.59 219.40,232.12 221.80,232.65\n             223.33,232.41 224.57,231.80 225.25,230.44\n             225.40,230.13 225.23,229.33 225.01,229.23\"\n        fill=\"#757575\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </svg>\n  );\n};\n\nexport function MuscleSelection({ onToggleMuscle, selectedMuscles }: MuscleSelectionProps) {\n  const t = useI18n();\n\n  return (\n    <div className=\"space-y-6\">\n      <div className=\"text-center mb-6\">\n        <p className=\"text-slate-600 dark:text-slate-300 text-sm italic\">{t(\"workout_builder.selection.muscle_selection_description\")}</p>\n      </div>\n\n      <div className=\"flex justify-center\">\n        <MuscleIllustration onToggleMuscle={onToggleMuscle} selectedMuscles={selectedMuscles} />\n      </div>\n\n      {env.NEXT_PUBLIC_MUSCLE_SELECTION_BANNER_AD_SLOT && (\n        <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_MUSCLE_SELECTION_BANNER_AD_SLOT} />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/abdominals-group.tsx",
    "content": "import React from \"react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const AbdominalsGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.ABDOMINALS)}>\n      <path\n        className=\"fill-transparent\"\n        d=\"M 100.75,123.75\n           C 100.75,123.75 97.50,126.50 97.50,126.50\n             97.50,126.50 94.50,140.25 94.50,140.25\n             94.50,140.25 93.00,152.50 93.00,152.50\n             93.00,152.50 93.00,162.25 93.00,162.25\n             93.00,162.25 92.75,172.25 93.00,172.25\n             93.25,172.25 97.00,177.00 97.00,177.00\n             97.00,177.00 95.50,180.75 95.50,180.75\n             95.50,180.75 96.50,193.75 96.50,193.75\n             96.50,193.75 99.00,202.00 99.00,202.00\n             99.00,202.00 103.50,215.75 103.50,215.75\n             103.50,215.75 107.25,220.75 107.25,220.75\n             107.25,220.75 111.75,224.50 111.75,224.50\n             111.75,224.50 119.25,224.75 119.25,224.75\n             119.25,224.75 125.25,217.00 125.25,217.00\n             125.25,217.00 131.25,201.75 131.25,201.75\n             131.25,201.75 134.75,190.50 134.75,190.50\n             134.75,190.50 134.75,179.50 134.75,179.50\n             134.75,179.50 132.25,178.25 132.25,178.25\n             132.25,178.25 135.25,173.00 135.25,173.00\n             135.25,173.00 137.00,166.75 137.00,166.75\n             137.00,166.75 136.75,156.50 136.75,156.50\n             136.75,156.50 135.75,144.50 135.75,144.50\n             135.75,144.50 133.75,133.25 133.75,133.25\n             133.75,133.25 130.00,124.75 130.00,124.75\n             130.00,124.75 122.50,122.00 122.50,122.00\n             122.50,122.00 119.50,122.00 119.50,122.00\n             119.50,122.00 114.50,126.75 114.50,126.75\n             114.50,126.75 109.50,120.75 109.50,120.75\n             109.50,120.75 101.00,124.00 101.00,124.00\"\n        data-elem={ExerciseAttributeValueEnum.ABDOMINALS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 108 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.ABDOMINALS)}\n        d=\"M 124.01,216.93\n           C 127.66,211.39 129.67,205.22 131.16,198.84\n             132.43,193.41 133.29,187.93 133.58,182.34\n             133.72,179.75 132.62,178.53 130.03,178.85\n             126.10,179.34 122.39,180.62 118.69,181.94\n             117.42,182.39 116.74,183.22 116.55,184.50\n             116.22,186.76 115.74,189.02 115.63,191.29\n             115.32,197.53 115.09,210.03 115.16,210.04\n             115.16,213.81 115.16,217.59 115.17,221.37\n             115.17,222.78 115.33,222.87 116.71,222.46\n             119.83,221.53 122.21,219.67 124.01,216.93\"\n        data-elem={ExerciseAttributeValueEnum.ABDOMINALS}\n        id=\"path108\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 110 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.ABDOMINALS)}\n        d=\"M 113.13,184.90\n           C 112.97,183.30 112.15,182.24 110.54,181.81\n             109.81,181.62 109.12,181.24 108.39,181.05\n             105.40,180.29 102.42,179.47 99.40,178.88\n             97.20,178.44 95.82,179.76 95.94,181.83\n             96.38,189.12 97.53,196.30 99.59,203.32\n             101.19,208.79 103.25,214.06 106.84,218.62\n             108.73,221.02 111.32,222.00 114.28,222.83\n             114.33,221.88 114.41,221.20 114.40,220.52\n             114.36,212.51 114.42,204.50 114.22,196.50\n             114.12,192.63 113.51,188.77 113.13,184.90\"\n        data-elem={ExerciseAttributeValueEnum.ABDOMINALS}\n        id=\"path110\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 116 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.ABDOMINALS)}\n        d=\"M 115.63,177.74\n           C 115.65,179.88 116.89,180.73 118.92,180.17\n             122.26,179.25 125.60,178.34 128.98,177.57\n             130.39,177.25 131.45,176.62 132.17,175.41\n             134.28,171.89 135.47,168.07 135.79,163.97\n             135.86,163.06 135.50,162.49 134.76,161.99\n             130.32,159.00 125.40,157.80 120.12,158.01\n             116.78,158.15 116.35,157.92 115.63,162.19\n             115.57,162.52 115.56,162.87 115.56,163.21\n             115.56,165.58 115.56,167.94 115.56,170.30\n             115.58,170.30 115.60,170.30 115.62,170.30\n             115.62,172.78 115.61,175.26 115.63,177.74\"\n        data-elem={ExerciseAttributeValueEnum.ABDOMINALS}\n        id=\"path116\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 118 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.ABDOMINALS)}\n        d=\"M 100.30,177.50\n           C 103.85,178.35 107.39,179.28 110.91,180.25\n             112.64,180.72 113.82,179.88 113.85,178.06\n             113.94,173.64 114.15,169.23 114.12,164.81\n             114.11,163.05 113.67,161.28 113.32,159.53\n             113.14,158.61 112.41,158.14 111.46,158.15\n             109.11,158.18 106.75,158.06 104.42,158.32\n             100.98,158.71 97.81,160.00 94.91,161.93\n             94.06,162.51 93.60,163.18 93.79,164.24\n             94.25,168.25 95.34,172.06 97.50,175.52\n             98.15,176.56 99.06,177.20 100.30,177.50\"\n        data-elem={ExerciseAttributeValueEnum.ABDOMINALS}\n        id=\"path118\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 120 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.ABDOMINALS)}\n        d=\"M 115.59,154.23\n           C 115.61,155.82 115.88,156.08 117.49,156.12\n             119.51,156.17 121.53,156.18 123.55,156.17\n             127.52,156.13 131.13,157.28 134.46,159.41\n             134.81,159.64 135.26,159.71 135.67,159.85\n             135.78,159.42 136.04,158.98 135.99,158.57\n             135.62,155.40 135.27,152.22 134.72,149.07\n             134.58,148.21 133.94,147.31 133.28,146.68\n             129.61,143.16 125.13,141.45 120.09,141.14\n             119.44,141.18 118.79,141.18 118.15,141.25\n             116.35,141.43 115.94,141.83 115.65,143.62\n             115.59,143.95 115.56,144.30 115.56,144.64\n             115.56,147.84 115.54,151.03 115.59,154.23\"\n        data-elem={ExerciseAttributeValueEnum.ABDOMINALS}\n        id=\"path120\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 122 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.ABDOMINALS)}\n        d=\"M 113.79,142.93\n           C 113.63,141.98 113.08,141.52 112.09,141.38\n             106.45,140.63 101.57,142.46 97.19,145.83\n             96.35,146.47 95.51,147.41 95.17,148.38\n             94.00,151.75 93.49,155.26 93.63,158.84\n             93.64,159.19 93.86,159.53 93.98,159.88\n             94.31,159.76 94.69,159.70 94.98,159.51\n             97.35,157.92 99.91,156.76 102.77,156.45\n             104.66,156.24 106.57,156.19 108.47,156.08\n             108.47,156.09 108.47,156.11 108.47,156.13\n             109.84,156.13 111.22,156.13 112.59,156.13\n             113.34,156.13 113.81,155.81 113.89,155.00\n             114.31,150.97 114.46,146.95 113.79,142.93\"\n        data-elem={ExerciseAttributeValueEnum.ABDOMINALS}\n        id=\"path122\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 124 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.ABDOMINALS)}\n        d=\"M 115.58,129.73\n           C 115.56,132.16 115.55,134.60 115.56,137.04\n             115.56,137.96 116.07,138.54 116.95,138.74\n             117.51,138.87 118.08,138.94 118.64,138.99\n             123.82,139.50 128.74,140.85 133.25,143.52\n             133.57,143.71 133.91,143.86 134.58,144.21\n             134.58,143.38 134.68,142.85 134.57,142.37\n             133.61,138.14 132.63,133.92 131.61,129.71\n             131.16,127.83 130.08,126.36 128.32,125.55\n             126.04,124.51 123.71,123.59 121.40,122.62\n             120.53,122.26 119.80,122.52 119.30,123.26\n             118.12,125.03 116.97,126.83 115.85,128.63\n             115.66,128.94 115.59,129.36 115.58,129.73\"\n        data-elem={ExerciseAttributeValueEnum.ABDOMINALS}\n        id=\"path124\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 126 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.ABDOMINALS)}\n        d=\"M 113.76,128.72\n           C 112.63,126.83 111.41,124.99 110.18,123.16\n             109.73,122.48 109.05,122.30 108.26,122.60\n             106.58,123.22 104.88,123.78 103.23,124.46\n             100.23,125.70 98.19,127.72 97.53,131.07\n             96.82,134.65 95.87,138.19 95.08,141.76\n             94.93,142.43 95.06,143.16 95.06,143.96\n             95.38,143.93 95.50,143.94 95.60,143.90\n             95.87,143.77 96.14,143.62 96.40,143.47\n             99.73,141.54 103.25,140.17 107.06,139.57\n             108.79,139.30 110.53,139.08 112.26,138.76\n             113.68,138.50 114.03,138.01 114.04,136.57\n             114.04,134.51 114.05,132.45 114.03,130.39\n             114.02,129.83 114.03,129.18 113.76,128.72\"\n        data-elem={ExerciseAttributeValueEnum.ABDOMINALS}\n        id=\"path126\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/back-group.tsx",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const BackGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.BACK)}>\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BACK)}\n        d=\"M 392.64,149.10\n           C 392.27,149.18 392.12,149.19 391.99,149.25\n             389.72,150.24 387.35,150.69 384.89,150.56\n             382.22,150.41 379.57,149.93 377.87,147.62\n             376.09,145.22 374.53,142.65 372.91,140.13\n             372.62,139.66 372.48,139.10 372.27,138.58\n             372.18,138.60 372.08,138.62 371.99,138.64\n             371.98,138.82 371.93,139.01 371.96,139.19\n             372.90,144.96 373.51,150.78 375.45,156.38\n             376.89,160.53 378.59,164.50 381.25,168.00\n             382.04,169.04 382.99,169.97 383.98,170.82\n             385.11,171.81 385.47,171.64 385.74,170.20\n             385.76,170.09 385.77,169.97 385.79,169.86\n             386.85,164.12 388.32,158.50 390.74,153.17\n             391.33,151.86 391.95,150.56 392.64,149.10\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        id=\"path30\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BACK)}\n        d=\"M 451.05,138.61\n           C 450.93,138.58 450.82,138.55 450.71,138.52\n             450.53,138.88 450.38,139.24 450.18,139.59\n             449.02,141.63 447.96,143.74 446.65,145.68\n             445.30,147.67 443.64,149.45 441.18,150.09\n             437.83,150.96 434.53,150.71 431.32,149.38\n             431.04,149.26 430.74,149.20 430.17,149.02\n             434.12,156.19 436.09,163.69 437.56,171.73\n             438.40,171.16 439.01,170.83 439.51,170.39\n             441.99,168.16 443.69,165.37 445.13,162.40\n             448.22,156.01 449.70,149.17 450.64,142.19\n             450.80,141.00 450.91,139.80 451.05,138.61\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        id=\"path28\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BACK)}\n        d=\"M 403.69,137.15\n           C 405.75,134.61 405.91,133.56 404.16,130.82\n             401.53,126.69 398.77,122.65 396.09,118.55\n             396.09,118.55 394.01,115.00 394.01,115.00\n             394.01,115.00 393.84,114.97 393.84,114.97\n             393.73,114.71 393.57,114.43 393.34,114.11\n             392.00,112.22 390.57,110.40 389.33,108.45\n             386.85,104.57 385.39,100.28 384.61,95.75\n             384.32,94.04 383.40,93.64 381.89,94.51\n             381.26,94.88 380.66,95.31 380.09,95.76\n             375.26,99.63 372.19,104.64 370.49,110.55\n             370.35,111.05 370.27,111.45 370.24,111.80\n             370.24,111.80 370.24,111.80 370.24,111.80\n             370.24,111.80 370.05,116.31 370.05,116.31\n             369.93,118.08 369.84,119.85 369.73,121.63\n             369.82,123.32 369.87,125.01 370.02,126.70\n             370.53,132.63 372.41,138.07 375.97,142.87\n             378.15,145.81 381.03,147.83 384.60,148.72\n             388.07,149.59 391.14,148.39 393.98,146.49\n             397.77,143.96 400.84,140.67 403.69,137.15\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        id=\"path26\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BACK)}\n        d=\"M 452.92,116.24\n           C 452.92,116.24 452.75,111.80 452.75,111.80\n             452.75,111.80 452.74,111.80 452.74,111.80\n             452.71,111.52 452.65,111.21 452.55,110.84\n             452.25,109.81 451.90,108.79 451.53,107.77\n             449.58,102.43 446.27,98.13 441.63,94.86\n             439.64,93.45 438.68,93.88 438.25,96.24\n             437.60,99.79 436.62,103.26 434.81,106.37\n             433.22,109.09 431.31,111.63 429.54,114.24\n             429.54,114.24 426.99,118.41 426.99,118.41\n             424.12,122.80 421.24,127.18 418.38,131.58\n             417.40,133.09 417.39,134.61 418.48,136.10\n             421.78,140.59 425.64,144.48 430.44,147.38\n             434.04,149.56 437.71,149.46 441.37,147.65\n             445.32,145.70 447.79,142.32 449.73,138.50\n             452.39,133.25 453.16,127.59 453.20,120.88\n             453.14,119.93 453.06,118.08 452.92,116.24\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        id=\"path24\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BACK)}\n        d=\"M 410.63,151.32\n           C 410.49,151.32 410.49,146.96 410.13,144.87\n             409.76,142.69 408.79,140.61 408.02,138.52\n             407.56,137.28 406.89,137.09 405.79,137.79\n             405.34,138.08 404.93,138.45 404.57,138.85\n             403.06,140.50 401.58,142.17 400.09,143.84\n             398.48,145.65 396.85,147.45 395.30,149.32\n             393.16,151.90 391.78,154.91 390.52,157.99\n             388.87,162.00 388.06,166.16 387.83,170.47\n             387.78,171.37 388.11,171.83 388.97,172.01\n             390.71,172.37 392.02,173.38 393.15,174.72\n             396.18,178.32 397.67,182.67 399.17,187.01\n             400.63,191.28 402.01,195.58 403.62,199.79\n             405.05,203.55 406.98,207.07 409.47,210.26\n             409.68,210.53 410.06,210.67 410.37,210.86\n             410.50,210.53 410.75,210.20 410.76,209.86\n             410.78,209.06 410.64,208.25 410.64,207.44\n             410.63,188.73 410.63,170.03 410.63,151.32\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        id=\"path22\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BACK)}\n        d=\"M 412.34,201.13\n           C 412.33,204.02 412.23,206.91 412.21,209.80\n             412.20,210.15 412.41,210.50 412.52,210.85\n             412.85,210.64 413.29,210.51 413.49,210.21\n             414.63,208.52 415.79,206.83 416.81,205.06\n             419.82,199.80 421.42,193.97 423.33,188.28\n             424.66,184.33 425.93,180.34 428.26,176.84\n             429.59,174.85 430.96,172.87 433.48,172.15\n             435.22,171.65 435.24,171.61 435.12,169.77\n             434.62,162.73 432.68,156.10 428.46,150.45\n             425.26,146.17 421.44,142.37 417.87,138.37\n             417.58,138.04 417.14,137.81 416.73,137.61\n             416.04,137.26 415.36,137.37 415.11,138.14\n             414.24,140.80 413.03,143.44 412.73,146.18\n             412.28,150.18 412.47,154.26 412.45,158.31\n             412.42,165.95 412.44,173.58 412.44,181.22\n             412.41,181.22 412.38,181.22 412.35,181.22\n             412.35,187.86 412.36,194.49 412.34,201.13\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        id=\"path20\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 385.00,92.75\n           C 385.00,92.75 379.75,94.25 379.75,94.50\n             379.75,94.75 372.75,102.25 372.75,102.25\n             372.75,102.25 370.25,106.75 370.25,106.75\n             370.25,106.75 369.50,115.75 369.50,115.75\n             369.50,115.75 369.25,130.00 369.25,130.00\n             369.25,130.00 369.25,139.25 369.25,139.25\n             369.25,139.25 370.00,151.25 370.00,151.25\n             370.00,151.25 373.25,159.75 373.25,159.75\n             373.25,159.75 375.00,163.25 375.00,163.25\n             375.00,163.25 369.75,170.50 369.75,170.50\n             369.75,170.50 369.75,177.25 369.75,177.25\n             369.75,177.25 370.25,180.25 370.25,180.25\n             370.25,180.25 368.25,191.00 368.25,191.00\n             368.25,191.00 367.25,202.00 367.25,202.00\n             367.25,202.00 367.75,207.25 367.75,207.25\n             367.75,207.25 375.75,196.25 375.75,196.00\n             375.75,195.75 381.75,189.25 381.75,189.25\n             381.75,189.25 396.25,184.50 396.25,184.50\n             396.25,184.50 391.75,176.25 391.75,176.25\n             391.75,176.25 386.50,172.25 386.50,172.25\n             386.50,172.25 388.25,161.50 388.25,161.50\n             388.25,161.50 393.75,148.75 393.75,148.75\n             393.75,148.75 399.75,141.50 399.75,141.50\n             399.75,141.50 406.50,135.75 406.50,135.75\n             406.50,135.75 405.50,129.75 405.50,129.75\n             405.50,129.75 398.75,120.75 398.75,120.75\n             398.75,120.75 393.75,112.75 393.75,112.75\n             393.75,112.75 388.75,106.00 388.75,106.00\n             388.75,106.00 386.25,99.25 386.25,99.25\n             386.25,99.25 385.25,93.75 385.25,93.75\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 415.75,134.25\n           C 415.75,134.25 422.25,123.00 422.25,123.00\n             422.25,123.00 432.00,109.75 432.00,109.75\n             432.00,109.75 436.25,101.25 436.25,101.25\n             436.25,101.25 437.75,93.00 437.75,93.00\n             437.75,93.00 443.00,95.50 443.00,95.50\n             443.00,95.50 449.75,100.25 449.75,100.25\n             449.75,100.25 453.00,107.00 453.00,107.00\n             453.00,107.00 453.50,118.25 453.50,118.25\n             453.50,118.25 453.75,132.25 453.75,132.25\n             453.75,132.25 453.75,139.75 453.75,139.75\n             453.75,139.75 451.00,153.25 451.00,153.25\n             451.00,153.25 448.25,163.25 448.25,163.25\n             448.25,163.25 452.00,168.50 452.00,168.50\n             452.00,168.50 452.50,177.00 452.50,177.00\n             452.50,177.00 452.50,182.00 452.50,182.00\n             452.50,182.00 457.00,197.50 457.00,197.50\n             457.00,197.50 456.25,205.75 456.25,205.75\n             456.25,205.75 451.50,199.75 451.50,199.75\n             451.50,199.75 446.75,195.00 446.75,195.00\n             446.75,195.00 443.00,190.75 443.00,190.75\n             443.00,190.75 437.00,187.50 437.00,187.50\n             437.00,187.50 428.75,186.00 428.75,186.00\n             428.75,186.00 427.25,184.25 427.25,184.25\n             427.25,184.25 430.00,178.50 430.00,178.50\n             430.00,178.50 433.00,174.00 433.00,174.00\n             433.00,174.00 436.25,172.75 436.25,172.75\n             436.25,172.75 436.00,165.75 436.00,165.75\n             436.00,165.75 433.25,156.25 433.25,156.25\n             433.25,156.25 428.25,148.25 428.25,148.25\n             428.25,148.25 422.50,142.00 422.50,142.00\n             422.50,142.00 418.50,137.75 416.00,134.75\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BACK)}\n        d=\"M 446.16,166.84\n           C 444.58,168.72 443.04,170.63 441.50,172.50\n             443.05,173.60 444.59,174.58 446.00,175.71\n             446.86,176.40 447.49,177.34 448.30,178.09\n             448.60,178.37 449.21,178.65 449.51,178.53\n             449.82,178.40 450.13,177.76 450.06,177.41\n             449.34,173.89 448.56,170.38 447.72,166.88\n             447.53,166.08 446.81,166.06 446.16,166.84\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        id=\"path60\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BACK)}\n        d=\"M 431.03,186.03\n           C 434.21,186.80 437.40,187.52 440.48,188.54\n             441.32,188.82 442.10,189.23 442.87,189.68\n             442.87,189.68 445.13,191.11 445.13,191.11\n             445.13,191.11 449.09,195.08 449.09,195.08\n             449.33,195.53 449.57,195.98 449.80,196.44\n             450.49,197.83 451.46,199.09 452.41,200.34\n             452.67,200.67 453.30,200.72 453.76,200.90\n             453.87,200.44 454.15,199.95 454.07,199.53\n             453.35,195.95 452.67,192.36 451.79,188.83\n             450.99,185.60 449.99,182.38 447.97,179.71\n             445.23,176.07 441.55,174.00 437.18,173.73\n             436.64,173.83 436.26,173.80 436.01,173.96\n             435.76,174.12 435.47,174.28 435.22,174.45\n             435.22,174.45 435.21,174.45 435.21,174.45\n             435.21,174.45 435.20,174.46 435.20,174.46\n             435.00,174.60 434.83,174.75 434.70,174.91\n             434.70,174.91 432.49,176.89 432.49,176.89\n             432.49,176.89 432.52,176.99 432.52,176.99\n             432.21,177.19 431.96,177.57 431.75,178.14\n             430.99,180.15 430.27,182.18 429.71,184.24\n             429.36,185.52 429.71,185.71 431.03,186.03\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        id=\"path56\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BACK)}\n        d=\"M 374.54,178.09\n           C 375.35,177.34 375.98,176.40 376.84,175.71\n             378.25,174.58 379.79,173.60 381.34,172.50\n             379.80,170.63 378.26,168.72 376.68,166.84\n             376.02,166.06 375.31,166.08 375.12,166.88\n             374.28,170.38 373.50,173.89 372.78,177.41\n             372.70,177.76 373.02,178.40 373.33,178.53\n             373.63,178.65 374.24,178.37 374.54,178.09\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        id=\"path44\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BACK)}\n        d=\"M 388.14,174.91\n           C 388.01,174.75 387.84,174.60 387.64,174.46\n             387.64,174.46 387.63,174.45 387.63,174.45\n             387.63,174.45 387.62,174.45 387.62,174.45\n             387.37,174.28 387.08,174.12 386.83,173.96\n             386.57,173.80 386.20,173.83 385.66,173.73\n             381.29,174.00 377.61,176.07 374.87,179.71\n             372.85,182.38 371.85,185.60 371.05,188.83\n             370.17,192.36 369.48,195.95 368.77,199.53\n             368.69,199.95 368.97,200.44 369.08,200.90\n             369.54,200.72 370.17,200.67 370.43,200.34\n             371.38,199.09 372.35,197.83 373.04,196.44\n             373.27,195.98 373.51,195.53 373.74,195.08\n             373.74,195.08 377.71,191.11 377.71,191.11\n             377.71,191.11 379.97,189.68 379.97,189.68\n             380.74,189.23 381.52,188.82 382.35,188.54\n             385.44,187.52 388.63,186.80 391.81,186.03\n             393.13,185.71 393.48,185.52 393.13,184.24\n             392.56,182.18 391.85,180.15 391.09,178.14\n             390.88,177.57 390.63,177.19 390.32,176.99\n             390.32,176.99 390.35,176.89 390.35,176.89\n             390.35,176.89 388.14,174.91 388.14,174.91\"\n        data-elem={ExerciseAttributeValueEnum.BACK}\n        id=\"path36\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/biceps-group.tsx",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const BicepsGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.BICEPS)}>\n      <path\n        className=\"fill-transparent\"\n        d=\"M 49.25,117.25\n           C 49.25,117.25 45.50,120.75 45.50,120.75\n             45.50,120.75 42.00,129.00 42.00,129.00\n             42.00,129.00 41.25,137.75 41.25,137.75\n             41.25,137.75 42.00,145.25 42.00,145.25\n             42.00,145.25 46.50,149.50 46.50,149.50\n             46.50,149.50 48.25,153.25 48.25,153.25\n             48.25,153.25 49.25,166.50 49.25,166.50\n             49.25,166.50 60.25,153.00 60.25,153.00\n             60.25,153.00 61.25,149.50 61.25,149.50\n             61.25,149.50 63.75,151.50 63.75,151.50\n             63.75,151.50 68.00,148.75 68.00,148.75\n             68.00,148.75 73.00,140.50 73.00,140.50\n             73.00,140.50 74.25,129.50 74.25,129.50\n             74.25,129.50 74.50,120.25 74.50,120.25\n             74.50,120.25 74.50,116.50 74.50,116.50\n             74.50,116.50 73.00,113.00 73.00,113.00\n             73.00,113.00 71.25,111.50 71.25,111.50\n             71.25,111.50 68.00,110.50 68.00,110.50\n             68.00,110.50 57.75,114.25 57.75,114.25\n             57.75,114.25 49.50,117.50 49.50,117.50\"\n        data-elem={ExerciseAttributeValueEnum.BICEPS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BICEPS)}\n        d=\"M 166.82,151.36\n           C 166.99,151.37 166.72,150.23 166.86,150.14\n             166.46,148.57 166.10,146.98 165.67,145.42\n             164.18,140.02 162.71,134.62 161.15,129.24\n             160.79,128.01 158.77,117.31 158.25,114.97\n             158.05,114.59 157.47,118.80 157.14,121.79\n             157.09,122.29 157.16,124.59 157.08,125.04\n             156.95,125.74 157.27,130.26 157.40,130.96\n             158.57,137.01 159.49,139.00 162.34,144.99\n             162.91,146.19 164.64,148.86 165.33,149.99\n             165.62,150.47 166.25,151.31 166.82,151.36\"\n        data-elem={ExerciseAttributeValueEnum.BICEPS}\n        id=\"path158\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BICEPS)}\n        d=\"M 160.10,114.96\n           C 160.64,121.59 161.87,128.11 163.78,134.48\n             164.74,137.69 165.80,140.87 166.86,144.05\n             168.73,149.65 171.29,154.88 174.82,159.66\n             176.46,161.88 177.68,164.42 179.09,166.81\n             179.16,166.93 179.26,167.02 179.52,167.32\n             179.64,166.25 179.80,165.40 179.82,164.55\n             179.94,159.48 179.56,151.75 182.68,147.85\n             184.94,145.02 185.76,140.37 185.60,137.71\n             185.30,132.58 184.87,130.15 182.68,125.43\n             177.61,114.52 170.89,114.39 163.98,111.93\n             161.46,111.02 159.88,112.29 160.10,114.96\"\n        data-elem={ExerciseAttributeValueEnum.BICEPS}\n        id=\"path150\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BICEPS)}\n        d=\"M 62.71,151.36\n           C 63.28,151.31 63.91,150.47 64.20,149.99\n             64.90,148.86 66.62,146.19 67.19,144.99\n             70.04,139.00 70.97,137.01 72.13,130.96\n             72.27,130.26 72.59,125.74 72.46,125.04\n             72.37,124.59 72.45,122.29 72.39,121.79\n             72.07,118.80 71.48,114.59 71.29,114.97\n             70.77,117.31 68.74,128.01 68.38,129.24\n             66.83,134.62 65.35,140.02 63.86,145.42\n             63.43,146.98 63.07,148.57 62.68,150.14\n             62.81,150.23 62.54,151.37 62.71,151.36\"\n        data-elem={ExerciseAttributeValueEnum.BICEPS}\n        id=\"path148\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.BICEPS)}\n        d=\"M 49.72,164.55\n           C 49.74,165.40 49.90,166.25 50.02,167.32\n             50.27,167.02 50.37,166.93 50.44,166.81\n             51.85,164.42 53.07,161.88 54.71,159.66\n             58.24,154.88 60.81,149.65 62.67,144.05\n             63.73,140.87 64.79,137.69 65.76,134.48\n             67.67,128.11 68.90,121.59 69.44,114.96\n             69.66,112.29 68.07,111.02 65.55,111.93\n             58.64,114.39 51.92,114.52 46.86,125.43\n             44.67,130.15 44.23,132.58 43.93,137.71\n             43.78,140.37 44.59,145.02 46.86,147.85\n             49.98,151.75 49.59,159.48 49.72,164.55\"\n        data-elem={ExerciseAttributeValueEnum.BICEPS}\n        id=\"path140\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 155.50,113.75\n           C 155.50,113.75 155.25,124.75 155.25,124.75\n             155.25,124.75 156.00,137.75 156.00,137.75\n             156.00,137.75 159.50,143.75 159.50,143.75\n             159.50,143.75 163.25,151.00 163.25,151.00\n             163.25,151.00 168.75,152.50 168.75,152.50\n             168.75,152.50 174.50,160.25 174.50,160.25\n             174.50,160.25 180.25,168.00 180.25,168.00\n             180.25,168.00 181.00,154.50 181.00,154.50\n             181.00,154.50 183.50,148.25 183.50,148.25\n             183.50,148.25 187.25,144.75 187.25,144.75\n             187.25,144.75 188.75,136.25 188.75,136.25\n             188.75,136.25 188.50,129.00 188.50,129.00\n             188.50,129.00 184.50,121.75 184.50,121.75\n             184.50,121.75 181.00,118.25 181.00,118.25\n             181.00,118.25 174.50,115.00 174.50,115.00\n             174.50,115.00 165.50,111.75 165.50,111.75\n             165.50,111.75 158.25,111.00 158.25,111.00\n             158.25,111.00 155.25,113.50 155.25,113.50\"\n        data-elem={ExerciseAttributeValueEnum.BICEPS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/calves-group.tsx",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const CalvesGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.CALVES)}>\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CALVES)}\n        d=\"M 432.29,380.53\n           C 433.40,381.41 434.63,381.43 435.95,380.70\n             439.00,379.01 440.64,376.49 441.69,373.67\n             443.04,370.01 443.56,366.26 443.57,362.48\n             443.57,358.91 443.33,355.35 443.19,351.78\n             442.63,345.13 441.79,338.51 440.02,332.04\n             439.37,329.64 438.21,327.33 436.04,325.58\n             435.22,324.92 434.11,324.49 433.07,324.05\n             432.14,323.66 431.50,324.04 431.47,324.89\n             431.41,326.32 431.40,327.76 431.48,329.19\n             431.60,331.20 431.67,334.59 431.68,336.61\n             431.71,344.21 430.33,346.48 428.25,353.24\n             426.65,358.46 426.07,363.45 426.64,368.76\n             426.90,371.21 427.71,373.95 429.59,377.24\n             429.96,377.89 431.65,380.01 432.29,380.53\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        id=\"path64\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CALVES)}\n        d=\"M 378.79,319.96\n           C 378.38,319.97 377.85,320.51 377.62,320.95\n             376.44,323.18 374.52,325.61 373.42,327.88\n             370.78,333.40 369.25,335.88 367.29,342.73\n             366.18,346.60 364.81,350.27 364.42,353.86\n             364.18,356.09 364.24,360.68 364.39,362.45\n             364.76,366.72 367.79,368.98 371.84,370.05\n             374.92,370.87 377.02,369.61 377.26,366.58\n             377.55,363.00 377.46,359.38 377.80,355.80\n             378.36,349.86 379.04,343.93 379.85,338.03\n             380.43,333.81 381.25,329.63 381.25,325.35\n             381.25,323.72 380.80,322.28 380.00,320.91\n             379.75,320.49 379.19,319.95 378.79,319.96\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        id=\"path46\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 90.50,307.75\n           C 90.50,307.75 77.50,324.75 77.50,324.75\n             77.50,324.75 72.50,337.00 72.50,337.00\n             72.50,337.00 68.25,350.75 68.25,350.75\n             68.25,350.75 68.25,368.75 68.25,368.75\n             68.25,368.75 69.75,385.25 69.75,385.25\n             69.75,385.25 74.25,402.00 74.25,402.00\n             74.25,402.00 79.50,411.25 79.50,411.25\n             79.50,411.25 88.25,422.25 88.25,422.25\n             88.25,422.25 93.75,428.25 93.75,428.25\n             93.75,428.25 94.00,421.25 94.00,421.25\n             94.00,421.25 91.50,404.75 91.50,404.75\n             91.50,404.75 93.75,391.75 93.75,391.75\n             93.75,391.75 99.25,378.25 99.25,378.25\n             99.25,378.25 101.25,365.75 101.25,365.75\n             101.25,365.75 97.50,346.75 97.50,346.75\n             97.50,346.75 95.25,332.50 95.25,332.50\n             95.25,332.50 94.50,319.75 94.50,319.75\n             94.50,319.75 92.75,310.25 92.75,310.25\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 137.50,309.75\n           C 137.50,309.75 135.75,325.75 135.75,325.75\n             135.75,325.75 134.50,335.75 134.50,335.75\n             134.50,335.75 131.25,347.00 131.25,347.00\n             131.25,347.00 128.75,358.75 128.75,358.75\n             128.75,358.75 128.75,369.50 128.75,369.50\n             128.75,369.50 132.00,382.75 132.00,382.75\n             132.00,382.75 135.75,391.25 135.75,391.25\n             135.75,391.25 138.00,403.25 138.00,403.25\n             138.00,403.25 137.50,413.25 137.50,413.25\n             137.50,413.25 135.50,425.75 135.50,425.75\n             135.50,425.75 141.00,423.75 141.00,423.75\n             141.00,423.75 149.00,413.75 149.00,413.75\n             149.00,413.75 153.75,408.00 153.75,408.00\n             153.75,408.00 159.50,394.00 159.50,394.00\n             159.50,394.00 161.75,375.50 161.75,375.50\n             161.75,375.50 163.00,361.25 163.00,361.25\n             163.00,361.25 162.00,347.00 162.00,347.00\n             162.00,347.00 159.00,337.50 159.00,337.50\n             159.00,337.50 154.75,330.00 154.75,329.75\n             154.75,329.50 147.25,320.25 147.25,320.25\n             147.25,320.25 142.75,314.25 142.75,314.25\n             142.75,314.25 141.75,310.75 139.50,309.25\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 370.00,328.00\n           C 370.00,328.00 365.25,339.75 365.25,339.75\n             365.25,339.75 361.75,349.00 361.75,349.00\n             361.75,349.00 361.75,362.25 361.75,362.25\n             361.75,362.25 364.25,377.00 364.25,377.00\n             364.25,377.00 370.50,395.75 370.50,395.75\n             370.50,395.75 373.00,409.50 373.00,409.50\n             373.00,409.50 372.00,428.25 372.00,428.25\n             372.00,428.25 375.00,425.75 375.00,425.75\n             375.00,425.75 378.50,419.00 378.50,419.00\n             378.50,419.00 382.75,415.75 382.75,415.75\n             382.75,415.75 388.50,415.75 388.50,415.75\n             388.50,415.75 393.25,421.75 393.25,421.75\n             393.25,421.75 390.25,408.25 390.25,408.25\n             390.25,408.25 390.75,397.00 390.75,397.00\n             390.75,397.00 395.50,379.75 395.50,379.75\n             395.50,379.75 399.75,371.75 399.75,371.75\n             399.75,371.75 400.00,360.00 400.00,360.00\n             400.00,360.00 396.00,348.50 396.00,348.50\n             396.00,348.50 393.00,339.75 393.00,339.75\n             393.00,339.75 392.25,324.00 392.25,324.00\n             392.25,324.00 391.50,322.00 391.50,322.00\n             391.50,322.00 384.00,326.25 384.00,326.25\n             384.00,326.25 381.25,322.00 381.25,322.00\n             381.25,322.00 378.75,319.00 378.75,319.00\n             378.75,319.00 374.00,325.75 374.00,325.75\n             374.00,325.75 370.00,327.75 370.00,327.75\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 428.50,342.00\n           C 428.50,342.00 424.50,355.00 424.50,355.00\n             424.50,355.00 424.75,364.25 424.50,364.25\n             424.25,364.25 424.75,374.50 424.75,374.75\n             424.75,375.00 429.75,386.00 429.75,386.00\n             429.75,386.00 433.00,403.00 433.00,403.00\n             433.00,403.00 430.00,417.75 430.00,417.75\n             430.00,417.75 435.25,416.25 435.25,416.25\n             435.25,416.25 439.25,416.25 439.25,416.25\n             439.25,416.25 444.00,417.50 444.00,417.50\n             444.00,417.50 449.50,428.00 449.50,428.00\n             449.50,428.00 449.50,417.00 449.50,417.00\n             449.50,417.00 452.00,402.75 452.00,402.75\n             452.00,402.75 456.75,385.00 456.75,385.00\n             456.75,385.00 461.00,367.75 461.00,367.75\n             461.00,367.75 461.75,350.75 461.75,350.75\n             461.75,350.75 459.25,339.50 459.25,339.50\n             459.25,339.50 453.00,328.25 453.00,328.25\n             453.00,328.25 447.75,324.50 447.75,324.50\n             447.75,324.50 445.25,319.75 445.25,319.75\n             445.25,319.75 442.75,320.00 442.75,320.00\n             442.75,320.00 439.75,326.25 439.75,326.25\n             439.75,326.25 436.75,324.75 436.75,324.75\n             436.75,324.75 432.50,322.75 432.50,322.75\n             432.50,322.75 431.25,324.75 431.25,324.75\n             431.25,324.75 430.75,338.00 430.75,338.00\n             430.75,338.00 429.00,342.25 429.00,342.25\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CALVES)}\n        d=\"M 139.32,424.05\n           C 141.70,421.08 144.08,418.10 146.45,415.12\n             148.97,411.96 152.26,405.62 153.51,401.86\n             156.87,391.85 157.28,386.72 158.94,376.31\n             160.09,369.05 159.82,359.41 158.94,352.14\n             157.62,341.33 153.96,331.32 147.64,322.44\n             147.29,321.95 146.94,321.45 146.58,320.96\n             146.54,320.91 146.44,320.91 146.13,320.79\n             146.43,322.78 146.67,324.61 146.98,326.43\n             147.91,331.81 148.64,337.21 148.60,342.67\n             148.56,347.32 148.37,351.96 148.03,356.59\n             147.31,366.52 146.82,376.44 146.72,386.40\n             146.63,395.40 145.83,404.38 143.15,413.06\n             142.59,414.84 141.90,416.58 141.26,418.33\n             141.22,418.46 141.14,418.57 140.94,418.63\n             141.67,416.00 142.38,413.37 143.13,410.74\n             144.74,405.12 145.31,399.35 145.15,393.54\n             144.98,387.94 144.69,382.34 144.13,376.77\n             143.78,373.26 142.90,369.80 142.14,366.34\n             140.73,359.82 139.24,353.32 137.77,346.81\n             137.50,345.59 137.23,344.36 136.95,343.14\n             136.88,342.82 135.74,338.70 135.55,338.10\n             135.34,338.63 135.20,345.86 135.15,346.10\n             134.32,350.59 133.77,354.30 132.82,358.77\n             131.80,363.49 132.05,369.81 133.01,374.36\n             135.19,384.69 138.33,388.97 140.42,399.31\n             141.25,403.45 141.10,409.29 139.84,413.31\n             138.78,416.70 138.50,420.33 137.91,423.85\n             137.85,424.21 138.01,424.61 138.12,425.30\n             138.67,424.74 139.02,424.42 139.32,424.05\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        id=\"path184\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CALVES)}\n        d=\"M 140.06,310.63\n           C 139.89,310.30 139.37,309.86 139.16,309.92\n             138.81,310.03 138.41,310.48 138.33,310.85\n             137.74,313.76 137.01,316.65 136.71,319.59\n             136.18,324.67 135.74,329.77 136.44,334.87\n             136.69,336.74 137.28,338.57 137.74,340.41\n             138.57,343.73 139.49,347.03 140.23,350.37\n             141.71,356.98 143.10,363.62 144.54,370.25\n             144.65,370.74 144.83,371.22 144.98,371.70\n             145.09,371.70 145.20,371.70 145.31,371.69\n             145.42,371.21 145.61,370.73 145.64,370.24\n             145.97,365.72 146.35,361.20 146.56,356.67\n             146.80,351.27 146.90,345.86 147.02,340.46\n             147.14,335.43 146.30,330.50 145.04,325.67\n             144.00,321.69 142.77,317.76 141.55,313.84\n             141.20,312.72 140.61,311.67 140.06,310.63\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        id=\"path178\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CALVES)}\n        d=\"M 89.90,310.63\n           C 89.35,311.67 88.76,312.72 88.42,313.84\n             87.19,317.76 85.96,321.69 84.92,325.67\n             83.66,330.50 82.83,335.43 82.94,340.46\n             83.07,345.86 83.17,351.27 83.41,356.67\n             83.61,361.20 84.00,365.72 84.32,370.24\n             84.36,370.73 84.54,371.21 84.65,371.69\n             84.76,371.70 84.87,371.70 84.98,371.70\n             85.13,371.22 85.32,370.74 85.43,370.25\n             86.86,363.62 88.25,356.98 89.73,350.37\n             90.48,347.03 91.40,343.73 92.22,340.41\n             92.68,338.57 93.27,336.74 93.53,334.87\n             94.22,329.77 93.78,324.67 93.26,319.59\n             92.95,316.65 92.23,313.76 91.63,310.85\n             91.55,310.48 91.15,310.03 90.81,309.92\n             90.59,309.86 90.08,310.30 89.90,310.63\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        id=\"path168\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CALVES)}\n        d=\"M 94.81,346.10\n           C 94.77,345.86 94.63,338.63 94.42,338.10\n             94.22,338.70 93.09,342.82 93.01,343.14\n             92.73,344.36 92.47,345.59 92.19,346.81\n             90.73,353.32 89.24,359.82 87.82,366.34\n             87.07,369.80 86.19,373.26 85.84,376.77\n             85.28,382.34 84.98,387.94 84.82,393.54\n             84.65,399.35 85.23,405.12 86.83,410.74\n             87.58,413.37 88.30,416.00 89.03,418.63\n             88.82,418.57 88.74,418.46 88.70,418.33\n             88.07,416.58 87.37,414.84 86.82,413.06\n             84.13,404.38 83.34,395.40 83.25,386.40\n             83.15,376.44 82.65,366.52 81.93,356.59\n             81.60,351.96 81.40,347.32 81.37,342.67\n             81.33,337.21 82.05,331.81 82.98,326.43\n             83.29,324.61 83.53,322.78 83.83,320.79\n             83.52,320.91 83.42,320.91 83.39,320.96\n             83.03,321.45 82.67,321.95 82.32,322.44\n             76.01,331.32 72.34,341.33 71.03,352.14\n             70.15,359.41 69.87,369.05 71.03,376.31\n             72.68,386.72 73.10,391.85 76.45,401.86\n             77.71,405.62 80.99,411.96 83.51,415.12\n             85.89,418.10 88.26,421.08 90.65,424.05\n             90.94,424.42 91.30,424.74 91.85,425.30\n             91.95,424.61 92.12,424.21 92.06,423.85\n             91.47,420.33 91.19,416.70 90.13,413.31\n             88.87,409.29 88.71,403.45 89.55,399.31\n             91.63,388.97 94.77,384.69 96.95,374.36\n             97.92,369.81 98.16,363.49 97.15,358.77\n             96.19,354.30 95.64,350.59 94.81,346.10\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        id=\"path166\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CALVES)}\n        d=\"M 435.16,400.96\n           C 435.80,403.90 435.68,408.43 434.86,411.24\n             434.49,412.51 434.02,413.76 433.62,415.03\n             433.48,415.49 433.43,415.97 433.28,416.72\n             434.00,416.23 434.43,415.89 434.91,415.61\n             437.86,413.92 442.04,414.73 444.56,417.47\n             446.39,419.46 447.22,421.86 447.69,424.42\n             447.77,424.86 447.87,425.29 447.96,425.73\n             448.07,425.72 448.19,425.72 448.31,425.72\n             448.35,425.30 448.45,424.87 448.41,424.47\n             448.14,422.20 447.95,419.91 447.52,417.67\n             446.73,413.51 447.11,409.44 448.07,405.33\n             450.25,396.03 452.42,386.74 455.38,377.61\n             456.43,374.38 457.84,370.84 457.82,367.15\n             456.04,369.15 453.82,371.17 453.13,373.46\n             450.31,382.77 448.01,392.19 446.07,401.70\n             446.04,401.85 445.98,401.99 445.67,402.09\n             446.09,399.65 446.48,397.21 446.94,394.78\n             447.41,392.33 447.93,389.89 448.46,387.46\n             449.01,384.99 449.60,382.52 450.17,380.05\n             450.73,377.64 451.28,375.23 451.90,372.58\n             450.31,373.01 449.02,373.47 447.67,373.68\n             446.49,373.87 445.25,373.88 444.05,373.77\n             443.28,373.70 443.14,374.02 442.97,374.54\n             442.31,376.62 441.32,378.52 439.63,380.05\n             436.39,382.97 434.38,383.07 430.92,380.26\n             431.36,382.20 431.74,383.91 432.13,385.61\n             433.40,391.29 433.93,395.27 435.16,400.96\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        id=\"path68\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CALVES)}\n        d=\"M 445.04,355.80\n           C 445.38,359.38 445.29,363.00 445.58,366.58\n             445.81,369.61 447.92,370.87 451.00,370.05\n             455.05,368.98 458.08,366.72 458.45,362.45\n             458.60,360.68 458.66,356.09 458.42,353.86\n             458.03,350.27 456.66,346.60 455.55,342.73\n             453.59,335.88 452.06,333.40 449.42,327.88\n             448.32,325.61 446.40,323.18 445.22,320.95\n             444.99,320.51 444.46,319.97 444.05,319.96\n             443.65,319.95 443.09,320.49 442.84,320.91\n             442.04,322.28 441.59,323.72 441.59,325.35\n             441.58,329.63 442.41,333.81 442.99,338.03\n             443.80,343.93 444.48,349.86 445.04,355.80\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        id=\"path62\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CALVES)}\n        d=\"M 383.21,380.05\n           C 381.52,378.52 380.53,376.62 379.87,374.54\n             379.70,374.02 379.56,373.70 378.79,373.77\n             377.59,373.88 376.35,373.87 375.17,373.68\n             373.82,373.47 372.53,373.01 370.94,372.58\n             371.56,375.23 372.11,377.64 372.67,380.05\n             373.24,382.52 373.83,384.99 374.38,387.46\n             374.91,389.89 375.43,392.33 375.90,394.78\n             376.36,397.21 376.75,399.65 377.17,402.09\n             376.86,401.99 376.80,401.85 376.77,401.70\n             374.83,392.19 372.53,382.77 369.71,373.46\n             369.02,371.17 366.80,369.15 365.02,367.15\n             365.00,370.84 366.41,374.38 367.46,377.61\n             370.42,386.74 372.59,396.03 374.76,405.33\n             375.73,409.44 376.11,413.51 375.32,417.67\n             374.89,419.91 374.70,422.20 374.43,424.47\n             374.39,424.87 374.49,425.30 374.53,425.72\n             374.65,425.72 374.76,425.72 374.88,425.73\n             374.97,425.29 375.07,424.86 375.15,424.42\n             375.62,421.86 376.45,419.46 378.28,417.47\n             380.80,414.73 384.98,413.92 387.93,415.61\n             388.41,415.89 388.84,416.23 389.56,416.72\n             389.41,415.97 389.36,415.49 389.22,415.03\n             388.82,413.76 388.35,412.51 387.98,411.24\n             387.16,408.43 387.04,403.90 387.68,400.96\n             388.91,395.27 389.44,391.29 390.71,385.61\n             391.09,383.91 391.48,382.20 391.92,380.26\n             388.46,383.07 386.45,382.97 383.21,380.05\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        id=\"path54\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CALVES)}\n        d=\"M 391.16,336.61\n           C 391.17,334.59 391.24,331.20 391.35,329.19\n             391.44,327.76 391.43,326.32 391.37,324.89\n             391.34,324.04 390.70,323.66 389.77,324.05\n             388.73,324.49 387.62,324.92 386.79,325.58\n             384.63,327.33 383.47,329.64 382.82,332.04\n             381.05,338.51 380.21,345.13 379.65,351.78\n             379.51,355.35 379.27,358.91 379.27,362.48\n             379.28,366.26 379.80,370.01 381.15,373.67\n             382.20,376.49 383.84,379.01 386.89,380.70\n             388.21,381.43 389.44,381.41 390.54,380.53\n             391.19,380.01 392.88,377.89 393.25,377.24\n             395.13,373.95 395.94,371.21 396.20,368.76\n             396.77,363.45 396.19,358.46 394.59,353.24\n             392.50,346.48 391.13,344.21 391.16,336.61\"\n        data-elem={ExerciseAttributeValueEnum.CALVES}\n        id=\"path50\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/chest-group.tsx",
    "content": "import React from \"react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const ChestGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.CHEST)}>\n      <path\n        className=\"fill-transparent\"\n        d=\"M 72.50,111.50\n           C 72.50,111.50 77.50,102.50 77.50,102.25\n             77.50,102.00 81.75,94.00 81.75,94.00\n             81.75,94.00 84.50,87.50 84.50,87.50\n             84.50,87.50 85.50,85.50 85.50,85.50\n             85.50,85.50 88.75,83.25 88.75,83.25\n             88.75,83.25 95.50,83.50 95.50,83.50\n             95.50,83.50 99.75,84.25 99.75,84.25\n             99.75,84.25 104.25,86.00 104.25,86.00\n             104.25,86.00 113.00,86.75 113.00,86.75\n             113.00,86.75 120.50,86.75 120.50,86.75\n             120.50,86.75 126.75,86.00 126.75,86.00\n             126.75,86.00 133.50,83.75 133.50,83.75\n             133.50,83.75 138.00,83.50 138.00,83.50\n             138.00,83.50 141.50,83.75 141.50,83.75\n             141.50,83.75 143.75,86.25 143.75,86.25\n             143.75,86.25 149.00,96.00 149.00,96.00\n             149.00,96.00 154.25,106.00 154.25,106.00\n             154.25,106.00 156.00,110.50 156.00,110.50\n             156.00,110.50 155.00,115.00 155.00,115.00\n             155.00,115.00 149.75,118.00 149.75,118.00M 136.75,123.50\n           C 136.75,123.50 132.50,124.25 132.50,124.25\n             132.50,124.25 127.75,123.75 127.75,123.75\n             127.75,123.75 119.75,120.25 119.75,120.25\n             119.75,120.25 115.00,127.75 115.00,127.75\n             115.00,127.75 109.25,120.50 109.25,120.50\n             109.25,120.50 103.00,124.00 103.00,124.00\n             103.00,124.00 98.50,124.75 98.50,124.75\n             98.50,124.75 91.75,123.25 91.75,123.25\n             91.75,123.25 80.00,118.00 80.00,118.00\n             80.00,118.00 73.00,111.75 73.00,111.75\"\n        data-elem={ExerciseAttributeValueEnum.CHEST}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CHEST)}\n        d=\"M 128.00,122.83\n           C 132.18,123.49 136.25,123.15 140.14,121.62\n             145.31,119.58 149.70,116.28 153.73,112.49\n             154.47,111.79 154.70,110.91 154.40,109.98\n             153.95,108.57 153.53,107.12 152.81,105.84\n             149.78,100.45 146.82,95.05 144.62,89.25\n             143.53,86.37 139.34,82.87 136.11,83.86\n             131.78,85.18 127.51,86.71 123.26,88.29\n             119.12,89.83 116.94,93.03 116.62,97.33\n             116.32,101.36 116.14,105.41 116.31,109.44\n             116.56,115.50 121.62,121.81 128.00,122.83\"\n        data-elem={ExerciseAttributeValueEnum.CHEST}\n        id=\"path106\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CHEST)}\n        d=\"M 115.70,124.93\n           C 116.59,123.70 117.47,122.46 118.32,121.20\n             118.61,120.76 118.81,120.26 119.06,119.77\n             118.96,119.45 118.93,119.13 118.77,118.89\n             117.79,117.39 116.84,115.87 115.76,114.45\n             114.99,113.43 114.47,113.43 113.69,114.49\n             112.62,115.92 111.67,117.44 110.74,118.97\n             110.55,119.30 110.53,119.94 110.72,120.25\n             111.72,121.86 112.79,123.44 113.91,124.98\n             114.45,125.72 115.15,125.69 115.70,124.93\"\n        data-elem={ExerciseAttributeValueEnum.CHEST}\n        id=\"path138\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.CHEST)}\n        d=\"M 112.42,97.33\n           C 112.10,93.03 109.92,89.83 105.78,88.29\n             101.53,86.71 97.26,85.18 92.93,83.86\n             89.70,82.87 85.51,86.37 84.42,89.25\n             82.22,95.05 79.26,100.45 76.23,105.84\n             75.51,107.12 75.09,108.57 74.64,109.98\n             74.34,110.91 74.57,111.79 75.31,112.49\n             79.34,116.28 83.73,119.58 88.90,121.62\n             92.79,123.15 96.86,123.49 101.04,122.83\n             107.42,121.81 112.48,115.50 112.73,109.44\n             112.90,105.41 112.72,101.36 112.42,97.33\"\n        data-elem={ExerciseAttributeValueEnum.CHEST}\n        id=\"path198\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        d=\"M 114.71,95.58\n           C 114.89,95.35 115.00,95.27 115.02,95.18\n             115.68,92.72 116.69,90.44 118.43,88.53\n             118.64,88.30 118.61,87.85 118.69,87.50\n             118.33,87.47 117.94,87.32 117.63,87.42\n             115.83,88.01 114.05,88.05 112.23,87.51\n             111.81,87.38 111.31,87.52 110.84,87.54\n             111.01,87.97 111.09,88.49 111.37,88.83\n             112.74,90.47 113.65,92.33 114.26,94.36\n             114.37,94.74 114.53,95.10 114.71,95.58\"\n        data-elem={ExerciseAttributeValueEnum.CHEST}\n        fill=\"#757575\"\n        id=\"path160\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/forearms-group.tsx",
    "content": "import React from \"react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const ForearmsGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.FOREARMS)}>\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.FOREARMS)}\n        d=\"M 355.58,156.70\n           C 355.59,156.41 355.36,155.93 355.14,155.87\n             354.89,155.80 354.44,156.04 354.24,156.27\n             353.09,157.56 351.97,158.87 350.89,160.21\n             349.34,162.13 347.83,164.08 346.29,166.01\n             343.77,169.18 342.67,172.95 341.43,176.65\n             338.78,184.50 336.21,192.37 333.61,200.23\n             333.56,200.37 333.47,200.49 333.41,200.62\n             333.41,200.62 333.22,200.56 333.22,200.56\n             333.93,198.29 334.63,196.01 335.37,193.75\n             337.89,185.98 340.45,178.23 342.93,170.45\n             343.64,168.21 345.02,166.40 346.37,164.53\n             347.10,163.53 347.70,162.43 348.24,161.32\n             348.74,160.29 348.53,159.30 347.48,158.68\n             346.54,158.12 345.55,157.52 344.49,157.26\n             343.88,157.12 343.42,157.14 343.10,157.31\n             343.10,157.31 343.10,157.31 343.10,157.31\n             343.10,157.31 343.08,157.33 343.08,157.33\n             342.97,157.39 342.88,157.48 342.81,157.58\n             342.81,157.58 340.41,159.83 340.41,159.83\n             339.15,160.69 337.92,161.59 336.53,162.57\n             336.53,162.57 333.38,166.06 333.38,166.06\n             332.94,166.53 332.53,167.03 332.12,167.52\n             327.93,172.51 326.70,178.25 326.95,184.54\n             327.20,190.89 326.96,197.24 325.80,203.51\n             325.66,203.51 325.52,203.50 325.38,203.50\n             325.49,199.61 325.66,195.72 325.69,191.83\n             325.71,189.79 325.50,187.74 325.37,185.71\n             325.35,185.36 325.33,184.98 325.16,184.69\n             324.95,184.33 324.59,184.05 324.29,183.73\n             324.05,184.06 323.71,184.35 323.60,184.71\n             322.71,187.71 322.00,190.76 320.98,193.70\n             318.82,199.88 316.52,206.00 314.26,212.14\n             313.85,213.26 314.07,213.77 315.21,213.55\n             319.80,212.64 323.64,214.58 327.53,216.36\n             328.39,216.75 328.98,216.80 329.48,215.84\n             333.40,208.28 337.44,200.78 341.28,193.19\n             344.89,186.07 347.98,178.75 350.48,171.20\n             350.94,169.78 351.85,168.51 352.52,167.15\n             353.47,165.20 354.59,163.29 355.25,161.25\n             355.71,159.84 355.54,158.22 355.58,156.70\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        id=\"path94\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.FOREARMS)}\n        d=\"M 328.92,170.86\n           C 331.73,166.23 335.11,161.99 338.87,158.01\n             339.52,157.32 339.80,156.59 339.61,155.68\n             339.18,153.57 338.81,151.44 338.37,149.33\n             338.28,148.91 338.00,148.53 337.81,148.13\n             337.65,148.13 337.49,148.12 337.33,148.12\n             332.60,155.30 328.07,162.55 327.92,171.34\n             328.04,171.41 328.15,171.47 328.27,171.54\n             328.49,171.31 328.76,171.12 328.92,170.86\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        id=\"path92\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.FOREARMS)}\n        d=\"M 507.15,213.55\n           C 508.29,213.77 508.51,213.26 508.10,212.14\n             505.85,206.00 503.54,199.88 501.38,193.70\n             500.36,190.76 499.65,187.71 498.76,184.71\n             498.65,184.35 498.31,184.06 498.08,183.73\n             497.78,184.05 497.41,184.33 497.20,184.69\n             497.03,184.98 497.01,185.36 496.99,185.71\n             496.86,187.74 496.65,189.79 496.67,191.83\n             496.70,195.72 496.87,199.61 496.98,203.50\n             496.84,203.50 496.70,203.51 496.56,203.51\n             495.41,197.24 495.16,190.89 495.41,184.54\n             495.66,178.25 494.43,172.51 490.25,167.52\n             489.97,167.19 489.70,166.86 489.42,166.54\n             489.42,166.54 489.42,166.54 489.42,166.54\n             489.28,166.37 489.13,166.22 488.98,166.06\n             488.98,166.06 485.83,162.57 485.83,162.57\n             484.44,161.59 483.21,160.69 481.95,159.83\n             481.95,159.83 479.55,157.58 479.55,157.58\n             479.48,157.48 479.39,157.39 479.28,157.33\n             479.28,157.33 479.26,157.31 479.26,157.31\n             479.26,157.31 479.26,157.31 479.26,157.31\n             478.94,157.14 478.48,157.12 477.87,157.26\n             476.81,157.52 475.82,158.12 474.88,158.68\n             473.83,159.30 473.62,160.29 474.12,161.32\n             474.66,162.43 475.26,163.53 475.99,164.53\n             477.34,166.40 478.72,168.21 479.43,170.45\n             481.91,178.23 484.47,185.98 486.99,193.75\n             487.73,196.01 488.43,198.29 489.14,200.56\n             489.08,200.58 489.02,200.60 488.96,200.62\n             488.89,200.49 488.80,200.37 488.75,200.23\n             486.15,192.37 483.58,184.50 480.94,176.65\n             479.69,172.95 478.59,169.18 476.07,166.01\n             474.53,164.08 473.02,162.13 471.47,160.21\n             470.39,158.87 469.27,157.56 468.12,156.27\n             467.92,156.04 467.47,155.80 467.22,155.87\n             467.00,155.93 466.77,156.41 466.78,156.70\n             466.83,158.22 466.65,159.84 467.11,161.25\n             467.77,163.29 468.89,165.20 469.84,167.15\n             470.51,168.51 471.42,169.78 471.89,171.20\n             474.38,178.75 477.47,186.07 481.08,193.19\n             484.92,200.78 488.96,208.28 492.89,215.84\n             493.38,216.80 493.97,216.75 494.83,216.36\n             498.72,214.58 502.56,212.64 507.15,213.55\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        id=\"path86\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.FOREARMS)}\n        d=\"M 483.49,158.01\n           C 487.25,161.99 490.63,166.23 493.44,170.86\n             493.60,171.12 493.87,171.31 494.09,171.54\n             494.21,171.47 494.32,171.41 494.44,171.34\n             494.30,162.55 489.77,155.30 485.03,148.12\n             484.87,148.12 484.72,148.13 484.56,148.13\n             484.36,148.53 484.08,148.91 483.99,149.33\n             483.55,151.44 483.18,153.57 482.75,155.68\n             482.57,156.59 482.84,157.32 483.49,158.01\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        id=\"path80\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 43.75,147.00\n           C 43.75,147.00 37.50,154.25 37.50,154.25\n             37.50,154.25 34.25,163.00 34.25,163.00\n             34.25,163.00 32.00,173.00 32.00,173.00\n             32.00,173.00 30.75,183.25 30.75,183.25\n             30.75,183.25 27.75,195.75 27.75,195.75\n             27.75,195.75 22.50,207.50 22.50,207.50\n             22.50,207.50 20.00,213.50 20.00,213.50\n             20.00,213.50 22.00,213.75 22.00,213.75\n             22.00,213.75 26.75,212.00 26.75,211.75\n             26.75,211.50 33.25,203.50 33.25,203.50\n             33.25,203.50 39.00,196.25 39.00,196.25\n             39.00,196.25 36.75,213.75 36.75,213.75\n             36.75,213.75 37.50,222.25 37.50,222.25\n             37.50,222.25 37.75,224.25 37.75,224.25\n             37.75,224.25 41.00,221.25 41.00,221.25\n             41.00,221.25 47.75,210.25 47.75,210.25\n             47.75,210.25 54.25,195.75 54.25,195.75\n             54.25,195.75 62.00,173.75 62.00,173.75M 63.75,166.75\n           C 63.75,166.75 66.25,161.25 66.25,161.25\n             66.25,161.25 66.25,153.50 66.25,153.50\n             66.25,153.50 63.00,150.75 63.00,150.75\n             63.00,150.75 59.00,155.50 59.00,155.50\n             59.00,155.50 50.75,166.00 50.75,166.00\n             50.75,166.00 50.00,157.50 50.00,157.50\n             50.00,157.50 48.25,150.25 48.25,150.25\n             48.25,150.25 46.75,148.75 46.75,148.75\n             46.75,148.75 43.50,146.75 43.50,146.75\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 168.50,176.75\n           C 168.50,176.75 171.50,183.25 171.50,183.25\n             171.50,183.25 175.50,195.75 175.50,195.75\n             175.50,195.75 184.50,212.50 184.50,212.50\n             184.50,212.50 191.00,227.00 191.00,227.00\n             191.00,227.00 192.25,220.50 192.25,220.50\n             192.25,220.50 192.25,213.25 192.25,213.25\n             192.25,213.25 191.25,200.00 191.25,200.00\n             191.25,200.00 188.75,193.25 188.75,193.25\n             188.75,193.25 200.00,207.00 200.00,207.00\n             200.00,207.00 205.00,212.00 205.00,212.00\n             205.00,212.00 210.00,213.50 210.00,213.50\n             210.00,213.50 206.50,207.25 206.50,207.25\n             206.50,207.25 201.75,194.50 201.75,194.50\n             201.75,194.50 198.75,183.25 198.75,183.25\n             198.75,183.25 197.25,168.25 197.25,168.25\n             197.25,168.25 192.75,158.50 192.75,158.50\n             192.75,158.50 189.00,151.50 189.00,151.50\n             189.00,151.50 186.50,148.50 186.50,148.50\n             186.50,148.50 183.25,148.50 183.25,148.50\n             183.25,148.50 181.50,152.50 181.50,152.50\n             181.50,152.50 180.50,155.75 180.50,155.75\n             180.50,155.75 180.75,159.25 180.75,159.25\n             180.75,159.25 179.25,167.75 179.25,167.75\n             179.25,167.75 173.25,158.50 173.25,158.50\n             173.25,158.50 167.00,152.25 167.00,152.25\n             167.00,152.25 164.00,152.00 164.00,152.00\n             164.00,152.00 164.00,155.00 164.00,155.00\n             164.00,155.00 165.25,164.00 165.25,164.00\n             165.25,164.00 167.25,175.75 167.25,175.75\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 334.25,147.25\n           C 334.25,147.25 326.75,161.25 326.75,161.25\n             326.75,161.25 324.25,172.50 324.25,172.50\n             324.25,172.50 324.00,180.00 324.00,180.00\n             324.00,180.00 318.75,189.25 318.75,189.25\n             318.75,189.25 315.25,200.50 315.25,200.75\n             315.25,201.00 310.50,214.25 310.50,214.25\n             310.50,214.25 317.25,214.25 317.25,214.25\n             317.25,214.25 325.75,217.00 325.75,217.00\n             325.75,217.00 329.75,218.75 329.75,218.75\n             329.75,218.75 335.25,213.75 335.25,213.75\n             332.22,215.34 348.00,186.50 348.00,186.50\n             348.00,186.50 356.75,167.00 356.75,167.00\n             356.75,167.00 358.25,155.50 358.25,155.50\n             358.25,155.50 355.50,154.50 355.50,154.50\n             355.50,154.50 350.00,156.50 350.00,156.50\n             350.00,156.50 342.75,155.25 342.75,155.25\n             342.75,155.25 340.25,153.25 340.25,153.25\n             340.25,153.25 338.00,146.25 338.00,146.25\n             338.00,146.25 333.75,147.50 333.75,147.50\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 464.50,162.00\n           C 464.50,162.00 485.75,208.75 485.75,208.75\n             485.75,208.75 491.00,215.75 491.00,215.75\n             491.00,215.75 492.00,218.25 492.00,218.25\n             492.00,218.25 498.50,215.75 498.50,215.75\n             498.50,215.75 510.25,214.25 510.25,214.25\n             510.25,214.25 510.25,211.25 510.25,211.25\n             510.25,211.25 506.00,196.50 506.00,196.50\n             506.00,196.50 500.00,182.25 500.00,182.25\n             500.00,182.25 498.50,178.25 498.50,178.25\n             498.50,178.25 497.25,162.75 497.25,162.75\n             497.25,162.75 492.75,153.50 492.75,153.50\n             492.75,153.50 487.50,146.25 487.50,146.25\n             487.50,146.25 485.50,145.00 485.50,145.00\n             485.50,145.00 483.25,150.00 483.25,150.00\n             483.25,150.00 481.25,154.00 481.25,154.00\n             481.25,154.00 472.50,156.75 472.50,156.75\n             472.50,156.75 464.75,155.00 464.75,155.00\n             464.75,155.00 463.75,157.50 463.75,157.50\n             463.75,157.50 463.50,159.50 464.50,162.25\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.FOREARMS)}\n        d=\"M 184.01,182.16\n           C 182.46,177.62 180.92,173.06 179.01,168.66\n             177.34,164.81 174.77,161.45 172.04,158.24\n             170.81,156.80 169.62,155.31 168.35,153.90\n             168.03,153.55 167.33,153.14 167.04,153.27\n             166.62,153.46 166.28,154.08 166.15,154.58\n             165.35,157.56 165.82,160.46 167.22,163.12\n             169.42,167.29 169.45,171.29 170.98,175.72\n             173.32,182.49 176.47,190.34 179.17,196.97\n             182.25,204.56 186.54,211.29 189.65,218.87\n             189.89,219.45 190.15,220.02 190.40,220.59\n             190.54,220.56 190.68,220.53 190.83,220.50\n             190.89,219.68 191.04,218.87 191.01,218.05\n             190.61,205.71 187.98,193.80 184.01,182.16\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        id=\"path154\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.FOREARMS)}\n        d=\"M 182.33,152.10\n           C 180.80,155.22 180.88,158.50 181.14,161.83\n             182.08,174.13 186.40,185.26 192.66,195.74\n             195.54,200.56 198.76,205.15 202.46,209.38\n             203.03,210.03 203.78,210.53 204.45,211.10\n             204.57,210.98 204.69,210.87 204.80,210.76\n             204.43,210.11 204.02,209.49 203.69,208.81\n             199.12,199.31 196.21,189.39 195.39,178.81\n             194.62,168.95 191.67,159.76 185.65,151.73\n             184.29,149.90 183.34,150.04 182.33,152.10\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        id=\"path156\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.FOREARMS)}\n        d=\"M 62.31,163.12\n           C 63.71,160.46 64.19,157.56 63.39,154.58\n             63.25,154.08 62.91,153.46 62.49,153.27\n             62.20,153.14 61.50,153.55 61.18,153.90\n             59.91,155.31 58.73,156.80 57.49,158.24\n             54.77,161.45 52.19,164.81 50.52,168.66\n             48.61,173.06 47.07,177.62 45.52,182.16\n             41.56,193.80 38.92,205.71 38.52,218.05\n             38.50,218.87 38.64,219.68 38.71,220.50\n             38.85,220.53 38.99,220.56 39.14,220.59\n             39.39,220.02 39.65,219.45 39.88,218.87\n             43.00,211.29 47.28,204.56 50.36,196.97\n             53.06,190.34 56.21,182.49 58.55,175.72\n             60.09,171.29 60.12,167.29 62.31,163.12\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        id=\"path144\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.FOREARMS)}\n        d=\"M 27.08,209.38\n           C 30.78,205.15 33.99,200.56 36.87,195.74\n             43.13,185.26 47.45,174.13 48.40,161.83\n             48.65,158.50 48.73,155.22 47.21,152.10\n             46.20,150.04 45.25,149.90 43.88,151.73\n             37.87,159.76 34.92,168.95 34.15,178.81\n             33.32,189.39 30.42,199.31 25.84,208.81\n             25.52,209.49 25.10,210.11 24.73,210.76\n             24.85,210.87 24.96,210.98 25.08,211.10\n             25.75,210.53 26.50,210.03 27.08,209.38\"\n        data-elem={ExerciseAttributeValueEnum.FOREARMS}\n        id=\"path146\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/glutes-group.tsx",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const GlutesGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.GLUTES)}>\n      <path\n        className=\"fill-transparent\"\n        d=\"M 394.25,186.00\n           C 394.25,186.00 387.50,187.25 387.50,187.25\n             387.50,187.25 376.00,194.00 376.00,194.00\n             376.00,194.00 370.00,202.75 370.00,202.75\n             370.00,202.75 366.25,207.25 366.25,207.25\n             366.25,207.25 372.75,211.25 372.75,211.25\n             372.75,211.25 377.00,217.25 377.00,217.25\n             377.00,217.25 378.75,228.25 378.75,228.25\n             378.75,228.25 377.25,238.75 377.25,238.75\n             377.25,238.75 377.00,241.25 377.00,241.25\n             377.00,241.25 384.50,240.00 384.50,240.00\n             384.50,240.00 396.25,235.75 396.25,235.75\n             396.25,235.75 405.75,229.50 405.75,229.50\n             405.75,229.50 411.25,225.50 411.25,225.50\n             411.25,225.50 418.75,231.50 418.75,231.50\n             418.75,231.50 429.75,236.25 429.75,236.25\n             429.75,236.25 441.50,240.00 441.50,240.00\n             441.50,240.00 445.75,240.00 445.75,240.00\n             445.75,240.00 445.00,231.75 445.00,231.75\n             445.00,231.75 444.50,223.50 444.50,223.50\n             444.50,223.50 446.00,215.25 446.00,215.25\n             446.00,215.25 451.50,209.75 451.50,209.75\n             451.50,209.75 455.75,208.00 455.75,208.00\n             455.75,208.00 452.00,200.75 452.00,200.75\n             452.00,200.75 448.00,196.75 448.00,196.75\n             448.00,196.75 441.00,190.00 441.00,190.00\n             441.00,190.00 436.75,187.75 436.75,187.75\n             436.75,187.75 429.25,186.00 429.25,186.00\n             429.25,186.00 426.00,185.75 426.00,185.75\n             426.00,185.75 423.50,194.75 423.50,194.75\n             423.50,194.75 418.75,205.25 418.75,205.25\n             418.75,205.25 413.75,211.25 413.75,211.25\n             413.75,211.25 411.00,213.75 411.00,213.75\n             411.00,213.75 408.25,210.75 408.25,210.75\n             408.25,210.75 402.50,201.50 402.50,201.50\n             402.50,201.50 399.50,192.50 399.50,192.50\n             399.50,192.50 397.25,186.25 397.25,186.25\n             397.25,186.25 394.50,185.75 394.50,185.75\"\n        data-elem={ExerciseAttributeValueEnum.GLUTES}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.GLUTES)}\n        d=\"M 424.40,232.59\n           C 429.67,235.69 435.46,237.94 441.58,239.60\n             443.95,240.25 445.11,239.44 444.76,237.31\n             444.43,234.27 443.96,231.23 443.80,228.18\n             443.60,224.00 444.35,219.87 445.72,215.83\n             446.63,213.13 448.53,211.05 451.46,209.66\n             453.26,208.80 453.59,208.14 452.95,206.57\n             450.74,201.11 447.74,196.03 442.81,191.94\n             439.51,189.20 435.63,187.30 430.95,186.71\n             428.73,186.43 427.74,186.99 427.22,188.85\n             424.62,198.20 419.82,206.79 413.60,214.80\n             411.72,217.20 411.67,219.22 412.98,221.68\n             415.46,226.35 419.58,229.74 424.40,232.59\"\n        data-elem={ExerciseAttributeValueEnum.GLUTES}\n        id=\"path66\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.GLUTES)}\n        d=\"M 395.62,188.85\n           C 395.10,186.99 394.11,186.43 391.89,186.71\n             387.21,187.30 383.33,189.20 380.03,191.94\n             375.10,196.03 372.10,201.11 369.89,206.57\n             369.25,208.14 369.58,208.80 371.38,209.66\n             374.31,211.05 376.21,213.13 377.12,215.83\n             378.49,219.87 379.24,224.00 379.04,228.18\n             378.88,231.23 378.41,234.27 378.08,237.31\n             377.73,239.44 378.89,240.25 381.26,239.60\n             387.38,237.94 393.17,235.69 398.44,232.59\n             403.26,229.74 407.38,226.35 409.86,221.68\n             411.17,219.22 411.12,217.20 409.24,214.80\n             403.02,206.79 398.22,198.20 395.62,188.85\"\n        data-elem={ExerciseAttributeValueEnum.GLUTES}\n        id=\"path42\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/hamstrings-group.tsx",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const HamstringsGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.HAMSTRINGS)}>\n      <path\n        className=\"fill-transparent\"\n        d=\"M 365.50,209.50\n           C 365.50,209.50 370.00,210.00 370.00,210.00\n             370.00,210.00 375.00,213.50 375.00,213.50\n             375.00,213.50 377.50,219.25 377.50,219.25\n             377.50,219.25 377.50,229.25 377.50,229.25\n             377.50,229.25 377.00,237.25 377.00,237.25\n             377.00,237.25 377.00,239.75 377.00,239.75\n             377.00,239.75 382.00,239.75 382.00,239.75\n             382.00,239.75 389.25,238.75 389.25,238.75\n             389.25,238.75 400.50,233.25 400.50,233.25\n             400.50,233.25 406.50,230.25 406.50,230.25\n             406.50,230.25 408.50,229.25 408.50,229.25\n             408.50,229.25 408.75,243.50 408.75,243.50\n             408.75,243.50 410.00,258.00 410.00,258.00\n             410.00,258.00 409.75,272.25 409.75,272.25\n             409.75,272.25 409.75,286.50 409.75,286.50\n             409.75,286.50 408.00,300.50 408.00,300.50\n             408.00,300.50 403.50,318.50 403.50,318.75\n             403.50,319.00 399.00,334.75 399.00,334.75\n             399.00,334.75 393.25,341.25 393.25,341.25\n             393.25,341.25 392.25,331.25 392.25,331.25\n             392.25,331.25 392.25,326.00 392.25,326.00\n             392.25,326.00 391.00,322.25 391.00,322.25\n             391.00,322.25 384.00,327.25 384.00,327.25\n             384.00,327.25 382.00,324.50 382.00,324.50\n             382.00,324.50 379.25,318.75 379.25,318.75\n             379.25,318.75 376.50,321.25 376.50,321.25\n             376.50,321.25 371.00,329.50 371.00,329.50\n             371.00,329.50 369.25,324.00 369.25,324.00\n             369.25,324.00 368.00,306.25 368.00,306.25\n             368.00,306.25 366.75,295.00 366.75,295.00\n             366.75,295.00 363.00,268.50 363.00,268.50\n             363.00,268.50 362.00,246.25 362.00,246.25\n             362.00,246.25 362.25,216.50 362.25,216.50\n             362.25,216.50 364.25,211.75 365.00,209.25\"\n        data-elem={ExerciseAttributeValueEnum.HAMSTRINGS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.HAMSTRINGS)}\n        d=\"M 441.45,241.64\n           C 440.46,241.28 439.45,240.94 438.44,240.64\n             436.85,240.18 436.23,240.55 436.10,242.07\n             435.85,244.93 435.62,247.81 435.72,250.66\n             435.90,255.66 436.30,260.65 436.66,265.64\n             436.81,267.61 437.17,269.56 437.26,271.53\n             437.59,278.00 437.84,284.47 438.11,290.93\n             438.45,299.17 440.40,307.00 444.28,314.45\n             446.28,318.27 447.81,322.30 449.56,326.23\n             449.65,326.44 449.77,326.63 449.88,326.83\n             450.01,326.85 450.14,326.86 450.28,326.88\n             450.60,325.93 451.09,325.00 451.21,324.03\n             451.51,321.76 451.84,319.46 451.78,317.19\n             451.66,312.62 451.12,308.06 451.05,303.49\n             451.05,303.27 451.05,303.05 451.05,302.83\n             451.05,302.83 453.07,291.65 453.07,291.65\n             453.26,290.74 453.49,289.83 453.62,288.91\n             454.93,279.90 456.51,270.91 457.44,261.87\n             458.22,254.46 458.36,247.00 458.53,239.56\n             458.64,234.72 458.43,229.86 458.12,225.03\n             457.86,221.14 457.32,217.25 456.72,213.38\n             456.38,211.20 454.98,210.62 452.62,211.34\n             450.64,211.95 449.28,213.23 448.21,214.79\n             445.94,218.14 445.41,221.86 445.29,225.66\n             445.12,230.79 446.16,235.77 447.42,240.71\n             447.37,243.31 443.19,242.21 441.45,241.64\"\n        data-elem={ExerciseAttributeValueEnum.HAMSTRINGS}\n        id=\"path74\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.HAMSTRINGS)}\n        d=\"M 429.00,338.21\n           C 429.23,338.17 429.46,338.13 429.69,338.09\n             429.69,338.09 429.79,327.66 429.79,327.66\n             429.83,326.68 429.94,325.69 429.92,324.71\n             429.88,323.32 430.47,322.23 431.49,321.13\n             434.73,317.60 437.07,313.64 437.94,309.12\n             438.19,307.88 438.03,306.68 437.66,305.53\n             437.66,305.53 437.66,305.53 437.66,305.53\n             437.66,305.53 435.81,287.16 435.81,287.16\n             435.81,287.16 435.81,287.13 435.81,287.13\n             435.82,286.71 435.84,286.29 435.84,285.87\n             435.89,281.30 435.47,276.73 435.16,272.17\n             434.79,266.95 434.36,261.73 433.85,256.52\n             433.43,252.12 433.01,247.71 432.31,243.35\n             431.70,239.54 429.74,236.47 425.50,234.84\n             422.94,233.86 420.36,232.82 418.43,230.88\n             417.71,230.16 416.55,230.62 416.44,231.60\n             416.43,231.77 416.43,231.95 416.45,232.13\n             416.45,232.13 416.46,235.53 416.46,235.53\n             416.43,236.96 415.11,254.53 415.20,255.95\n             415.78,266.09 415.88,279.95 416.44,290.10\n             416.76,295.71 418.39,304.42 420.34,311.87\n             422.59,320.53 423.36,325.91 427.66,335.88\n             428.14,336.65 428.55,337.43 429.00,338.21\"\n        data-elem={ExerciseAttributeValueEnum.HAMSTRINGS}\n        id=\"path70\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.HAMSTRINGS)}\n        d=\"M 434.82,322.36\n           C 435.93,323.13 436.89,324.09 437.93,324.95\n             438.28,325.24 438.65,325.50 439.19,325.91\n             439.48,324.90 439.65,324.07 439.97,323.29\n             440.65,321.65 441.34,319.99 442.17,318.41\n             442.59,317.62 442.83,316.93 442.40,316.12\n             441.33,314.11 440.26,312.11 439.06,309.86\n             438.81,310.34 438.69,310.50 438.65,310.67\n             437.79,314.07 436.20,317.16 434.26,320.11\n             433.54,321.21 433.74,321.62 434.82,322.36\"\n        data-elem={ExerciseAttributeValueEnum.HAMSTRINGS}\n        id=\"path58\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.HAMSTRINGS)}\n        d=\"M 406.39,232.13\n           C 406.41,231.95 406.41,231.77 406.40,231.60\n             406.29,230.62 405.13,230.16 404.41,230.88\n             402.47,232.82 399.90,233.86 397.34,234.84\n             393.10,236.47 391.14,239.54 390.53,243.35\n             389.83,247.71 389.41,252.12 388.99,256.52\n             388.48,261.73 388.05,266.95 387.68,272.17\n             387.37,276.73 386.95,281.30 387.00,285.87\n             387.00,286.29 387.02,286.71 387.03,287.13\n             387.03,287.13 387.03,287.16 387.03,287.16\n             387.03,287.16 385.18,305.53 385.18,305.53\n             385.18,305.53 385.18,305.53 385.18,305.53\n             384.81,306.68 384.65,307.88 384.89,309.12\n             385.77,313.64 388.11,317.60 391.35,321.13\n             392.37,322.23 392.96,323.32 392.92,324.71\n             392.90,325.69 393.01,326.68 393.05,327.66\n             393.05,327.66 393.15,338.09 393.15,338.09\n             393.38,338.13 393.61,338.17 393.84,338.21\n             394.29,337.43 394.70,336.65 395.18,335.88\n             399.48,325.91 400.25,320.53 402.50,311.87\n             404.45,304.42 406.08,295.71 406.40,290.10\n             406.96,279.95 407.06,266.09 407.64,255.95\n             407.73,254.53 406.41,236.96 406.38,235.53\n             406.38,235.53 406.39,232.13 406.39,232.13\"\n        data-elem={ExerciseAttributeValueEnum.HAMSTRINGS}\n        id=\"path48\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.HAMSTRINGS)}\n        d=\"M 383.65,325.91\n           C 384.19,325.50 384.56,325.24 384.91,324.95\n             385.95,324.09 386.91,323.13 388.02,322.36\n             389.10,321.62 389.30,321.21 388.58,320.11\n             386.64,317.16 385.05,314.07 384.19,310.67\n             384.15,310.50 384.03,310.34 383.78,309.86\n             382.58,312.11 381.51,314.11 380.44,316.12\n             380.01,316.93 380.25,317.62 380.67,318.41\n             381.50,319.99 382.19,321.65 382.87,323.29\n             383.19,324.07 383.36,324.90 383.65,325.91\"\n        data-elem={ExerciseAttributeValueEnum.HAMSTRINGS}\n        id=\"path40\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.HAMSTRINGS)}\n        d=\"M 377.55,225.66\n           C 377.43,221.86 376.90,218.14 374.63,214.79\n             373.56,213.23 372.20,211.95 370.22,211.34\n             367.86,210.62 366.46,211.20 366.12,213.38\n             365.52,217.25 364.98,221.14 364.72,225.03\n             364.41,229.86 364.20,234.72 364.31,239.56\n             364.48,247.00 364.62,254.46 365.39,261.87\n             366.33,270.91 367.91,279.90 369.22,288.91\n             369.35,289.83 369.58,290.74 369.77,291.65\n             369.77,291.65 371.79,302.83 371.79,302.83\n             371.79,303.05 371.79,303.27 371.79,303.49\n             371.72,308.06 371.18,312.62 371.06,317.19\n             371.00,319.46 371.33,321.76 371.63,324.03\n             371.75,325.00 372.24,325.93 372.56,326.88\n             372.70,326.86 372.83,326.85 372.96,326.83\n             373.07,326.63 373.19,326.44 373.28,326.23\n             375.03,322.30 376.56,318.27 378.55,314.45\n             382.44,307.00 384.39,299.17 384.73,290.93\n             385.00,284.47 385.25,278.00 385.58,271.53\n             385.67,269.56 386.03,267.61 386.18,265.64\n             386.53,260.65 386.94,255.66 387.12,250.66\n             387.22,247.81 386.99,244.93 386.74,242.07\n             386.61,240.55 385.99,240.18 384.40,240.64\n             383.39,240.94 382.38,241.28 381.39,241.64\n             379.65,242.21 375.47,243.31 375.42,240.71\n             376.68,235.77 377.72,230.79 377.55,225.66\"\n        data-elem={ExerciseAttributeValueEnum.HAMSTRINGS}\n        id=\"path38\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 414.25,228.25\n           C 414.25,228.25 421.50,232.00 421.50,232.00\n             421.50,232.00 437.75,239.75 437.75,239.75\n             437.75,239.75 445.50,240.50 445.50,240.50\n             445.50,240.50 445.00,227.00 445.00,227.00\n             445.00,227.00 446.00,217.00 446.00,217.00\n             446.00,217.00 450.25,210.75 450.25,210.75\n             450.25,210.75 455.25,210.50 455.25,210.50\n             455.25,210.50 457.25,208.75 457.25,208.75\n             457.25,208.75 459.75,211.50 459.75,211.50\n             459.75,211.50 461.00,228.00 461.00,228.00\n             461.00,228.00 461.00,243.75 461.00,243.75\n             461.00,243.75 460.50,261.25 460.50,261.25\n             460.50,261.25 456.75,282.75 456.75,282.75\n             456.75,282.75 453.75,302.50 453.75,302.50\n             453.75,302.50 453.75,321.50 453.75,321.50\n             453.75,321.50 452.00,329.25 452.00,329.25\n             452.00,329.25 447.00,322.25 447.00,322.25\n             447.00,322.25 444.50,318.00 444.50,318.00\n             444.50,318.00 441.75,322.00 441.75,322.00\n             441.75,322.00 439.75,327.75 439.75,327.75\n             439.75,327.75 433.00,322.00 433.00,322.00\n             433.00,322.00 431.50,323.25 431.50,323.25\n             431.50,323.25 430.25,340.00 430.25,340.00\n             430.25,340.00 425.75,338.00 425.75,338.00\n             425.75,338.00 421.25,326.00 421.25,326.00\n             421.25,326.00 418.75,315.00 418.50,315.00\n             418.25,315.00 414.50,295.25 414.50,295.25\n             414.50,295.25 414.25,274.50 414.25,274.50\n             414.25,274.50 414.00,251.50 414.00,251.50\n             414.00,251.50 414.25,237.25 414.25,237.25\n             414.25,237.25 414.75,232.00 414.75,229.25\"\n        data-elem={ExerciseAttributeValueEnum.HAMSTRINGS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/obliques-group.tsx",
    "content": "import React from \"react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const ObliquesGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.OBLIQUES)}>\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.OBLIQUES)}\n        d=\"M 134.28,178.67\n           C 135.80,178.08 137.44,177.63 138.74,176.72\n             142.62,174.02 145.10,170.17 146.78,165.84\n             147.58,163.80 147.29,161.70 146.02,159.83\n             143.83,156.62 141.13,153.89 138.15,151.43\n             137.82,151.16 137.39,151.02 136.74,150.68\n             136.87,151.36 136.92,151.76 137.02,152.15\n             138.19,156.68 138.78,161.29 138.44,165.97\n             138.11,170.47 136.77,174.56 133.37,177.76\n             133.19,177.94 133.06,178.17 132.66,178.69\n             133.43,178.69 133.91,178.81 134.28,178.67\"\n        data-elem={ExerciseAttributeValueEnum.OBLIQUES}\n        id=\"path128\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.OBLIQUES)}\n        d=\"M 96.36,177.87\n           C 93.31,175.14 91.86,171.59 91.33,167.64\n             90.63,162.39 91.23,157.23 92.57,152.14\n             92.68,151.73 92.71,151.30 92.78,150.88\n             92.69,150.83 92.60,150.78 92.51,150.72\n             92.06,151.03 91.54,151.26 91.17,151.65\n             88.72,154.19 86.19,156.67 83.92,159.37\n             82.10,161.53 81.92,164.15 83.14,166.68\n             84.09,168.66 85.19,170.60 86.47,172.39\n             88.38,175.05 90.74,177.28 93.94,178.36\n             94.81,178.66 95.73,179.06 96.79,178.44\n             96.58,178.17 96.50,177.99 96.36,177.87\"\n        data-elem={ExerciseAttributeValueEnum.OBLIQUES}\n        id=\"path130\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.OBLIQUES)}\n        d=\"M 133.30,127.21\n           C 134.46,133.05 135.57,138.89 136.92,144.67\n             137.30,146.28 138.27,147.83 139.28,149.17\n             140.60,150.93 142.19,152.50 143.76,154.05\n             146.20,156.46 148.04,159.20 148.93,162.54\n             149.03,162.88 149.17,163.22 149.39,163.82\n             150.71,162.19 151.16,160.53 151.28,158.82\n             151.28,158.82 152.05,150.83 152.05,150.83\n             152.19,149.95 152.36,149.07 152.47,148.18\n             153.20,142.15 153.30,136.10 152.92,130.04\n             152.72,126.77 152.45,123.50 152.14,120.24\n             152.01,118.81 151.55,118.53 150.26,119.02\n             150.26,119.02 135.99,124.40 135.99,124.40\n             135.99,124.40 135.99,124.41 135.99,124.41\n             135.78,124.42 135.56,124.44 135.32,124.48\n             133.82,124.70 133.01,125.72 133.30,127.21\"\n        data-elem={ExerciseAttributeValueEnum.OBLIQUES}\n        id=\"path192\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.OBLIQUES)}\n        d=\"M 79.63,119.11\n           C 79.59,119.09 79.56,119.08 79.52,119.06\n             79.52,119.06 79.39,119.02 79.39,119.02\n             79.39,119.02 79.39,119.02 79.39,119.02\n             77.87,118.42 77.56,119.01 77.32,121.30\n             76.60,128.01 76.33,134.74 76.69,141.49\n             76.83,144.18 77.31,147.83 77.60,150.78\n             77.60,150.78 77.60,150.83 77.60,150.83\n             77.60,150.83 78.30,158.17 78.30,158.17\n             78.30,158.23 78.30,158.29 78.30,158.36\n             78.33,160.24 78.75,162.03 80.08,163.65\n             80.28,163.40 80.39,163.33 80.42,163.23\n             80.55,162.88 80.67,162.51 80.78,162.15\n             81.49,159.74 82.57,157.54 84.32,155.70\n             85.82,154.13 87.32,152.56 88.81,150.98\n             91.12,148.51 92.82,145.72 93.31,142.29\n             93.42,141.50 93.60,140.72 93.76,139.94\n             94.61,135.79 95.50,131.66 96.28,127.50\n             96.64,125.60 95.80,124.63 93.88,124.42\n             93.80,124.42 93.73,124.42 93.65,124.41\n             93.65,124.41 93.66,124.40 93.66,124.40\n             93.66,124.40 79.63,119.11 79.63,119.11\"\n        data-elem={ExerciseAttributeValueEnum.OBLIQUES}\n        id=\"path194\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 131.50,125.25\n           C 131.50,125.25 135.75,145.25 135.75,145.25\n             135.75,145.25 136.75,154.00 136.75,154.00\n             136.75,154.00 137.00,164.75 137.00,164.75\n             137.00,164.75 136.25,171.75 136.25,171.75\n             136.25,171.75 132.75,176.75 132.75,176.75\n             132.75,176.75 134.75,180.25 134.75,180.25\n             134.75,180.25 133.50,192.00 133.50,192.00\n             133.50,192.00 130.75,202.50 130.75,202.50\n             130.75,202.50 124.75,217.25 124.75,217.25\n             124.75,217.25 123.25,220.50 123.25,220.50\n             123.25,220.50 132.00,217.25 132.00,217.25\n             132.00,217.25 144.75,204.00 144.75,204.00\n             144.75,204.00 151.25,192.75 151.25,192.75\n             151.25,192.75 152.50,184.25 152.50,184.25\n             152.50,184.25 152.00,171.75 152.00,171.75\n             152.00,171.75 151.25,167.00 151.25,167.00\n             151.25,167.00 154.75,158.50 154.75,158.50\n             154.75,158.50 155.25,133.75 155.25,133.75\n             155.25,133.75 154.50,116.50 154.50,116.25\n             154.50,116.00 152.00,117.00 152.00,117.00\n             152.00,117.00 145.75,120.25 145.75,120.25\n             145.75,120.25 138.25,123.50 138.25,123.50\n             138.25,123.50 131.50,125.00 131.50,125.00\"\n        data-elem={ExerciseAttributeValueEnum.OBLIQUES}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* front  */}\n      <path\n        className=\"fill-transparent\"\n        d=\"M 75.00,115.75\n           C 75.00,115.75 74.00,138.50 74.00,138.50\n             74.00,138.50 75.50,151.00 75.50,151.00\n             75.50,151.00 75.75,163.25 75.75,163.25\n             75.75,163.25 78.75,166.50 78.75,166.50\n             78.75,166.50 76.00,179.50 76.00,179.50\n             76.00,179.50 76.00,187.00 76.00,187.00\n             76.00,187.00 77.75,192.00 77.75,192.00\n             77.75,192.00 81.50,197.50 81.50,197.50\n             81.50,197.50 87.25,205.50 87.25,205.50\n             87.25,205.50 94.00,213.50 94.00,213.50\n             94.00,213.50 102.25,219.25 102.25,219.25\n             102.25,219.25 106.00,220.75 106.00,220.75\n             106.00,220.75 101.25,207.00 101.25,207.00\n             101.25,207.00 96.75,194.50 96.75,194.50\n             96.75,194.50 95.00,180.00 94.75,180.00\n             94.50,180.00 96.25,177.25 96.25,177.25\n             96.25,177.25 93.50,170.50 93.50,170.50\n             93.50,170.50 93.25,155.00 93.25,155.00\n             93.25,155.00 94.00,142.75 94.00,142.75\n             94.00,142.75 96.75,127.75 96.75,127.75\n             96.75,127.75 98.50,125.00 98.50,125.00\n             98.50,125.00 80.50,119.00 80.50,119.00\n             80.50,119.00 75.25,115.50 75.25,115.50\"\n        data-elem={ExerciseAttributeValueEnum.OBLIQUES}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.OBLIQUES)}\n        d=\"M 125.27,217.07\n           C 125.09,217.39 125.10,217.82 125.02,218.20\n             125.41,218.20 125.87,218.33 126.20,218.18\n             127.38,217.66 128.62,217.18 129.65,216.44\n             137.46,210.87 143.48,203.73 147.82,195.19\n             149.29,192.30 150.60,189.30 150.80,186.01\n             151.14,180.36 150.16,174.85 148.66,169.42\n             148.53,168.97 148.27,168.57 147.93,167.83\n             147.49,168.64 147.24,169.11 146.99,169.57\n             144.93,173.36 142.29,176.69 138.49,178.77\n             136.30,179.97 135.45,181.45 135.46,183.84\n             135.49,188.83 134.47,193.68 133.09,198.45\n             131.22,204.97 128.54,211.14 125.27,217.07\"\n        data-elem={ExerciseAttributeValueEnum.OBLIQUES}\n        id=\"path112\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.OBLIQUES)}\n        d=\"M 97.07,214.29\n           C 99.04,215.92 101.02,217.56 103.56,218.25\n             103.88,218.33 104.26,218.19 104.62,218.15\n             104.55,217.83 104.56,217.48 104.41,217.20\n             103.37,215.13 102.21,213.10 101.24,211.00\n             97.13,202.07 94.13,192.84 94.06,182.87\n             94.05,181.21 93.41,180.03 91.92,179.28\n             87.87,177.21 85.11,173.86 82.81,170.05\n             82.45,169.45 82.15,168.80 81.81,168.18\n             81.68,168.18 81.55,168.18 81.41,168.17\n             81.20,168.73 80.92,169.26 80.80,169.83\n             80.19,172.85 79.48,175.86 79.05,178.91\n             78.44,183.21 78.48,187.48 80.18,191.62\n             83.88,200.63 89.61,208.11 97.07,214.29\"\n        data-elem={ExerciseAttributeValueEnum.OBLIQUES}\n        id=\"path114\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/quadriceps-group.tsx",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const QuadricepsGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.QUADRICEPS)}>\n      <path\n        className=\"fill-transparent\"\n        d=\"M 77.00,190.50\n           C 77.00,190.50 83.75,201.75 83.75,201.75\n             83.75,201.75 88.25,208.75 88.25,208.75\n             88.25,208.75 95.25,214.50 95.25,214.50\n             95.25,214.50 106.75,222.50 106.75,222.50\n             106.75,222.50 112.25,224.50 112.25,224.50\n             112.25,224.50 112.75,239.50 112.75,239.50\n             112.75,239.50 107.75,269.00 107.75,269.00\n             107.75,269.00 104.00,280.75 104.00,280.75\n             104.00,280.75 105.75,298.25 105.75,298.25\n             105.75,298.25 104.50,314.50 104.50,314.50\n             104.50,314.50 99.25,331.25 99.25,331.25\n             99.25,331.25 95.50,336.75 95.50,336.75\n             95.50,336.75 94.25,316.00 94.25,316.00\n             94.25,316.00 90.75,307.75 90.75,307.75\n             90.75,307.75 85.50,315.75 85.50,315.75\n             85.50,315.75 78.00,326.25 78.00,326.25\n             78.00,326.25 75.25,328.75 75.25,328.75\n             75.25,328.75 74.00,303.00 74.00,303.00\n             74.00,303.00 64.75,276.75 64.75,276.75\n             64.75,276.75 64.25,264.25 64.25,264.25\n             64.25,264.25 64.25,248.00 64.25,248.00\n             64.25,248.00 65.75,221.25 65.75,221.25\n             65.75,221.25 69.50,210.75 69.50,210.75\n             69.50,210.75 74.00,198.50 74.00,198.50\n             74.00,198.50 77.00,190.75 77.00,190.75M 118.25,225.00\n           C 118.25,225.00 128.75,219.00 128.75,219.00\n             128.75,219.00 137.25,212.25 137.25,212.25\n             137.25,212.25 145.25,202.00 145.25,202.00\n             145.25,202.00 152.75,190.75 152.75,190.75\n             152.75,190.75 157.25,198.50 157.25,198.50\n             157.25,198.50 162.00,212.25 162.00,212.25\n             162.00,212.25 165.50,229.00 165.50,229.00\n             165.50,229.00 166.50,244.25 166.50,244.25\n             166.50,244.25 167.50,256.25 167.50,256.25\n             167.50,256.25 164.25,273.00 164.25,273.00\n             164.25,273.00 160.50,288.00 160.50,288.00\n             160.50,288.00 157.00,298.75 157.00,298.75\n             157.00,298.75 155.50,303.75 155.50,303.75\n             155.50,303.75 155.50,316.25 155.50,316.25\n             155.50,316.25 155.00,331.00 155.00,331.00\n             155.00,331.00 147.75,319.75 147.75,319.75\n             147.75,319.75 139.50,307.50 139.50,307.50\n             139.50,307.50 138.00,308.75 138.00,308.75\n             138.00,308.75 136.75,316.00 136.75,316.00\n             136.75,316.00 135.75,324.00 135.75,324.00\n             135.75,324.00 134.50,337.50 134.50,337.50\n             134.50,337.50 128.00,324.25 128.00,324.25\n             128.00,324.25 125.50,310.50 125.50,310.50\n             125.50,310.50 125.50,298.25 125.50,298.25\n             125.50,298.25 125.50,282.75 125.50,282.75\n             125.50,282.75 125.75,279.50 125.75,279.50\n             125.75,279.50 120.75,258.75 120.75,258.75\n             120.75,258.75 118.25,247.00 118.25,247.00\n             118.25,247.00 118.25,236.75 118.25,236.75\n             118.25,236.75 118.00,228.75 118.00,228.75\n             118.00,228.75 118.00,224.50 118.00,224.50\"\n        data-elem={ExerciseAttributeValueEnum.QUADRICEPS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.QUADRICEPS)}\n        d=\"M 98.39,269.85\n           C 98.33,270.21 98.24,270.57 98.21,270.93\n             97.87,274.80 97.58,278.68 97.20,282.55\n             96.90,285.46 96.66,288.40 96.09,291.27\n             95.09,296.32 93.86,301.31 92.76,306.33\n             92.65,306.84 92.62,307.42 92.77,307.90\n             95.03,314.93 95.42,322.18 95.37,329.48\n             95.36,330.86 95.28,332.24 95.24,333.62\n             95.34,333.63 95.45,333.64 95.55,333.65\n             99.74,323.86 101.95,313.60 102.45,302.95\n             102.99,291.71 101.95,280.64 98.86,269.81\n             98.71,269.82 98.55,269.84 98.39,269.85\"\n        data-elem={ExerciseAttributeValueEnum.QUADRICEPS}\n        id=\"path170\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.QUADRICEPS)}\n        d=\"M 95.65,269.75\n           C 95.73,264.98 95.58,260.18 94.51,255.53\n             92.85,248.29 90.84,241.13 89.07,233.91\n             86.68,224.21 84.38,214.49 82.04,204.79\n             81.75,203.60 81.43,202.43 81.05,201.27\n             80.97,201.01 80.60,200.84 80.36,200.63\n             80.20,200.90 79.99,201.16 79.88,201.45\n             79.79,201.70 79.80,201.98 79.80,202.25\n             79.80,205.26 79.86,208.27 79.79,211.28\n             79.71,214.97 79.65,218.68 79.36,222.36\n             79.04,226.31 78.40,230.23 78.03,234.17\n             77.30,241.88 76.57,249.59 76.03,257.31\n             75.66,262.48 75.32,267.67 75.48,272.84\n             75.74,281.94 76.41,291.03 76.86,300.13\n             77.09,304.74 77.26,309.35 77.37,313.96\n             77.46,317.76 77.42,321.57 77.44,325.64\n             77.79,325.27 78.01,325.08 78.17,324.84\n             80.70,321.12 83.31,317.46 85.70,313.66\n             87.96,310.08 90.00,306.35 91.28,302.28\n             93.64,294.74 94.44,286.92 95.25,279.11\n             95.58,276.01 95.60,272.87 95.65,269.75\"\n        data-elem={ExerciseAttributeValueEnum.QUADRICEPS}\n        id=\"path172\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.QUADRICEPS)}\n        d=\"M 108.84,225.24\n           C 106.63,223.43 100.82,220.73 98.71,218.80\n             93.08,213.68 90.27,212.08 86.11,205.66\n             85.16,204.20 84.27,202.70 83.35,201.22\n             83.23,201.24 83.11,201.26 82.99,201.28\n             83.06,202.02 83.11,202.76 83.22,203.48\n             84.36,210.42 86.19,217.18 88.33,223.88\n             93.24,239.25 97.20,254.88 100.81,270.61\n             101.42,273.27 101.95,275.95 102.52,278.62\n             102.83,278.34 102.95,278.05 103.00,277.74\n             103.47,274.88 105.14,269.24 105.53,266.37\n             107.68,256.43 107.79,254.80 108.84,246.48\n             109.82,238.30 110.01,236.87 110.01,232.26\n             110.01,227.97 110.07,226.24 108.84,225.24\"\n        data-elem={ExerciseAttributeValueEnum.QUADRICEPS}\n        id=\"path174\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.QUADRICEPS)}\n        d=\"M 68.30,275.34\n           C 68.32,275.60 75.63,303.92 75.71,304.19\n             75.94,302.37 73.80,274.89 73.95,266.76\n             74.17,255.40 75.57,245.74 76.61,234.43\n             76.79,232.45 77.08,230.49 77.26,228.52\n             77.65,224.23 78.22,219.94 78.30,215.64\n             78.42,210.12 78.15,204.59 78.00,199.07\n             77.94,197.03 77.76,194.99 77.63,192.95\n             77.50,192.96 77.37,192.96 77.25,192.96\n             77.17,193.24 77.09,193.53 77.02,193.81\n             76.34,196.78 75.80,199.79 74.96,202.72\n             72.10,212.75 68.60,224.48 67.71,234.79\n             67.13,241.61 65.44,249.80 65.76,255.84\n             66.15,262.99 67.13,267.34 68.30,275.34\"\n        data-elem={ExerciseAttributeValueEnum.QUADRICEPS}\n        id=\"path176\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.QUADRICEPS)}\n        d=\"M 152.52,325.64\n           C 152.54,321.57 152.50,317.76 152.60,313.96\n             152.70,309.35 152.87,304.74 153.10,300.13\n             153.55,291.03 154.23,281.94 154.49,272.84\n             154.64,267.67 154.30,262.48 153.94,257.31\n             153.39,249.59 152.66,241.88 151.93,234.17\n             151.56,230.23 150.92,226.31 150.61,222.36\n             150.32,218.68 150.25,214.97 150.17,211.28\n             150.11,208.27 150.16,205.26 150.16,202.25\n             150.16,201.98 150.17,201.70 150.08,201.45\n             149.98,201.16 149.77,200.90 149.60,200.63\n             149.36,200.84 148.99,201.01 148.91,201.27\n             148.54,202.43 148.21,203.60 147.93,204.79\n             145.58,214.49 143.28,224.21 140.90,233.91\n             139.12,241.13 137.11,248.29 135.45,255.53\n             134.38,260.18 134.23,264.98 134.32,269.75\n             134.37,272.87 134.39,276.01 134.71,279.11\n             135.53,286.92 136.33,294.74 138.69,302.28\n             139.96,306.35 142.00,310.08 144.26,313.66\n             146.66,317.46 149.27,321.12 151.79,324.84\n             151.95,325.08 152.18,325.27 152.52,325.64\"\n        data-elem={ExerciseAttributeValueEnum.QUADRICEPS}\n        id=\"path180\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.QUADRICEPS)}\n        d=\"M 134.73,333.62\n           C 134.68,332.24 134.61,330.86 134.60,329.48\n             134.55,322.18 134.93,314.93 137.19,307.90\n             137.35,307.42 137.32,306.84 137.21,306.33\n             136.10,301.31 134.87,296.32 133.88,291.27\n             133.31,288.40 133.06,285.46 132.77,282.55\n             132.38,278.68 132.10,274.80 131.76,270.93\n             131.73,270.57 131.63,270.21 131.57,269.85\n             131.41,269.84 131.26,269.82 131.10,269.81\n             128.01,280.64 126.98,291.71 127.51,302.95\n             128.01,313.60 130.23,323.86 134.41,333.65\n             134.52,333.64 134.62,333.63 134.73,333.62\"\n        data-elem={ExerciseAttributeValueEnum.QUADRICEPS}\n        id=\"path182\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.QUADRICEPS)}\n        d=\"M 161.67,275.34\n           C 162.84,267.34 163.81,262.99 164.20,255.84\n             164.53,249.80 162.84,241.61 162.25,234.79\n             161.37,224.48 157.87,212.75 155.00,202.72\n             154.17,199.79 153.62,196.78 152.94,193.81\n             152.88,193.53 152.79,193.24 152.72,192.96\n             152.59,192.96 152.46,192.96 152.34,192.95\n             152.21,194.99 152.02,197.03 151.97,199.07\n             151.82,204.59 151.55,210.12 151.66,215.64\n             151.75,219.94 152.32,224.23 152.71,228.52\n             152.88,230.49 153.18,232.45 153.36,234.43\n             154.39,245.74 155.80,255.40 156.01,266.76\n             156.17,274.89 154.02,302.37 154.26,304.19\n             154.33,303.92 161.64,275.60 161.67,275.34\"\n        data-elem={ExerciseAttributeValueEnum.QUADRICEPS}\n        id=\"path186\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.QUADRICEPS)}\n        d=\"M 129.16,270.61\n           C 132.77,254.88 136.72,239.25 141.63,223.88\n             143.77,217.18 145.61,210.42 146.74,203.48\n             146.86,202.76 146.90,202.02 146.98,201.28\n             146.86,201.26 146.74,201.24 146.61,201.22\n             145.70,202.70 144.80,204.20 143.85,205.66\n             139.69,212.08 136.88,213.68 131.26,218.80\n             129.14,220.73 123.33,223.43 121.12,225.24\n             119.90,226.24 119.95,227.97 119.95,232.26\n             119.95,236.87 120.14,238.30 121.12,246.48\n             122.17,254.80 122.29,256.43 124.43,266.37\n             124.83,269.24 126.49,274.88 126.96,277.74\n             127.02,278.05 127.13,278.34 127.45,278.62\n             128.01,275.95 128.55,273.27 129.16,270.61\"\n        data-elem={ExerciseAttributeValueEnum.QUADRICEPS}\n        id=\"path188\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/shoulders-group.tsx",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const ShouldersGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.SHOULDERS)}>\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.SHOULDERS)}\n        d=\"M 349.30,110.19\n           C 350.53,110.32 351.75,110.47 352.37,110.54\n             358.40,110.63 363.20,108.85 367.19,105.28\n             370.34,102.46 373.12,99.22 376.21,96.32\n             377.79,94.83 379.66,93.63 381.47,92.40\n             382.21,91.90 382.52,91.35 382.46,90.50\n             382.16,85.93 377.78,80.55 373.38,79.53\n             372.52,79.33 371.48,79.36 370.65,79.64\n             364.68,81.65 359.28,84.64 354.93,89.27\n             351.53,92.89 349.96,97.39 349.10,102.16\n             348.71,104.40 348.57,106.68 348.31,108.93\n             348.22,109.69 348.54,110.10 349.30,110.19\"\n        data-elem={ExerciseAttributeValueEnum.SHOULDERS}\n        id=\"path96\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.SHOULDERS)}\n        d=\"M 439.90,90.50\n           C 439.84,91.35 440.15,91.90 440.89,92.40\n             442.70,93.63 444.57,94.83 446.15,96.32\n             449.24,99.22 452.02,102.46 455.17,105.28\n             459.17,108.85 463.96,110.63 469.99,110.54\n             470.61,110.47 471.83,110.32 473.06,110.19\n             473.82,110.10 474.14,109.69 474.05,108.93\n             473.80,106.68 473.66,104.40 473.26,102.16\n             472.41,97.39 470.83,92.89 467.44,89.27\n             463.08,84.64 457.68,81.65 451.71,79.64\n             450.88,79.36 449.84,79.33 448.98,79.53\n             444.58,80.55 440.20,85.93 439.90,90.50\"\n        data-elem={ExerciseAttributeValueEnum.SHOULDERS}\n        id=\"path88\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 82.75,82.25\n           C 82.75,82.25 75.75,78.50 75.75,78.50\n             75.75,78.50 64.00,80.50 64.00,80.50\n             64.00,80.50 58.25,85.25 58.25,85.25\n             58.25,85.25 53.25,90.75 53.25,90.75\n             53.25,90.75 50.25,98.25 50.25,98.25\n             50.25,98.25 48.50,109.50 48.50,109.50\n             48.50,109.50 48.75,118.50 48.75,118.50\n             48.75,118.50 55.50,114.25 55.50,114.25\n             55.50,114.25 62.50,111.75 62.50,111.75\n             62.50,111.75 71.50,110.50 71.50,110.25\n             71.50,110.00 76.00,105.00 76.00,105.00\n             76.00,105.00 80.75,96.50 80.75,96.50\n             80.75,96.50 85.00,86.25 85.00,86.25\n             85.00,86.25 82.75,82.50 82.75,82.50\"\n        data-elem={ExerciseAttributeValueEnum.SHOULDERS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className=\"fill-transparent\"\n        d=\"M 144.25,85.00\n           C 144.25,85.00 150.50,79.25 150.75,79.25\n             151.00,79.25 157.25,78.50 157.25,78.50\n             157.25,78.50 167.00,81.75 167.00,81.75\n             167.00,81.75 172.75,86.75 172.75,86.75\n             172.75,86.75 177.00,94.00 177.00,94.00\n             177.00,94.00 180.50,103.75 180.50,103.75\n             180.50,103.75 180.50,113.25 180.50,113.25\n             180.50,113.25 180.50,117.50 180.50,117.50\n             180.50,117.50 176.25,115.75 176.25,115.75\n             176.25,115.75 166.00,112.25 166.00,112.25\n             166.00,112.25 159.00,111.00 159.00,111.00\n             159.00,111.00 155.25,107.75 155.25,107.75\n             155.25,107.75 149.25,97.00 149.25,97.00\n             149.25,97.00 146.25,90.50 146.25,90.50\n             146.25,90.50 145.00,87.00 145.00,87.00\"\n        data-elem={ExerciseAttributeValueEnum.SHOULDERS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n      <path\n        className=\"fill-transparent\"\n        d=\"M 364.00,79.50\n           C 364.00,79.50 357.50,82.75 357.50,82.75\n             357.50,82.75 352.50,87.50 352.50,87.50\n             352.50,87.50 349.25,93.50 349.25,93.50\n             349.25,93.50 346.25,102.25 346.25,102.25\n             346.25,102.25 345.75,109.50 345.75,109.50\n             345.75,109.50 348.25,111.00 348.25,111.00\n             348.25,111.00 354.75,111.25 354.75,111.25\n             354.75,111.25 362.50,110.00 362.50,110.00\n             362.50,110.00 367.75,106.25 367.75,106.25\n             367.75,106.25 373.75,101.00 373.75,101.00\n             373.75,101.00 379.00,95.50 379.00,95.50\n             379.00,95.50 384.25,92.25 384.25,92.25\n             384.25,92.25 382.25,85.50 382.25,85.50\n             382.25,85.50 378.50,80.25 378.50,80.25\n             378.50,80.25 374.25,79.00 374.25,79.00\n             374.25,79.00 369.50,78.25 369.50,78.25\n             369.50,78.25 370.75,78.25 367.25,78.75\"\n        data-elem={ExerciseAttributeValueEnum.SHOULDERS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 453.25,76.50\n           C 453.25,76.50 462.00,80.25 462.00,80.25\n             462.00,80.25 467.75,85.50 467.75,85.50\n             467.75,85.50 473.00,92.50 473.00,92.50\n             473.00,92.50 476.75,103.00 476.75,103.00\n             476.75,103.00 476.75,109.00 476.75,109.00\n             476.75,109.00 475.50,111.00 475.50,111.00\n             475.50,111.00 471.25,111.00 471.25,111.00\n             471.25,111.00 464.50,111.00 464.50,111.00\n             464.50,111.00 457.50,108.00 457.50,108.00\n             457.50,108.00 452.00,102.75 452.00,102.75\n             452.00,102.75 445.25,96.75 445.25,96.75\n             445.25,96.75 440.00,93.25 440.00,93.25\n             440.00,93.25 439.00,90.00 439.00,90.00\n             439.00,90.00 441.00,86.00 441.00,86.00\n             441.00,86.00 443.50,81.75 443.50,81.75\n             443.50,81.75 448.50,78.00 448.50,78.00\n             448.50,78.00 453.75,76.75 453.75,76.75\"\n        data-elem={ExerciseAttributeValueEnum.SHOULDERS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.SHOULDERS)}\n        d=\"M 145.59,86.76\n           C 146.96,92.75 151.13,99.57 154.22,104.96\n             156.56,109.06 156.74,107.90 160.65,109.64\n             162.67,110.54 168.21,111.43 170.18,112.42\n             171.86,113.27 174.37,114.35 175.95,115.39\n             176.25,115.59 177.13,116.42 177.51,116.76\n             177.60,116.40 177.69,116.23 177.69,116.06\n             177.46,109.59 178.00,102.38 175.66,96.29\n             174.38,92.97 170.21,88.28 167.38,86.15\n             162.73,82.66 159.09,81.86 152.93,80.92\n             152.27,80.82 151.35,80.76 150.79,81.09\n             149.27,82.00 147.83,83.08 146.44,84.18\n             145.66,84.81 145.36,85.71 145.59,86.76\"\n        data-elem={ExerciseAttributeValueEnum.SHOULDERS}\n        id=\"path152\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.SHOULDERS)}\n        d=\"M 53.58,115.39\n           C 55.17,114.35 57.67,113.27 59.35,112.42\n             61.32,111.43 66.87,110.54 68.88,109.64\n             72.80,107.90 72.98,109.06 75.32,104.96\n             78.40,99.57 82.58,92.75 83.94,86.76\n             84.18,85.71 83.88,84.81 83.10,84.18\n             81.70,83.08 80.27,82.00 78.74,81.09\n             78.18,80.76 77.27,80.82 76.60,80.92\n             70.44,81.86 66.80,82.66 62.16,86.15\n             59.33,88.28 55.15,92.97 53.87,96.29\n             51.53,102.38 52.08,109.59 51.85,116.06\n             51.84,116.23 51.93,116.40 52.02,116.76\n             52.40,116.42 53.28,115.59 53.58,115.39\"\n        data-elem={ExerciseAttributeValueEnum.SHOULDERS}\n        id=\"path142\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/traps-group.tsx",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const TrapsGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.TRAPS)}>\n      <path\n        className=\"fill-transparent\"\n        d=\"M 85.67,85.05\n           C 85.67,85.05 87.62,84.15 87.62,84.15\n             87.62,84.15 89.87,83.25 89.72,83.25\n             89.57,83.25 92.27,83.10 92.27,83.10\n             92.27,83.10 94.82,83.55 94.82,83.55\n             94.82,83.55 97.52,84.60 97.52,84.60\n             97.52,84.60 100.82,85.35 100.82,85.35\n             100.82,85.35 107.72,87.75 107.72,87.75\n             107.72,87.75 108.62,87.15 108.62,87.15\n             108.62,87.15 110.72,86.40 110.72,86.40\n             110.72,86.40 118.67,86.40 118.67,86.40\n             118.67,86.40 121.07,87.90 121.07,87.90\n             121.07,87.90 124.37,87.15 124.37,87.15\n             124.37,87.15 127.97,85.50 127.97,85.50\n             127.97,85.50 132.77,84.15 132.77,84.15\n             132.77,84.15 136.38,83.10 136.38,83.10\n             136.38,83.10 139.08,82.95 139.08,82.95\n             139.08,82.95 141.18,83.55 141.18,83.55\n             141.18,83.55 144.48,85.65 144.48,85.65\n             144.48,85.65 145.38,83.85 145.38,83.85\n             145.38,83.85 148.08,81.45 148.08,81.45\n             148.08,81.45 152.73,78.90 152.73,78.90\n             152.73,78.90 131.57,69.15 131.57,69.15\n             131.57,69.15 128.87,73.80 128.87,73.80\n             128.87,73.80 124.37,79.50 124.37,79.50\n             124.37,79.50 119.27,85.20 119.27,85.20\n             119.27,85.20 115.22,86.10 115.22,86.10\n             115.22,86.10 111.62,85.20 111.62,85.20\n             111.62,85.20 109.22,83.40 109.22,83.40\n             109.22,83.40 106.37,81.15 106.37,81.15\n             106.37,81.15 102.32,76.20 102.32,76.20\n             102.32,76.20 99.17,71.70 99.17,71.70\n             99.17,71.70 96.32,69.60 96.32,69.60\n             96.32,69.60 76.21,78.45 76.21,78.45\n             76.21,78.45 77.86,79.95 77.86,79.95\n             77.86,79.95 80.42,81.30 80.42,81.30\n             80.42,81.30 82.52,82.65 82.52,82.65\n             82.52,82.65 84.77,85.20 84.77,85.20\"\n        data-elem={ExerciseAttributeValueEnum.TRAPS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRAPS)}\n        d=\"M 424.86,75.58\n           C 421.55,75.92 418.66,76.93 416.95,80.04\n             414.97,83.62 413.60,87.52 413.16,91.51\n             412.49,97.59 412.33,103.74 412.23,109.87\n             412.08,118.66 412.18,127.46 412.19,136.26\n             412.20,137.66 412.33,139.06 412.40,140.46\n             413.72,133.20 418.01,127.57 422.22,121.89\n             424.83,118.35 427.44,114.82 429.97,111.22\n             432.62,107.45 435.01,103.52 436.13,98.99\n             436.99,95.51 437.70,91.98 438.61,88.52\n             439.53,85.05 441.09,81.93 444.29,79.96\n             445.13,79.45 446.07,79.11 446.96,78.68\n             446.97,78.57 446.98,78.46 446.98,78.35\n             444.21,77.57 441.46,76.69 438.66,76.03\n             434.10,74.96 429.48,75.11 424.86,75.58\"\n        data-elem={ExerciseAttributeValueEnum.TRAPS}\n        id=\"path18\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRAPS)}\n        d=\"M 406.94,81.88\n           C 405.66,78.80 403.68,76.45 400.04,75.90\n             395.59,75.23 391.12,74.84 386.69,75.63\n             383.08,76.27 379.54,77.36 375.97,78.25\n             375.97,78.42 375.97,78.59 375.97,78.76\n             381.23,80.32 383.31,84.46 384.56,89.25\n             385.03,91.07 385.37,92.93 385.78,94.78\n             386.65,98.66 387.50,102.60 389.72,105.96\n             393.31,111.39 397.04,116.74 400.84,122.03\n             404.25,126.77 407.77,131.41 409.53,137.08\n             409.90,138.27 410.14,139.50 410.51,140.99\n             410.61,139.02 410.79,137.33 410.77,135.65\n             410.67,122.97 410.48,110.28 410.41,97.59\n             410.37,92.08 409.03,86.89 406.94,81.88\"\n        data-elem={ExerciseAttributeValueEnum.TRAPS}\n        id=\"path16\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className=\"fill-transparent\"\n        d=\"M 410.93,37.50\n           C 410.93,37.50 416.18,38.40 416.18,38.40\n             416.18,38.40 418.73,39.60 418.73,39.60\n             418.73,39.60 422.18,43.05 422.18,43.05\n             422.18,43.05 424.28,48.60 424.28,48.60\n             424.28,48.60 425.48,55.35 425.48,55.35\n             425.48,55.35 426.53,61.65 426.53,61.65\n             426.53,61.65 428.48,65.55 428.48,65.55\n             428.48,65.55 434.33,69.60 434.33,69.60\n             434.33,69.60 440.18,72.75 440.18,72.75\n             440.18,72.75 446.48,75.00 446.48,75.00\n             446.48,75.00 449.93,75.75 449.93,75.75\n             449.93,75.75 445.28,79.80 445.28,79.80\n             445.28,79.80 441.23,84.30 441.23,84.30\n             441.23,84.30 439.58,87.75 439.58,87.75\n             439.58,87.75 438.23,93.15 438.23,93.15\n             438.23,93.15 436.58,100.35 436.58,100.35\n             436.58,100.35 434.18,105.75 434.18,105.75\n             434.18,105.75 430.28,112.05 430.28,112.05\n             430.28,112.05 420.08,126.30 420.08,126.30\n             420.08,126.30 414.68,136.80 414.68,136.80\n             414.68,136.80 411.23,142.95 411.23,142.95\n             411.23,142.95 407.78,135.60 407.78,135.60\n             407.78,135.60 405.23,130.50 405.23,130.50\n             405.23,130.50 402.68,126.60 402.68,126.60\n             402.68,126.60 398.17,118.95 398.17,118.95\n             398.17,118.95 394.87,114.45 394.87,114.45\n             394.87,114.45 390.37,108.15 390.37,108.15\n             390.37,108.15 387.52,102.15 387.52,102.15\n             387.52,102.15 385.12,94.05 385.12,94.05\n             385.12,94.05 383.02,86.85 383.02,86.85\n             383.02,86.85 380.17,82.65 380.17,82.65\n             380.17,82.65 376.12,79.35 376.12,79.35\n             376.12,79.35 374.32,78.15 374.32,78.15\n             374.32,78.15 372.37,76.05 372.37,76.05\n             372.37,76.05 380.62,74.55 380.62,74.55\n             380.62,74.55 387.22,71.25 387.22,71.25\n             387.22,71.25 394.42,65.85 394.42,65.85\n             394.42,65.85 396.67,61.50 396.67,61.50\n             396.67,61.50 397.57,57.15 397.57,57.15\n             397.57,57.15 399.82,48.15 399.82,48.15\n             399.82,48.15 400.42,44.40 400.42,44.40\n             400.42,44.40 403.28,40.95 403.28,40.95\n             403.28,40.95 408.08,38.40 408.08,38.40\n             408.08,38.40 410.78,37.80 410.78,37.80\"\n        data-elem={ExerciseAttributeValueEnum.TRAPS}\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRAPS)}\n        d=\"M 85.02,84.19\n           C 86.82,83.05 90.21,81.65 92.21,82.21\n             92.23,82.21 92.25,82.22 92.27,82.22\n             93.75,82.66 95.25,83.04 96.74,83.43\n             99.90,84.27 103.08,85.07 106.25,85.88\n             106.95,86.06 107.66,86.23 108.37,86.34\n             108.63,86.37 108.92,86.19 109.19,86.11\n             109.11,85.84 109.10,85.49 108.93,85.30\n             107.66,83.93 106.33,82.62 105.07,81.24\n             102.63,78.57 100.12,75.95 98.64,72.56\n             98.25,71.65 97.57,71.78 96.83,72.13\n             93.52,73.69 90.21,75.24 86.89,76.78\n             86.04,77.18 85.16,77.53 84.06,78.00\n             84.06,78.00 79.16,80.15 79.16,80.15\n             79.16,80.15 84.02,81.63 85.02,84.19\"\n        data-elem={ExerciseAttributeValueEnum.TRAPS}\n        id=\"path200\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRAPS)}\n        d=\"M 130.40,72.56\n           C 128.92,75.95 126.41,78.57 123.97,81.24\n             122.71,82.62 121.38,83.93 120.11,85.30\n             119.94,85.49 119.93,85.84 119.85,86.11\n             120.12,86.19 120.41,86.37 120.67,86.34\n             121.38,86.23 122.09,86.06 122.79,85.88\n             125.96,85.07 129.14,84.27 132.30,83.43\n             133.79,83.04 135.29,82.66 136.77,82.22\n             136.79,82.22 136.80,82.21 136.83,82.21\n             138.83,81.65 142.22,83.05 144.02,84.19\n             145.02,81.63 149.88,80.15 149.88,80.15\n             149.88,80.15 144.98,78.00 144.98,78.00\n             143.88,77.53 143.00,77.18 142.15,76.78\n             138.83,75.24 135.52,73.69 132.21,72.13\n             131.47,71.78 130.79,71.65 130.40,72.56\"\n        data-elem={ExerciseAttributeValueEnum.TRAPS}\n        id=\"path196\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRAPS)}\n        d=\"M 419.29,75.38\n           C 419.29,75.38 424.79,74.06 424.79,74.06\n             424.79,74.06 424.79,74.06 424.79,74.06\n             428.21,74.17 431.63,74.14 435.05,74.19\n             435.22,74.20 435.39,74.11 435.85,74.00\n             435.36,73.69 435.09,73.53 434.82,73.37\n             432.73,72.13 430.62,70.93 428.57,69.64\n             425.94,68.01 424.70,65.57 424.56,62.50\n             424.44,59.85 424.17,57.19 424.06,54.54\n             423.90,50.55 422.75,46.88 420.76,43.45\n             420.58,43.15 420.31,42.90 420.09,42.63\n             419.96,42.67 419.83,42.71 419.71,42.76\n             420.15,47.98 420.60,53.21 421.05,58.46\n             420.96,58.13 420.84,57.82 420.80,57.49\n             420.11,52.46 419.45,47.42 418.72,42.39\n             418.64,41.83 418.26,41.11 417.81,40.84\n             413.99,38.61 410.08,38.57 406.09,40.49\n             404.91,41.06 404.55,41.84 404.48,43.11\n             404.20,48.78 403.43,54.39 401.96,59.89\n             401.81,60.47 401.54,61.01 401.33,61.57\n             402.70,53.56 403.58,44.00 402.82,42.59\n             402.50,43.02 402.16,43.38 401.92,43.80\n             400.42,46.37 399.44,49.12 399.18,52.09\n             398.84,55.85 398.64,59.62 398.25,63.38\n             397.98,65.90 397.07,68.20 394.81,69.65\n             392.86,70.89 390.86,72.05 388.87,73.23\n             388.53,73.44 388.16,73.62 387.58,73.93\n             388.04,74.07 388.25,74.18 388.45,74.17\n             392.08,74.12 395.71,74.07 399.34,73.99\n             399.34,73.99 399.33,74.00 399.33,74.00\n             399.33,74.00 404.10,75.42 404.10,75.42\n             408.96,79.25 410.94,84.36 411.51,90.45\n             412.37,87.37 412.94,84.65 413.89,82.08\n             414.88,79.39 416.88,77.35 419.29,75.38\"\n        data-elem={ExerciseAttributeValueEnum.TRAPS}\n        id=\"path32\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles/triceps-group.tsx",
    "content": "import React from \"react\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nexport const TricepsGroup = ({\n  onToggleMuscle,\n  getMuscleClasses,\n}: {\n  onToggleMuscle: (muscle: ExerciseAttributeValueEnum) => void;\n  getMuscleClasses: (muscle: ExerciseAttributeValueEnum) => string;\n}) => {\n  return (\n    <g className=\"group cursor-pointer\" onClick={() => onToggleMuscle(ExerciseAttributeValueEnum.TRICEPS)}>\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRICEPS)}\n        d=\"M 478.44,132.70\n           C 477.17,132.60 475.86,132.25 474.77,131.75\n             472.83,130.86 471.05,129.76 468.96,128.62\n             468.99,129.09 469.03,129.35 469.03,129.62\n             469.02,131.66 469.21,133.71 468.95,135.73\n             468.64,138.15 467.53,140.35 464.61,141.69\n             464.57,141.72 464.53,141.77 464.49,141.82\n             464.31,141.83 463.98,141.83 463.36,141.77\n             462.16,141.67 460.56,140.30 459.67,139.45\n             459.46,139.24 459.25,139.02 459.04,138.81\n             459.02,138.79 459.01,138.78 459.01,138.78\n             459.01,138.78 459.02,138.79 459.02,138.79\n             458.42,138.20 457.82,137.61 457.24,137.01\n             457.12,137.06 457.00,137.12 456.88,137.18\n             456.94,137.39 456.98,137.61 457.09,137.81\n             459.43,142.05 461.66,146.33 464.22,150.51\n             465.11,151.97 466.74,153.23 468.22,154.47\n             468.66,154.84 469.71,154.83 470.47,155.00\n             470.47,155.00 474.13,155.27 474.13,155.27\n             474.54,155.34 475.01,155.25 475.55,155.06\n             477.12,154.50 478.73,153.95 480.39,153.63\n             481.50,153.43 482.01,153.04 482.10,152.23\n             482.37,149.83 482.84,147.43 482.80,145.04\n             482.73,141.31 482.10,137.61 480.56,134.07\n             480.20,133.25 479.68,132.80 478.44,132.70\"\n        data-elem={ExerciseAttributeValueEnum.TRICEPS}\n        fill=\"#757575\"\n        id=\"path78\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 82 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRICEPS)}\n        d=\"M 466.43,137.75\n           C 466.50,137.55 466.56,137.34 466.58,137.14\n             466.79,135.14 466.99,133.14 467.13,131.75\n             466.82,129.82 466.57,128.54 466.41,127.25\n             465.53,120.08 461.16,114.46 456.77,108.87\n             456.41,108.40 455.41,108.13 454.74,108.19\n             454.37,108.23 454.05,109.10 453.78,109.64\n             453.64,109.93 453.63,110.30 453.63,110.63\n             453.65,115.00 453.56,119.37 453.74,123.73\n             454.00,129.73 456.89,134.72 461.26,139.01\n             463.57,141.28 465.45,140.75 466.43,137.75\"\n        data-elem={ExerciseAttributeValueEnum.TRICEPS}\n        fill=\"#757575\"\n        id=\"path82\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 84 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRICEPS)}\n        d=\"M 465.31,111.50\n           C 464.02,111.23 462.74,110.93 461.05,110.56\n             461.36,111.45 461.48,112.00 461.74,112.50\n             463.17,115.34 464.82,118.12 466.05,121.01\n             467.83,125.22 471.46,128.13 475.39,130.86\n             477.62,132.42 479.64,131.80 480.31,129.45\n             480.47,128.91 480.61,128.33 480.52,127.79\n             480.15,125.50 479.92,123.17 479.22,120.94\n             478.34,118.21 477.07,115.56 475.92,112.89\n             475.60,112.16 475.11,111.53 473.87,111.81\n             470.99,112.47 468.13,112.09 465.31,111.50\"\n        data-elem={ExerciseAttributeValueEnum.TRICEPS}\n        fill=\"#757575\"\n        id=\"path84\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      {/* Path 98 */}\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRICEPS)}\n        d=\"M 368.73,110.63\n           C 368.73,110.30 368.72,109.93 368.58,109.64\n             368.32,109.10 367.99,108.23 367.62,108.19\n             366.96,108.13 365.95,108.40 365.59,108.87\n             361.20,114.46 356.83,120.08 355.95,127.25\n             355.79,128.54 355.54,129.82 355.23,131.75\n             355.37,133.14 355.57,135.14 355.78,137.14\n             355.80,137.34 355.86,137.55 355.93,137.75\n             356.91,140.75 358.79,141.28 361.10,139.01\n             365.47,134.72 368.36,129.73 368.62,123.73\n             368.80,119.37 368.71,115.00 368.73,110.63\"\n        data-elem={ExerciseAttributeValueEnum.TRICEPS}\n        fill=\"#757575\"\n        id=\"path98\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRICEPS)}\n        d=\"M 341.97,153.63\n           C 343.63,153.95 345.24,154.50 346.81,155.06\n             347.35,155.25 347.82,155.34 348.23,155.27\n             348.23,155.27 351.89,155.00 351.89,155.00\n             352.66,154.83 353.70,154.84 354.14,154.47\n             355.62,153.23 357.25,151.97 358.14,150.51\n             360.70,146.33 362.93,142.05 365.27,137.81\n             365.38,137.61 365.42,137.39 365.49,137.18\n             365.36,137.12 365.24,137.06 365.12,137.01\n             364.54,137.61 363.94,138.20 363.34,138.79\n             363.34,138.79 363.35,138.78 363.35,138.78\n             363.35,138.78 363.34,138.79 363.33,138.81\n             363.11,139.02 362.90,139.24 362.69,139.45\n             361.80,140.30 360.20,141.67 359.00,141.77\n             358.38,141.83 358.05,141.83 357.88,141.82\n             357.84,141.77 357.79,141.72 357.75,141.69\n             354.83,140.35 353.72,138.15 353.41,135.73\n             353.15,133.71 353.34,131.66 353.33,129.62\n             353.33,129.35 353.37,129.09 353.40,128.62\n             351.31,129.76 349.53,130.86 347.60,131.75\n             346.50,132.25 345.19,132.60 343.92,132.70\n             342.68,132.80 342.16,133.25 341.80,134.07\n             340.26,137.61 339.63,141.31 339.56,145.04\n             339.52,147.43 340.00,149.83 340.26,152.23\n             340.35,153.04 340.86,153.43 341.97,153.63\"\n        data-elem={ExerciseAttributeValueEnum.TRICEPS}\n        fill=\"#757575\"\n        id=\"path100\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n\n      <path\n        className={getMuscleClasses(ExerciseAttributeValueEnum.TRICEPS)}\n        d=\"M 346.98,130.86\n           C 350.90,128.13 354.53,125.22 356.32,121.01\n             357.54,118.12 359.19,115.34 360.62,112.50\n             360.88,112.00 361.00,111.45 361.31,110.56\n             359.62,110.93 358.34,111.23 357.05,111.50\n             354.23,112.09 351.37,112.47 348.49,111.81\n             347.26,111.53 346.76,112.16 346.44,112.89\n             345.29,115.56 344.01,118.21 343.15,120.94\n             342.44,123.17 342.21,125.50 341.84,127.79\n             341.75,128.33 341.89,128.91 342.05,129.45\n             342.72,131.80 344.74,132.42 346.98,130.86\"\n        data-elem={ExerciseAttributeValueEnum.TRICEPS}\n        fill=\"#757575\"\n        id=\"path102\"\n        stroke=\"black\"\n        strokeWidth=\"0\"\n      />\n    </g>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-builder/ui/muscles.module.css",
    "content": ".svgContainer {\n  text-align: center;\n  margin: 2em 0;\n  position: relative;\n}\n\n.illustration {\n  width: 100%;\n}\n\n.illustration path {\n  position: relative;\n  z-index: 0;\n}\n\n.muscle {\n}\n.muscleContainer {\n  z-index: 1;\n  cursor: pointer;\n}\n\n.muscle.enabled.hover {\n  fill: #69b0ee;\n  cursor: pointer;\n}\n\n.muscle.enabled.active {\n  fill: #228be6 !important;\n}\n\n.muscle.enabled {\n  fill: #bdbdbd;\n}\n\n.loading {\n  fill: #757575;\n  animation: pulse 2s linear infinite;\n}\n\n@keyframes pulse {\n  0% {\n    opacity: 0.5;\n  }\n  50% {\n    opacity: 0.8;\n  }\n  100% {\n    opacity: 0.5;\n  }\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/quit-workout-dialog.tsx",
    "content": "\"use client\";\n\nimport { AlertTriangle, Trash2 } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface QuitWorkoutDialogProps {\n  isOpen: boolean;\n  onClose: VoidFunction;\n  onQuitWithoutSave: VoidFunction;\n  exercisesCompleted: number;\n  totalExercises: number;\n}\n\nexport function QuitWorkoutDialog({ isOpen, onClose, onQuitWithoutSave, exercisesCompleted, totalExercises }: QuitWorkoutDialogProps) {\n  const t = useI18n();\n\n  return (\n    <Dialog onOpenChange={onClose} open={isOpen}>\n      <DialogContent className=\"max-w-md bg-slate-900/95 border-slate-700/50 backdrop-blur-md\">\n        <DialogHeader className=\"pb-6\">\n          <DialogTitle className=\"flex items-center gap-3 text-xl font-bold text-white\">\n            <div className=\"flex items-center justify-center w-10 h-10 rounded-full bg-amber-500/20\">\n              <AlertTriangle className=\"h-5 w-5 text-amber-400\" />\n            </div>\n            {t(\"workout_builder.session.quit_workout_title\")}\n          </DialogTitle>\n        </DialogHeader>\n\n        {/* Progress Summary */}\n        <div className=\"bg-slate-800/50 rounded-xl p-4 mb-6\">\n          <div className=\"space-y-3\">\n            <div className=\"flex justify-between items-center\">\n              <span className=\"text-slate-300\">{t(\"workout_builder.session.progress\")}</span>\n              <span className=\"font-bold text-white\">\n                {exercisesCompleted} / {totalExercises}\n              </span>\n            </div>\n            <div className=\"w-full bg-slate-700 rounded-full h-2\">\n              <div\n                className=\"h-full bg-gradient-to-r from-amber-500 to-orange-500 rounded-full transition-all duration-300\"\n                style={{ width: `${(exercisesCompleted / totalExercises) * 100}%` }}\n              />\n            </div>\n          </div>\n        </div>\n\n        {/* Warning Message */}\n        <div className=\"text-center mb-6\">\n          <p className=\"text-slate-300 leading-relaxed\">{t(\"workout_builder.session.quit_warning\")}</p>\n        </div>\n\n        {/* Action Buttons */}\n        <div className=\"space-y-3\">\n          {/* Quit without saving */}\n          <Button\n            className=\"w-full bg-red-500/10 border-red-500/30 text-red-400 hover:bg-red-500/10 hover:border-red-500\"\n            onClick={onQuitWithoutSave}\n            size=\"large\"\n            variant=\"default\"\n          >\n            <Trash2 className=\"h-4 w-4 mr-2\" />\n            {t(\"workout_builder.session.quit_without_save\")}\n          </Button>\n\n          {/* Cancel */}\n          <Button className=\"w-full text-slate-400 hover:text-white hover:bg-slate-800\" onClick={onClose} size=\"large\" variant=\"ghost\">\n            {t(\"workout_builder.session.continue_workout\")}\n          </Button>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/stepper-header.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { Check } from \"lucide-react\";\n\nimport { cn } from \"@/shared/lib/utils\";\n\nimport { StepperStepProps } from \"../types\";\n\ninterface StepperHeaderProps {\n  steps: StepperStepProps[];\n  currentStep: number;\n  onStepClick?: (stepNumber: number) => void;\n}\n\nfunction StepperStep({\n  description,\n  isActive,\n  isCompleted,\n  stepNumber,\n  title,\n  currentStep,\n  onStepClick,\n}: StepperStepProps & { currentStep: number; onStepClick?: (stepNumber: number) => void }) {\n  const canClick = stepNumber < currentStep || isCompleted;\n\n  const handleClick = () => {\n    if (canClick && onStepClick) {\n      onStepClick(stepNumber);\n    }\n  };\n\n  return (\n    <>\n      {/* Layout mobile - vertical avec texte à droite */}\n      <div className=\"flex items-center text-left md:hidden\">\n        {/* Cercle */}\n        <div\n          className={cn(\n            \"flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all duration-200 flex-shrink-0\",\n            {\n              \"border-green-500 bg-green-500 text-white\": isCompleted,\n              \"border-blue-500 bg-blue-500 text-white\": isActive,\n              \"border-gray-300 bg-gray-100 text-gray-400 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-500\":\n                !isActive && !isCompleted,\n            },\n            canClick ? \"cursor-pointer\" : \"cursor-default\",\n          )}\n          onClick={handleClick}\n        >\n          {isCompleted ? <Check className=\"h-6 w-6\" /> : <span className=\"text-sm font-semibold\">{stepNumber}</span>}\n        </div>\n\n        {/* Contenu textuel à droite */}\n        <div className=\"ml-4\">\n          <h3\n            className={cn(\"font-semibold text-sm transition-colors\", {\n              \"text-green-600 dark:text-green-400\": isCompleted,\n              \"text-blue-600 dark:text-blue-400\": isActive,\n              \"text-gray-500 dark:text-gray-400\": !isActive && !isCompleted,\n            })}\n          >\n            {title}\n          </h3>\n          <p\n            className={cn(\n              \"text-xs mt-1 transition-colors\",\n              isActive || isCompleted ? \"text-gray-600 dark:text-gray-300\" : \"text-gray-400 dark:text-gray-500\",\n            )}\n          >\n            {description}\n          </p>\n        </div>\n      </div>\n\n      {/* Layout desktop - horizontal avec texte en bas */}\n      <div className=\"hidden md:flex flex-col items-center text-center\">\n        {/* Cercle */}\n        <div\n          className={cn(\n            \"flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all duration-200\",\n            {\n              \"border-green-500 bg-green-500 text-white\": isCompleted,\n              \"border-blue-500 bg-blue-500 text-white\": isActive,\n              \"border-gray-300 bg-gray-100 text-gray-400 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-500\":\n                !isActive && !isCompleted,\n            },\n            canClick ? \"cursor-pointer\" : \"cursor-default\",\n          )}\n          onClick={handleClick}\n        >\n          {isCompleted ? <Check className=\"h-6 w-6\" /> : <span className=\"text-sm font-semibold\">{stepNumber}</span>}\n        </div>\n\n        {/* Contenu textuel en bas */}\n        <div className=\"mt-3\">\n          <h3\n            className={cn(\"font-semibold text-sm transition-colors\", {\n              \"text-green-600 dark:text-green-400\": isCompleted,\n              \"text-blue-600 dark:text-blue-400\": isActive,\n              \"text-gray-500 dark:text-gray-400\": !isActive && !isCompleted,\n            })}\n          >\n            {title}\n          </h3>\n          <p\n            className={cn(\n              \"text-xs mt-1 transition-colors\",\n              isActive || isCompleted ? \"text-gray-600 dark:text-gray-300\" : \"text-gray-400 dark:text-gray-500\",\n            )}\n          >\n            {description}\n          </p>\n        </div>\n      </div>\n    </>\n  );\n}\n\nexport function StepperHeader({ steps, currentStep, onStepClick }: StepperHeaderProps) {\n  return (\n    <div className={cn(\"w-full my-8 px-2 sm:px-6\")}>\n      {/* Layout mobile - vertical */}\n      <div className=\"flex flex-col space-y-6 md:hidden\">\n        {steps.map((step, index) => (\n          <div className=\"relative\" key={step.stepNumber}>\n            <StepperStep {...step} currentStep={currentStep} onStepClick={onStepClick} />\n\n            {/* Ligne de connexion verticale */}\n            {index < steps.length - 1 && (\n              <div className=\"absolute left-6 top-12 w-0.5 h-6 -translate-x-0.5\">\n                <div\n                  className={cn(\n                    \"w-full h-full transition-colors duration-300\",\n                    step.isCompleted ? \"bg-green-500\" : \"bg-gray-300 dark:bg-gray-600\",\n                  )}\n                />\n              </div>\n            )}\n          </div>\n        ))}\n      </div>\n\n      {/* Layout desktop - horizontal */}\n      <div className=\"hidden md:flex items-start\">\n        {steps.map((step, index) => (\n          <React.Fragment key={step.stepNumber}>\n            {/* Étape */}\n            <div className=\"flex flex-col items-center\">\n              <StepperStep {...step} currentStep={currentStep} onStepClick={onStepClick} />\n            </div>\n\n            {/* Ligne de connexion horizontale */}\n            {index < steps.length - 1 && (\n              <div className=\"flex-1 flex items-center pt-6\">\n                <div\n                  className={cn(\n                    \"w-full h-1 transition-colors duration-300\",\n                    step.isCompleted ? \"bg-green-500\" : \"bg-gray-300 dark:bg-gray-600\",\n                  )}\n                />\n              </div>\n            )}\n          </React.Fragment>\n        ))}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/workout-stepper-footer.tsx",
    "content": "\"use client\";\nimport { ArrowLeft, ArrowRight } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { Button } from \"@/components/ui/button\";\n\nexport function WorkoutBuilderFooter({\n  currentStep,\n  totalSteps,\n  canContinue,\n  onPrevious,\n  onNext,\n  onStartWorkout,\n}: {\n  currentStep: number;\n  totalSteps: number;\n  canContinue: boolean;\n  onPrevious: VoidFunction;\n  onNext: VoidFunction;\n  onStartWorkout?: VoidFunction;\n}) {\n  const t = useI18n();\n  const isFirstStep = currentStep === 1;\n  const isFinalStep = currentStep === totalSteps;\n\n  return (\n    <div className=\"w-full sticky bottom-0 \">\n      {/* Mobile layout - vertical stack */}\n      <div className=\"flex flex-col gap-4 px-2 sm:px-6 pb-2\">\n        {/* Center stats on top for mobile */}\n\n        {/* Navigation buttons */}\n        <div className=\"mt-4 min-h-12 flex items-center justify-between gap-3 bg-white dark:bg-slate-900 w-full p-0.5 border border-slate-400 dark:border-slate-700 rounded-full\">\n          {/* Previous button */}\n          <Button className=\"flex-1 rounded-full min-h-12\" disabled={isFirstStep} onClick={onPrevious} size=\"default\" variant=\"ghost\">\n            <div className=\"flex items-center gap-2\">\n              <ArrowLeft className=\"h-4 w-4\" />\n              <span className=\"font-medium\">{t(\"workout_builder.navigation.previous\")}</span>\n            </div>\n          </Button>\n\n          {/* Next/Start Workout button */}\n          <Button\n            className=\"flex-1 rounded-full bg-blue-600 hover:bg-blue-700 min-h-12 dark:bg-blue-500 dark:hover:bg-blue-600\"\n            disabled={!canContinue}\n            onClick={isFinalStep ? () => onStartWorkout?.() : onNext}\n            size=\"default\"\n            variant=\"default\"\n          >\n            <div className=\"flex items-center justify-center gap-2\">\n              <span className=\"font-semibold\">{t(\"workout_builder.navigation.continue\")}</span>\n              <ArrowRight className=\"h-4 w-4\" />\n            </div>\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-builder/ui/workout-stepper.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect, useMemo } from \"react\";\nimport { useQueryState } from \"nuqs\";\nimport { useRouter } from \"next/navigation\";\nimport Image from \"next/image\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport Trophy from \"@public/images/trophy.png\";\nimport useBoolean from \"@/shared/hooks/useBoolean\";\nimport { WorkoutSessionSets } from \"@/features/workout-session/ui/workout-session-sets\";\nimport { WorkoutSessionHeader } from \"@/features/workout-session/ui/workout-session-header\";\nimport { DonationModal } from \"@/features/workout-session/ui/donation-modal\";\nimport { useDonationModal } from \"@/features/workout-session/hooks/use-donation-modal\";\nimport { WorkoutBuilderFooter } from \"@/features/workout-builder/ui/workout-stepper-footer\";\nimport { env } from \"@/env\";\nimport { Button } from \"@/components/ui/button\";\nimport { NutripureAffiliateBanner } from \"@/components/ads/nutripure-affiliate-banner\";\nimport { HorizontalTopBanner } from \"@/components/ads\";\n\nimport { StepperStepProps } from \"../types\";\nimport { useWorkoutStepper } from \"../hooks/use-workout-stepper\";\nimport { useWorkoutSession } from \"../../workout-session/model/use-workout-session\";\nimport { StepperHeader } from \"./stepper-header\";\nimport { MuscleSelection } from \"./muscle-selection\";\nimport { ExercisesSelection } from \"./exercises-selection\";\nimport { EquipmentSelection } from \"./equipment-selection\";\nimport { AddExerciseModal } from \"./add-exercise-modal\";\n\nimport type { ExerciseWithAttributes, WorkoutBuilderStep } from \"../types\";\n\nexport function WorkoutStepper() {\n  const { loadSessionFromLocal } = useWorkoutSession();\n\n  const t = useI18n();\n  const router = useRouter();\n  const [fromSession, setFromSession] = useQueryState(\"fromSession\");\n  const {\n    currentStep,\n    selectedEquipment,\n    selectedMuscles,\n    exercisesByMuscle,\n    isLoadingExercises,\n    exercisesError,\n    nextStep,\n    prevStep,\n    toggleEquipment,\n    clearEquipment,\n    toggleMuscle,\n    canProceedToStep2,\n    canProceedToStep3,\n    fetchExercises,\n    exercisesOrder,\n    shuffleExercise,\n    pickExercise,\n    shufflingExerciseId,\n    goToStep,\n    deleteExercise,\n  } = useWorkoutStepper();\n  const locale = useCurrentLocale();\n  useEffect(() => {\n    loadSessionFromLocal();\n  }, []);\n\n  const [flatExercises, setFlatExercises] = useState<{ id: string; muscle: string; exercise: ExerciseWithAttributes }[]>([]);\n\n  useEffect(() => {\n    if (exercisesByMuscle.length > 0) {\n      const flat = exercisesByMuscle.flatMap((group) =>\n        group.exercises.map((exercise: ExerciseWithAttributes) => ({\n          id: exercise.id,\n          muscle: group.muscle,\n          exercise,\n        })),\n      );\n      setFlatExercises(flat);\n    }\n  }, [exercisesByMuscle]);\n\n  useEffect(() => {\n    if (currentStep === 3 && !fromSession) {\n      fetchExercises();\n    }\n  }, [currentStep, selectedEquipment, selectedMuscles, fromSession]);\n\n  const { isWorkoutActive, session, startWorkout, quitWorkout } = useWorkoutSession();\n\n  const canContinue = currentStep === 1 ? canProceedToStep2 : currentStep === 2 ? canProceedToStep3 : exercisesByMuscle.length > 0;\n\n  const handleShuffleExercise = async (exerciseId: string, muscle: string) => {\n    try {\n      const muscleEnum = muscle as ExerciseAttributeValueEnum;\n      await shuffleExercise(exerciseId, muscleEnum);\n    } catch (error) {\n      console.error(\"Error shuffling exercise:\", error);\n      alert(\"Error shuffling exercise. Please try again.\");\n    }\n  };\n\n  const handlePickExercise = async (exerciseId: string) => {\n    try {\n      await pickExercise(exerciseId);\n      console.log(\"Exercise picked successfully!\");\n    } catch (error) {\n      console.error(\"Error picking exercise:\", error);\n      alert(\"Error picking exercise. Please try again.\");\n    }\n  };\n\n  const handleDeleteExercise = (exerciseId: string) => {\n    deleteExercise(exerciseId);\n  };\n\n  const addExerciseModal = useBoolean();\n\n  const handleAddExercise = () => {\n    addExerciseModal.setTrue();\n  };\n\n  // Fix: Use flatExercises as the source of truth, respecting exercisesOrder when possible\n  const orderedExercises = useMemo(() => {\n    if (flatExercises.length === 0) return [];\n\n    if (exercisesOrder.length === 0) {\n      // No custom order, use flatExercises as-is\n      return flatExercises.map((item) => item.exercise);\n    }\n\n    // Create a map for quick lookup\n    const exerciseMap = new Map(flatExercises.map((item) => [item.id, item.exercise]));\n\n    // Get ordered exercises that exist in flatExercises\n    const orderedResults = exercisesOrder.map((id) => exerciseMap.get(id)).filter(Boolean) as ExerciseWithAttributes[];\n\n    // Add any remaining exercises from flatExercises that aren't in exercisesOrder\n    const remainingExercises = flatExercises.filter((item) => !exercisesOrder.includes(item.id)).map((item) => item.exercise);\n\n    return [...orderedResults, ...remainingExercises];\n  }, [flatExercises, exercisesOrder]);\n\n  const handleStartWorkout = () => {\n    if (orderedExercises.length > 0) {\n      startWorkout(orderedExercises, selectedEquipment, selectedMuscles);\n    } else {\n      console.log(\"🚀 [WORKOUT-STEPPER] No exercises to start workout with!\");\n    }\n  };\n\n  const [showCongrats, setShowCongrats] = useState(false);\n  const { showModal, openModal, closeModal } = useDonationModal();\n\n  const goToProfile = () => {\n    router.push(\"/profile\");\n  };\n\n  const handleCongrats = () => {\n    setShowCongrats(true);\n    // Show donation modal after congrats screen appears\n    setTimeout(() => {\n      openModal();\n    }, 400);\n  };\n\n  const handleToggleEquipment = (equipment: ExerciseAttributeValueEnum) => {\n    toggleEquipment(equipment);\n    if (fromSession) setFromSession(null);\n  };\n\n  const handleClearEquipment = () => {\n    clearEquipment();\n    if (fromSession) setFromSession(null);\n  };\n\n  const handleToggleMuscle = (muscle: ExerciseAttributeValueEnum) => {\n    toggleMuscle(muscle);\n    if (fromSession) setFromSession(null);\n  };\n\n  const handleStepClick = (stepNumber: number) => {\n    if (stepNumber < currentStep) {\n      goToStep(stepNumber as WorkoutBuilderStep);\n    }\n  };\n\n  if (showCongrats && !isWorkoutActive) {\n    return (\n      <>\n        <div className=\"flex flex-col items-center justify-center py-16 h-full\">\n          <Image alt=\"Trophée\" className=\"w-56 h-56\" src={Trophy} />\n          <h2 className=\"text-2xl font-bold mb-2 text-center\">{t(\"workout_builder.session.congrats\")}</h2>\n          <p className=\"text-lg text-slate-600 mb-6\">{t(\"workout_builder.session.congrats_subtitle\")}</p>\n          <Button onClick={goToProfile}>{t(\"commons.go_to_profile\")}</Button>\n        </div>\n        {/* Donation Modal */}\n        <DonationModal isOpen={showModal} onClose={closeModal} />\n      </>\n    );\n  }\n\n  if (isWorkoutActive && session) {\n    return (\n      <div className=\"w-full max-w-6xl mx-auto\">\n        {env.NEXT_PUBLIC_TOP_WORKOUT_SESSION_BANNER_AD_SLOT && (\n          <HorizontalTopBanner adSlot={env.NEXT_PUBLIC_TOP_WORKOUT_SESSION_BANNER_AD_SLOT} />\n        )}\n        {!showCongrats && <WorkoutSessionHeader onQuitWorkout={quitWorkout} />}\n        <WorkoutSessionSets isWorkoutActive={isWorkoutActive} onCongrats={handleCongrats} showCongrats={showCongrats} />\n      </div>\n    );\n  }\n\n  const STEPPER_STEPS: StepperStepProps[] = [\n    {\n      stepNumber: 1,\n      title: t(\"workout_builder.steps.equipment.title\"),\n      description: t(\"workout_builder.steps.equipment.description\"),\n      isActive: false,\n      isCompleted: false,\n    },\n    {\n      stepNumber: 2,\n      title: t(\"workout_builder.steps.muscles.title\"),\n      description: t(\"workout_builder.steps.muscles.description\"),\n      isActive: false,\n      isCompleted: false,\n    },\n    {\n      stepNumber: 3,\n      title: t(\"workout_builder.steps.exercises.title\"),\n      description: t(\"workout_builder.steps.exercises.description\"),\n      isActive: false,\n      isCompleted: false,\n    },\n  ];\n\n  const steps = STEPPER_STEPS.map((step) => ({\n    ...step,\n    isActive: step.stepNumber === currentStep,\n    isCompleted: step.stepNumber < currentStep,\n  }));\n\n  const renderStepContent = () => {\n    switch (currentStep) {\n      case 1:\n        return (\n          <EquipmentSelection\n            onClearEquipment={handleClearEquipment}\n            onToggleEquipment={handleToggleEquipment}\n            selectedEquipment={selectedEquipment}\n          />\n        );\n      case 2:\n        return (\n          <MuscleSelection onToggleMuscle={handleToggleMuscle} selectedEquipment={selectedEquipment} selectedMuscles={selectedMuscles} />\n        );\n      case 3:\n        return (\n          <ExercisesSelection\n            error={exercisesError}\n            exercisesByMuscle={exercisesByMuscle}\n            isLoading={isLoadingExercises}\n            onAdd={handleAddExercise}\n            onDelete={handleDeleteExercise}\n            onPick={handlePickExercise}\n            onShuffle={handleShuffleExercise}\n            shufflingExerciseId={shufflingExerciseId}\n          />\n        );\n      default:\n        return null;\n    }\n  };\n\n  const renderTopBanner = () => {\n    if (currentStep === 1) {\n      // if (locale === \"fr\") {\n      //   return <NutripureAffiliateBanner />;\n      // }\n\n      if (env.NEXT_PUBLIC_TOP_STEPPER_STEP_1_BANNER_AD_SLOT || env.NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_1_PLACEMENT_ID) {\n        return (\n          <HorizontalTopBanner\n            adSlot={env.NEXT_PUBLIC_TOP_STEPPER_STEP_1_BANNER_AD_SLOT}\n            ezoicPlacementId={env.NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_1_PLACEMENT_ID}\n          />\n        );\n      }\n    }\n\n    if (currentStep === 2) {\n      if (locale === \"fr\") {\n        return <NutripureAffiliateBanner />;\n      }\n\n      if (env.NEXT_PUBLIC_TOP_STEPPER_STEP_2_BANNER_AD_SLOT || env.NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_2_PLACEMENT_ID) {\n        return (\n          <HorizontalTopBanner\n            adSlot={env.NEXT_PUBLIC_TOP_STEPPER_STEP_2_BANNER_AD_SLOT}\n            ezoicPlacementId={env.NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_2_PLACEMENT_ID}\n          />\n        );\n      }\n    }\n\n    if (currentStep === 3) {\n      if (locale === \"fr\") {\n        return <NutripureAffiliateBanner />;\n      }\n\n      if (env.NEXT_PUBLIC_TOP_STEPPER_STEP_3_BANNER_AD_SLOT || env.NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_3_PLACEMENT_ID) {\n        return (\n          <HorizontalTopBanner\n            adSlot={env.NEXT_PUBLIC_TOP_STEPPER_STEP_3_BANNER_AD_SLOT}\n            ezoicPlacementId={env.NEXT_PUBLIC_EZOIC_TOP_STEPPER_STEP_3_PLACEMENT_ID}\n          />\n        );\n      }\n    }\n  };\n\n  return (\n    <div className=\"w-full max-w-6xl mx-auto h-full\">\n      {renderTopBanner()}\n\n      <StepperHeader currentStep={currentStep} onStepClick={handleStepClick} steps={steps} />\n\n      <div className=\"px-2 sm:px-6\">{renderStepContent()}</div>\n\n      <WorkoutBuilderFooter\n        canContinue={canContinue}\n        currentStep={currentStep}\n        onNext={nextStep}\n        onPrevious={prevStep}\n        onStartWorkout={handleStartWorkout}\n        totalSteps={STEPPER_STEPS.length}\n      />\n\n      <AddExerciseModal isOpen={addExerciseModal.value} onClose={addExerciseModal.setFalse} selectedEquipment={selectedEquipment} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-session/actions/delete-workout-session.action.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\n\nconst deleteWorkoutSessionSchema = z.object({\n  id: z.string(),\n});\n\nexport const deleteWorkoutSessionAction = actionClient.schema(deleteWorkoutSessionSchema).action(async ({ parsedInput }) => {\n  try {\n    const { id } = parsedInput;\n\n    const session = await prisma.workoutSession.findUnique({\n      where: { id },\n      select: { userId: true },\n    });\n\n    if (!session) {\n      console.error(\"❌ Session not found:\", id);\n      return { serverError: \"Session not found\" };\n    }\n\n    // Supprimer la session (cascade supprimera automatiquement les exercices et sets)\n    await prisma.workoutSession.delete({\n      where: { id },\n    });\n\n    if (process.env.NODE_ENV === \"development\") {\n      console.log(\"✅ Workout session deleted successfully:\", id);\n    }\n\n    return { success: true };\n  } catch (error) {\n    console.error(\"❌ Error deleting workout session:\", error);\n    return { serverError: \"Failed to delete workout session\" };\n  }\n});\n"
  },
  {
    "path": "src/features/workout-session/actions/get-workout-sessions.action.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\n\nconst getWorkoutSessionsSchema = z.object({\n  userId: z.string().optional(),\n});\n\nexport const getWorkoutSessionsAction = actionClient.schema(getWorkoutSessionsSchema).action(async ({ parsedInput }) => {\n  try {\n    const { userId } = parsedInput;\n\n    if (!userId) {\n      return { serverError: \"User ID is required\" };\n    }\n\n    const sessions = await prisma.workoutSession.findMany({\n      where: { userId },\n      include: {\n        exercises: {\n          include: {\n            exercise: {\n              include: {\n                attributes: {\n                  include: {\n                    attributeName: true,\n                    attributeValue: true,\n                  },\n                },\n              },\n            },\n            sets: true,\n          },\n        },\n      },\n      orderBy: {\n        startedAt: \"desc\",\n      },\n    });\n    return { sessions };\n  } catch (error) {\n    console.error(\"Error fetching workout sessions:\", error);\n    return { serverError: \"Failed to fetch workout sessions\" };\n  }\n});\n"
  },
  {
    "path": "src/features/workout-session/actions/sync-workout-sessions.action.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\nimport { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { workoutSessionStatuses } from \"@/shared/lib/workout-session/types/workout-session\";\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { ALL_WORKOUT_SET_TYPES, WORKOUT_SET_UNITS_TUPLE } from \"@/shared/constants/workout-set-types\";\nimport { ERROR_MESSAGES } from \"@/shared/constants/errors\";\nimport { actionClient } from \"@/shared/api/safe-actions\";\n\nconst workoutSetSchema = z.object({\n  id: z.string(),\n  setIndex: z.number(),\n  types: z.array(z.enum(ALL_WORKOUT_SET_TYPES)),\n  valuesInt: z.array(z.number()).optional(),\n  valuesSec: z.array(z.number()).optional(),\n  units: z.array(z.enum(WORKOUT_SET_UNITS_TUPLE)).optional(),\n  completed: z.boolean(),\n});\n\nconst workoutSessionExerciseSchema = z.object({\n  id: z.string(),\n  order: z.number(),\n  sets: z.array(workoutSetSchema),\n});\n\nconst syncWorkoutSessionSchema = z.object({\n  session: z.object({\n    id: z.string(),\n    userId: z.string(),\n    startedAt: z.string(),\n    endedAt: z.string().optional(),\n    exercises: z.array(workoutSessionExerciseSchema),\n    status: z.enum(workoutSessionStatuses),\n    muscles: z.array(z.nativeEnum(ExerciseAttributeValueEnum)),\n    rating: z.number().min(1).max(5).nullable().optional(),\n    ratingComment: z.string().nullable().optional(),\n  }),\n});\n\nexport const syncWorkoutSessionAction = actionClient.schema(syncWorkoutSessionSchema).action(async ({ parsedInput }) => {\n  try {\n    const { session } = parsedInput;\n\n    // Check if user exists\n    const userExists = await prisma.user.findUnique({\n      where: { id: session.userId },\n    });\n\n    if (!userExists) {\n      console.error(`User with ID ${session.userId} does not exist`);\n      return { serverError: ERROR_MESSAGES.USER_NOT_FOUND };\n    }\n\n    // Check if all exercises exist\n    const exerciseIds = session.exercises.map((e) => e.id);\n    const existingExercises = await prisma.exercise.findMany({\n      where: { id: { in: exerciseIds } },\n      select: { id: true },\n    });\n\n    const existingExerciseIds = new Set(existingExercises.map((e) => e.id));\n    const missingExercises = exerciseIds.filter((id) => !existingExerciseIds.has(id));\n\n    if (missingExercises.length > 0) {\n      console.error(\"Missing exercises:\", missingExercises);\n      return { serverError: `Exercises not found: ${missingExercises.join(\", \")}` };\n    }\n\n    const { status: _s, ...sessionData } = session;\n\n    const result = await prisma.workoutSession.upsert({\n      where: { id: session.id },\n      create: {\n        ...sessionData,\n        muscles: session.muscles,\n        rating: session.rating,\n        ratingComment: session.ratingComment,\n        exercises: {\n          create: session.exercises.map((exercise) => ({\n            order: exercise.order,\n            exercise: { connect: { id: exercise.id } },\n            sets: {\n              create: exercise.sets.map((set) => ({\n                setIndex: set.setIndex,\n                types: set.types,\n                valuesInt: set.valuesInt,\n                valuesSec: set.valuesSec,\n                units: set.units,\n                completed: set.completed,\n                type: set.types && set.types.length > 0 ? set.types[0] : \"NA\",\n              })),\n            },\n          })),\n        },\n      },\n      update: {\n        muscles: session.muscles,\n        rating: session.rating,\n        ratingComment: session.ratingComment,\n        exercises: {\n          deleteMany: {},\n          create: session.exercises.map((exercise) => ({\n            order: exercise.order,\n            exercise: { connect: { id: exercise.id } },\n            sets: {\n              create: exercise.sets.map((set) => ({\n                ...set,\n                type: set.types && set.types.length > 0 ? set.types[0] : \"NA\",\n              })),\n            },\n          })),\n        },\n      },\n    });\n\n    console.log(\"✅ Workout session synced successfully:\", result.id);\n\n    return { data: result };\n  } catch (error) {\n    console.error(\"❌ Error syncing workout session:\", error);\n    return { serverError: \"Failed to sync workout session\" };\n  }\n});\n"
  },
  {
    "path": "src/features/workout-session/hooks/use-donation-modal.ts",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\n\nexport function useDonationModal() {\n  const [showModal, setShowModal] = useState(false);\n\n  const openModal = () => {\n    setShowModal(true);\n  };\n\n  const closeModal = () => {\n    setShowModal(false);\n  };\n\n  return {\n    showModal,\n    openModal,\n    closeModal,\n  };\n}"
  },
  {
    "path": "src/features/workout-session/lib/workout-set-labels.ts",
    "content": "import { TFunction } from \"locales/client\";\n\nimport { WorkoutSetType } from \"../types/workout-set\";\n\nexport function getWorkoutSetTypeLabels(t: TFunction): Record<WorkoutSetType, string> {\n  return {\n    TIME: t(\"workout_builder.session.time\"),\n    WEIGHT: t(\"workout_builder.session.weight\"),\n    REPS: t(\"workout_builder.session.reps\"),\n    BODYWEIGHT: t(\"workout_builder.session.bodyweight\"),\n    NA: \"N/A\",\n  };\n}\n"
  },
  {
    "path": "src/features/workout-session/model/use-sync-workout-sessions.ts",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { workoutSessionLocal } from \"@/shared/lib/workout-session/workout-session.local\";\nimport { useSession } from \"@/features/auth/lib/auth-client\";\n\nimport { syncWorkoutSessionAction } from \"../actions/sync-workout-sessions.action\";\n\ninterface SyncState {\n  isSyncing: boolean;\n  error: Error | null;\n  lastSyncAt: Date | null;\n}\n\nconst SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes\n\nexport function useSyncWorkoutSessions() {\n  const { data: session, isPending: isSessionLoading } = useSession();\n\n  const [syncState, setSyncState] = useState<SyncState>({\n    isSyncing: false,\n    error: null,\n    lastSyncAt: null,\n  });\n\n  const syncSessions = async () => {\n    if (!session?.user) return;\n\n    setSyncState((prev) => ({ ...prev, isSyncing: true, error: null }));\n\n    try {\n      const localSessions = workoutSessionLocal.getAll().filter((s) => s.status === \"completed\");\n\n      for (const localSession of localSessions) {\n        try {\n          const result = await syncWorkoutSessionAction({\n            session: {\n              ...localSession,\n              userId: localSession.userId === \"local\" ? session.user.id : localSession.userId,\n              status: \"synced\",\n            },\n          });\n\n          if (result && result.serverError) {\n            console.log(\"result:\", result);\n            throw new Error(result.serverError);\n          }\n\n          if (result && result.data) {\n            const { data } = result.data;\n\n            if (data) {\n              workoutSessionLocal.markSynced(localSession.id, data.id);\n            }\n          }\n        } catch (error) {\n          console.error(`Failed to sync session ${localSession.id}:`, error);\n        }\n      }\n\n      workoutSessionLocal.purgeSynced();\n\n      setSyncState((prev) => ({\n        ...prev,\n        isSyncing: false,\n        lastSyncAt: new Date(),\n      }));\n    } catch (error) {\n      console.log(\"error:\", error);\n      setSyncState((prev) => ({\n        ...prev,\n        isSyncing: false,\n        error: error as Error,\n      }));\n    }\n  };\n\n  // Sync on login\n  useEffect(() => {\n    if (!isSessionLoading && session?.user) {\n      syncSessions();\n    }\n  }, [session, isSessionLoading]);\n\n  // Periodic sync\n  useEffect(() => {\n    if (!session?.user) return;\n\n    const interval = setInterval(syncSessions, SYNC_INTERVAL);\n    return () => clearInterval(interval);\n  }, [session]);\n\n  return {\n    syncSessions,\n    ...syncState,\n  };\n}\n"
  },
  {
    "path": "src/features/workout-session/model/use-workout-session.ts",
    "content": "\"use client\";\n\nimport { useWorkoutSessionStore } from \"./workout-session.store\";\n\nexport function useWorkoutSession() {\n  return useWorkoutSessionStore();\n}\n"
  },
  {
    "path": "src/features/workout-session/model/use-workout-sessions.ts",
    "content": "\"use client\";\n\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useWorkoutSessionService } from \"@/shared/lib/workout-session/use-workout-session.service\";\nimport { useSession } from \"@/features/auth/lib/auth-client\";\n\nexport function useWorkoutSessions() {\n  const { data: session } = useSession();\n\n  const { getAll } = useWorkoutSessionService();\n\n  return useQuery({\n    queryKey: [\"workout-sessions\", session?.user?.id],\n    queryFn: async () => {\n      return getAll();\n    },\n  });\n}\n"
  },
  {
    "path": "src/features/workout-session/model/workout-session.store.ts",
    "content": "import { create } from \"zustand\";\n\nimport { workoutSessionLocal } from \"@/shared/lib/workout-session/workout-session.local\";\nimport { WorkoutSession } from \"@/shared/lib/workout-session/types/workout-session\";\nimport { convertWeight, type WeightUnit } from \"@/shared/lib/weight-conversion\";\nimport { WorkoutSessionExercise, WorkoutSet, WorkoutSetType, WorkoutSetUnit } from \"@/features/workout-session/types/workout-set\";\nimport { useWorkoutBuilderStore } from \"@/features/workout-builder/model/workout-builder.store\";\nimport { ExerciseWithAttributes } from \"@/entities/exercise/types/exercise.types\";\n\ninterface WorkoutSessionProgress {\n  exerciseId: string;\n  sets: {\n    reps: number;\n    weight?: number;\n    duration?: number;\n  }[];\n  completed: boolean;\n}\n\ninterface WorkoutSessionState {\n  session: WorkoutSession | null;\n  progress: Record<string, WorkoutSessionProgress>;\n  elapsedTime: number;\n  isTimerRunning: boolean;\n  isWorkoutActive: boolean;\n  currentExerciseIndex: number;\n  currentExercise: WorkoutSessionExercise | null;\n\n  // Progression\n  exercisesCompleted: number;\n  totalExercises: number;\n  progressPercent: number;\n\n  // Actions\n  startWorkout: (exercises: ExerciseWithAttributes[] | WorkoutSessionExercise[], equipment: any[], muscles: any[]) => void;\n  quitWorkout: () => void;\n  completeWorkout: () => void;\n  toggleTimer: () => void;\n  resetTimer: () => void;\n  updateExerciseProgress: (exerciseId: string, progressData: Partial<WorkoutSessionProgress>) => void;\n  addSet: () => void;\n  updateSet: (exerciseIndex: number, setIndex: number, data: Partial<WorkoutSet>) => void;\n  removeSet: (exerciseIndex: number, setIndex: number) => void;\n  finishSet: (exerciseIndex: number, setIndex: number) => void;\n  goToNextExercise: () => void;\n  goToPrevExercise: () => void;\n  goToExercise: (targetIndex: number) => void;\n  formatElapsedTime: () => string;\n  getExercisesCompleted: () => number;\n  getTotalExercises: () => number;\n  getTotalVolume: () => number;\n  getTotalVolumeInUnit: (unit: WeightUnit) => number;\n  loadSessionFromLocal: () => void;\n  addExerciseToSession: (exercise: ExerciseWithAttributes) => void;\n}\n\nexport const useWorkoutSessionStore = create<WorkoutSessionState>((set, get) => ({\n  session: null,\n  progress: {},\n  elapsedTime: 0,\n  isTimerRunning: false,\n  isWorkoutActive: false,\n  currentExerciseIndex: 0,\n  currentExercise: null,\n  exercisesCompleted: 0,\n  totalExercises: 0,\n  progressPercent: 0,\n\n  startWorkout: (exercises, _equipment, muscles) => {\n    const sessionExercises: WorkoutSessionExercise[] = exercises.map((ex, idx) => {\n      // Check if exercise already has sets (from program)\n      if (\"sets\" in ex && ex.sets && ex.sets.length > 0) {\n        return {\n          ...ex,\n          order: idx,\n        } as WorkoutSessionExercise;\n      }\n\n      // Default sets for custom workouts\n      return {\n        ...ex,\n        order: idx,\n        sets: [\n          {\n            id: `${ex.id}-set-1`,\n            setIndex: 0,\n            types: [\"REPS\", \"WEIGHT\"],\n            valuesInt: [],\n            valuesSec: [],\n            units: [],\n            completed: false,\n          },\n        ],\n      } as WorkoutSessionExercise;\n    });\n\n    const newSession: WorkoutSession = {\n      id: Date.now().toString(),\n      userId: \"local\",\n      startedAt: new Date().toISOString(),\n      exercises: sessionExercises,\n      status: \"active\",\n      muscles,\n    };\n\n    workoutSessionLocal.add(newSession);\n    workoutSessionLocal.setCurrent(newSession.id);\n\n    set({\n      session: newSession,\n      elapsedTime: 0,\n      isTimerRunning: false,\n      isWorkoutActive: true,\n      currentExercise: sessionExercises[0],\n    });\n  },\n\n  quitWorkout: () => {\n    const { session } = get();\n    if (session) {\n      workoutSessionLocal.remove(session.id);\n    }\n    set({\n      session: null,\n      progress: {},\n      elapsedTime: 0,\n      isTimerRunning: false,\n      isWorkoutActive: false,\n      currentExerciseIndex: 0,\n      currentExercise: null,\n    });\n  },\n\n  completeWorkout: () => {\n    const { session } = get();\n\n    if (session) {\n      workoutSessionLocal.update(session.id, { status: \"completed\", endedAt: new Date().toISOString() });\n      console.log({\n        session: { ...session, status: \"completed\", endedAt: new Date().toISOString() },\n        progress: {},\n        elapsedTime: 0,\n        isTimerRunning: false,\n        isWorkoutActive: false,\n      });\n      set({\n        session: { ...session, status: \"completed\", endedAt: new Date().toISOString() },\n        progress: {},\n        elapsedTime: 0,\n        isTimerRunning: false,\n        isWorkoutActive: false,\n      });\n    }\n\n    useWorkoutBuilderStore.getState().setStep(1);\n  },\n\n  toggleTimer: () => {\n    set((state) => {\n      const newIsRunning = !state.isTimerRunning;\n      if (state.session) {\n        workoutSessionLocal.update(state.session.id, { isActive: newIsRunning });\n      }\n      return { isTimerRunning: newIsRunning };\n    });\n  },\n\n  resetTimer: () => {\n    set((state) => {\n      if (state.session) {\n        workoutSessionLocal.update(state.session.id, { duration: 0 });\n      }\n      return { elapsedTime: 0 };\n    });\n  },\n\n  updateExerciseProgress: (exerciseId, progressData) => {\n    set((state) => ({\n      progress: {\n        ...state.progress,\n        [exerciseId]: {\n          ...state.progress[exerciseId],\n          exerciseId,\n          sets: [],\n          completed: false,\n          ...progressData,\n        },\n      },\n    }));\n  },\n\n  addSet: () => {\n    const { session, currentExerciseIndex } = get();\n    if (!session) return;\n\n    const exIdx = currentExerciseIndex;\n    const currentExercise = session.exercises[exIdx];\n    const sets = currentExercise.sets;\n\n    let typesToCopy: WorkoutSetType[] = [\"REPS\"];\n    let unitsToCopy: WorkoutSetUnit[] = [];\n\n    if (sets.length > 0) {\n      const lastSet = sets[sets.length - 1];\n\n      if (lastSet.types && lastSet.types.length > 0) {\n        typesToCopy = [...lastSet.types];\n        if (lastSet.units && lastSet.units.length > 0) {\n          unitsToCopy = [...lastSet.units];\n        }\n      }\n    }\n\n    const newSet: WorkoutSet = {\n      id: `${currentExercise.id}-set-${sets.length + 1}`,\n      setIndex: sets.length,\n      types: typesToCopy,\n      valuesInt: [],\n      valuesSec: [],\n      units: unitsToCopy,\n      completed: false,\n    };\n\n    const updatedExercises = session.exercises.map((ex, idx) => (idx === exIdx ? { ...ex, sets: [...ex.sets, newSet] } : ex));\n\n    workoutSessionLocal.update(session.id, { exercises: updatedExercises });\n\n    set({\n      session: { ...session, exercises: updatedExercises },\n      currentExercise: { ...updatedExercises[exIdx] },\n    });\n  },\n\n  updateSet: (exerciseIndex, setIndex, data) => {\n    const { session } = get();\n    if (!session) return;\n\n    const targetExercise = session.exercises[exerciseIndex];\n    if (!targetExercise) return;\n\n    const updatedSets = targetExercise.sets.map((set, idx) => (idx === setIndex ? { ...set, ...data } : set));\n    const updatedExercises = session.exercises.map((ex, idx) => (idx === exerciseIndex ? { ...ex, sets: updatedSets } : ex));\n\n    workoutSessionLocal.update(session.id, { exercises: updatedExercises });\n\n    set({\n      session: { ...session, exercises: updatedExercises },\n      currentExercise: { ...updatedExercises[exerciseIndex] },\n    });\n\n    // handle exercisesCompleted\n  },\n\n  removeSet: (exerciseIndex, setIndex) => {\n    const { session } = get();\n    if (!session) return;\n    const targetExercise = session.exercises[exerciseIndex];\n    if (!targetExercise) return;\n    const updatedSets = targetExercise.sets.filter((_, idx) => idx !== setIndex);\n    const updatedExercises = session.exercises.map((ex, idx) => (idx === exerciseIndex ? { ...ex, sets: updatedSets } : ex));\n    workoutSessionLocal.update(session.id, { exercises: updatedExercises });\n    set({\n      session: { ...session, exercises: updatedExercises },\n      currentExercise: { ...updatedExercises[exerciseIndex] },\n    });\n  },\n\n  finishSet: (exerciseIndex, setIndex) => {\n    get().updateSet(exerciseIndex, setIndex, { completed: true });\n\n    // if has completed all sets, go to next exercise\n    const { session } = get();\n    if (!session) return;\n\n    const exercise = session.exercises[exerciseIndex];\n    if (!exercise) return;\n\n    if (exercise.sets.every((set) => set.completed)) {\n      // get().goToNextExercise();\n      // update exercisesCompleted\n      const exercisesCompleted = get().exercisesCompleted;\n      set({ exercisesCompleted: exercisesCompleted + 1 });\n    }\n  },\n\n  goToNextExercise: () => {\n    const { session, currentExerciseIndex } = get();\n    if (!session) return;\n    const idx = currentExerciseIndex;\n    if (idx < session.exercises.length - 1) {\n      workoutSessionLocal.update(session.id, { currentExerciseIndex: idx + 1 });\n      set({\n        currentExerciseIndex: idx + 1,\n        currentExercise: session.exercises[idx + 1],\n      });\n    }\n  },\n\n  goToPrevExercise: () => {\n    const { session, currentExerciseIndex } = get();\n    if (!session) return;\n    const idx = currentExerciseIndex;\n    if (idx > 0) {\n      workoutSessionLocal.update(session.id, { currentExerciseIndex: idx - 1 });\n      set({\n        currentExerciseIndex: idx - 1,\n        currentExercise: session.exercises[idx - 1],\n      });\n    }\n  },\n\n  goToExercise: (targetIndex) => {\n    const { session } = get();\n    if (!session) return;\n    if (targetIndex >= 0 && targetIndex < session.exercises.length) {\n      workoutSessionLocal.update(session.id, { currentExerciseIndex: targetIndex });\n      set({\n        currentExerciseIndex: targetIndex,\n        currentExercise: session.exercises[targetIndex],\n      });\n    }\n  },\n\n  getExercisesCompleted: () => {\n    const { session } = get();\n    if (!session) return 0;\n\n    // only count exercises with at least one set\n    return session.exercises\n      .filter((exercise) => exercise.sets.length > 0)\n      .filter((exercise) => exercise.sets.every((set) => set.completed)).length;\n  },\n\n  getTotalExercises: () => {\n    const { session } = get();\n    if (!session) return 0;\n    return session.exercises.length;\n  },\n\n  getTotalVolume: () => {\n    const { session } = get();\n    if (!session) return 0;\n\n    let totalVolume = 0;\n\n    session.exercises.forEach((exercise) => {\n      exercise.sets.forEach((set) => {\n        // Vérifier si le set est complété et contient REPS et WEIGHT\n        if (set.completed && set.types.includes(\"REPS\") && set.types.includes(\"WEIGHT\") && set.valuesInt) {\n          const repsIndex = set.types.indexOf(\"REPS\");\n          const weightIndex = set.types.indexOf(\"WEIGHT\");\n\n          const reps = set.valuesInt[repsIndex] || 0;\n          const weight = set.valuesInt[weightIndex] || 0;\n\n          // Convertir les livres en kg si nécessaire\n          const weightInKg =\n            set.units && set.units[weightIndex] === \"lbs\"\n              ? weight * 0.453592 // 1 lb = 0.453592 kg\n              : weight;\n\n          totalVolume += reps * weightInKg;\n        }\n      });\n    });\n\n    return Math.round(totalVolume);\n  },\n\n  getTotalVolumeInUnit: (unit: WeightUnit) => {\n    const { session } = get();\n    if (!session) return 0;\n\n    let totalVolume = 0;\n\n    session.exercises.forEach((exercise) => {\n      exercise.sets.forEach((set) => {\n        // Vérifier si le set est complété et contient REPS et WEIGHT\n        if (set.completed && set.types.includes(\"REPS\") && set.types.includes(\"WEIGHT\") && set.valuesInt) {\n          const repsIndex = set.types.indexOf(\"REPS\");\n          const weightIndex = set.types.indexOf(\"WEIGHT\");\n\n          const reps = set.valuesInt[repsIndex] || 0;\n          const weight = set.valuesInt[weightIndex] || 0;\n\n          // Déterminer l'unité de poids originale de la série\n          const originalUnit: WeightUnit = set.units && set.units[weightIndex] === \"lbs\" ? \"lbs\" : \"kg\";\n\n          // Convertir vers l'unité demandée\n          const convertedWeight = convertWeight(weight, originalUnit, unit);\n\n          totalVolume += reps * convertedWeight;\n        }\n      });\n    });\n\n    return Math.round(totalVolume * 10) / 10; // Arrondir à 1 décimale\n  },\n\n  formatElapsedTime: () => {\n    const { elapsedTime } = get();\n    const hours = Math.floor(elapsedTime / 3600);\n    const minutes = Math.floor((elapsedTime % 3600) / 60);\n    const secs = elapsedTime % 60;\n    if (hours > 0) {\n      return `${hours.toString().padStart(2, \"0\")}:${minutes.toString().padStart(2, \"0\")}:${secs.toString().padStart(2, \"0\")}`;\n    }\n    return `${minutes.toString().padStart(2, \"0\")}:${secs.toString().padStart(2, \"0\")}`;\n  },\n\n  loadSessionFromLocal: () => {\n    const currentId = workoutSessionLocal.getCurrent();\n    if (currentId) {\n      const session = workoutSessionLocal.getById(currentId);\n      if (session && session.status === \"active\") {\n        set({\n          session,\n          isWorkoutActive: true,\n          currentExerciseIndex: session.currentExerciseIndex ?? 0,\n          currentExercise: session.exercises[session.currentExerciseIndex ?? 0],\n          elapsedTime: 0,\n          isTimerRunning: false,\n        });\n      }\n    }\n  },\n\n  addExerciseToSession: (exercise) => {\n    const { session } = get();\n\n    if (!session) {\n      return;\n    }\n\n    // Create new exercise with default sets\n    const newExercise: WorkoutSessionExercise = {\n      ...exercise,\n      order: session.exercises.length,\n      sets: [\n        {\n          id: `${exercise.id}-set-1`,\n          setIndex: 0,\n          types: [\"REPS\", \"WEIGHT\"],\n          valuesInt: [],\n          valuesSec: [],\n          units: [],\n          completed: false,\n        },\n      ],\n    };\n\n    // Check if exercise already exists to avoid duplicates\n    const exerciseExists = session.exercises.some((ex) => ex.id === exercise.id);\n    if (exerciseExists) {\n      console.log(\"🟡 [WORKOUT-SESSION] Exercise already exists in session, skipping add\");\n      return;\n    }\n\n    const updatedExercises = [...session.exercises, newExercise];\n    const updatedSession = { ...session, exercises: updatedExercises };\n\n    // Update local storage\n    workoutSessionLocal.update(session.id, { exercises: updatedExercises });\n\n    // Update state\n    set({ session: updatedSession });\n\n    console.log(\"🟡 [WORKOUT-SESSION] Exercise added successfully to session\");\n  },\n}));\n"
  },
  {
    "path": "src/features/workout-session/types/workout-set.ts",
    "content": "import { ExerciseWithAttributes } from \"@/entities/exercise/types/exercise.types\";\n\nexport type WorkoutSetType = \"TIME\" | \"WEIGHT\" | \"REPS\" | \"BODYWEIGHT\" | \"NA\";\nexport type WorkoutSetUnit = \"kg\" | \"lbs\";\n\nexport interface WorkoutSet {\n  id: string;\n  setIndex: number;\n  types: WorkoutSetType[]; // To support multiple columns\n  valuesInt?: number[]; // To support multiple columns\n  valuesSec?: number[]; // To support multiple columns\n  units?: WorkoutSetUnit[]; // Pour supporter plusieurs colonnes\n  completed: boolean;\n}\n\nexport interface WorkoutSessionExercise extends ExerciseWithAttributes {\n  id: string;\n  order: number;\n  sets: WorkoutSet[];\n}\n"
  },
  {
    "path": "src/features/workout-session/ui/donation-modal.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { Heart, X, Code, Server, Coffee, Github } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface DonationModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nexport function DonationModal({ isOpen, onClose }: DonationModalProps) {\n  const t = useI18n();\n  const modalRef = useRef<HTMLDialogElement>(null);\n\n  useEffect(() => {\n    const modal = modalRef.current;\n    if (!modal) return;\n\n    if (isOpen) {\n      modal.showModal();\n    } else {\n      modal.close();\n    }\n  }, [isOpen]);\n\n  useEffect(() => {\n    const modal = modalRef.current;\n    if (!modal) return;\n\n    const handleClose = () => {\n      onClose();\n    };\n\n    modal.addEventListener(\"close\", handleClose);\n    return () => modal.removeEventListener(\"close\", handleClose);\n  }, [onClose]);\n\n  const handleDonateKofi = () => {\n    window.open(\"https://ko-fi.com/workoutcool\", \"_blank\");\n    onClose();\n  };\n\n  const handleDonateGitHub = () => {\n    window.open(\"https://github.com/sponsors/snouzy\", \"_blank\");\n    onClose();\n  };\n\n  return (\n    <dialog className=\"modal modal-bottom sm:modal-middle\" ref={modalRef} style={{ padding: 0 }}>\n      <div className=\"modal-box max-w-lg flex flex-col max-h-[90vh]\">\n        {/* Header */}\n        <div className=\"flex items-center justify-between mb-4 flex-shrink-0\">\n          <div className=\"flex items-center gap-2\">\n            <Heart className=\"h-6 w-6 text-red-500\" />\n            <h3 className=\"font-bold text-lg text-slate-900 dark:text-slate-100\">{t(\"donation_modal.title\")}</h3>\n          </div>\n          <form method=\"dialog\">\n            <Button className=\"p-1\" size=\"small\" variant=\"ghost\">\n              <X className=\"h-4 w-4\" />\n            </Button>\n          </form>\n        </div>\n\n        {/* Content */}\n        <div className=\"space-y-4 mb-6 flex-1 overflow-y-auto\">\n          <div className=\"text-center\">\n            <p className=\"text-slate-600 dark:text-slate-400 leading-relaxed\">{t(\"donation_modal.congrats\")}</p>\n            <p className=\"text-slate-600 dark:text-slate-400 leading-relaxed mt-2\">{t(\"donation_modal.subtitle\")}</p>\n          </div>\n\n          {/* Transparency section */}\n          <div className=\"bg-gradient-to-r from-orange-50 to-red-50 dark:from-orange-900/20 dark:to-red-900/20 p-4 rounded-lg border border-orange-200 dark:border-orange-800\">\n            <div className=\"flex items-center gap-2 mb-3\">\n              <Server className=\"h-4 w-4 text-orange-600\" />\n              <span className=\"font-semibold text-sm text-slate-900 dark:text-slate-100\">{t(\"donation_modal.costs_title\")}</span>\n            </div>\n            <p className=\"text-sm text-slate-600 dark:text-slate-400 leading-relaxed mb-2\">{t(\"donation_modal.costs_description\")}</p>\n          </div>\n\n          {/* Open source value */}\n          <div className=\"bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800\">\n            <div className=\"flex items-center gap-2 mb-2\">\n              <Code className=\"h-4 w-4 text-blue-600\" />\n              <span className=\"font-semibold text-sm text-slate-900 dark:text-slate-100\">{t(\"donation_modal.open_source_title\")}</span>\n            </div>\n            <p className=\"text-sm text-slate-600 dark:text-slate-400 leading-relaxed mb-2\">{t(\"donation_modal.open_source_description\")}</p>\n            <div className=\"grid grid-cols-2 gap-2 text-xs \">\n              <div className=\"flex items-center justify-center gap-1 text-blue-700 dark:text-blue-400\">\n                <Heart className=\"h-3 w-3\" />\n                <span>{t(\"donation_modal.no_ads\")}</span>\n              </div>\n              <div className=\"flex items-center justify-center gap-1 text-blue-700 dark:text-blue-400\">\n                <Heart className=\"h-3 w-3\" />\n                <span>{t(\"donation_modal.no_tracking\")}</span>\n              </div>\n            </div>\n          </div>\n\n          {/* Impact section */}\n          <div className=\"bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 p-4 rounded-lg border border-green-200 dark:border-green-800\">\n            <div className=\"flex items-center gap-2 mb-2\">\n              <Heart className=\"h-4 w-4 text-green-600\" />\n              <span className=\"font-semibold text-sm text-slate-900 dark:text-slate-100\">{t(\"donation_modal.impact_title\")}</span>\n            </div>\n            <ul className=\"text-sm text-slate-600 dark:text-slate-400 space-y-1\">\n              <li>{t(\"donation_modal.impact_3_euros\")}</li>\n\n              <li>{t(\"donation_modal.impact_support\")}</li>\n            </ul>\n            <p className=\"text-xs text-center text-green-700 dark:text-green-400 mt-2 font-medium\">{t(\"donation_modal.impact_footer\")}</p>\n          </div>\n\n          <iframe\n            height=\"700\"\n            id=\"kofiframe\"\n            src=\"https://ko-fi.com/workoutcool/?hidefeed=true&widget=true&embed=true&preview=true\"\n            style={{ border: \"none\", width: \"100%\", padding: \"4px\" }}\n            title=\"workoutcool\"\n          ></iframe>\n        </div>\n\n        {/* Actions */}\n        <div className=\"modal-action flex-shrink-0 mt-auto\">\n          <form className=\"flex gap-2 w-full flex-col\" method=\"dialog\">\n            <p className=\"flex gap-2 flex-col sm:flex-row text-sm text-slate-600 dark:text-slate-400\">{t(\"donation_modal.support_via\")}</p>\n            <div className=\"flex gap-2 flex-col sm:flex-row\">\n              <Button\n                className=\"flex-1 bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 text-white border-0\"\n                onClick={handleDonateKofi}\n                size=\"large\"\n              >\n                <Coffee className=\"h-4 w-4 mr-2\" />\n                Ko-fi\n              </Button>\n              <Button\n                className=\"flex-1 bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white border-0\"\n                onClick={handleDonateGitHub}\n                size=\"large\"\n              >\n                <Github className=\"h-4 w-4 mr-2\" />\n                GitHub Sponsors\n              </Button>\n            </div>\n            <Button className=\"w-full\" onClick={onClose} size=\"small\" variant=\"outline\">\n              {t(\"donation_modal.later_button\")}\n            </Button>\n          </form>\n        </div>\n      </div>\n    </dialog>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-session/ui/workout-session-header.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { X, Target, Weight } from \"lucide-react\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport { type WeightUnit } from \"@/shared/lib/weight-conversion\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { useWorkoutSession } from \"@/features/workout-session/model/use-workout-session\";\nimport { Button } from \"@/components/ui/button\";\n\nimport { QuitWorkoutDialog } from \"../../workout-builder/ui/quit-workout-dialog\";\n\ninterface WorkoutSessionHeaderProps {\n  onQuitWorkout: VoidFunction;\n}\n\nexport function WorkoutSessionHeader({ onQuitWorkout }: WorkoutSessionHeaderProps) {\n  const t = useI18n();\n  const [showQuitDialog, setShowQuitDialog] = useState(false);\n  const [volumeUnit, setVolumeUnit] = useState<WeightUnit>(\"kg\");\n  const locale = useCurrentLocale();\n  const { getExercisesCompleted, getTotalExercises, session, getTotalVolumeInUnit } = useWorkoutSession();\n  const exercisesCompleted = getExercisesCompleted();\n  const totalExercises = getTotalExercises();\n  const totalVolume = getTotalVolumeInUnit(volumeUnit);\n\n  // Format time with animated colons\n  const formatTimeWithAnimatedColons = (date: Date) => {\n    const timeString = date.toLocaleTimeString(locale, { hour: \"2-digit\", minute: \"2-digit\" });\n    const parts = timeString.split(\":\");\n\n    if (parts.length === 2) {\n      return (\n        <>\n          {parts[0]}\n          <span className=\"animate-colon-blink\">:</span>\n          {parts[1]}\n        </>\n      );\n    }\n    return timeString;\n  };\n\n  // Load volume unit preference from localStorage\n  useEffect(() => {\n    const savedUnit = localStorage.getItem(\"volumeUnit\") as WeightUnit;\n    if (savedUnit === \"kg\" || savedUnit === \"lbs\") {\n      setVolumeUnit(savedUnit);\n    }\n  }, []);\n\n  // Save volume unit preference to localStorage\n  const handleVolumeUnitChange = (unit: WeightUnit) => {\n    setVolumeUnit(unit);\n    localStorage.setItem(\"volumeUnit\", unit);\n  };\n\n  const handleQuitClick = () => {\n    setShowQuitDialog(true);\n  };\n\n  const handleQuitWithoutSave = () => {\n    onQuitWorkout();\n    setShowQuitDialog(false);\n  };\n\n  return (\n    <>\n      <div className=\"w-full mt-2 mb-6 px-2 sm:px-6\">\n        <div className=\"rounded-lg p-2 sm:p-3 bg-slate-50 dark:bg-slate-900/80 border border-slate-200 dark:border-slate-700\">\n          <div className=\"flex items-center justify-between mb-2 sm:mb-3\">\n            <span className=\"text-emerald-400 font-medium text-xs tracking-wide\">\n              {t(\"workout_builder.session.started_at\")} {formatTimeWithAnimatedColons(new Date(session?.startedAt || \"\"))}\n            </span>\n\n            <Button\n              className=\"border-red-500/30 text-red-400 hover:bg-red-500/10 hover:border-red-500 px-2 py-1 text-xs dark:border-red-700/40 dark:text-red-300 dark:hover:bg-red-700/10\"\n              onClick={handleQuitClick}\n              variant=\"outline\"\n            >\n              <X className=\"h-3 w-3 mr-1\" />\n              {t(\"workout_builder.session.quit_workout\")}\n            </Button>\n          </div>\n\n          <div className=\"grid grid-cols-2 gap-2\">\n            {/* Card 1: Exercise Progress */}\n            <div className=\"bg-white dark:bg-slate-800 rounded-md p-2 border border-slate-200 dark:border-slate-700\">\n              <div className=\"flex items-center gap-2 mb-2\">\n                <div className=\"w-5 h-5 rounded-full bg-purple-500/20 flex items-center justify-center shrink-0\">\n                  <Target className=\"h-3 w-3 text-purple-400\" />\n                </div>\n                <h3 className=\"text-slate-700 dark:text-white font-medium text-xs truncate\">\n                  {t(\"workout_builder.session.exercise_progress\")}\n                </h3>\n              </div>\n\n              <div className=\"space-y-1\">\n                <div className=\"flex items-center justify-between\">\n                  <span className=\"text-lg font-bold text-slate-900 dark:text-white\">{exercisesCompleted}</span>\n                  <span className=\"text-slate-400 text-sm\">/ {totalExercises}</span>\n                </div>\n\n                <div className=\"w-full bg-slate-200 dark:bg-slate-700 rounded-full h-1.5 overflow-hidden\">\n                  <div\n                    className=\"h-full bg-gradient-to-r from-purple-500 to-pink-500 transition-all duration-500 ease-out\"\n                    style={{ width: `${(exercisesCompleted / totalExercises) * 100}%` }}\n                  />\n                </div>\n\n                <div className=\"text-center\">\n                  <span className=\"text-xs text-slate-400\">\n                    {Math.round((exercisesCompleted / totalExercises) * 100)}% {t(\"workout_builder.session.complete\")}\n                  </span>\n                </div>\n              </div>\n            </div>\n\n            {/* Card 2: Total Volume */}\n            <div className=\"bg-white dark:bg-slate-800 rounded-md p-2 border border-slate-200 dark:border-slate-700\">\n              <div className=\"flex items-center gap-2 mb-2\">\n                <div className=\"w-5 h-5 rounded-full bg-orange-500/20 flex items-center justify-center shrink-0\">\n                  <Weight className=\"h-3 w-3 text-orange-400\" />\n                </div>\n                <h3 className=\"text-slate-700 dark:text-white font-medium text-xs truncate\">{t(\"workout_builder.session.total_volume\")}</h3>\n              </div>\n\n              <div className=\"text-center\">\n                <div className=\"text-lg font-bold text-slate-900 dark:text-white mb-1\">\n                  {totalVolume.toFixed(volumeUnit === \"lbs\" ? 1 : 0)}\n                </div>\n                <div className=\"flex items-center justify-center gap-1\">\n                  <button\n                    className={cn(\n                      \"text-xs px-1.5 py-0.5 rounded transition-colors\",\n                      volumeUnit === \"kg\"\n                        ? \"bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-100\"\n                        : \"text-slate-400 hover:text-slate-600 dark:hover:text-slate-300\",\n                    )}\n                    onClick={() => handleVolumeUnitChange(\"kg\")}\n                  >\n                    kg\n                  </button>\n                  <span className=\"text-slate-300 dark:text-slate-600\">|</span>\n                  <button\n                    className={cn(\n                      \"text-xs px-1.5 py-0.5 rounded transition-colors\",\n                      volumeUnit === \"lbs\"\n                        ? \"bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-100\"\n                        : \"text-slate-400 hover:text-slate-600 dark:hover:text-slate-300\",\n                    )}\n                    onClick={() => handleVolumeUnitChange(\"lbs\")}\n                  >\n                    lbs\n                  </button>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <QuitWorkoutDialog\n        exercisesCompleted={exercisesCompleted}\n        isOpen={showQuitDialog}\n        onClose={() => setShowQuitDialog(false)}\n        onQuitWithoutSave={handleQuitWithoutSave}\n        totalExercises={totalExercises}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-session/ui/workout-session-heatmap.tsx",
    "content": "import React, { useRef, useEffect, useState } from \"react\";\nimport dayjs from \"dayjs\";\n\nimport { useI18n, useCurrentLocale } from \"locales/client\";\n\ninterface Props {\n  panelColors?: string[];\n  values: { [date: string]: number };\n  until: string;\n}\n\nconst DEFAULT_PANEL_COLORS = [\n  \"var(--color-base-300)\", // 0: empty\n  \"var(--color-success)\", // 1: low activity\n  \"var(--color-success-content)\", // 2: medium activity\n  \"var(--color-success-content)\", // 3: high activity\n  \"var(--color-success-content)\", // 4: max activity\n];\n\nconst PANEL_SIZE = 18;\nconst PANEL_MARGIN = 2;\nconst WEEK_LABEL_WIDTH = 18;\nconst MONTH_LABEL_HEIGHT = 18;\nconst MIN_COLUMNS = 10;\nconst MAX_COLUMNS = 53;\n\nexport const WorkoutSessionHeatmap: React.FC<Props> = ({ panelColors = DEFAULT_PANEL_COLORS, values, until }) => {\n  const t = useI18n();\n  const currentLocale = useCurrentLocale();\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [columns, setColumns] = useState(MAX_COLUMNS);\n  const [hovered, setHovered] = useState<null | {\n    i: number;\n    j: number;\n    tooltip: React.ReactNode;\n    mouseX: number;\n    mouseY: number;\n  }>(null);\n\n  // Create a locale-specific date formatter for tooltips\n  const formatDate = (date: dayjs.Dayjs) => {\n    return new Intl.DateTimeFormat(currentLocale, {\n      year: \"numeric\",\n      month: \"short\",\n      day: \"numeric\",\n    }).format(date.toDate());\n  };\n\n  // Use ISO format for internal date calculations and storage\n  const ISO_DATE_FORMAT = \"YYYY-MM-DD\";\n\n  // Use localized translations for week and month names\n  const weekNames = [\n    t(\"heatmap.week_days_short.sunday\"),\n    t(\"heatmap.week_days_short.monday\"),\n    t(\"heatmap.week_days_short.tuesday\"),\n    t(\"heatmap.week_days_short.wednesday\"),\n    t(\"heatmap.week_days_short.thursday\"),\n    t(\"heatmap.week_days_short.friday\"),\n    t(\"heatmap.week_days_short.saturday\"),\n  ];\n  const monthNames = [\n    t(\"heatmap.month_names_short.january\"),\n    t(\"heatmap.month_names_short.february\"),\n    t(\"heatmap.month_names_short.march\"),\n    t(\"heatmap.month_names_short.april\"),\n    t(\"heatmap.month_names_short.may\"),\n    t(\"heatmap.month_names_short.june\"),\n    t(\"heatmap.month_names_short.july\"),\n    t(\"heatmap.month_names_short.august\"),\n    t(\"heatmap.month_names_short.september\"),\n    t(\"heatmap.month_names_short.october\"),\n    t(\"heatmap.month_names_short.november\"),\n    t(\"heatmap.month_names_short.december\"),\n  ];\n\n  //   responsive: adapt the number of columns to the width\n  useEffect(() => {\n    function updateColumns() {\n      if (!containerRef.current) return;\n      const width = containerRef.current.offsetWidth;\n      const available = Math.floor((width - WEEK_LABEL_WIDTH) / (PANEL_SIZE + PANEL_MARGIN));\n      setColumns(Math.max(MIN_COLUMNS, Math.min(MAX_COLUMNS, available)));\n    }\n    updateColumns();\n    const observer = new window.ResizeObserver(updateColumns);\n    if (containerRef.current) observer.observe(containerRef.current);\n    return () => observer.disconnect();\n  }, []);\n\n  //   matrix of contributions\n  function makeCalendarData(history: { [k: string]: number }, lastDay: string, columns: number) {\n    const d = dayjs(lastDay, ISO_DATE_FORMAT);\n    const lastWeekend = d.endOf(\"week\");\n    const endDate = d.endOf(\"day\");\n    const result: ({ value: number; month: number } | null)[][] = [];\n    for (let i = 0; i < columns; i++) {\n      result[i] = [];\n      for (let j = 0; j < 7; j++) {\n        const date = lastWeekend.subtract((columns - i - 1) * 7 + (6 - j), \"day\");\n        if (date <= endDate) {\n          result[i][j] = {\n            value: history[date.format(ISO_DATE_FORMAT)] || 0,\n            month: date.month(),\n          };\n        } else {\n          result[i][j] = null;\n        }\n      }\n    }\n    return result;\n  }\n\n  const contributions = makeCalendarData(values, until, columns);\n  const innerDom: React.ReactElement[] = [];\n\n  for (let i = 0; i < columns; i++) {\n    for (let j = 0; j < 7; j++) {\n      const contribution = contributions[i][j];\n      if (contribution === null) continue;\n      const x = WEEK_LABEL_WIDTH + (PANEL_SIZE + PANEL_MARGIN) * i;\n      const y = MONTH_LABEL_HEIGHT + (PANEL_SIZE + PANEL_MARGIN) * j;\n      const numOfColors = panelColors.length;\n      const color = contribution.value >= numOfColors ? panelColors[numOfColors - 1] : panelColors[contribution.value];\n      const d = dayjs(until, ISO_DATE_FORMAT)\n        .endOf(\"week\")\n        .subtract((columns - i - 1) * 7 + (6 - j), \"day\");\n      const dateStr = formatDate(d);\n\n      // Create tooltip content based on workout count\n      const createTooltip = (workoutCount: number, date: string) => (\n        <div className=\"text-xs text-slate-50\">\n          {date} : <br />\n          {workoutCount} {t(\"heatmap.workout\", { count: workoutCount })}\n        </div>\n      );\n\n      const tooltip = createTooltip(contribution.value, dateStr);\n      innerDom.push(\n        <rect\n          fill={color}\n          height={PANEL_SIZE}\n          key={`panel_${i}_${j}`}\n          onMouseEnter={(e) => setHovered({ i, j, tooltip, mouseX: e.clientX, mouseY: e.clientY })}\n          onMouseLeave={() => setHovered(null)}\n          onMouseMove={(e) => setHovered((prev) => prev && { ...prev, mouseX: e.clientX, mouseY: e.clientY })}\n          rx={3}\n          style={{\n            cursor: \"pointer\",\n            stroke: hovered && hovered.i === i && hovered.j === j ? \"#059669\" : \"transparent\",\n            strokeWidth: hovered && hovered.i === i && hovered.j === j ? 2 : 0,\n            opacity: hovered && hovered.i === i && hovered.j === j ? 0.85 : 1,\n            transition: \"stroke 0.1s, opacity 0.1s\",\n          }}\n          width={PANEL_SIZE}\n          x={x}\n          y={y}\n        />,\n      );\n    }\n  }\n\n  for (let i = 0; i < weekNames.length; i++) {\n    const x = WEEK_LABEL_WIDTH / 2;\n    const y = MONTH_LABEL_HEIGHT + (PANEL_SIZE + PANEL_MARGIN) * i + PANEL_SIZE / 2;\n    innerDom.push(\n      <text\n        alignmentBaseline=\"central\"\n        fill=\"var(--color-base-content)\"\n        fontSize={10}\n        key={`week_label_${i}`}\n        textAnchor=\"middle\"\n        x={x}\n        y={y}\n      >\n        {weekNames[i]}\n      </text>,\n    );\n  }\n\n  let prevMonth = -1;\n  for (let i = 0; i < columns; i++) {\n    const c = contributions[i][0];\n    if (c === null) continue;\n    if (columns > 1 && i === 0 && c.month !== contributions[i + 1][0]?.month) {\n      continue;\n    }\n    if (c.month !== prevMonth) {\n      const x = WEEK_LABEL_WIDTH + (PANEL_SIZE + PANEL_MARGIN) * i + PANEL_SIZE / 2;\n      const y = MONTH_LABEL_HEIGHT / 1.5;\n      innerDom.push(\n        <text\n          alignmentBaseline=\"central\"\n          fill=\"var(--color-base-content)\"\n          fontSize={12}\n          key={`month_label_${i}`}\n          textAnchor=\"middle\"\n          x={x}\n          y={y}\n        >\n          {monthNames[c.month]}\n        </text>,\n      );\n    }\n    prevMonth = c.month;\n  }\n\n  const tooltipNode = hovered ? (\n    <div\n      style={{\n        position: \"fixed\",\n        left: hovered.mouseX - 100,\n        top: hovered.mouseY - 8,\n        pointerEvents: \"none\",\n        zIndex: 9999,\n        background: \"rgba(33,33,33,0.97)\",\n        color: \"#fff\",\n        padding: \"6px 12px\",\n        borderRadius: 6,\n        fontSize: 13,\n        boxShadow: \"0 2px 8px rgba(0,0,0,0.15)\",\n        whiteSpace: \"nowrap\",\n        maxWidth: 220,\n        border: \"1px solid var(--color-base-300)\",\n      }}\n    >\n      {hovered.tooltip}\n    </div>\n  ) : null;\n\n  return (\n    <div ref={containerRef} style={{ width: \"100%\", position: \"relative\" }}>\n      <svg\n        height={MONTH_LABEL_HEIGHT + (PANEL_SIZE + PANEL_MARGIN) * 7}\n        style={{ fontFamily: \"Helvetica, Arial, sans-serif\", width: \"100%\", display: \"block\" }}\n      >\n        {innerDom}\n      </svg>\n      {tooltipNode}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/features/workout-session/ui/workout-session-list.tsx",
    "content": "import { useRouter } from \"next/navigation\";\nimport { Play, Repeat2, Trash2 } from \"lucide-react\";\nimport { ExerciseAttributeNameEnum } from \"@prisma/client\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport { useWorkoutSessionService } from \"@/shared/lib/workout-session/use-workout-session.service\";\nimport { useWorkoutSessions } from \"@/features/workout-session/model/use-workout-sessions\";\nimport { useWorkoutBuilderStore } from \"@/features/workout-builder/model/workout-builder.store\";\nimport { getExerciseAttributesValueOf, getPrimaryMuscle, getAttributeValue } from \"@/entities/exercise/shared/muscles\";\nimport { Link } from \"@/components/ui/link\";\nimport { Button } from \"@/components/ui/button\";\n\nconst BADGE_COLORS = [\n  \"bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-900 dark:text-blue-100 dark:border-blue-800\",\n  \"bg-green-100 text-green-700 border-green-300 dark:bg-green-900 dark:text-green-100 dark:border-green-800\",\n  \"bg-red-100 text-red-700 border-red-300 dark:bg-red-900 dark:text-red-300 dark:border-red-700\",\n  \"bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-900 dark:text-purple-100 dark:border-purple-800\",\n  \"bg-orange-100 text-orange-700 border-orange-300 dark:bg-orange-900 dark:text-orange-100 dark:border-orange-800\",\n  \"bg-pink-100 text-pink-700 border-pink-300 dark:bg-pink-900 dark:text-pink-100 dark:border-pink-800\",\n];\n\nexport function WorkoutSessionList() {\n  const locale = useCurrentLocale();\n  const t = useI18n();\n  const router = useRouter();\n  const loadFromSession = useWorkoutBuilderStore((s) => s.loadFromSession);\n  const { remove } = useWorkoutSessionService();\n\n  const { data: sessions = [], refetch } = useWorkoutSessions();\n  const activeSession = sessions.find((s) => s.status === \"active\");\n\n  const handleDelete = async (id: string) => {\n    const confirmed = window.confirm(t(\"workout_builder.confirm_delete\"));\n\n    if (!confirmed) return;\n\n    try {\n      await remove(id);\n      refetch();\n    } catch (error) {\n      console.error(\"Error deleting session:\", error);\n      alert(\"Error deleting session\");\n    }\n  };\n\n  const handleRepeat = (id: string) => {\n    const sessionToCopy = sessions.find((s) => s.id === id);\n    if (!sessionToCopy) return;\n\n    const allEquipment = Array.from(\n      new Set(\n        sessionToCopy.exercises.flatMap((ex) => getExerciseAttributesValueOf(ex, ExerciseAttributeNameEnum.EQUIPMENT)).filter(Boolean),\n      ),\n    );\n\n    // Utilise les muscles stockés dans la session, sinon fallback sur les muscles primaires des exercices\n    const allMuscles =\n      sessionToCopy.muscles && sessionToCopy.muscles.length > 0\n        ? sessionToCopy.muscles\n        : Array.from(\n            new Set(\n              sessionToCopy.exercises\n                .flatMap((ex) => getExerciseAttributesValueOf(ex, ExerciseAttributeNameEnum.PRIMARY_MUSCLE))\n                .filter(Boolean),\n            ),\n          );\n\n    const exercisesByMuscle = allMuscles.map((muscle) => ({\n      muscle,\n      exercises: sessionToCopy.exercises\n        .filter((ex) => {\n          const primaryMuscle = getPrimaryMuscle(ex.attributes || []);\n          return primaryMuscle && getAttributeValue(primaryMuscle) === muscle;\n        })\n        .map((ex) => ({\n          ...ex,\n          id: ex.id,\n          workoutSessionId: sessionToCopy.id,\n          exerciseId: ex.id,\n          order: ex.order,\n        })),\n    }));\n\n    const exercisesOrder = sessionToCopy.exercises.map((ex) => ex.id);\n\n    // 5. inject in the builder and go step 3\n    loadFromSession({\n      equipment: allEquipment,\n      muscles: allMuscles,\n      exercisesByMuscle,\n      exercisesOrder,\n    });\n    router.push(\"/?fromSession=1\");\n  };\n\n  return (\n    <div className=\"space-y-4 mt-10\">\n      <h2 className=\"text-xl font-bold mt-5 mb-2 text-slate-900 dark:text-slate-200\">\n        {t(\"workout_builder.session.history\", { count: sessions.length })}\n      </h2>\n      {sessions.length === 0 && <div className=\"text-slate-500 dark:text-slate-400\">{t(\"workout_builder.session.no_workout_yet\")}</div>}\n      <ul className=\"divide-y divide-slate-200 dark:divide-slate-700/50\">\n        {sessions.map((session) => {\n          const isActive = session.status === \"active\";\n          return (\n            <li\n              className=\"px-2 flex flex-col sm:flex-row items-start sm:items-center justify-between py-4 gap-2 sm:gap-0 hover:bg-slate-50 dark:hover:bg-slate-800/70 rounded-lg space-x-4\"\n              key={session.id}\n            >\n              <div className=\"flex items-center flex-col\">\n                <span className=\"font-bold text-base tabular-nums text-slate-900 dark:text-slate-200\">\n                  {new Date(session.startedAt).toLocaleDateString(locale)}\n                </span>\n                <span className=\"text-xs text-slate-700 dark:text-slate-300 tabular-nums\">\n                  {t(\"workout_builder.session.start\") || \"start\"}\n                  {\" : \"}\n                  {new Date(session.startedAt).toLocaleTimeString(locale, { hour: \"2-digit\", minute: \"2-digit\" })}\n                </span>\n                {session.endedAt && (\n                  <span className=\"text-xs text-slate-500 dark:text-slate-400 tabular-nums\">\n                    {t(\"workout_builder.session.end\") || \"end\"}\n                    {\" : \"}\n                    {new Date(session.endedAt).toLocaleTimeString(locale, { hour: \"2-digit\", minute: \"2-digit\" })}\n                  </span>\n                )}\n                {session.muscles && session.muscles.length > 0 && (\n                  <div className=\"flex flex-wrap gap-1 mt-1 justify-center\">\n                    {session.muscles.map((muscle, idx) => (\n                      <span\n                        className={`inline-block border rounded-full px-2 py-0.5 text-xs font-semibold ${BADGE_COLORS[idx % BADGE_COLORS.length]}`}\n                        key={muscle}\n                      >\n                        {t((\"workout_builder.muscles.\" + muscle.toLowerCase()) as keyof typeof t)}\n                      </span>\n                    ))}\n                  </div>\n                )}\n                {session.status === \"active\" && (\n                  <div className=\"relative mt-1\">\n                    <span className=\"px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 border border-emerald-300 text-xs font-semibold\">\n                      {t(\"commons.in_progress\")}\n                    </span>\n                    <span className=\"absolute top-0 right-0 w-2 h-2 bg-emerald-500 rounded-full animate-ping\"></span>\n                  </div>\n                )}\n              </div>\n              <div className=\"flex flex-wrap gap-2 flex-1\">\n                {session.exercises?.map((ex, idx) => {\n                  const exerciseName = locale === \"fr\" ? ex.name : ex.nameEn;\n                  return (\n                    <span\n                      className={`inline-block border rounded-full px-1 text-xs font-semibold ${BADGE_COLORS[idx % BADGE_COLORS.length]}`}\n                      key={ex.id}\n                    >\n                      {exerciseName?.toUpperCase() || t(\"workout_builder.session.exercise\")}\n                    </span>\n                  );\n                })}\n              </div>\n              <div className=\"flex gap-2 items-center mt-2 sm:mt-0\">\n                {isActive && (\n                  <Link className=\"w-auto flex items-center gap-2 flex-col\" href=\"/\" variant=\"nav\">\n                    <Play className=\"w-7 h-7 text-blue-500 dark:text-blue-400\" />\n                    <span className=\"sr-only\">{t(\"workout_builder.session.back_to_workout\")}</span>\n                    <span>{t(\"workout_builder.session.back_to_workout\")}</span>\n                  </Link>\n                )}\n                {!isActive && (\n                  <div\n                    className=\"tooltip tooltip-left\"\n                    data-tip={\n                      activeSession ? t(\"workout_builder.session.already_have_a_active_session\") : t(\"workout_builder.session.repeat\")\n                    }\n                  >\n                    <Button\n                      aria-label={t(\"workout_builder.session.repeat\")}\n                      className=\"w-12 h-12\"\n                      disabled={!!activeSession}\n                      onClick={() => handleRepeat(session.id)}\n                      size=\"icon\"\n                      variant=\"ghost\"\n                    >\n                      <Repeat2 className=\"w-7 h-7 text-blue-500 dark:text-blue-400\" />\n                    </Button>\n                  </div>\n                )}\n\n                {!isActive && (\n                  <div className=\"tooltip\" data-tip={t(\"workout_builder.session.delete\")}>\n                    <Button\n                      aria-label={t(\"workout_builder.session.delete\")}\n                      onClick={() => handleDelete(session.id)}\n                      size=\"icon\"\n                      variant=\"ghost\"\n                    >\n                      <Trash2 className=\"w-7 h-7 text-red-500 dark:text-red-400\" />\n                    </Button>\n                  </div>\n                )}\n              </div>\n            </li>\n          );\n        })}\n      </ul>\n      {/* TODO: create a new workout */}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-session/ui/workout-session-set.tsx",
    "content": "import { Plus, Minus, Trash2 } from \"lucide-react\";\n\nimport { useI18n } from \"locales/client\";\nimport { AVAILABLE_WORKOUT_SET_TYPES, MAX_WORKOUT_SET_COLUMNS } from \"@/shared/constants/workout-set-types\";\nimport { WorkoutSet, WorkoutSetType, WorkoutSetUnit } from \"@/features/workout-session/types/workout-set\";\nimport { getWorkoutSetTypeLabels } from \"@/features/workout-session/lib/workout-set-labels\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface WorkoutSetRowProps {\n  set: WorkoutSet;\n  setIndex: number;\n  onChange: (setIndex: number, data: Partial<WorkoutSet>) => void;\n  onFinish: () => void;\n  onRemove: () => void;\n}\n\nexport function WorkoutSessionSet({ set, setIndex, onChange, onFinish, onRemove }: WorkoutSetRowProps) {\n  const t = useI18n();\n  const types = set.types || [];\n  const typeLabels = getWorkoutSetTypeLabels(t);\n\n  const handleTypeChange = (columnIndex: number) => (e: React.ChangeEvent<HTMLSelectElement>) => {\n    const newTypes = [...types];\n    newTypes[columnIndex] = e.target.value as WorkoutSetType;\n    onChange(setIndex, { types: newTypes });\n  };\n\n  const handleValueIntChange = (columnIndex: number) => (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newValuesInt = Array.isArray(set.valuesInt) ? [...set.valuesInt] : [];\n    newValuesInt[columnIndex] = e.target.value ? parseInt(e.target.value, 10) : 0;\n    onChange(setIndex, { valuesInt: newValuesInt });\n  };\n\n  const handleValueSecChange = (columnIndex: number) => (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newValuesSec = Array.isArray(set.valuesSec) ? [...set.valuesSec] : [];\n    newValuesSec[columnIndex] = e.target.value ? parseInt(e.target.value, 10) : 0;\n    onChange(setIndex, { valuesSec: newValuesSec });\n  };\n\n  const handleUnitChange = (columnIndex: number) => (e: React.ChangeEvent<HTMLSelectElement>) => {\n    const newUnits = Array.isArray(set.units) ? [...set.units] : [];\n    newUnits[columnIndex] = e.target.value as WorkoutSetUnit;\n    onChange(setIndex, { units: newUnits });\n  };\n\n  const addColumn = () => {\n    if (types.length < MAX_WORKOUT_SET_COLUMNS) {\n      const firstAvailableType = AVAILABLE_WORKOUT_SET_TYPES.find((t) => !types.includes(t));\n      if (firstAvailableType) {\n        const newTypes = [...types, firstAvailableType];\n        onChange(setIndex, { types: newTypes });\n      }\n    }\n  };\n\n  const removeColumn = (columnIndex: number) => {\n    const newTypes = types.filter((_, idx) => idx !== columnIndex);\n    const newValuesInt = Array.isArray(set.valuesInt) ? set.valuesInt.filter((_, idx) => idx !== columnIndex) : [];\n    const newValuesSec = Array.isArray(set.valuesSec) ? set.valuesSec.filter((_, idx) => idx !== columnIndex) : [];\n    const newUnits = Array.isArray(set.units) ? set.units.filter((_, idx) => idx !== columnIndex) : [];\n\n    onChange(setIndex, {\n      types: newTypes,\n      valuesInt: newValuesInt,\n      valuesSec: newValuesSec,\n      units: newUnits,\n    });\n  };\n\n  const handleEdit = () => {\n    onChange(setIndex, { completed: false });\n  };\n\n  const renderInputForType = (type: WorkoutSetType, columnIndex: number) => {\n    const valuesInt = set.valuesInt || [];\n    const valuesSec = set.valuesSec || [];\n    const units = set.units || [];\n\n    switch (type) {\n      case \"TIME\":\n        return (\n          <div className=\"flex gap-1 w-full\">\n            <input\n              className=\"border border-black rounded px-1 py-2 w-1/2 text-base text-center font-bold dark:bg-slate-800 dark:placeholder:text-slate-500\"\n              disabled={set.completed}\n              min={0}\n              onChange={handleValueIntChange(columnIndex)}\n              pattern=\"[0-9]*\"\n              placeholder=\"min\"\n              type=\"number\"\n              value={valuesInt[columnIndex] ?? \"\"}\n            />\n            <input\n              className=\"border border-black rounded px-1 py-2 w-1/2 text-base text-center font-bold dark:bg-slate-800 dark:placeholder:text-slate-500\"\n              disabled={set.completed}\n              max={59}\n              min={0}\n              onChange={handleValueSecChange(columnIndex)}\n              pattern=\"[0-9]*\"\n              placeholder=\"sec\"\n              type=\"number\"\n              value={valuesSec[columnIndex] ?? \"\"}\n            />\n          </div>\n        );\n      case \"WEIGHT\":\n        return (\n          <div className=\"flex gap-1 w-full items-center\">\n            <input\n              className=\"border border-black rounded px-1 py-2 w-1/2 text-base text-center font-bold dark:bg-slate-800\"\n              disabled={set.completed}\n              min={0}\n              onChange={handleValueIntChange(columnIndex)}\n              pattern=\"[0-9]*\"\n              placeholder=\"\"\n              type=\"number\"\n              value={valuesInt[columnIndex] ?? \"\"}\n            />\n            <select\n              className=\"border border-black rounded px-1 py-2 w-1/2 text-base font-bold bg-white dark:bg-slate-800 dark:text-gray-200 h-10 \"\n              disabled={set.completed}\n              onChange={handleUnitChange(columnIndex)}\n              value={units[columnIndex] ?? \"kg\"}\n            >\n              <option value=\"kg\">kg</option>\n              <option value=\"lbs\">lbs</option>\n            </select>\n          </div>\n        );\n      case \"REPS\":\n        return (\n          <input\n            className=\"border border-black rounded px-1 py-2 w-full text-base text-center font-bold dark:bg-slate-800\"\n            disabled={set.completed}\n            min={0}\n            onChange={handleValueIntChange(columnIndex)}\n            pattern=\"[0-9]*\"\n            placeholder=\"\"\n            type=\"number\"\n            value={valuesInt[columnIndex] ?? \"\"}\n          />\n        );\n      case \"BODYWEIGHT\":\n        return (\n          <input\n            className=\"border border-black rounded px-1 py-2 w-full text-base text-center font-bold dark:bg-slate-800\"\n            disabled={set.completed}\n            placeholder=\"\"\n            readOnly\n            value=\"✔\"\n          />\n        );\n      default:\n        return null;\n    }\n  };\n\n  return (\n    <div className=\"w-full py-4 flex flex-col gap-2 bg-slate-50 dark:bg-slate-900/80 border border-slate-200 dark:border-slate-700/50 rounded-xl shadow-sm mb-3 relative px-2 sm:px-4\">\n      <div className=\"flex items-center justify-between mb-2\">\n        <div className=\"bg-blue-500 text-white text-xs font-bold px-3 py-1 rounded-full shadow dark:bg-blue-900 dark:text-blue-300\">\n          SET {setIndex + 1}\n        </div>\n        <Button\n          aria-label=\"Supprimer la série\"\n          className=\"bg-red-100 hover:bg-red-200 dark:bg-red-900/30 dark:hover:bg-red-900/60 text-red-600 dark:text-red-300 rounded-full p-1 h-8 w-8 flex items-center justify-center shadow transition\"\n          disabled={set.completed}\n          onClick={onRemove}\n          type=\"button\"\n        >\n          <Trash2 className=\"h-4 w-4\" />\n        </Button>\n      </div>\n\n      {/* Columns of types, stack vertical on mobile, horizontal on md+ */}\n      <div className=\"flex flex-col md:flex-row gap-6 md:gap-2 w-full\">\n        {types.map((type, columnIndex) => {\n          // An option is available if it's not used by another column, OR it's the current column's type.\n          const availableTypes = AVAILABLE_WORKOUT_SET_TYPES.filter((option) => !types.includes(option) || option === type);\n\n          return (\n            <div className=\"flex flex-col w-full md:w-auto\" key={columnIndex}>\n              <div className=\"flex items-center w-full gap-1 mb-1\">\n                <select\n                  className=\"border border-black dark:border-slate-700 rounded font-bold px-1 py-2 text-base w-full bg-white dark:bg-slate-800 dark:text-gray-200 min-w-0 h-10 \"\n                  disabled={set.completed}\n                  onChange={handleTypeChange(columnIndex)}\n                  value={type}\n                >\n                  {availableTypes.map((availableType) => (\n                    <option key={availableType} value={availableType}>\n                      {typeLabels[availableType]}\n                    </option>\n                  ))}\n                </select>\n                {types.length > 1 && (\n                  <Button\n                    className=\"p-1 h-auto bg-red-500 hover:bg-red-600 dark:bg-red-900 dark:hover:bg-red-800 flex-shrink-0\"\n                    onClick={() => removeColumn(columnIndex)}\n                    size=\"small\"\n                    variant=\"destructive\"\n                  >\n                    <Minus className=\"h-3 w-3\" />\n                  </Button>\n                )}\n              </div>\n              {renderInputForType(type, columnIndex)}\n            </div>\n          );\n        })}\n      </div>\n\n      {/* Add column button */}\n      {types.length < MAX_WORKOUT_SET_COLUMNS && !set.completed && (\n        <div className=\"flex w-full justify-start mt-1\">\n          <Button\n            className=\"font-bold px-4 py-2 text-sm rounded-xl w-full md:w-auto mt-2\"\n            disabled={set.completed}\n            onClick={addColumn}\n            variant=\"outline\"\n          >\n            <Plus className=\"h-4 w-4\" />\n            <span className=\"block md:hidden\">{t(\"workout_builder.session.add_row\")}</span>\n            <span className=\"hidden md:block\">{t(\"workout_builder.session.add_column\")}</span>\n          </Button>\n        </div>\n      )}\n\n      {/* Finish & Edit buttons, full width on mobile */}\n      <div className=\"flex gap-2 w-full md:w-auto mt-2\">\n        <Button\n          className=\" dark:text-white font-bold px-4 py-2 text-sm rounded-xl flex-1\"\n          disabled={set.completed}\n          onClick={onFinish}\n          variant=\"default\"\n        >\n          {t(\"workout_builder.session.finish_set\")}\n        </Button>\n        {set.completed && (\n          <Button\n            className=\"bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold px-4 py-2 text-sm rounded-xl flex-1 border border-gray-300\"\n            onClick={handleEdit}\n            variant=\"outline\"\n          >\n            {t(\"commons.edit\")}\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-session/ui/workout-session-sets.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect, useRef } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport Image from \"next/image\";\nimport { Check, Play, ArrowRight, Trophy as TrophyIcon, Plus, Hourglass } from \"lucide-react\";\nimport confetti from \"canvas-confetti\";\n\nimport { useCurrentLocale, useI18n } from \"locales/client\";\nimport TrophyImg from \"@public/images/trophy.png\";\nimport { cn } from \"@/shared/lib/utils\";\nimport { useWorkoutSession } from \"@/features/workout-session/model/use-workout-session\";\nimport { useSyncWorkoutSessions } from \"@/features/workout-session/model/use-sync-workout-sessions\";\nimport { ExerciseVideoModal } from \"@/features/workout-builder/ui/exercise-video-modal\";\nimport { useSyncFavoriteExercises } from \"@/features/workout-builder/hooks/use-sync-favorite-exercises\";\nimport { env } from \"@/env\";\nimport { PremiumUpsellAlert } from \"@/components/ui/premium-upsell-alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { HorizontalBottomBanner } from \"@/components/ads\";\n\nimport { FavoriteExerciseButton } from \"../../workout-builder/ui/favorite-exercise-button\";\nimport { WorkoutSessionSet } from \"./workout-session-set\";\n\nexport function WorkoutSessionSets({\n  showCongrats,\n  onCongrats,\n  isWorkoutActive,\n}: {\n  showCongrats: boolean;\n  onCongrats: VoidFunction;\n  isWorkoutActive: boolean;\n}) {\n  const t = useI18n();\n  const router = useRouter();\n  const locale = useCurrentLocale();\n  const { currentExerciseIndex, session, addSet, updateSet, removeSet, finishSet, goToNextExercise, goToExercise, completeWorkout } =\n    useWorkoutSession();\n  const exerciseDetailsMap = Object.fromEntries(session?.exercises.map((ex) => [ex.id, ex]) || []);\n  const [videoModal, setVideoModal] = useState<{ open: boolean; exerciseId?: string }>({ open: false });\n  const { syncSessions } = useSyncWorkoutSessions();\n  const prevExerciseIndexRef = useRef<number>(currentExerciseIndex);\n  const { syncFavoriteExercises } = useSyncFavoriteExercises();\n\n  // auto-scroll to current exercise when index changes (but not when adding sets)\n  useEffect(() => {\n    if (session && currentExerciseIndex >= 0 && prevExerciseIndexRef.current !== currentExerciseIndex) {\n      const exerciseElement = document.getElementById(`exercise-${currentExerciseIndex}`);\n      if (exerciseElement) {\n        const scrollContainer = exerciseElement.closest(\".overflow-auto\");\n\n        if (scrollContainer) {\n          const containerRect = scrollContainer.getBoundingClientRect();\n          const elementRect = exerciseElement.getBoundingClientRect();\n          const offset = 10;\n\n          const scrollTop = scrollContainer.scrollTop + elementRect.top - containerRect.top - offset;\n\n          scrollContainer.scrollTo({\n            top: scrollTop,\n            behavior: \"smooth\",\n          });\n        } else {\n          exerciseElement.scrollIntoView({\n            behavior: \"smooth\",\n            block: \"center\",\n          });\n        }\n      }\n      prevExerciseIndexRef.current = currentExerciseIndex;\n    }\n  }, [currentExerciseIndex, session]);\n\n  if (showCongrats) {\n    return (\n      <div className=\"flex flex-col items-center justify-center py-16 h-full\">\n        <Image alt={t(\"workout_builder.session.complete\") + \" trophy\"} className=\"w-56 h-56\" src={TrophyImg} />\n        <h2 className=\"text-2xl font-bold mb-2\">{t(\"workout_builder.session.complete\") + \" ! 🎉\"}</h2>\n        <p className=\"text-lg text-slate-600 mb-6\">{t(\"workout_builder.session.workout_in_progress\")}</p>\n        <Button onClick={() => router.push(\"/profile\")}>{t(\"commons.go_to_profile\")}</Button>\n      </div>\n    );\n  }\n\n  if (!session) {\n    return <div className=\"text-center text-slate-500 py-12\">{t(\"workout_builder.session.no_exercise_selected\")}</div>;\n  }\n\n  const handleExerciseClick = (targetIndex: number) => {\n    if (targetIndex !== currentExerciseIndex) {\n      goToExercise(targetIndex);\n    }\n  };\n\n  const renderStepIcon = (idx: number, allSetsCompleted: boolean) => {\n    if (allSetsCompleted) {\n      return <Check aria-label=\"Exercice terminé\" className=\"w-4 h-4 text-white\" />;\n    }\n    if (idx === currentExerciseIndex) {\n      return (\n        <svg aria-label=\"Exercice en cours\" className=\"w-8 h-8 animate-ping text-emerald-500\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n          <circle cx=\"12\" cy=\"12\" r=\"12\" />\n        </svg>\n      );\n    }\n\n    return <Hourglass aria-label=\"Exercice en cours\" className=\"w-4 h-4 text-gray-600 dark:text-slate-900\" />;\n  };\n\n  const renderStepBackground = (idx: number, allSetsCompleted: boolean) => {\n    if (allSetsCompleted) {\n      return \"bg-green-500 border-green-500\";\n    }\n    if (idx === currentExerciseIndex) {\n      return \"bg-gray-300 border-gray-400 dark:bg-slate-500 dark:border-slate-500\";\n    }\n    return \"bg-slate-200 border-slate-200\";\n  };\n\n  const handleFinishSession = () => {\n    completeWorkout();\n    syncFavoriteExercises();\n    syncSessions();\n    onCongrats();\n    confetti({ particleCount: 100, spread: 70, origin: { y: 0.6 } });\n  };\n\n  return (\n    <div className=\"w-full max-w-3xl mx-auto pb-8 px-3 sm:px-6\">\n      <div className=\"mb-6\">\n        <PremiumUpsellAlert />\n      </div>\n      <ol className=\"relative border-l-2 ml-2 border-slate-200 dark:border-slate-700\">\n        {session.exercises.map((ex, idx) => {\n          const allSetsCompleted = ex.sets.length > 0 && ex.sets.every((set) => set.completed);\n          const exerciseName = locale === \"fr\" ? ex.name : ex.nameEn;\n\n          const details = exerciseDetailsMap[ex.id];\n          return (\n            <li\n              className={`mb-8 ml-4 ${idx !== currentExerciseIndex ? \"cursor-pointer hover:opacity-80\" : \"\"}`}\n              id={`exercise-${idx}`}\n              key={ex.id}\n              onClick={() => handleExerciseClick(idx)}\n            >\n              {/* Cercle étape */}\n              <span\n                className={cn(\n                  \"absolute -left-4 flex items-center justify-center w-8 h-8 rounded-full border-4 z-10\",\n                  renderStepBackground(idx, allSetsCompleted),\n                )}\n              >\n                {renderStepIcon(idx, allSetsCompleted)}\n              </span>\n              {/* Image + nom de l'exercice */}\n              <div className=\"flex items-center gap-3 ml-2 hover:opacity-80\">\n                {details?.fullVideoImageUrl && (\n                  <div\n                    className=\"relative aspect-video max-w-24 rounded-lg overflow-hidden shrink-0 bg-slate-200 dark:bg-slate-800 border border-slate-200 dark:border-slate-700/50 cursor-pointer\"\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      setVideoModal({ open: true, exerciseId: ex.id });\n                    }}\n                  >\n                    <Image\n                      alt={exerciseName || \"Exercise image\"}\n                      className=\"w-full h-full object-cover scale-[1.35]\"\n                      height={48}\n                      src={details.fullVideoImageUrl}\n                      width={48}\n                    />\n                    <div className=\"absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity duration-200\">\n                      <Button className=\"bg-white/80\" size=\"icon\" variant=\"ghost\">\n                        <Play className=\"h-4 w-4 text-blue-600\" />\n                      </Button>\n                    </div>\n                  </div>\n                )}\n                <div\n                  className={cn(\n                    \"text-xl leading-[1.3] flex-1\",\n                    idx === currentExerciseIndex\n                      ? \"font-bold text-blue-600\"\n                      : \"text-slate-700 dark:text-slate-300 transition-colors hover:text-blue-500\",\n                  )}\n                >\n                  <span className=\"text-xl leading-[1.3] flex-1\">{exerciseName}</span>\n                  {details?.introduction && (\n                    <span\n                      className=\"flex text-xs mt-1 text-slate-500 dark:text-slate-400 underline cursor-pointer hover:text-blue-600\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        setVideoModal({ open: true, exerciseId: ex.id });\n                      }}\n                    >\n                      {t(\"workout_builder.session.see_instructions\")}\n                    </span>\n                  )}\n                </div>\n              </div>\n              {/* Modale vidéo */}\n              {details && details.fullVideoUrl && videoModal.open && videoModal.exerciseId === ex.id && (\n                <ExerciseVideoModal\n                  exercise={details}\n                  onOpenChange={(open) => setVideoModal({ open, exerciseId: open ? ex.id : undefined })}\n                  open={videoModal.open}\n                />\n              )}\n              {/* Si exercice courant, afficher le détail */}\n              {idx === currentExerciseIndex && (\n                <div className=\"bg-white dark:bg-transparent rounded-xl mt-6 mb-10\">\n                  {/* Liste des sets */}\n                  <div className=\"flex justify-start items-center gap-2\">\n                    <FavoriteExerciseButton exerciseId={ex.id} />\n                  </div>\n                  <div className=\"space-y-10 mb-8\">\n                    {ex.sets.map((set, setIdx) => (\n                      <WorkoutSessionSet\n                        key={set.id}\n                        onChange={(sIdx: number, data: Partial<typeof set>) => updateSet(idx, sIdx, data)}\n                        onFinish={() => finishSet(idx, setIdx)}\n                        onRemove={() => removeSet(idx, setIdx)}\n                        set={set}\n                        setIndex={setIdx}\n                      />\n                    ))}\n                  </div>\n                  {/* Actions bas de page */}\n                  <div className=\"flex flex-col md:flex-row gap-3 w-full mt-2 px-2\">\n                    <Button\n                      aria-label=\"Ajouter une série\"\n                      className=\"flex-1 flex items-center justify-center gap-2 bg-green-500 hover:bg-green-600 text-white font-bold py-3 rounded-xl border border-green-600 transition-all duration-200 active:scale-95 focus:ring-2 focus:ring-green-400\"\n                      onClick={addSet}\n                    >\n                      <Plus className=\"h-5 w-5\" />\n                      {t(\"workout_builder.session.add_set\")}\n                    </Button>\n                    <Button\n                      aria-label=\"Exercice suivant\"\n                      className=\"flex-1 flex items-center justify-center gap-2 bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 rounded-xl border border-blue-600 transition-all duration-200 active:scale-95 focus:ring-2 focus:ring-blue-400\"\n                      onClick={goToNextExercise}\n                    >\n                      <ArrowRight className=\"h-5 w-5\" />\n                      {t(\"workout_builder.session.next_exercise\")}\n                    </Button>\n                  </div>\n                </div>\n              )}\n            </li>\n          );\n        })}\n      </ol>\n      {isWorkoutActive && (\n        <div className=\"flex justify-center mt-8 mb-24\">\n          <Button\n            aria-label={t(\"workout_builder.session.finish_session\")}\n            className=\"flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white font-bold px-8 py-3 text-lg rounded-2xl border border-green-700 transition-all duration-200 active:scale-95 focus:ring-2 focus:ring-green-400\"\n            onClick={handleFinishSession}\n          >\n            <TrophyIcon className=\"h-6 w-6\" />\n            {t(\"workout_builder.session.finish_session\")}\n          </Button>\n        </div>\n      )}\n\n      {env.NEXT_PUBLIC_BOTTOM_WORKOUT_SESSION_BANNER_AD_SLOT && (\n        <HorizontalBottomBanner adSlot={env.NEXT_PUBLIC_BOTTOM_WORKOUT_SESSION_BANNER_AD_SLOT} />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-session/ui/workout-session-timer.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Play, Pause, RotateCcw } from \"lucide-react\";\n\nimport { cn } from \"@/shared/lib/utils\";\nimport { useWorkoutSession } from \"@/features/workout-builder\";\nimport { Timer } from \"@/components/ui/timer\";\nimport { Button } from \"@/components/ui/button\";\n\nexport function WorkoutSessionTimer() {\n  const { isWorkoutActive, isTimerRunning, toggleTimer, resetTimer } = useWorkoutSession();\n\n  const [resetCount, setResetCount] = useState(0);\n\n  const handleReset = () => {\n    resetTimer();\n    setResetCount((c) => c + 1);\n  };\n\n  if (!isWorkoutActive) {\n    return null;\n  }\n\n  return (\n    <div className=\"absolute bottom-32 left-1/2 transform -translate-x-1/2 mb-3\">\n      <div className=\"bg-white dark:bg-slate-900 rounded-full px-6 py-4 border border-slate-200 dark:border-slate-700 shadow-lg backdrop-blur-sm\">\n        <div className=\"flex items-center justify-between gap-4\">\n          {/* Timer display */}\n          <div className=\"flex items-center gap-3\">\n            <div className=\"text-xl font-mono font-bold text-slate-900 dark:text-white tracking-wider\">\n              <Timer initialSeconds={0} isRunning={isTimerRunning} key={resetCount} />\n            </div>\n          </div>\n\n          {/* Control buttons */}\n          <div className=\"flex items-center gap-3\">\n            <Button\n              className={cn(\n                \"w-12 h-12 rounded-full p-0 text-white shadow-md\",\n                isTimerRunning ? \"bg-amber-500 hover:bg-amber-600\" : \"bg-emerald-500 hover:bg-emerald-600\",\n              )}\n              onClick={toggleTimer}\n            >\n              {isTimerRunning ? <Pause className=\"h-5 w-5\" /> : <Play className=\"h-5 w-5\" />}\n            </Button>\n\n            <Button\n              className=\"w-12 h-12 rounded-full p-0 border-slate-200 text-slate-400 hover:bg-slate-200 dark:border-slate-600 hover:dark:bg-slate-700 shadow-md\"\n              onClick={handleReset}\n              variant=\"outline\"\n            >\n              <RotateCcw className=\"h-5 w-5\" />\n            </Button>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/features/workout-session/ui/workout-sessions-synchronizer.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\n\nimport { useSyncWorkoutSessions } from \"../model/use-sync-workout-sessions\";\n\nexport const WorkoutSessionsSynchronizer = () => {\n  const { syncSessions } = useSyncWorkoutSessions();\n  const searchParams = useSearchParams();\n  const isSigninParam = searchParams.get(\"signin\") === \"true\";\n\n  useEffect(() => {\n    if (isSigninParam) {\n      syncSessions();\n    }\n  }, [isSigninParam]);\n\n  return null;\n};\n"
  },
  {
    "path": "src/index.d.ts",
    "content": "// add opera to the user agent\ndeclare global {\n  interface Navigator {\n    opera: any;\n  }\n}\n"
  },
  {
    "path": "src/shared/api/README.md",
    "content": "# Mobile App Authentication Support\n\nThis directory contains utilities to handle authentication issues with the mobile app (Expo/React Native).\n\n## The Problem\n\nThe Expo app sometimes sends malformed cookies in the format:\n```\nbetter-auth.session_token=abc,better-auth.session_token=xyz\n```\n\nInstead of the correct format:\n```\nbetter-auth.session_token=abc; better-auth.session_token=xyz\n```\n\nThis causes authentication to fail with 401 errors.\n\n## The Solution\n\nWe provide two simple utilities:\n\n### 1. For API Routes: `getMobileCompatibleSession`\n\n```typescript\nimport { getMobileCompatibleSession } from \"@/shared/api/mobile-auth\";\n\nexport async function GET(req: NextRequest) {\n  const session = await getMobileCompatibleSession(req);\n  \n  if (!session?.user) {\n    return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n  }\n  \n  // Use session.user...\n}\n```\n\n### 2. For Server Actions: `mobileAuthenticatedActionClient`\n\n```typescript\nimport { mobileAuthenticatedActionClient } from \"@/shared/api/mobile-safe-actions\";\n\nexport const myAction = mobileAuthenticatedActionClient\n  .schema(mySchema)\n  .action(async ({ parsedInput, ctx }) => {\n    const { user } = ctx; // user is guaranteed to be authenticated\n    // Your action logic...\n  });\n```\n\n## How It Works\n\n1. **Cookie Cleaning**: The utilities detect and fix malformed cookie separators\n2. **Deduplication**: If multiple cookies with the same name exist, it keeps the last one (most recent)\n3. **Transparent**: Works exactly like the regular auth utilities, just handles mobile app issues\n\n## When to Use\n\n- Use these utilities for any authenticated endpoints that need to work with the mobile app\n- For web-only endpoints, you can continue using the regular `auth.api.getSession()` and `authenticatedActionClient`\n\n## Adding to New Routes\n\nWhen creating a new authenticated API route that needs mobile support:\n\n```typescript\n// ❌ Don't use this for mobile-compatible routes\nconst session = await auth.api.getSession({ headers: req.headers });\n\n// ✅ Use this instead\nconst session = await getMobileCompatibleSession(req);\n```\n\n## Testing\n\nTo test if your endpoint works with mobile cookies, use this curl command:\n\n```bash\ncurl -X GET http://localhost:3000/api/your-endpoint \\\n  -H \"Cookie: better-auth.session_token=token1,better-auth.session_token=token2; better-auth.session_data=data\"\n```\n\nIf it returns data instead of 401, it's working correctly!"
  },
  {
    "path": "src/shared/api/createHandler.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nimport type { TypeOf, ZodError, ZodSchema } from \"zod\";\nimport type { NextRequest } from \"next/server\";\n\nexport type HandleReturnedServerErrorFn = (e: unknown) => NextResponse | string;\n\nexport type MiddlewareFn<TContext> = (req: NextRequest) => Promise<TContext>;\n\ntype CreateHandlerParams<TContext> = {\n  middleware?: MiddlewareFn<TContext>;\n  handleReturnedServerError?: HandleReturnedServerErrorFn;\n};\n\ntype HandlerParams<TBody, TParams, TSearchParams> = {\n  bodySchema?: TBody;\n  searchSchema?: TSearchParams;\n  paramsSchema?: TParams;\n};\n\ntype Callback<TContext, TBody, TParams, TSearchParams> = (params: {\n  request: NextRequest;\n  context: TContext;\n  body: TBody extends ZodSchema ? TypeOf<TBody> : undefined;\n  searchParams: TSearchParams extends ZodSchema ? TypeOf<TSearchParams> : undefined;\n  params: TParams extends ZodSchema ? TypeOf<TParams> : undefined;\n}) => Promise<Response> | Response;\n\nclass CustomZodError extends Error {\n  type: \"body\" | \"params\";\n  zodError: ZodError;\n  received: unknown;\n  constructor(type: \"body\" | \"params\", zodError: ZodError, received?: unknown) {\n    super();\n    this.type = type;\n    this.zodError = zodError;\n    this.received = received;\n  }\n}\n\nexport function createSafeHandler<TContext>({ middleware, handleReturnedServerError }: CreateHandlerParams<TContext>) {\n  function handler<TBody extends ZodSchema | undefined, TParams extends ZodSchema | undefined, TSearchParams extends ZodSchema | undefined>(\n    { bodySchema, searchSchema, paramsSchema }: HandlerParams<TBody, TParams, TSearchParams>,\n    callback: Callback<TContext, TBody, TParams, TSearchParams>,\n  ) {\n    return async (req: NextRequest, { params: baseParams }: { params: Promise<Record<string, string | string[]>> }) => {\n      try {\n        const body = await parseBody(req, bodySchema);\n        const searchParams = await parseSearchParams(req, searchSchema);\n        const params = await parseParams(baseParams, paramsSchema);\n\n        const context = await middleware?.(req);\n\n        const response = await callback({\n          request: req,\n          body: body as never,\n          context: context as never,\n          searchParams: searchParams as never,\n          params: params as never,\n        });\n\n        return response;\n      } catch (error: unknown) {\n        // check if error is from zod\n        if (error instanceof CustomZodError) {\n          return NextResponse.json(\n            {\n              error: `Invalid ${error.type}.`,\n              validation: error.zodError.errors,\n              received: error.received,\n            },\n            {\n              status: 400,\n            },\n          );\n        }\n\n        const returnedError = handleReturnedServerError?.(error);\n\n        if (returnedError && typeof returnedError !== \"string\") {\n          return returnedError;\n        }\n\n        return NextResponse.json(\n          {\n            error: returnedError ?? \"An unexpected error occurred.\",\n          },\n          {\n            status: 400,\n          },\n        );\n      }\n    };\n  }\n\n  return handler;\n}\n\nconst parseBody = async <T>(req: NextRequest, schema?: ZodSchema<T>) => {\n  let parsedBody: T | undefined = undefined;\n\n  if (schema && req.method !== \"GET\") {\n    const json = await req.json();\n    const bodyParseResult = schema.safeParse(json);\n    if (bodyParseResult.success) {\n      parsedBody = bodyParseResult.data;\n    } else {\n      throw new CustomZodError(\"body\", bodyParseResult.error, json);\n    }\n  }\n  return parsedBody;\n};\n\nconst parseSearchParams = async <T>(req: NextRequest, schema?: ZodSchema<T>) => {\n  const url = new URL(req.url);\n  const searchParams = url.searchParams;\n  const params = {} as Record<string, string | string[]>;\n\n  for (const [key, value] of searchParams.entries()) {\n    params[key] = value;\n  }\n\n  let parsedSearchParams: T | undefined = undefined;\n\n  if (schema) {\n    const paramsParseResult = schema.safeParse(params);\n    if (paramsParseResult.success) {\n      parsedSearchParams = paramsParseResult.data;\n    } else {\n      throw new CustomZodError(\"params\", paramsParseResult.error, params);\n    }\n  }\n\n  return parsedSearchParams;\n};\n\nconst parseParams = async <T>(params: Promise<Record<string, string | string[]>>, schema?: ZodSchema<T>) => {\n  let parsedParams: T | undefined = undefined;\n  const resolvedParams = await params;\n\n  if (schema) {\n    const paramsParseResult = schema.safeParse(resolvedParams);\n    if (paramsParseResult.success) {\n      parsedParams = paramsParseResult.data;\n    } else {\n      throw new CustomZodError(\"params\", paramsParseResult.error, resolvedParams);\n    }\n  }\n\n  return parsedParams;\n};\n"
  },
  {
    "path": "src/shared/api/handlers.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nimport { serverAuth } from \"@/entities/user/model/get-server-session-user\";\n\nimport { createSafeHandler } from \"./createHandler\";\n\nimport type { HandleReturnedServerErrorFn } from \"./createHandler\";\n\nexport class HandlerError extends Error {\n  status = 400;\n  constructor(message: string, status?: number) {\n    super(message);\n    if (status) {\n      this.status = status;\n    }\n  }\n}\n\nconst handleReturnedServerError: HandleReturnedServerErrorFn = (e) => {\n  if (e instanceof HandlerError) {\n    return NextResponse.json(\n      {\n        error: e.message,\n      },\n      {\n        status: e.status,\n      },\n    );\n  }\n\n  return \"An unexpected error occurred.\";\n};\n\nexport const handler = createSafeHandler({\n  handleReturnedServerError,\n});\n\nexport const authHandler = createSafeHandler({\n  handleReturnedServerError,\n\n  async middleware() {\n    const user = await serverAuth();\n\n    if (!user) {\n      throw new HandlerError(\"Session not found!\", 401);\n    }\n\n    if (!user.id || !user.email) {\n      throw new HandlerError(\"Session is not valid!\", 401);\n    }\n\n    return {\n      user: user as {\n        id: string;\n        email: string;\n        image?: string;\n        firstName?: string;\n        lastName?: string;\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "src/shared/api/mobile-auth.ts",
    "content": "import { NextRequest } from \"next/server\";\n\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nimport { cleanMobileCookies } from \"./mobile-cookie-utils\";\n\n/**\n * Gets the authenticated session from a request, handling mobile app cookie issues\n *\n * @param req - The NextRequest object\n * @returns The session object or null if not authenticated\n *\n * @example\n * ```ts\n * export async function GET(req: NextRequest) {\n *   const session = await getMobileCompatibleSession(req);\n *   if (!session) {\n *     return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n *   }\n *   // Use session.user...\n * }\n * ```\n */\nexport async function getMobileCompatibleSession(req: NextRequest) {\n  const rawCookieHeader = req.headers.get(\"cookie\");\n\n  // If no cookies, no session\n  if (!rawCookieHeader) {\n    return null;\n  }\n\n  // Clean the cookies\n  const cleanedCookieHeader = cleanMobileCookies(rawCookieHeader);\n\n  // Create new headers with cleaned cookies\n  const cleanHeaders = new Headers(req.headers);\n  cleanHeaders.set(\"cookie\", cleanedCookieHeader);\n\n  // Get session with cleaned headers\n  const session = await auth.api.getSession({\n    headers: cleanHeaders,\n  });\n\n  return session;\n}\n\n/**\n * Type guard to check if a session has a valid user\n */\nexport function hasValidUser(session: any): session is { user: { id: string; email: string } } {\n  return session?.user?.id && session?.user?.email;\n}\n"
  },
  {
    "path": "src/shared/api/mobile-cookie-utils.ts",
    "content": "/**\n * Utilities for handling malformed cookies from the mobile app\n */\n\n/**\n * Cleans malformed cookies from mobile app\n * \n * Converts: \"token=abc,token=xyz\" \n * To: \"token=abc; token=xyz\"\n * \n * Also handles:\n * - Duplicate cookie names (keeps better-auth.* cookies)\n * - Trailing commas and whitespace\n */\nexport function cleanMobileCookies(cookieHeader: string): string {\n  if (!cookieHeader) return \"\";\n\n  // Replace comma separators between better-auth cookies with semicolons\n  const cleaned = cookieHeader.replace(/,better-auth\\./g, \"; better-auth.\");\n\n  // Parse and deduplicate cookies\n  const cookieMap = new Map<string, string>();\n\n  cleaned.split(\";\").forEach((cookie) => {\n    const trimmed = cookie.trim();\n    if (trimmed && trimmed.includes(\"=\")) {\n      const [name, ...valueParts] = trimmed.split(\"=\");\n      if (name && valueParts.length > 0) {\n        const cleanName = name.trim();\n        const value = valueParts.join(\"=\").replace(/,\\s*$/, \"\");\n\n        // Prioritize better-auth cookies\n        if (cleanName.startsWith(\"better-auth.\")) {\n          cookieMap.set(cleanName, value);\n        } else if (!cookieMap.has(cleanName)) {\n          cookieMap.set(cleanName, value);\n        }\n      }\n    }\n  });\n\n  // Reconstruct the cookie header\n  return Array.from(cookieMap.entries())\n    .map(([name, value]) => `${name}=${value}`)\n    .join(\"; \");\n}"
  },
  {
    "path": "src/shared/api/mobile-safe-actions.ts",
    "content": "import { createSafeActionClient } from \"next-safe-action\";\nimport { headers } from \"next/headers\";\n\nimport { auth } from \"@/features/auth/lib/better-auth\";\n\nimport { ActionError } from \"./safe-actions\";\nimport { cleanMobileCookies } from \"./mobile-cookie-utils\";\n\n/**\n * Gets the authenticated user from headers, handling mobile app cookie issues\n */\nasync function getMobileCompatibleUser() {\n  const headersList = await headers();\n  const rawCookieHeader = headersList.get(\"cookie\");\n\n  if (!rawCookieHeader) {\n    throw new ActionError(\"Session not found!\");\n  }\n\n  // Clean the cookies\n  const cleanedCookieHeader = cleanMobileCookies(rawCookieHeader);\n\n  // Create new headers with cleaned cookies\n  const cleanHeaders = new Headers(headersList);\n  cleanHeaders.set(\"cookie\", cleanedCookieHeader);\n\n  // Get session with cleaned headers\n  const session = await auth.api.getSession({\n    headers: cleanHeaders,\n  });\n\n  if (!session?.user) {\n    throw new ActionError(\"Session not found!\");\n  }\n\n  if (!session.user.id || !session.user.email) {\n    throw new ActionError(\"Session is not valid!\");\n  }\n\n  return session.user;\n}\n\n/**\n * Safe action client that handles mobile app authentication issues\n *\n * Use this instead of authenticatedActionClient for actions that need to work\n * with the mobile app.\n *\n * @example\n * ```ts\n * export const updateUserAction = mobileAuthenticatedActionClient\n *   .schema(updateUserSchema)\n *   .action(async ({ parsedInput, ctx }) => {\n *     const { user } = ctx;\n *     // user is guaranteed to be authenticated\n *   });\n * ```\n */\nexport const mobileAuthenticatedActionClient = createSafeActionClient({\n  handleServerError: (e: Error) => {\n    if (e instanceof ActionError) {\n      return e.message;\n    }\n    return \"An unexpected error occurred.\";\n  },\n}).use(async ({ next }: { next: any }) => {\n  const user = await getMobileCompatibleUser();\n  return await next({ ctx: { user } });\n});\n"
  },
  {
    "path": "src/shared/api/safe-actions.ts",
    "content": "import { createSafeActionClient } from \"next-safe-action\";\n\nimport { serverAuth } from \"@/entities/user/model/get-server-session-user\";\n\nexport class ActionError extends Error {\n  constructor(message: string) {\n    super(message);\n  }\n}\n\ntype HandleReturnedServerError = (e: Error) => string;\n\nconst handleReturnedServerError: HandleReturnedServerError = (e) => {\n  if (e instanceof ActionError) {\n    return e.message;\n  }\n\n  return \"An unexpected error occurred.\";\n};\n\nexport const actionClient = createSafeActionClient({\n  handleServerError: handleReturnedServerError,\n});\n\nconst getUser = async () => {\n  const user = await serverAuth();\n\n  if (!user) {\n    throw new ActionError(\"Session not found!\");\n  }\n\n  if (!user.id || !user.email) {\n    throw new ActionError(\"Session is not valid!\");\n  }\n\n  return user;\n};\n\nexport const authenticatedActionClient = createSafeActionClient({\n  handleServerError: handleReturnedServerError,\n} as const).use(async ({ next, clientInput: _clientInput, metadata: _metadata }) => {\n  const user = await getUser();\n\n  return await next({ ctx: { user } });\n});\n"
  },
  {
    "path": "src/shared/config/localized-metadata.ts",
    "content": "export const LocalizedMetadata = {\n  en: {\n    title: \"Workout Cool\",\n    description: \"Modern fitness coaching platform with comprehensive exercise database\",\n    keywords: [\n      \"fitness\",\n      \"workout\",\n      \"exercise\",\n      \"training\",\n      \"muscle building\",\n      \"strength training\",\n      \"bodybuilding\",\n      \"fitness app\",\n      \"workout planner\",\n      \"exercise database\",\n    ],\n    ogAlt: \"Workout Cool - Modern fitness platform\",\n    applicationName: \"Workout Cool\",\n    category: \"fitness\",\n    classification: \"Fitness & Health\",\n  },\n  fr: {\n    title: \"Workout Cool\",\n    description: \"Plateforme de coaching fitness moderne avec base de données d'exercices complète\",\n    keywords: [\n      \"fitness\",\n      \"entraînement\",\n      \"exercice\",\n      \"musculation\",\n      \"sport\",\n      \"coaching\",\n      \"programme d'entraînement\",\n      \"application fitness\",\n      \"planificateur d'entraînement\",\n      \"base de données d'exercices\",\n    ],\n    ogAlt: \"Workout Cool - Plateforme de fitness moderne\",\n    applicationName: \"Workout Cool\",\n    category: \"fitness\",\n    classification: \"Fitness et Santé\",\n  },\n  es: {\n    title: \"Workout Cool\",\n    description: \"Plataforma moderna de entrenamiento fitness con base de datos completa de ejercicios\",\n    keywords: [\n      \"fitness\",\n      \"entrenamiento\",\n      \"ejercicio\",\n      \"musculación\",\n      \"deporte\",\n      \"coaching\",\n      \"programa de entrenamiento\",\n      \"aplicación fitness\",\n      \"planificador de entrenamientos\",\n      \"base de datos de ejercicios\",\n    ],\n    ogAlt: \"Workout Cool - Plataforma de fitness moderna\",\n    applicationName: \"Workout Cool\",\n    category: \"fitness\",\n    classification: \"Fitness y Salud\",\n  },\n  pt: {\n    title: \"Workout Cool\",\n    description: \"Plataforma moderna de coaching fitness com base de dados abrangente de exercícios\",\n    keywords: [\n      \"fitness\",\n      \"treino\",\n      \"exercício\",\n      \"musculação\",\n      \"esporte\",\n      \"coaching\",\n      \"programa de treino\",\n      \"aplicativo fitness\",\n      \"planejador de treinos\",\n      \"base de dados de exercícios\",\n    ],\n    ogAlt: \"Workout Cool - Plataforma de fitness moderna\",\n    applicationName: \"Workout Cool\",\n    category: \"fitness\",\n    classification: \"Fitness e Saúde\",\n  },\n  ru: {\n    title: \"Workout Cool\",\n    description: \"Современная платформа фитнес-коучинга с comprehensive базой данных упражнений\",\n    keywords: [\n      \"фитнес\",\n      \"тренировка\",\n      \"упражнение\",\n      \"бодибилдинг\",\n      \"спорт\",\n      \"коучинг\",\n      \"программа тренировок\",\n      \"фитнес приложение\",\n      \"планировщик тренировок\",\n      \"база данных упражнений\",\n    ],\n    ogAlt: \"Workout Cool - Современная фитнес платформа\",\n    applicationName: \"Workout Cool\",\n    category: \"фитнес\",\n    classification: \"Фитнес и Здоровье\",\n  },\n  \"zh-CN\": {\n    title: \"Workout Cool\",\n    description: \"现代健身教练平台，拥有全面的运动数据库\",\n    keywords: [\"健身\", \"锻炼\", \"运动\", \"训练\", \"肌肉训练\", \"力量训练\", \"健美\", \"健身应用\", \"锻炼计划\", \"运动数据库\"],\n    ogAlt: \"Workout Cool - 现代健身平台\",\n    applicationName: \"Workout Cool\",\n    category: \"健身\",\n    classification: \"健身与健康\",\n  },\n} as const;\n\nexport type SupportedLocale = keyof typeof LocalizedMetadata;\n\nexport function getLocalizedMetadata(locale: string) {\n  const supportedLocales: SupportedLocale[] = [\"en\", \"fr\", \"es\", \"pt\", \"ru\", \"zh-CN\"];\n\n  if (supportedLocales.includes(locale as SupportedLocale)) {\n    return LocalizedMetadata[locale as SupportedLocale];\n  }\n\n  return LocalizedMetadata.en;\n}\n"
  },
  {
    "path": "src/shared/config/site-config.ts",
    "content": "export const SiteConfig = {\n  title: \"Workout Cool\",\n  description: \"Modern fitness coaching platform with comprehensive exercise database\",\n  keywords: [\n    \"fitness\",\n    \"workout\",\n    \"exercise\",\n    \"training\",\n    \"muscle building\",\n    \"strength training\",\n    \"bodybuilding\",\n    \"fitness app\",\n    \"workout planner\",\n    \"exercise database\",\n  ],\n  prodUrl: \"https://workout.cool\",\n  logo: \"/images/logo.png\",\n  domain: \"workout.cool\",\n  appIcon: \"/images/logo4.jpg\",\n  company: {\n    name: \"Workout Cool\",\n    address: \"34 avenue des champ Elysée 75008 Paris, France\",\n  },\n  brand: {\n    primary: \"#007291\",\n  },\n  email: {\n    from: \"Workout Cool <hello@workout.cool>\",\n    contact: \"hello@workout.cool\",\n  },\n  maker: {\n    image: \"https://workout.cool/images/me/twitter-en.jpg\",\n    website: \"https://workout.cool\",\n    twitter: \"https://twitter.com/workout_cool\",\n    name: \"Workout Cool\",\n  },\n  auth: {\n    password: false,\n  },\n  seo: {\n    ogImage: {\n      width: 1200,\n      height: 630,\n    },\n    twitterHandle: \"@snouzy_biceps\",\n    applicationName: \"Workout Cool\",\n    category: \"fitness\",\n    classification: \"Fitness & Health\",\n  },\n};\n"
  },
  {
    "path": "src/shared/constants/cookies.ts",
    "content": "export const Cookies = {\n  TrackingConsent: \"tracking-consent\",\n} as const;\n"
  },
  {
    "path": "src/shared/constants/errors.ts",
    "content": "export const ERROR_MESSAGES = {\n  USER_NOT_FOUND: \"USER_NOT_FOUND\",\n  PAGE_NOT_FOUND: \"PAGE_NOT_FOUND\",\n  UNAUTHORIZED: \"UNAUTHORIZED\",\n  USERNAME_ALREADY_TAKEN: \"USERNAME_ALREADY_TAKEN\",\n  INVALID_CURRENT_PASSWORD: \"INVALID_CURRENT_PASSWORD\",\n  INVALID_NEW_PASSWORD: \"INVALID_NEW_PASSWORD\",\n  PASSWORDS_DO_NOT_MATCH: \"PASSWORDS_DO_NOT_MATCH\",\n  EMAIL_ALREADY_USED: \"EMAIL_ALREADY_USED\",\n};\n"
  },
  {
    "path": "src/shared/constants/paths.ts",
    "content": "export const paths = {\n  root: \"/\",\n  signUp: \"auth/signup\",\n  signIn: \"auth/signin\",\n  forgotPassword: \"auth/forgot-password\",\n  resetPassword: \"auth/reset-password\",\n  verifyEmail: \"auth/verify-email\",\n  profile: \"profile\",\n  privacy: \"/legal/privacy\",\n  terms: \"/legal/terms\",\n  programs: \"/programs\",\n  leaderboard: \"/leaderboard\",\n} as const;\n"
  },
  {
    "path": "src/shared/constants/placeholders.ts",
    "content": "export const PLACEHOLDERS = {\n  PROFILE_IMAGE: \"/images/placeholders/coach-avatar.png\",\n  HERO_IMAGE: \"/images/placeholders/hero-image.png\",\n  DESIGN_BACKGROUND_IMAGE: \"/images/placeholders/design-background-image.png\",\n  DEFAULT_HERO_IMAGE: \"/images/patterns/7.png\",\n};\n"
  },
  {
    "path": "src/shared/constants/regexs.ts",
    "content": "export const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\\d).{8,}$/;\n"
  },
  {
    "path": "src/shared/constants/screen.ts",
    "content": "export const Screens = {\n  linkInBio: \"link-in-bio\",\n  dashboard: \"dashboard\",\n  website: \"website\",\n  editor: \"editor\",\n};\n"
  },
  {
    "path": "src/shared/constants/social-platforms.tsx",
    "content": "import {\n  FaXTwitter,\n  FaFacebook,\n  FaEnvelope,\n  FaWhatsapp,\n  FaGlobe,\n  FaPhone,\n  FaYoutube,\n  FaLinkedin,\n  FaSnapchat,\n  FaInstagram,\n  FaTiktok,\n  FaThreads,\n} from \"react-icons/fa6\";\n\nimport { TFunction } from \"locales/client\";\n\nexport const SOCIAL_FIELD_PLACEHOLDERS: Record<string, string> = {\n  x: \"ex: johndoe\",\n  facebook: \"ex: johndoe\",\n  instagram: \"ex: johndoe\",\n  tiktok: \"ex: johndoe\",\n  threads: \"ex: johndoe\",\n  youtube: \"ex: johndoe\",\n  linkedin: \"ex: johndoe\",\n  snapchat: \"ex: johndoe\",\n  twitch: \"ex: johndoe\",\n  whatsapp: \"ex: +33612345678\",\n  email: \"ex: john@email.com\",\n  website: \"ex: https://site.com\",\n  phone: \"ex: +33612345678\",\n};\n\nexport const SOCIAL_FIELD_ICONS: Record<string, React.ReactNode> = {\n  x: <FaXTwitter className=\"h-5 w-5 text-[#000]\" />,\n  facebook: <FaFacebook className=\"h-5 w-5 text-[#1877f3]\" />,\n  email: <FaEnvelope className=\"h-5 w-5 text-[#80b0f4]\" />,\n  whatsapp: <FaWhatsapp className=\"h-5 w-5 text-[#25d366]\" />,\n  website: <FaGlobe className=\"h-5 w-5 text-[#334155]\" />,\n  phone: <FaPhone className=\"h-5 w-5 text-[#66e534]\" />,\n  youtube: <FaYoutube className=\"h-5 w-5 text-[#ff0000]\" />,\n  linkedin: <FaLinkedin className=\"h-5 w-5 text-[#0077b5]\" />,\n  snapchat: <FaSnapchat className=\"h-5 w-5 text-[#fffc00]\" />,\n  instagram: <FaInstagram className=\"h-5 w-5 text-[#e1306c]\" />,\n  tiktok: <FaTiktok className=\"h-5 w-5 text-black\" />,\n  threads: <FaThreads className=\"h-5 w-5 text-black\" />,\n};\n\nexport const SOCIAL_PLATFORMS = (t: TFunction) =>\n  [\n    { value: \"x\", label: t(\"social_platforms.x\"), icon: SOCIAL_FIELD_ICONS.x },\n    { value: \"facebook\", label: t(\"social_platforms.facebook\"), icon: SOCIAL_FIELD_ICONS.facebook },\n    { value: \"email\", label: t(\"social_platforms.email\"), icon: SOCIAL_FIELD_ICONS.email },\n    { value: \"whatsapp\", label: t(\"social_platforms.whatsapp\"), icon: SOCIAL_FIELD_ICONS.whatsapp },\n    { value: \"website\", label: t(\"social_platforms.website\"), icon: SOCIAL_FIELD_ICONS.website },\n    { value: \"phone\", label: t(\"social_platforms.phone\"), icon: SOCIAL_FIELD_ICONS.phone },\n    { value: \"youtube\", label: t(\"social_platforms.youtube\"), icon: SOCIAL_FIELD_ICONS.youtube },\n    { value: \"linkedin\", label: t(\"social_platforms.linkedin\"), icon: SOCIAL_FIELD_ICONS.linkedin },\n    { value: \"snapchat\", label: t(\"social_platforms.snapchat\"), icon: SOCIAL_FIELD_ICONS.snapchat },\n    { value: \"instagram\", label: t(\"social_platforms.instagram\"), icon: SOCIAL_FIELD_ICONS.instagram },\n    { value: \"tiktok\", label: t(\"social_platforms.tiktok\"), icon: SOCIAL_FIELD_ICONS.tiktok },\n    { value: \"threads\", label: t(\"social_platforms.threads\"), icon: SOCIAL_FIELD_ICONS.threads },\n  ] as const;\n"
  },
  {
    "path": "src/shared/constants/statistics.ts",
    "content": "export const STATISTICS_TIMEFRAMES = {\n  FOUR_WEEKS: \"4weeks\",\n  EIGHT_WEEKS: \"8weeks\", \n  TWELVE_WEEKS: \"12weeks\",\n  ONE_YEAR: \"1year\",\n} as const;\n\nexport type StatisticsTimeframe = typeof STATISTICS_TIMEFRAMES[keyof typeof STATISTICS_TIMEFRAMES];\n\nexport const DEFAULT_TIMEFRAME = STATISTICS_TIMEFRAMES.EIGHT_WEEKS;\n\nexport const TIMEFRAME_DAYS = {\n  [STATISTICS_TIMEFRAMES.FOUR_WEEKS]: 28,\n  [STATISTICS_TIMEFRAMES.EIGHT_WEEKS]: 56,\n  [STATISTICS_TIMEFRAMES.TWELVE_WEEKS]: 84,\n  [STATISTICS_TIMEFRAMES.ONE_YEAR]: 365,\n} as const;\n\nexport const TIMEFRAME_LABELS = {\n  [STATISTICS_TIMEFRAMES.FOUR_WEEKS]: \"4 Weeks\",\n  [STATISTICS_TIMEFRAMES.EIGHT_WEEKS]: \"8 Weeks\",\n  [STATISTICS_TIMEFRAMES.TWELVE_WEEKS]: \"12 Weeks\",\n  [STATISTICS_TIMEFRAMES.ONE_YEAR]: \"1 Year\",\n} as const;\n\n// Cache TTL for statistics data (1 hour in seconds)\nexport const STATISTICS_CACHE_TTL = 3600;\n\n// Lombardi formula constant\nexport const LOMBARDI_DIVISOR = 30;"
  },
  {
    "path": "src/shared/constants/success.ts",
    "content": "export const SUCCESS_MESSAGES = {\n  DELETE_SUCCESS: \"DELETE_SUCCESS\",\n};\n"
  },
  {
    "path": "src/shared/constants/workout-set-types.ts",
    "content": "export const AVAILABLE_WORKOUT_SET_TYPES = [\"TIME\", \"WEIGHT\", \"REPS\", \"BODYWEIGHT\"] as const;\nexport const ALL_WORKOUT_SET_TYPES = [\"TIME\", \"WEIGHT\", \"REPS\", \"BODYWEIGHT\", \"NA\"] as const;\nexport const MAX_WORKOUT_SET_COLUMNS = 4;\nexport const WORKOUT_SET_UNITS_TUPLE = [\"kg\", \"lbs\"] as const;\n"
  },
  {
    "path": "src/shared/hooks/use-clipboard.ts",
    "content": "import { useState, useCallback, useEffect } from \"react\";\n\ninterface UseClipboardOptions {\n  /** Timeout in milliseconds to reset the copied state */\n  timeout?: number;\n}\n\ninterface UseClipboardReturn {\n  /** Function to copy text to clipboard */\n  copy: (text: string) => Promise<boolean>;\n  /** Boolean state indicating if the text was recently copied */\n  isCopied: boolean;\n  /** Boolean state indicating if the Clipboard API is supported */\n  isSupported: boolean;\n  /** Potential error object if copying failed */\n  error: Error | null;\n}\n\n/**\n * A hook to handle copying text to the clipboard.\n * It uses the modern Clipboard API with a fallback to document.execCommand.\n */\nexport function useClipboard({ timeout = 1500 }: UseClipboardOptions = {}): UseClipboardReturn {\n  const [isCopied, setIsCopied] = useState(false);\n  const [error, setError] = useState<Error | null>(null);\n  const [isSupported, setIsSupported] = useState(false);\n\n  useEffect(() => {\n    // Check for Clipboard API support on mount\n    setIsSupported(Boolean(navigator.clipboard));\n  }, []);\n\n  const copy = useCallback(\n    async (text: string): Promise<boolean> => {\n      setError(null); // Reset error on new copy attempt\n\n      // Try modern Clipboard API\n      if (navigator.clipboard) {\n        try {\n          await navigator.clipboard.writeText(text);\n          setIsCopied(true);\n          setTimeout(() => setIsCopied(false), timeout);\n          return true;\n        } catch (err) {\n          console.error(\"Clipboard API failed:\", err);\n          // Fallback will be attempted below if this fails\n        }\n      }\n\n      // Fallback using document.execCommand\n      let success = false;\n      const tempTextArea = document.createElement(\"textarea\");\n      tempTextArea.value = text;\n      // Prevent scrolling to bottom\n      tempTextArea.style.position = \"fixed\";\n      tempTextArea.style.top = \"-9999px\";\n      tempTextArea.style.left = \"-9999px\";\n      document.body.appendChild(tempTextArea);\n\n      try {\n        tempTextArea.select();\n        tempTextArea.setSelectionRange(0, 99999); // For mobile devices\n        success = document.execCommand(\"copy\");\n\n        if (success) {\n          setIsCopied(true);\n          setTimeout(() => setIsCopied(false), timeout);\n        } else {\n          const cmdError = new Error(\"document.execCommand('copy') failed.\");\n          console.error(cmdError);\n          setError(cmdError);\n        }\n      } catch (err) {\n        const execError = err instanceof Error ? err : new Error(\"Error executing document.execCommand('copy').\");\n        console.error(execError);\n        setError(execError);\n        success = false; // Ensure success is false on error\n      } finally {\n        document.body.removeChild(tempTextArea);\n      }\n\n      return success;\n    },\n    [timeout],\n  );\n\n  return { copy, isCopied, isSupported, error };\n}\n"
  },
  {
    "path": "src/shared/hooks/use-premium-plans.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\n\ninterface PremiumPlan {\n  id: string;\n  internalId: string;\n  name: string;\n  type: string;\n  priceMonthly: number;\n  priceYearly: number;\n  currency: \"EUR\" | \"USD\" | \"GBP\";\n  features: string[];\n}\n\ninterface PlansResponse {\n  plans: PremiumPlan[];\n  detectedRegion: string;\n  debug?: {\n    headers: {\n      country: string | null;\n      acceptLanguage: string | null;\n      timezone: string | null;\n    };\n  };\n}\n\nexport function usePremiumPlans() {\n  return useQuery<PlansResponse>({\n    queryKey: [\"premium-plans\"],\n    queryFn: async () => {\n      // Get user timezone for better region detection\n      const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n\n      const response = await fetch(`/api/premium/plans?tz=${encodeURIComponent(timezone)}`);\n      if (!response.ok) {\n        throw new Error(\"Failed to fetch plans\");\n      }\n      const data = await response.json();\n      return data;\n    },\n    staleTime: 1000 * 60 * 30, // 30 minutes\n    gcTime: 1000 * 60 * 60, // 1 hour (previously cacheTime)\n  });\n}\n"
  },
  {
    "path": "src/shared/hooks/useBoolean.ts",
    "content": "import { Dispatch, SetStateAction, useCallback, useState } from \"react\";\n\nexport interface UseBooleanReturn {\n  value: boolean;\n  setValue: Dispatch<SetStateAction<boolean>>;\n  setTrue: () => void;\n  setFalse: () => void;\n  toggle: () => void;\n}\n\nfunction useBoolean(defaultValue?: boolean): UseBooleanReturn {\n  const [value, setValue] = useState(Boolean(defaultValue));\n\n  const setTrue = useCallback(() => setValue(true), []);\n  const setFalse = useCallback(() => setValue(false), []);\n  const toggle = useCallback(() => setValue((prev) => !prev), []);\n\n  return { value, setValue, setTrue, setFalse, toggle };\n}\n\nexport default useBoolean;\n"
  },
  {
    "path": "src/shared/hooks/useIsMobile.ts",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\n\nexport function useIsMobile(breakpoint: number = 768) {\n  const [isMobile, setIsMobile] = useState(false);\n\n  useEffect(() => {\n    // Check if window is defined (client-side)\n    if (typeof window === \"undefined\") return;\n\n    const checkIsMobile = () => {\n      setIsMobile(window.innerWidth < breakpoint);\n    };\n\n    // Initial check\n    checkIsMobile();\n\n    // Add event listener\n    window.addEventListener(\"resize\", checkIsMobile);\n\n    // Cleanup\n    return () => window.removeEventListener(\"resize\", checkIsMobile);\n  }, [breakpoint]);\n\n  return isMobile;\n}\n"
  },
  {
    "path": "src/shared/hooks/useScrollToTop.ts",
    "content": "import { useCallback } from \"react\";\n\n/**\n * Custom hook to handle scroll to top in a robust way\n * Handles cases where window.scrollTo doesn't work or isn't available\n * Takes into account containers with overflow-auto\n */\nexport function useScrollToTop() {\n  const scrollToTop = useCallback(() => {\n    try {\n      // Check if window is available (SSR safety)\n      if (typeof window === \"undefined\") {\n        console.warn(\"Window is not available - likely running on server\");\n        return;\n      }\n\n      // First look for a scrollable container with overflow-auto\n      const scrollableContainers = document.querySelectorAll(\".overflow-auto, [class*='overflow-auto']\");\n\n      if (scrollableContainers.length > 0) {\n        // Go through all scrollable containers and scroll them to the top\n        scrollableContainers.forEach((container) => {\n          if (container instanceof HTMLElement) {\n            container.scrollTo({ top: 0, behavior: \"smooth\" });\n          }\n        });\n\n        // Also try to scroll the main container that might have flex-1 overflow-auto\n        const mainContainer = document.querySelector(\".flex-1.overflow-auto\");\n        if (mainContainer instanceof HTMLElement) {\n          mainContainer.scrollTo({ top: 0, behavior: \"smooth\" });\n        }\n\n        return;\n      }\n\n      // If no scrollable container found, use classic methods\n      // Try window.scrollTo first (modern method)\n      if (window.scrollTo) {\n        window.scrollTo({ top: 0, behavior: \"smooth\" });\n      }\n\n      // Fallback 1: use scrollTop on documentElement\n      if (document.documentElement) {\n        document.documentElement.scrollTop = 0;\n      }\n\n      // Fallback 2: use scrollTop on body\n      if (document.body) {\n        document.body.scrollTop = 0;\n      }\n    } catch (error) {\n      console.error(\"Error scrolling to top:\", error);\n\n      // Emergency fallback: try to find any scrollable element\n      try {\n        const allElements = document.querySelectorAll(\"*\");\n        allElements.forEach((element) => {\n          if (element instanceof HTMLElement && element.scrollTop > 0) {\n            element.scrollTop = 0;\n          }\n        });\n      } catch (fallbackError) {\n        console.error(\"Fallback scroll failed:\", fallbackError);\n      }\n    }\n  }, []);\n\n  return scrollToTop;\n}\n"
  },
  {
    "path": "src/shared/lib/access-control.ts",
    "content": "/**\n * Access control utilities for program sessions\n * Determines user access based on authentication and premium status\n */\n\nexport interface AccessControlContext {\n  isAuthenticated: boolean;\n  isPremium: boolean;\n  isSessionPremium: boolean;\n}\n\nexport type AccessAction = \n  | \"allow\" \n  | \"require_auth\" \n  | \"require_premium\";\n\n/**\n * Determines what action should be taken based on user status and session requirements\n */\nexport function getSessionAccess(context: AccessControlContext): AccessAction {\n  const { isAuthenticated, isPremium, isSessionPremium } = context;\n\n  // Rule 1: Not authenticated -> require auth\n  if (!isAuthenticated) {\n    return \"require_auth\";\n  }\n\n  // Rule 2: Authenticated + free session -> allow\n  if (isAuthenticated && !isSessionPremium) {\n    return \"allow\";\n  }\n\n  // Rule 3: Authenticated + premium session + no subscription -> require premium\n  if (isAuthenticated && isSessionPremium && !isPremium) {\n    return \"require_premium\";\n  }\n\n  // Rule 4: Authenticated + premium session + has subscription -> allow\n  if (isAuthenticated && isSessionPremium && isPremium) {\n    return \"allow\";\n  }\n\n  // Fallback (should not happen)\n  return \"require_auth\";\n}\n\n/**\n * Helper to check if user can start the session\n */\nexport function canStartSession(context: AccessControlContext): boolean {\n  return getSessionAccess(context) === \"allow\";\n}"
  },
  {
    "path": "src/shared/lib/analytics/client.tsx",
    "content": "import { OpenPanelComponent, type PostEventPayload, useOpenPanel } from \"@openpanel/nextjs\";\n\nimport { env } from \"@/env\";\n\nconst isProd = process.env.NODE_ENV === \"production\";\n\nconst AnalyticsProvider = function () {\n  if (!env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID) {\n    return null;\n  }\n\n  return (\n    <OpenPanelComponent\n      clientId={env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID}\n      trackAttributes={true}\n      trackOutgoingLinks={isProd}\n      trackScreenViews={isProd}\n    />\n  );\n};\n\nconst track = (options: { event: string } & PostEventPayload[\"properties\"]) => {\n  if (!env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID) {\n    return;\n  }\n\n  if (!isProd) {\n    console.log(\"Track\", options);\n    return;\n  }\n\n  // eslint-disable-next-line react-hooks/rules-of-hooks\n  const { track: openTrack } = useOpenPanel();\n\n  const { event, ...rest } = options;\n\n  openTrack(event, rest);\n};\n\nexport { AnalyticsProvider, track };\n"
  },
  {
    "path": "src/shared/lib/analytics/events.ts",
    "content": "export const LogEvents = {\n  Registered: {\n    name: \"User Registered\",\n    channel: \"registered\",\n  },\n  EnrolledInProgram: {\n    name: \"User Enrolled in Program\",\n    channel: \"program\",\n  },\n  PremiumDiscovery: {\n    name: \"Premium Features Discovered\",\n    channel: \"premium\",\n  },\n  PaywallViewed: {\n    name: \"Paywall Viewed\",\n    channel: \"premium\",\n  },\n  PaywallPurchased: {\n    name: \"Paywall Purchase Completed\",\n    channel: \"premium\",\n  },\n  PaywallCancelled: {\n    name: \"Paywall Cancelled\",\n    channel: \"premium\",\n  },\n  PaywallRestored: {\n    name: \"Purchases Restored\",\n    channel: \"premium\",\n  },\n};\n"
  },
  {
    "path": "src/shared/lib/analytics/server.ts",
    "content": "import { cookies } from \"next/headers\";\nimport { waitUntil } from \"@vercel/functions\";\nimport { OpenPanel, type PostEventPayload } from \"@openpanel/nextjs\";\n\nimport { env } from \"@/env\";\n\ntype Props = {\n  userId?: string;\n  fullName?: string | null;\n  email?: string | null;\n};\n\nexport const setupAnalytics = async (options?: Props) => {\n  const cookiesStore = await cookies();\n  const { userId, fullName, email } = options ?? {};\n  const trackingConsent = !cookiesStore.has(\"tracking-consent\") || cookiesStore.get(\"tracking-consent\")?.value === \"1\";\n\n  const client = new OpenPanel({\n    clientId: env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID!,\n    clientSecret: env.OPENPANEL_SECRET_KEY,\n  });\n\n  if (trackingConsent && userId && fullName) {\n    const [firstName, lastName] = fullName.split(\" \");\n\n    waitUntil(\n      client.identify({\n        profileId: userId,\n        firstName,\n        lastName,\n        email: email ?? undefined,\n      }),\n    );\n  }\n\n  return {\n    track: (options: { event: string } & PostEventPayload[\"properties\"]) => {\n      if (env.NODE_ENV !== \"production\") {\n        console.log(\"Track\", options);\n        return;\n      }\n\n      const { event, ...rest } = options;\n\n      waitUntil(client.track(event, rest));\n    },\n  };\n};\n"
  },
  {
    "path": "src/shared/lib/attribute-value-translation.ts",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { TFunction } from \"locales/client\";\n\n/**\n * Map enum values to translation keys for i18n\n */\nexport const ATTRIBUTE_VALUE_TRANSLATION_KEYS: Record<ExerciseAttributeValueEnum, string> = {\n  // exercise types\n  BODYWEIGHT: \"workout_builder.attribute_value.bodyweight\",\n  STRENGTH: \"workout_builder.attribute_value.strength\",\n  POWERLIFTING: \"workout_builder.attribute_value.powerlifting\",\n  CALISTHENIC: \"workout_builder.attribute_value.calisthenic\",\n  PLYOMETRICS: \"workout_builder.attribute_value.plyometrics\",\n  STRETCHING: \"workout_builder.attribute_value.stretching\",\n  STRONGMAN: \"workout_builder.attribute_value.strongman\",\n  CARDIO: \"workout_builder.attribute_value.cardio\",\n  STABILIZATION: \"workout_builder.attribute_value.stabilization\",\n  POWER: \"workout_builder.attribute_value.power\",\n  RESISTANCE: \"workout_builder.attribute_value.resistance\",\n  CROSSFIT: \"workout_builder.attribute_value.crossfit\",\n  WEIGHTLIFTING: \"workout_builder.attribute_value.weightlifting\",\n\n  // muscles\n  BICEPS: \"workout_builder.muscles.biceps\",\n  SHOULDERS: \"workout_builder.muscles.shoulders\",\n  CHEST: \"workout_builder.muscles.chest\",\n  BACK: \"workout_builder.muscles.back\",\n  GLUTES: \"workout_builder.muscles.glutes\",\n  TRICEPS: \"workout_builder.muscles.triceps\",\n  HAMSTRINGS: \"workout_builder.muscles.hamstrings\",\n  QUADRICEPS: \"workout_builder.muscles.quadriceps\",\n  FOREARMS: \"workout_builder.muscles.forearms\",\n  CALVES: \"workout_builder.muscles.calves\",\n  TRAPS: \"workout_builder.muscles.traps\",\n  ABDOMINALS: \"workout_builder.muscles.abdominals\",\n  NECK: \"workout_builder.attribute_value.neck\",\n  LATS: \"workout_builder.attribute_value.lats\",\n  ADDUCTORS: \"workout_builder.attribute_value.adductors\",\n  ABDUCTORS: \"workout_builder.attribute_value.abductors\",\n  OBLIQUES: \"workout_builder.muscles.obliques\",\n  GROIN: \"workout_builder.attribute_value.groin\",\n  FULL_BODY: \"workout_builder.attribute_value.full_body\",\n  ROTATOR_CUFF: \"workout_builder.attribute_value.rotator_cuff\",\n  HIP_FLEXOR: \"workout_builder.attribute_value.hip_flexor\",\n  ACHILLES_TENDON: \"workout_builder.attribute_value.achilles_tendon\",\n  FINGERS: \"workout_builder.attribute_value.fingers\",\n\n  // equipment\n  DUMBBELL: \"workout_builder.equipment.dumbbell.label\",\n  KETTLEBELLS: \"workout_builder.equipment.kettlebell.label\",\n  BARBELL: \"workout_builder.equipment.barbell.label\",\n  SMITH_MACHINE: \"workout_builder.attribute_value.smith_machine\",\n  BODY_ONLY: \"workout_builder.equipment.bodyweight.label\",\n  OTHER: \"workout_builder.attribute_value.other\",\n  BANDS: \"workout_builder.equipment.band.label\",\n  EZ_BAR: \"workout_builder.attribute_value.ez_bar\",\n  MACHINE: \"workout_builder.attribute_value.machine\",\n  DESK: \"workout_builder.attribute_value.desk\",\n  PULLUP_BAR: \"workout_builder.equipment.pullup_bar.label\",\n  NONE: \"workout_builder.attribute_value.none\",\n  CABLE: \"workout_builder.attribute_value.cable\",\n  MEDICINE_BALL: \"workout_builder.attribute_value.medicine_ball\",\n  SWISS_BALL: \"workout_builder.attribute_value.swiss_ball\",\n  FOAM_ROLL: \"workout_builder.attribute_value.foam_roll\",\n  WEIGHT_PLATE: \"workout_builder.equipment.plate.label\",\n  TRX: \"workout_builder.attribute_value.trx\",\n  BOX: \"workout_builder.attribute_value.box\",\n  ROPES: \"workout_builder.attribute_value.ropes\",\n  SPIN_BIKE: \"workout_builder.attribute_value.spin_bike\",\n  STEP: \"workout_builder.attribute_value.step\",\n  BOSU: \"workout_builder.attribute_value.bosu\",\n  TYRE: \"workout_builder.attribute_value.tyre\",\n  SANDBAG: \"workout_builder.attribute_value.sandbag\",\n  POLE: \"workout_builder.attribute_value.pole\",\n  BENCH: \"workout_builder.equipment.bench.label\",\n  WALL: \"workout_builder.attribute_value.wall\",\n  BAR: \"workout_builder.attribute_value.bar\",\n  RACK: \"workout_builder.attribute_value.rack\",\n  CAR: \"workout_builder.attribute_value.car\",\n  SLED: \"workout_builder.attribute_value.sled\",\n  CHAIN: \"workout_builder.attribute_value.chain\",\n  SKIERG: \"workout_builder.attribute_value.skierg\",\n  ROPE: \"workout_builder.attribute_value.rope\",\n  NA: \"workout_builder.attribute_value.na\",\n\n  ISOLATION: \"workout_builder.attribute_value.isolation\",\n  COMPOUND: \"workout_builder.attribute_value.compound\",\n};\n\n/**\n * Get the localized label for an ExerciseAttributeValueEnum\n */\nexport function getAttributeValueLabel(value: ExerciseAttributeValueEnum, t: TFunction): string {\n  const key = ATTRIBUTE_VALUE_TRANSLATION_KEYS[value];\n  return key ? t(key as keyof typeof t) : value;\n}\n"
  },
  {
    "path": "src/shared/lib/date.ts",
    "content": "import relativeTime from \"dayjs/plugin/relativeTime\";\nimport dayjs from \"dayjs\";\nimport \"dayjs/locale/fr\";\nimport \"dayjs/locale/en\";\n\n\ndayjs.extend(relativeTime);\n\n/**\n * Default date formats for different locales\n */\nconst DEFAULT_FORMATS = {\n  en: \"MMMM D, YYYY\", // January 15, 2024\n  fr: \"D MMMM YYYY\", // 15 janvier 2024,\n  es: \"D MMMM YYYY\", // 15 de enero de 2024,\n  \"zh-CN\": \"YYYY年M月D日\", // 2024年1月15日,\n  ru: \"D MMMM YYYY\", // 15 января 2024,\n  pt: \"D MMMM YYYY\", // 15 de janeiro de 2024,\n} as const;\n\n\n/**\n * Short date formats for compact display\n */\nconst SHORT_FORMATS = {\n  en: \"MMM YYYY\", // Jan 2024\n  fr: \"MMM YYYY\", // janv. 2024\n  es: \"MMM YYYY\", // ene 2024\n  \"zh-CN\": \"YYYY年M月\", // 2024年1月\n  ru: \"MMM YYYY\", // янв 2024\n  pt: \"MMM YYYY\", // jan 2024\n} as const;\n\n\n/**\n * Date utility abstraction that properly handles locales\n * Abstracts dayjs usage according to FSD architecture\n */\nexport const formatDate = (date: string | Date, locale: string = \"en\", format?: string): string => {\n  const defaultFormat = DEFAULT_FORMATS[locale as keyof typeof DEFAULT_FORMATS] || DEFAULT_FORMATS.en;\n  return dayjs(date)\n    .locale(locale)\n    .format(format || defaultFormat);\n};\n\n/**\n * Get current date in specified locale\n */\nexport const getCurrentDate = (locale: string = \"en\"): dayjs.Dayjs => {\n  return dayjs().locale(locale);\n};\n\n\n/**\n * Format date for compact display (month + year)\n */\nexport const formatDateShort = (date: string | Date, locale: string = \"en\"): string => {\n  const shortFormat = SHORT_FORMATS[locale as keyof typeof SHORT_FORMATS] || SHORT_FORMATS.en;\n  return dayjs(date)\n    .locale(locale)\n    .format(shortFormat);\n};\n\n/**\n * Format relative time (e.g., \"2 hours ago\", \"3 days ago\")\n */\nexport const formatRelativeTime = (\n  date: string | Date | null,\n  locale: string = \"en\",\n  justNowText: string = \"just now\"\n): string | null => {\n  if (!date) return null;\n\n  const target = dayjs(date).locale(locale);\n  const now = dayjs().locale(locale);\n\n\n  // Safety check: if date is in the future, treat as \"just now\"\n  if (target.isAfter(now)) {\n    console.warn(\"date is in the future:\", target.format(), \"treating as \\\"just now\\\"\");\n    return justNowText;\n  }\n\n  // If less than 1 minute ago, show \"just now\" instead of \"in a few seconds\"\n  if (now.diff(target, \"minute\") < 1) {\n    return justNowText;\n  }\n\n  return target.fromNow();\n};\n\n/**\n * Parse date and set locale\n */\nexport const parseDate = (date: string | Date, locale: string = \"en\"): dayjs.Dayjs => {\n  return dayjs(date).locale(locale);\n};\n"
  },
  {
    "path": "src/shared/lib/format.ts",
    "content": "export function nullToUndefined<T>(value: T | null): T | undefined {\n  return value === null ? undefined : value;\n}\n"
  },
  {
    "path": "src/shared/lib/guards.ts",
    "content": "export const isObject = (value: unknown): value is Record<string, unknown> => {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\n};\n"
  },
  {
    "path": "src/shared/lib/i18n-mapper.ts",
    "content": "import { Locale } from \"locales/types\";\nimport { getLocaleSuffix } from \"@/shared/types/i18n.types\";\n\n/**\n * Generic mapper for i18n fields\n * Gets the correct field value based on locale\n */\nexport function getI18nField<T extends Record<string, any>>(\n  obj: T | null | undefined,\n  fieldName: string,\n  locale: Locale,\n  defaultValue: string = \"\"\n): string {\n  if (!obj) return defaultValue;\n  \n  const suffix = getLocaleSuffix(locale);\n  const fieldKey = suffix ? `${fieldName}${suffix}` : fieldName;\n  \n  return (obj[fieldKey] as string) || defaultValue;\n}\n\n/**\n * Maps common i18n fields for an object\n */\nexport function mapI18nFields<T extends Record<string, any>>(\n  obj: T | null | undefined,\n  locale: Locale\n) {\n  if (!obj) return null;\n  \n  return {\n    title: getI18nField(obj, \"title\", locale),\n    description: getI18nField(obj, \"description\", locale),\n    slug: getI18nField(obj, \"slug\", locale),\n    name: getI18nField(obj, \"name\", locale),\n  };\n}"
  },
  {
    "path": "src/shared/lib/locale-slug.ts",
    "content": "/**\n * Utility functions for getting localized slugs\n */\n\ninterface SlugData {\n  slug: string;\n  slugEn: string;\n  slugEs: string;\n  slugPt: string;\n  slugRu: string;\n  slugZhCn: string;\n}\n\n/**\n * Gets the appropriate slug for the given locale\n */\nexport function getSlugForLocale(slugData: SlugData, locale: string): string {\n  switch (locale) {\n    case \"en\":\n      return slugData.slugEn;\n    case \"es\":\n      return slugData.slugEs;\n    case \"pt\":\n      return slugData.slugPt;\n    case \"ru\":\n      return slugData.slugRu;\n    case \"zh-CN\":\n      return slugData.slugZhCn;\n    default:\n      return slugData.slug; // French default\n  }\n}\n\n/**\n * Gets the appropriate title for the given locale\n */\ninterface TitleData {\n  title: string;\n  titleEn: string;\n  titleEs: string;\n  titlePt: string;\n  titleRu: string;\n  titleZhCn: string;\n}\n\nexport function getTitleForLocale(titleData: TitleData, locale: string): string {\n  switch (locale) {\n    case \"en\":\n      return titleData.titleEn;\n    case \"es\":\n      return titleData.titleEs;\n    case \"pt\":\n      return titleData.titlePt;\n    case \"ru\":\n      return titleData.titleRu;\n    case \"zh-CN\":\n      return titleData.titleZhCn;\n    default:\n      return titleData.title; // French default\n  }\n}"
  },
  {
    "path": "src/shared/lib/location/eu-countries.ts",
    "content": "export const EU_COUNTRY_CODES = [\n  \"AT\",\n  \"BE\",\n  \"BG\",\n  \"HR\",\n  \"CY\",\n  \"CZ\",\n  \"DK\",\n  \"EE\",\n  \"FI\",\n  \"FR\",\n  \"DE\",\n  \"GR\",\n  \"HU\",\n  \"IE\",\n  \"IT\",\n  \"LV\",\n  \"LT\",\n  \"LU\",\n  \"MT\",\n  \"NL\",\n  \"PL\",\n  \"PT\",\n  \"RO\",\n  \"SK\",\n  \"SI\",\n  \"ES\",\n  \"SE\",\n  \"GB\",\n  \"GI\",\n  \"IS\",\n  \"LI\",\n  \"NO\",\n  \"CH\",\n  \"ME\",\n  \"MK\",\n  \"RS\",\n  \"TR\",\n  \"AL\",\n  \"BA\",\n  \"XK\",\n  \"AD\",\n  \"BY\",\n  \"MD\",\n  \"MC\",\n  \"RU\",\n  \"UA\",\n  \"VA\",\n  \"AX\",\n  \"FO\",\n  \"GL\",\n  \"SJ\",\n  \"IM\",\n  \"JE\",\n  \"GG\",\n  \"RS\",\n  \"ME\",\n  \"XK\",\n  \"RS\",\n];\n"
  },
  {
    "path": "src/shared/lib/location/location.ts",
    "content": "import { headers } from \"next/headers\";\n\nimport { EU_COUNTRY_CODES } from \"@/shared/lib/location/eu-countries\";\n\nexport async function getCountryCode() {\n  const headersList = await headers();\n\n  return headersList.get(\"x-vercel-ip-country\") || \"SE\";\n}\n\nexport async function isEU() {\n  const countryCode = await getCountryCode();\n\n  if (countryCode && EU_COUNTRY_CODES.includes(countryCode)) {\n    return true;\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "src/shared/lib/logger.ts",
    "content": "import { Logger } from \"tslog\";\n\nexport const logger = new Logger({\n  name: \"AppLogger\",\n  // Don't use `env` here, because we can use the logger in the browser\n  minLevel: process.env.NODE_ENV === \"production\" ? 3 : 0,\n});\n"
  },
  {
    "path": "src/shared/lib/mail/sendEmail.ts",
    "content": "import nodemailer from \"nodemailer\";\nimport { render } from \"@react-email/components\";\n\nimport { env } from \"@/env\";\n\ntype EmailPayload = {\n  from?: string;\n  to: string;\n  subject: string;\n  text: string;\n  react?: React.ReactElement;\n};\n\nconst transporter = nodemailer.createTransport({\n  host: env.SMTP_HOST,\n  port: env.SMTP_PORT,\n  secure: env.SMTP_SECURE,\n  auth:\n    env.SMTP_USER && env.SMTP_PASS\n      ? {\n          user: env.SMTP_USER,\n          pass: env.SMTP_PASS,\n        }\n      : undefined,\n});\n\nexport const sendEmail = async ({ from, to, subject, text, react }: EmailPayload) => {\n  try {\n    return transporter.sendMail({\n      from: from ?? env.SMTP_FROM,\n      to,\n      subject,\n      text,\n      html: react ? await render(react) : undefined,\n    });\n  } catch (error) {\n    console.error(error);\n  }\n};\n"
  },
  {
    "path": "src/shared/lib/mdx/load-mdx.ts",
    "content": "import path from \"path\";\nimport { readFile } from \"fs/promises\";\n\nimport { compileMDX } from \"next-mdx-remote/rsc\";\n\nimport { WorkoutLol } from \"@/components/ui/workout-lol\";\n\nexport async function getLocalizedMdx(\n  pageSlug: string, // ex: \"privacy-policy\"\n  locale: string, // ex: \"fr\" or \"en\"\n) {\n  const filePath = path.join(process.cwd(), \"content\", pageSlug, `${locale}.mdx`);\n\n  const source = await readFile(filePath, \"utf-8\");\n\n  const { content } = await compileMDX({\n    source,\n    options: {\n      parseFrontmatter: true,\n    },\n    components: {\n      WorkoutLol,\n    },\n  });\n\n  return content;\n}\n"
  },
  {
    "path": "src/shared/lib/network/use-network-status.ts",
    "content": "// src/shared/lib/network/useNetworkStatus.ts\nimport { useEffect, useState } from \"react\";\n\nexport function useNetworkStatus() {\n  const [isOnline, setIsOnline] = useState(typeof window !== \"undefined\" ? navigator.onLine : true);\n\n  useEffect(() => {\n    const handleOnline = () => setIsOnline(true);\n    const handleOffline = () => setIsOnline(false);\n    window.addEventListener(\"online\", handleOnline);\n    window.addEventListener(\"offline\", handleOffline);\n    return () => {\n      window.removeEventListener(\"online\", handleOnline);\n      window.removeEventListener(\"offline\", handleOffline);\n    };\n  }, []);\n\n  return { isOnline };\n}\n"
  },
  {
    "path": "src/shared/lib/premium/premium.manager.ts",
    "content": "import { PaymentProcessor, PlanProviderMapping } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\n\nimport { StripeProvider } from \"./providers/stripe-provider\";\nimport { PremiumService } from \"./premium.service\";\n\nimport type { PremiumPlan, CheckoutResult } from \"@/shared/types/premium.types\";\nimport type { PaymentProvider } from \"./providers/base-provider\";\n\n/**\n * Premium Manager - KISS orchestrator\n *\n * Simple manager that coordinates between providers and services\n * Easy to extend with new payment providers\n */\nexport class PremiumManager {\n  private static providers: Record<string, PaymentProvider> = {\n    stripe: new StripeProvider(),\n  };\n\n  /**\n   * Get available premium plans with provider mappings\n   * Returns 3 plans: Free, Supporter, Premium - fitness focused\n   */\n  static async getAvailablePlans(provider?: string, region?: string): Promise<PremiumPlan[]> {\n    // Try to get plans from database first\n    const dbPlans = await prisma.subscriptionPlan.findMany({\n      where: {\n        isActive: true,\n        // Filter by region if specified\n        ...(region && {\n          availableRegions: {\n            hasSome: [region],\n          },\n        }),\n      },\n      include: {\n        providerMappings: {\n          where: {\n            isActive: true,\n            ...(provider && { provider: provider.toUpperCase() as PaymentProcessor }),\n            ...(region && { region }),\n          },\n        },\n      },\n      orderBy: { priceMonthly: \"asc\" },\n    });\n\n    // Convert database plans to PremiumPlan format\n    let paidPlans: PremiumPlan[] = [];\n\n    if (dbPlans.length > 0) {\n      paidPlans = dbPlans.map((plan) => {\n        // Get the appropriate provider mapping\n        const mapping = plan.providerMappings.find(\n          (m) => (!provider || m.provider === provider.toUpperCase()) && (!region || m.region === region || !m.region),\n        );\n\n        // Determine plan type from pricing structure\n        const isYearly = plan.priceYearly && plan.priceYearly.toNumber() > 0;\n        const planType = isYearly ? \"yearly\" : \"monthly\";\n\n        return {\n          id: mapping?.externalId || plan.id,\n          internalId: plan.id, // Keep internal ID for database operations\n          name: `Premium ${planType.charAt(0).toUpperCase() + planType.slice(1)}`,\n          type: \"premium\", // Database plans are premium by default\n          priceMonthly: plan.priceMonthly?.toNumber() || 0,\n          priceYearly: plan.priceYearly?.toNumber() || 0,\n          currency: (plan.currency || \"EUR\") as \"EUR\" | \"USD\",\n          features: [], // Features handled client-side\n        };\n      });\n    }\n\n    return paidPlans;\n  }\n\n  /**\n   * Create checkout for a plan using provider mapping\n   */\n  static async createCheckout(userId: string, planId: string, provider: string = \"stripe\", region?: string): Promise<CheckoutResult> {\n    const paymentProvider = this.providers[provider];\n    if (!paymentProvider) {\n      return {\n        success: false,\n        error: `Provider ${provider} not supported`,\n        provider: provider as any,\n      };\n    }\n\n    // Get plans filtered by provider and region\n    const plans = await this.getAvailablePlans(provider, region);\n    const plan = plans.find((p) => p.id === planId);\n    if (!plan) {\n      return {\n        success: false,\n        error: \"Plan not found\",\n        provider: provider as any,\n      };\n    }\n\n    return paymentProvider.createCheckoutSession(userId, plan);\n  }\n\n  /**\n   * Process webhook from any provider\n   */\n  static async processWebhook(provider: string, payload: any, signature: string): Promise<{ success: boolean; error?: string }> {\n    const paymentProvider = this.providers[provider];\n    if (!paymentProvider) {\n      return { success: false, error: `Provider ${provider} not supported` };\n    }\n\n    try {\n      const result = await paymentProvider.processWebhook(payload, signature);\n\n      if (result.success && result.userId && result.action) {\n        // Update user premium status based on webhook\n\n        switch (result.action) {\n          case \"subscription_created\":\n          case \"payment_succeeded\":\n          case \"subscription_updated\":\n            if (result.expiresAt) {\n              await PremiumService.grantPremiumAccess(result.userId, result.expiresAt, {\n                planId: result.planId,\n                platform: result.platform,\n                paymentProcessor: provider === \"stripe\" ? \"STRIPE\" : (\"OTHER\" as any),\n              });\n            }\n            break;\n\n          case \"subscription_cancelled\":\n            await PremiumService.revokePremiumAccess(result.userId, result.platform);\n            break;\n        }\n      }\n\n      return { success: result.success, error: result.error };\n    } catch (error) {\n      console.error(\"Webhook processing error:\", error);\n      return {\n        success: false,\n        error: error instanceof Error ? error.message : \"Unknown error\",\n      };\n    }\n  }\n\n  /**\n   * Get plan by external ID across all providers\n   */\n  static async getPlanByInternalId(internalId: string, provider: string): Promise<PlanProviderMapping> {\n    const mapping = await prisma.planProviderMapping.findFirst({\n      where: {\n        planId: internalId,\n        provider: provider.toUpperCase() as any,\n        isActive: true,\n      },\n      include: {\n        plan: true,\n      },\n    });\n\n    if (!mapping) {\n      throw new Error(`Plan ${internalId} not found for provider ${provider}`);\n    }\n\n    return mapping;\n  }\n\n  /**\n   * Get plan by external ID across all providers\n   */\n  static async getPlanByExternalId(externalId: string, provider: string): Promise<any> {\n    const mapping = await prisma.planProviderMapping.findFirst({\n      where: {\n        externalId,\n        provider: provider.toUpperCase() as any,\n        isActive: true,\n      },\n      include: {\n        plan: true,\n      },\n    });\n\n    return mapping?.plan;\n  }\n\n  /**\n   * Create billing portal session for subscription management\n   */\n  static async createBillingPortal(userId: string, provider: string = \"stripe\", returnUrl?: string): Promise<CheckoutResult> {\n    const paymentProvider = this.providers[provider];\n    if (!paymentProvider) {\n      return {\n        success: false,\n        error: `Provider ${provider} not supported`,\n        provider: provider as any,\n      };\n    }\n\n    // For Stripe, we need to find the customer ID\n    if (provider === \"stripe\") {\n      const stripeProvider = paymentProvider as StripeProvider;\n\n      // Get customer by user ID\n      const customer = await stripeProvider.getCustomerByUserId(userId);\n\n      if (!customer) {\n        return {\n          success: false,\n          error: \"No Stripe customer found. Please subscribe first.\",\n          provider: \"stripe\",\n        };\n      }\n\n      return stripeProvider.createPortalSession(customer.id, returnUrl);\n    }\n\n    return {\n      success: false,\n      error: \"Billing portal not implemented for this provider\",\n      provider: provider as any,\n    };\n  }\n\n  /**\n   * Add new payment provider (for future expansion)\n   */\n  static addProvider(name: string, provider: PaymentProvider): void {\n    this.providers[name] = provider;\n  }\n}\n"
  },
  {
    "path": "src/shared/lib/premium/premium.service.ts",
    "content": "import { PaymentProcessor, Platform } from \"@prisma/client\";\n\nimport { revenueCatApi } from \"@/shared/lib/revenuecat\";\nimport { prisma } from \"@/shared/lib/prisma\";\n\nimport type { PremiumStatus, UserSubscription } from \"@/shared/types/premium.types\";\n\n/**\n * Premium Service - KISS approach\n *\n * Single responsibility: Determine if a user has premium access\n * Provider agnostic: Works with any payment system\n * Type safe: Strict TypeScript to prevent errors\n */\nexport class PremiumService {\n  /**\n   * Check if user has premium access\n   */\n  static async checkUserPremiumStatus(userId: string): Promise<PremiumStatus> {\n    const user = await prisma.user.findUnique({\n      where: { id: userId },\n      // select: {\n      //   isPremium: true,\n      //   subscriptions: {\n      //     where: { status: \"ACTIVE\" },\n      //     select: {\n      //       currentPeriodEnd: true,\n      //       plan: {\n      //         select: { id: true },\n      //       },\n      //     },\n      //     orderBy: { createdAt: \"desc\" },\n      //     take: 1,\n      //   },\n      // },\n    });\n\n    if (!user || !user.isPremium) {\n      return { isPremium: false };\n    }\n\n    return { isPremium: true };\n  }\n\n  /**\n   * Get user subscription info for UI\n   */\n  static async getUserSubscription(userId: string): Promise<UserSubscription> {\n    const premiumStatus = await this.checkUserPremiumStatus(userId);\n\n    if (!premiumStatus.isPremium) {\n      return { isActive: false };\n    }\n\n    return {\n      isActive: true,\n      nextBillingDate: premiumStatus.expiresAt,\n      cancelAtPeriodEnd: false, // TODO: implement based on provider\n    };\n  }\n\n  /**\n   * Grant premium access (for webhooks or admin)\n   * Creates/updates subscription record and maintains backward compatibility\n   */\n  static async grantPremiumAccess(\n    userId: string,\n    expiresAt: Date,\n    options?: {\n      planId?: string;\n      platform?: Platform;\n      paymentProcessor?: PaymentProcessor;\n      revenueCatUserId?: string;\n    },\n  ): Promise<void> {\n    // Get default premium plan if not specified\n    let planId = options?.planId;\n    if (!planId) {\n      const defaultPlan = await prisma.subscriptionPlan.findFirst({\n        where: { isActive: true },\n        orderBy: { priceMonthly: \"asc\" }, // Get cheapest as default\n      });\n      planId = defaultPlan?.id;\n    }\n\n    // Transaction to ensure data consistency\n    await prisma.$transaction(async (tx) => {\n      // Update user isPremium flag\n      await tx.user.update({\n        where: { id: userId },\n        data: {\n          isPremium: true,\n        },\n      });\n\n      // Create or update subscription record\n      if (planId) {\n        const platform = options?.platform || Platform.WEB;\n\n        await tx.subscription.upsert({\n          where: {\n            userId_platform: {\n              userId,\n              platform,\n            },\n          },\n          update: {\n            status: \"ACTIVE\",\n            currentPeriodEnd: expiresAt,\n            planId,\n            updatedAt: new Date(),\n          },\n          create: {\n            userId,\n            planId,\n            platform,\n            status: \"ACTIVE\",\n            startedAt: new Date(),\n            currentPeriodEnd: expiresAt,\n            revenueCatUserId: options?.revenueCatUserId,\n          },\n        });\n      }\n    });\n  }\n\n  /**\n   * Assign RevenueCat user ID to a user\n   * Links RevenueCat user ID to BetterAuth user account\n   */\n  static async assignRevenueCatUserId(userId: string, revenueCatUserId: string): Promise<void> {\n    console.log(`Assigning RevenueCat user ID ${revenueCatUserId} to user ${userId}`);\n\n    // Check if RevenueCat user ID is already assigned to another user\n    const existingUser = await prisma.user.findFirst({\n      where: {\n        subscriptions: {\n          some: {\n            revenueCatUserId: revenueCatUserId,\n            userId: { not: userId },\n          },\n        },\n      },\n    });\n\n    if (existingUser) {\n      throw new Error(`RevenueCat user ID ${revenueCatUserId} is already assigned to another user`);\n    }\n\n    // Get or create a default subscription plan\n    let defaultPlan = await prisma.subscriptionPlan.findFirst({\n      where: { isActive: true },\n      orderBy: { priceMonthly: \"asc\" },\n    });\n\n    if (!defaultPlan) {\n      // Create a default plan if none exists\n      defaultPlan = await prisma.subscriptionPlan.create({\n        data: {\n          priceMonthly: 0,\n          priceYearly: 0,\n          currency: \"EUR\",\n          interval: \"month\",\n          isActive: true,\n          availableRegions: [],\n        },\n      });\n    }\n\n    // Create or update subscription record with RevenueCat user ID\n    await prisma.subscription.upsert({\n      where: {\n        userId_platform: {\n          userId,\n          platform: Platform.IOS, // Default to iOS for mobile\n        },\n      },\n      update: {\n        revenueCatUserId,\n        updatedAt: new Date(),\n      },\n      create: {\n        userId,\n        planId: defaultPlan.id,\n        platform: Platform.IOS,\n        status: \"ACTIVE\",\n        startedAt: new Date(),\n        revenueCatUserId,\n      },\n    });\n\n    // After assigning the RevenueCat user ID, sync the current subscription status\n    // This ensures the user's premium status is accurate based on current entitlements\n    try {\n      await this.syncRevenueCatStatus(userId, revenueCatUserId);\n    } catch (error) {\n      console.error(\"Error syncing RevenueCat status after assignment:\", error);\n      // Don't throw here to avoid breaking the assignment flow\n      // The sync failure will be logged but won't prevent assignment\n    }\n  }\n\n  /**\n   * Link anonymous RevenueCat user to authenticated BetterAuth user\n   * Migrates subscription data from anonymous to authenticated user\n   */\n  static async linkRevenueCatUser(authenticatedUserId: string, anonymousRevenueCatUserId: string): Promise<void> {\n    console.log(`Linking anonymous RevenueCat user ${anonymousRevenueCatUserId} to authenticated user ${authenticatedUserId}`);\n\n    await prisma.$transaction(async (tx) => {\n      // Find subscriptions with the anonymous RevenueCat user ID\n      const anonymousSubscriptions = await tx.subscription.findMany({\n        where: {\n          revenueCatUserId: anonymousRevenueCatUserId,\n        },\n      });\n\n      console.log(`Found ${anonymousSubscriptions.length} anonymous subscriptions to transfer`);\n\n      // Transfer subscriptions to authenticated user\n      for (const subscription of anonymousSubscriptions) {\n        await tx.subscription.update({\n          where: { id: subscription.id },\n          data: {\n            userId: authenticatedUserId,\n            updatedAt: new Date(),\n          },\n        });\n      }\n\n      // Update user premium status if any active subscriptions exist\n      const hasActiveSubscriptions = anonymousSubscriptions.some((sub) => sub.status === \"ACTIVE\");\n\n      if (hasActiveSubscriptions) {\n        await tx.user.update({\n          where: { id: authenticatedUserId },\n          data: { isPremium: true },\n        });\n      }\n    });\n\n    // After linking, sync the current RevenueCat subscription status\n    // This ensures the user's premium status is accurate based on current entitlements\n    try {\n      await this.syncRevenueCatStatus(authenticatedUserId, anonymousRevenueCatUserId);\n    } catch (error) {\n      console.error(\"Error syncing RevenueCat status after linking:\", error);\n      // Don't throw here to avoid breaking the linking flow\n      // The sync failure will be logged but won't prevent account linking\n    }\n  }\n\n  /**\n   * Get user by RevenueCat user ID\n   */\n  static async getUserByRevenueCatId(revenueCatUserId: string) {\n    const subscription = await prisma.subscription.findFirst({\n      where: {\n        revenueCatUserId: revenueCatUserId,\n      },\n      include: {\n        user: true,\n      },\n    });\n\n    return subscription?.user || null;\n  }\n\n  /**\n   * Check if user has RevenueCat user ID assigned\n   */\n  static async hasRevenueCatUserId(userId: string): Promise<boolean> {\n    const subscription = await prisma.subscription.findFirst({\n      where: {\n        userId: userId,\n        revenueCatUserId: { not: null },\n      },\n    });\n\n    return !!subscription;\n  }\n\n  /**\n   * Get user's RevenueCat user ID\n   */\n  static async getUserRevenueCatId(userId: string): Promise<string | null> {\n    const subscription = await prisma.subscription.findFirst({\n      where: {\n        userId: userId,\n        revenueCatUserId: { not: null },\n      },\n    });\n\n    return subscription?.revenueCatUserId || null;\n  }\n\n  /**\n   * Revoke premium access\n   * Updates subscription status and maintains backward compatibility\n   */\n  static async revokePremiumAccess(userId: string, platform?: Platform): Promise<void> {\n    await prisma.$transaction(async (tx) => {\n      // Update user isPremium flag\n      await tx.user.update({\n        where: { id: userId },\n        data: {\n          isPremium: false,\n        },\n      });\n\n      // Cancel active subscriptions\n      if (platform) {\n        // Cancel specific platform subscription\n        await tx.subscription.updateMany({\n          where: {\n            userId,\n            platform,\n            status: \"ACTIVE\",\n          },\n          data: {\n            status: \"CANCELLED\",\n            cancelledAt: new Date(),\n          },\n        });\n      } else {\n        // Cancel all active subscriptions\n        await tx.subscription.updateMany({\n          where: {\n            userId,\n            status: \"ACTIVE\",\n          },\n          data: {\n            status: \"CANCELLED\",\n            cancelledAt: new Date(),\n          },\n        });\n      }\n    });\n  }\n\n  /**\n   * Fetch current subscription status from RevenueCat API\n   * This method queries RevenueCat directly to get the most up-to-date subscription status\n   */\n  static async fetchRevenueCatSubscriptionStatus(revenueCatUserId: string): Promise<{\n    isPremium: boolean;\n    expiresAt: Date | null;\n    entitlementIds: string[];\n    error?: string;\n  }> {\n    try {\n      console.log(`Fetching RevenueCat subscription status for user: ${revenueCatUserId}`);\n\n      // Check if user has active entitlements\n      const hasActiveEntitlements = await revenueCatApi.hasActiveEntitlements(revenueCatUserId);\n\n      if (!hasActiveEntitlements) {\n        console.log(`No active entitlements found for RevenueCat user: ${revenueCatUserId}`);\n        return {\n          isPremium: false,\n          expiresAt: null,\n          entitlementIds: [],\n        };\n      }\n\n      // Get entitlement details\n      const [expiresAt, entitlementIds] = await Promise.all([\n        revenueCatApi.getLatestExpirationDate(revenueCatUserId),\n        revenueCatApi.getActiveEntitlementIds(revenueCatUserId),\n      ]);\n\n      console.log(`RevenueCat subscription status for ${revenueCatUserId}:`, {\n        isPremium: true,\n        expiresAt,\n        entitlementIds,\n      });\n\n      return {\n        isPremium: true,\n        expiresAt,\n        entitlementIds,\n      };\n    } catch (error) {\n      console.error(\"Error fetching RevenueCat subscription status:\", error);\n\n      const errorMessage = error instanceof Error ? error.message : \"Unknown error\";\n\n      return {\n        isPremium: false,\n        expiresAt: null,\n        entitlementIds: [],\n        error: errorMessage,\n      };\n    }\n  }\n\n  /**\n   * Sync user's premium status with RevenueCat\n   * Updates the database with the current RevenueCat subscription status\n   */\n  static async syncRevenueCatStatus(userId: string, revenueCatUserId: string): Promise<void> {\n    try {\n      console.log(`Syncing RevenueCat status for user ${userId} with RevenueCat user ${revenueCatUserId}`);\n\n      // First, process any pending webhook events\n      await this.processPendingWebhookEvents(userId, revenueCatUserId);\n\n      // Fetch current status from RevenueCat\n      const revenueCatStatus = await this.fetchRevenueCatSubscriptionStatus(revenueCatUserId);\n      console.log(\"revenueCatStatus before sync:\", revenueCatStatus);\n\n      if (revenueCatStatus.error) {\n        console.warn(`Failed to fetch RevenueCat status, skipping sync: ${revenueCatStatus.error}`);\n        return;\n      }\n\n      // Update user's premium status in database\n      await prisma.$transaction(async (tx) => {\n        // Update user's isPremium flag\n        await tx.user.update({\n          where: { id: userId },\n          data: { isPremium: revenueCatStatus.isPremium },\n        });\n\n        // Update subscription records\n        await tx.subscription.updateMany({\n          where: { userId, revenueCatUserId },\n          data: {\n            status: revenueCatStatus.isPremium ? \"ACTIVE\" : \"EXPIRED\",\n            currentPeriodEnd: revenueCatStatus.expiresAt,\n            revenueCatUserId,\n            updatedAt: new Date(),\n          },\n        });\n      });\n\n      console.log(`Successfully synced RevenueCat status for user ${userId}: isPremium=${revenueCatStatus.isPremium}`);\n    } catch (error) {\n      console.error(\"Error syncing RevenueCat status:\", error);\n\n      // Don't throw here to avoid breaking the user flow\n      // The sync failure will be logged but won't prevent account linking\n    }\n  }\n\n  /**\n   * Process pending webhook events for a user\n   * This handles anonymous purchases that happened before authentication\n   */\n  static async processPendingWebhookEvents(userId: string, revenueCatUserId: string): Promise<void> {\n    try {\n      console.log(`Processing pending webhook events for user ${userId}`);\n\n      // Find pending events for the anonymous RevenueCat user ID\n      const pendingEvents = await prisma.revenueCatWebhookEvent.findMany({\n        where: {\n          appUserId: revenueCatUserId,\n          processed: false,\n        },\n        orderBy: {\n          eventTimestamp: \"asc\",\n        },\n      });\n\n      if (pendingEvents.length === 0) {\n        console.log(\"No pending webhook events found\");\n        return;\n      }\n\n      console.log(`Found ${pendingEvents.length} pending webhook events to process`);\n\n      // Process each event\n      for (const event of pendingEvents) {\n        console.log(`Processing ${event.eventType} event from ${event.eventTimestamp}`);\n\n        // Update the event with the authenticated user ID and mark as processed\n        await prisma.revenueCatWebhookEvent.update({\n          where: { id: event.id },\n          data: {\n            processed: true,\n            processingError: null,\n            updatedAt: new Date(),\n          },\n        });\n      }\n\n      console.log(\"Finished processing pending webhook events\");\n    } catch (error) {\n      console.error(\"Error processing pending webhook events:\", error);\n      // Don't throw - continue with the sync even if event processing fails\n    }\n  }\n}\n"
  },
  {
    "path": "src/shared/lib/premium/providers/base-provider.ts",
    "content": "import type { CheckoutResult, PremiumPlan } from \"@/shared/types/premium.types\";\n\n/**\n * Base Payment Provider Interface\n *\n * KISS approach: Simple interface that any provider can implement\n * Easy to switch between Stripe, LemonSqueezy, PayPal, etc.\n */\nexport interface PaymentProvider {\n  name: string;\n\n  /**\n   * Create checkout session for a plan\n   */\n  createCheckoutSession(userId: string, plan: PremiumPlan, options?: CheckoutOptions): Promise<CheckoutResult>;\n\n  /**\n   * Verify webhook signature\n   */\n  verifyWebhookSignature(payload: string, signature: string, secret: string): boolean;\n\n  /**\n   * Process webhook event\n   */\n  processWebhook(payload: any, signature: string): Promise<WebhookResult>;\n}\n\nexport interface CheckoutOptions {\n  successUrl?: string;\n  cancelUrl?: string;\n  metadata?: Record<string, string>;\n}\n\nexport interface WebhookResult {\n  success: boolean;\n  userId?: string;\n  action?: \"subscription_created\" | \"subscription_updated\" | \"subscription_cancelled\" | \"payment_succeeded\" | \"payment_failed\";\n  expiresAt?: Date;\n  planId?: string;\n  platform?: \"WEB\" | \"IOS\" | \"ANDROID\";\n  paymentId?: string;\n  amount?: number;\n  currency?: string;\n  error?: string;\n}\n"
  },
  {
    "path": "src/shared/lib/premium/providers/stripe-provider.ts",
    "content": "/* eslint-disable no-fallthrough */\nimport Stripe from \"stripe\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\nimport { env } from \"@/env\";\n\nimport type { CheckoutResult, PremiumPlan } from \"@/shared/types/premium.types\";\nimport type { CheckoutOptions, PaymentProvider, WebhookResult } from \"./base-provider\";\n\n/**\n * Stripe Payment Provider\n *\n * Simple implementation of PaymentProvider interface\n * Easy to understand and maintain\n */\nexport class StripeProvider implements PaymentProvider {\n  name = \"stripe\";\n  private stripe: Stripe;\n\n  constructor() {\n    if (!env.STRIPE_SECRET_KEY) {\n      throw new Error(\"STRIPE_SECRET_KEY is required\");\n    }\n\n    this.stripe = new Stripe(env.STRIPE_SECRET_KEY, {\n      apiVersion: \"2025-05-28.basil\",\n    });\n  }\n\n  /**\n   * Create checkout session for a plan\n   */\n  async createCheckoutSession(userId: string, plan: PremiumPlan, options?: CheckoutOptions): Promise<CheckoutResult> {\n    try {\n      // Create or get existing customer\n      const customer = await this.getOrCreateCustomer(userId);\n\n      const session = await this.stripe.checkout.sessions.create({\n        mode: \"subscription\",\n        // payment_method_types: [\"card\", \"paypal\", \"apple_pay\", \"google_pay\", \"revolut_pay\", \"samsung_pay\", \"link\", \"klarna\"],\n        customer: customer.id,\n        line_items: [\n          {\n            price: plan.id, // Stripe price ID\n            quantity: 1,\n          },\n        ],\n        automatic_tax: {\n          enabled: true,\n        },\n        metadata: {\n          userId,\n          planId: plan.internalId || plan.id,\n          ...options?.metadata,\n        },\n        subscription_data: {\n          metadata: {\n            userId,\n            planId: plan.internalId || plan.id,\n          },\n        },\n        success_url: options?.successUrl || `${env.NEXT_PUBLIC_APP_URL}/premium?success=true`,\n        cancel_url: options?.cancelUrl || `${env.NEXT_PUBLIC_APP_URL}/premium?cancelled=true`,\n        allow_promotion_codes: true,\n        billing_address_collection: \"auto\",\n        customer_update: {\n          name: \"auto\",\n          address: \"auto\",\n        },\n        tax_id_collection: {\n          enabled: true,\n        },\n      });\n\n      return {\n        success: true,\n        checkoutUrl: session.url!,\n        provider: \"stripe\",\n      };\n    } catch (error) {\n      console.error(\"Stripe checkout session creation failed:\", error);\n      return {\n        success: false,\n        error: error instanceof Error ? error.message : \"Unknown error\",\n        provider: \"stripe\",\n      };\n    }\n  }\n\n  /**\n   * Verify webhook signature\n   */\n  verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {\n    try {\n      this.stripe.webhooks.constructEvent(payload, signature, secret);\n      return true;\n    } catch (error) {\n      console.error(\"Stripe webhook signature verification failed:\", error);\n      return false;\n    }\n  }\n\n  /**\n   * Process webhook event\n   */\n  async processWebhook(payload: string, signature: string): Promise<WebhookResult> {\n    try {\n      if (!env.STRIPE_WEBHOOK_SECRET) {\n        throw new Error(\"STRIPE_WEBHOOK_SECRET is required\");\n      }\n\n      const event = this.stripe.webhooks.constructEvent(payload, signature, env.STRIPE_WEBHOOK_SECRET);\n\n      switch (event.type) {\n        case \"checkout.session.completed\": {\n          // First payment is successful and the subscription is created | or the subscription was canceled so create new one\n          const session = event.data.object as Stripe.Checkout.Session;\n\n          if (session.mode === \"subscription\" && session.subscription) {\n            const subscription = await this.stripe.subscriptions.retrieve(session.subscription as string);\n\n            // Extra: You might want to send a welcome email here\n            // Example: await sendEmail.welcome(user.email, subscription);\n\n            // Extra: Track conversion in analytics\n            // Example: await analytics.track('subscription_created', { planId, userId });\n\n            return {\n              success: true,\n              userId: session.metadata?.userId,\n              action: \"subscription_created\",\n              expiresAt: new Date(subscription.items.data[0].current_period_end * 1000),\n              planId: session.metadata?.planId,\n              platform: \"WEB\",\n            };\n          }\n          break;\n        }\n\n        case \"invoice.payment_succeeded\": {\n          // Sent each billing interval when payment succeeds (renewal)\n          // Also sent when switching plans if additional payment is needed\n          const invoice = event.data.object as Stripe.Invoice;\n\n          if (invoice.parent?.subscription_details?.subscription) {\n            const subscription = await this.stripe.subscriptions.retrieve(invoice.parent?.subscription_details?.subscription as string);\n\n            // Extra: Send receipt email\n            // Example: await sendEmail.receipt(user.email, invoice);\n\n            // Extra: Update user credits/features if your app uses them\n            // Example: await updateUserCredits(userId, plan.credits);\n\n            return {\n              success: true,\n              userId: subscription.metadata?.userId,\n              action: \"payment_succeeded\",\n              expiresAt: new Date(subscription.items.data[0].current_period_end * 1000),\n              paymentId: invoice.id,\n              amount: invoice.amount_paid / 100, // Convert from cents\n              currency: invoice.currency.toUpperCase(),\n              platform: \"WEB\",\n            };\n          }\n          break;\n        }\n\n        case \"customer.subscription.updated\": {\n          // Sent when subscription is changed (plan switch, quantity change, etc.)\n          // Also sent when subscription renews with a new billing period\n          const subscription = event.data.object as Stripe.Subscription;\n\n          // Get the new plan ID from the subscription items\n          const newStripePriceId = subscription.items.data[0]?.price?.id;\n          let planId = subscription.metadata?.planId;\n          console.log(\"planId:\", planId);\n          console.log(\"newStripePriceId:\", newStripePriceId);\n\n          // If the price changed, we need to map it to our internal plan ID\n          if (newStripePriceId) {\n            try {\n              // Use the PremiumManager to map Stripe price ID to our internal plan ID\n              const { PremiumManager } = await import(\"@/shared/lib/premium/premium.manager\");\n              const plan = await PremiumManager.getPlanByExternalId(newStripePriceId, \"stripe\");\n              if (plan) {\n                planId = plan.id;\n              }\n            } catch (error) {\n              console.error(\"Failed to map Stripe price ID to internal plan ID:\", error);\n              // Fallback to metadata planId if mapping fails\n            }\n          }\n\n          // Fetch the full subscription object to get all fields\n          const fullSubscription = await this.stripe.subscriptions.retrieve(subscription.id);\n\n          return {\n            success: true,\n            userId: fullSubscription.metadata?.userId,\n            action: \"subscription_updated\",\n            expiresAt: new Date(fullSubscription.items.data[0].current_period_end * 1000),\n            planId: planId,\n            platform: \"WEB\",\n          };\n        }\n\n        case \"customer.subscription.deleted\": {\n          // Sent when subscription is cancelled immediately or when it ends after cancel_at_period_end\n          const subscription = event.data.object as Stripe.Subscription;\n\n          // Extra: Send cancellation confirmation email\n          // Example: await sendEmail.cancelled(user.email);\n\n          // Extra: Revoke premium features immediately or schedule for period end\n          // Example: if (subscription.cancel_at_period_end) { scheduleRevoke(userId, subscription.current_period_end) }\n\n          return {\n            success: true,\n            userId: subscription.metadata?.userId,\n            action: \"subscription_cancelled\",\n            platform: \"WEB\",\n          };\n        }\n\n        case \"invoice.payment_failed\": {\n          // Sent when payment fails (card declined, insufficient funds, etc.)\n          // Stripe will retry based on retry settings (3 times by default)\n          const invoice = event.data.object as Stripe.Invoice;\n\n          if (invoice.parent?.subscription_details?.subscription) {\n            const subscription = await this.stripe.subscriptions.retrieve(invoice.parent?.subscription_details?.subscription as string);\n\n            // Extra: Send payment failed email with update payment link\n            // Example: await sendEmail.paymentFailed(user.email, updatePaymentUrl);\n\n            // Extra: After X failures, you might want to pause premium features\n            // Example: if (invoice.attempt_count > 3) { await pausePremiumFeatures(userId) }\n\n            return {\n              success: true,\n              userId: subscription.metadata?.userId,\n              action: \"payment_failed\",\n              paymentId: invoice.id,\n              amount: invoice.amount_due / 100,\n              currency: invoice.currency.toUpperCase(),\n              platform: \"WEB\",\n            };\n          }\n          break;\n        }\n\n        case \"customer.created\": {\n          // Sent when a new customer is created in Stripe\n          const customer = event.data.object as Stripe.Customer;\n          console.log(`Stripe customer created: ${customer.id}`);\n          return { success: true };\n        }\n\n        case \"checkout.session.expired\": {\n          // User didn't complete the transaction (abandoned checkout)\n          const session = event.data.object as Stripe.Checkout.Session;\n\n          // Extra: Send abandoned cart email to recover the sale\n          // Example: await sendEmail.abandonedCart(user.email, checkoutUrl);\n\n          console.log(`Checkout session expired for user: ${session.metadata?.userId}`);\n          return { success: true };\n        }\n\n        // Informational events - acknowledge without specific action\n        case \"customer.updated\":\n        // Sent when customer details are updated (email, address, etc.)\n        case \"customer.subscription.created\":\n        // Sent when subscription is first created (we handle via checkout.session.completed)\n        case \"payment_intent.created\":\n        // Sent when payment process starts\n        case \"payment_intent.succeeded\":\n        // Sent when payment intent succeeds (before invoice.payment_succeeded)\n        case \"payment_method.attached\":\n        // Sent when payment method is attached to customer\n        case \"charge.succeeded\":\n        // Sent for successful charges (handled via invoice.payment_succeeded for subscriptions)\n        case \"invoice.created\":\n        // Sent when invoice is first created (draft state)\n        case \"invoice.finalized\":\n        // Sent when invoice is finalized and ready for payment\n        case \"invoice.updated\":\n        // Sent when invoice is modified\n        case \"invoice.paid\":\n        case \"billing_portal.session.created\":\n        case \"invoiceitem.created\": {\n          // Sent when invoice is marked as paid or portal session created\n          console.log(`Acknowledged Stripe event: ${event.type}`);\n          return { success: true };\n        }\n\n        default:\n          console.log(`INFO : Unhandled Stripe event type: ${event.type}`);\n      }\n\n      return { success: false, error: \"Event type not handled\" };\n    } catch (error) {\n      console.error(\"Stripe webhook processing failed:\", error);\n      return {\n        success: false,\n        error: error instanceof Error ? error.message : \"Unknown error\",\n      };\n    }\n  }\n\n  /**\n   * Create customer portal session\n   */\n  async createPortalSession(customerId: string, returnUrl?: string): Promise<CheckoutResult> {\n    try {\n      const session = await this.stripe.billingPortal.sessions.create({\n        customer: customerId,\n        return_url: returnUrl || `${env.NEXT_PUBLIC_APP_URL}/premium`,\n      });\n\n      return {\n        success: true,\n        checkoutUrl: session.url,\n        provider: \"stripe\",\n      };\n    } catch (error) {\n      console.error(\"Stripe portal session creation failed:\", error);\n      return {\n        success: false,\n        error: error instanceof Error ? error.message : \"Unknown error\",\n        provider: \"stripe\",\n      };\n    }\n  }\n\n  /**\n   * Get subscription details\n   */\n  async getSubscription(subscriptionId: string): Promise<Stripe.Subscription | null> {\n    try {\n      return await this.stripe.subscriptions.retrieve(subscriptionId);\n    } catch (error) {\n      console.error(\"Failed to retrieve Stripe subscription:\", error);\n      return null;\n    }\n  }\n\n  /**\n   * Cancel subscription\n   */\n  async cancelSubscription(subscriptionId: string, immediately = false): Promise<boolean> {\n    try {\n      if (immediately) {\n        await this.stripe.subscriptions.cancel(subscriptionId);\n      } else {\n        await this.stripe.subscriptions.update(subscriptionId, {\n          cancel_at_period_end: true,\n        });\n      }\n      return true;\n    } catch (error) {\n      console.error(\"Failed to cancel Stripe subscription:\", error);\n      return false;\n    }\n  }\n\n  /**\n   * Get or create Stripe customer for user\n   */\n  private async getOrCreateCustomer(userId: string): Promise<Stripe.Customer> {\n    try {\n      // First, check if we have a customer ID stored in our database\n      // For simplicity, we'll search by email or create a new customer\n      // In production, you'd want to store the customer ID in the database\n\n      // Search for existing customer by metadata\n      const customers = await this.stripe.customers.search({\n        query: `metadata['userId']:'${userId}'`,\n        limit: 1,\n      });\n\n      const dbUser = await prisma.user.findUniqueOrThrow({\n        where: {\n          id: userId,\n        },\n      });\n\n      if (customers.data.length > 0) {\n        return customers.data[0];\n      }\n\n      // Create new customer\n      return await this.stripe.customers.create({\n        email: dbUser.email,\n        name: `${dbUser.firstName || \"unknown\"} ${dbUser.lastName || \"unknown\"}`,\n        metadata: {\n          userId,\n          first_name: dbUser.firstName,\n          last_name: dbUser.lastName,\n        },\n      });\n    } catch (error) {\n      console.error(\"Failed to get or create Stripe customer:\", error);\n      throw error;\n    }\n  }\n\n  /**\n   * Get customer by user ID\n   */\n  async getCustomerByUserId(userId: string): Promise<Stripe.Customer | null> {\n    try {\n      const customers = await this.stripe.customers.search({\n        query: `metadata['userId']:'${userId}'`,\n        limit: 1,\n      });\n\n      return customers.data.length > 0 ? customers.data[0] : null;\n    } catch (error) {\n      console.error(\"Failed to get Stripe customer:\", error);\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "src/shared/lib/premium/use-pending-checkout.ts",
    "content": "\"use client\";\n\nimport { useCallback } from \"react\";\n\nconst PENDING_CHECKOUT_KEY = \"pendingCheckout\";\n\ninterface PendingCheckout {\n  planId: string;\n  timestamp: number;\n}\n\nexport function usePendingCheckout() {\n  const storePendingCheckout = useCallback((planId: string) => {\n    if (typeof window === \"undefined\") return;\n    \n    const pendingCheckout: PendingCheckout = {\n      planId,\n      timestamp: Date.now(),\n    };\n    \n    localStorage.setItem(PENDING_CHECKOUT_KEY, JSON.stringify(pendingCheckout));\n  }, []);\n\n  const getPendingCheckout = useCallback((): PendingCheckout | null => {\n    if (typeof window === \"undefined\") return null;\n    \n    const stored = localStorage.getItem(PENDING_CHECKOUT_KEY);\n    if (!stored) return null;\n    \n    try {\n      const parsed = JSON.parse(stored) as PendingCheckout;\n      \n      // Check if it's not too old (1 hour max)\n      const isExpired = Date.now() - parsed.timestamp > 60 * 60 * 1000;\n      if (isExpired) {\n        localStorage.removeItem(PENDING_CHECKOUT_KEY);\n        return null;\n      }\n      \n      return parsed;\n    } catch {\n      localStorage.removeItem(PENDING_CHECKOUT_KEY);\n      return null;\n    }\n  }, []);\n\n  const clearPendingCheckout = useCallback(() => {\n    if (typeof window === \"undefined\") return;\n    localStorage.removeItem(PENDING_CHECKOUT_KEY);\n  }, []);\n\n  return {\n    storePendingCheckout,\n    getPendingCheckout,\n    clearPendingCheckout,\n  };\n}"
  },
  {
    "path": "src/shared/lib/premium/use-premium-redirect.ts",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\n\nimport { useIsPremium } from \"./use-premium\";\n\nexport function usePremiumRedirect() {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const isPremium = useIsPremium();\n\n  useEffect(() => {\n    // Check if user just became premium and has a return URL\n    if (isPremium) {\n      const returnUrl = searchParams.get(\"return\");\n      if (returnUrl) {\n        // Small delay to ensure premium status is fully updated\n        setTimeout(() => {\n          router.push(returnUrl);\n        }, 1000);\n      }\n    }\n  }, [isPremium, searchParams, router]);\n}"
  },
  {
    "path": "src/shared/lib/premium/use-premium.ts",
    "content": "\"use client\";\n\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useSession } from \"@/features/auth/lib/auth-client\";\n\nimport type { PremiumStatus, UserSubscription } from \"@/shared/types/premium.types\";\n\nexport function usePremiumStatus() {\n  const { data: session } = useSession();\n\n  return useQuery({\n    queryKey: [\"premium-status\", session?.user?.id],\n    queryFn: async (): Promise<PremiumStatus> => {\n      if (!session?.user?.id) {\n        return { isPremium: false };\n      }\n\n      const response = await fetch(\"/api/premium/status\");\n      if (!response.ok) {\n        throw new Error(\"Failed to fetch premium status\");\n      }\n\n      return response.json();\n    },\n    enabled: !!session?.user?.id,\n    staleTime: 5 * 60 * 1000, // 5 minutes\n    gcTime: 10 * 60 * 1000, // 10 minutes\n  });\n}\n\nexport function useSubscription() {\n  const { data: session } = useSession();\n\n  return useQuery({\n    queryKey: [\"subscription\", session?.user?.id],\n    queryFn: async (): Promise<UserSubscription> => {\n      if (!session?.user?.id) {\n        return { isActive: false };\n      }\n\n      const response = await fetch(\"/api/premium/subscription\");\n      if (!response.ok) {\n        throw new Error(\"Failed to fetch subscription\");\n      }\n\n      return response.json();\n    },\n    enabled: !!session?.user?.id,\n    staleTime: 5 * 60 * 1000,\n    gcTime: 10 * 60 * 1000,\n  });\n}\n\n// Simple boolean check - most common use case\nexport function useIsPremium(): boolean {\n  const { data: premiumStatus } = usePremiumStatus();\n  return premiumStatus?.isPremium ?? false;\n}\n"
  },
  {
    "path": "src/shared/lib/prisma.ts",
    "content": "import { PrismaClient } from \"@prisma/client\";\n\nconst prismaClientSingleton = () => {\n  return new PrismaClient();\n};\n\ntype PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>;\n\nconst globalForPrisma = globalThis as unknown as {\n  prisma: PrismaClientSingleton | undefined;\n};\n\nexport const prisma = globalForPrisma.prisma ?? prismaClientSingleton();\n\nif (process.env.NODE_ENV !== \"production\") globalForPrisma.prisma = prisma;\n"
  },
  {
    "path": "src/shared/lib/revenuecat/index.ts",
    "content": "// RevenueCat integration exports\nexport { RevenueCatConfig } from \"./revenuecat.config\";\nexport { RevenueCatMapping } from \"./revenuecat.mapping\";\nexport { RevenueCatApiClient, revenueCatApi } from \"./revenuecat.api\";\n\n// Test that our files compile correctly\nexport type { } from \"./revenuecat.config\";\nexport type { RevenueCatSubscriber, RevenueCatApiError } from \"./revenuecat.api\";"
  },
  {
    "path": "src/shared/lib/revenuecat/revenuecat.api.ts",
    "content": "import { RevenueCatConfig } from \"./revenuecat.config\";\n\n/**\n * RevenueCat API Client\n * \n * Provides methods to interact with RevenueCat's REST API\n * Handles authentication, error handling, and response parsing\n */\n\nexport interface RevenueCatSubscriber {\n  object: \"subscriber\";\n  app_user_id: string;\n  original_app_user_id: string;\n  entitlements: {\n    [key: string]: {\n      expires_date: string | null;\n      grace_period_expires_date: string | null;\n      product_identifier: string;\n      purchase_date: string;\n      billing_issues_detected_at: string | null;\n      owns_product: boolean;\n      period_type: \"normal\" | \"trial\" | \"intro\";\n      store: \"app_store\" | \"mac_app_store\" | \"play_store\" | \"stripe\" | \"promotional\";\n      unsubscribe_detected_at: string | null;\n      auto_renew_status: boolean;\n      is_sandbox: boolean;\n    };\n  };\n  first_seen: string;\n  last_seen: string;\n  management_url: string | null;\n  non_subscriptions: {\n    [key: string]: Array<{\n      id: string;\n      is_sandbox: boolean;\n      original_purchase_date: string;\n      purchase_date: string;\n      store: string;\n    }>;\n  };\n  original_application_version: string | null;\n  original_purchase_date: string | null;\n  other_purchases: {\n    [key: string]: {\n      purchase_date: string;\n    };\n  };\n  subscriptions: {\n    [key: string]: {\n      auto_renew_status: boolean;\n      billing_issues_detected_at: string | null;\n      expires_date: string | null;\n      grace_period_expires_date: string | null;\n      is_sandbox: boolean;\n      original_purchase_date: string;\n      ownership_type: \"PURCHASED\" | \"FAMILY_SHARED\";\n      period_type: \"normal\" | \"trial\" | \"intro\";\n      product_identifier: string;\n      purchase_date: string;\n      refunded_at: string | null;\n      store: \"app_store\" | \"mac_app_store\" | \"play_store\" | \"stripe\" | \"promotional\";\n      unsubscribe_detected_at: string | null;\n    };\n  };\n}\n\nexport interface RevenueCatApiError {\n  message: string;\n  code: number;\n  more_info?: string;\n}\n\nexport class RevenueCatApiClient {\n  private baseUrl: string;\n  private headers: Record<string, string>;\n\n  constructor() {\n    this.baseUrl = RevenueCatConfig.getApiBaseUrl();\n    this.headers = RevenueCatConfig.getApiHeaders();\n  }\n\n  /**\n   * Fetch subscriber information from RevenueCat\n   * @param appUserId - The RevenueCat app user ID\n   * @returns Promise<RevenueCatSubscriber | null>\n   */\n  async getSubscriber(appUserId: string): Promise<RevenueCatSubscriber | null> {\n    try {\n      const url = `${this.baseUrl}/subscribers/${encodeURIComponent(appUserId)}`;\n      \n      const response = await fetch(url, {\n        method: \"GET\",\n        headers: this.headers,\n      });\n\n      if (response.status === 404) {\n        // Subscriber not found - this is expected for new users\n        return null;\n      }\n\n      if (!response.ok) {\n        const errorBody = await response.text();\n        let errorMessage = `RevenueCat API error: ${response.status} ${response.statusText}`;\n        \n        try {\n          const errorData: RevenueCatApiError = JSON.parse(errorBody);\n          errorMessage = errorData.message || errorMessage;\n        } catch {\n          // If error body is not JSON, use the default message\n        }\n\n        throw new Error(errorMessage);\n      }\n\n      const data = await response.json();\n      return data.subscriber as RevenueCatSubscriber;\n\n    } catch (error) {\n      console.error(\"Error fetching RevenueCat subscriber:\", error);\n      \n      // Re-throw the error to be handled by the calling code\n      if (error instanceof Error) {\n        throw error;\n      }\n      \n      throw new Error(\"Failed to fetch RevenueCat subscriber information\");\n    }\n  }\n\n  /**\n   * Check if a subscriber has active entitlements\n   * @param appUserId - The RevenueCat app user ID\n   * @returns Promise<boolean>\n   */\n  async hasActiveEntitlements(appUserId: string): Promise<boolean> {\n    try {\n      const subscriber = await this.getSubscriber(appUserId);\n      \n      if (!subscriber) {\n        return false;\n      }\n\n      // Check if any entitlements are active\n      const entitlements = subscriber.entitlements;\n      const now = new Date();\n\n      for (const [, entitlement] of Object.entries(entitlements)) {\n        if (entitlement.owns_product) {\n          // Check if entitlement is not expired\n          if (!entitlement.expires_date) {\n            // No expiration date means lifetime or active subscription\n            return true;\n          }\n\n          const expiresAt = new Date(entitlement.expires_date);\n          if (expiresAt > now) {\n            return true;\n          }\n        }\n      }\n\n      return false;\n    } catch (error) {\n      console.error(\"Error checking RevenueCat entitlements:\", error);\n      // In case of API error, assume no active entitlements\n      return false;\n    }\n  }\n\n  /**\n   * Get the most recent expiration date from active entitlements\n   * @param appUserId - The RevenueCat app user ID\n   * @returns Promise<Date | null>\n   */\n  async getLatestExpirationDate(appUserId: string): Promise<Date | null> {\n    try {\n      const subscriber = await this.getSubscriber(appUserId);\n      \n      if (!subscriber) {\n        return null;\n      }\n\n      const entitlements = subscriber.entitlements;\n      const now = new Date();\n      let latestExpiration: Date | null = null;\n\n      for (const [, entitlement] of Object.entries(entitlements)) {\n        if (entitlement.owns_product) {\n          if (!entitlement.expires_date) {\n            // No expiration date means lifetime subscription\n            return null;\n          }\n\n          const expiresAt = new Date(entitlement.expires_date);\n          if (expiresAt > now) {\n            if (!latestExpiration || expiresAt > latestExpiration) {\n              latestExpiration = expiresAt;\n            }\n          }\n        }\n      }\n\n      return latestExpiration;\n    } catch (error) {\n      console.error(\"Error getting RevenueCat expiration date:\", error);\n      return null;\n    }\n  }\n\n  /**\n   * Get active entitlement IDs for a subscriber\n   * @param appUserId - The RevenueCat app user ID\n   * @returns Promise<string[]>\n   */\n  async getActiveEntitlementIds(appUserId: string): Promise<string[]> {\n    try {\n      const subscriber = await this.getSubscriber(appUserId);\n      \n      if (!subscriber) {\n        return [];\n      }\n\n      const entitlements = subscriber.entitlements;\n      const now = new Date();\n      const activeEntitlements: string[] = [];\n\n      for (const [entitlementId, entitlement] of Object.entries(entitlements)) {\n        if (entitlement.owns_product) {\n          // Check if entitlement is not expired\n          if (!entitlement.expires_date) {\n            activeEntitlements.push(entitlementId);\n          } else {\n            const expiresAt = new Date(entitlement.expires_date);\n            if (expiresAt > now) {\n              activeEntitlements.push(entitlementId);\n            }\n          }\n        }\n      }\n\n      return activeEntitlements;\n    } catch (error) {\n      console.error(\"Error getting RevenueCat active entitlements:\", error);\n      return [];\n    }\n  }\n}\n\n// Export a singleton instance\nexport const revenueCatApi = new RevenueCatApiClient();"
  },
  {
    "path": "src/shared/lib/revenuecat/revenuecat.config.ts",
    "content": "import { env } from \"@/env\";\n\n/**\n * RevenueCat Configuration Service\n *\n * Centralized configuration for RevenueCat integration\n * Follows KISS principle - simple and easy to maintain\n */\nexport class RevenueCatConfig {\n  /**\n   * Get RevenueCat secret key for API calls\n   */\n  static getSecretKey(): string {\n    const secretKey = env.REVENUECAT_SECRET_KEY;\n    if (!secretKey) {\n      throw new Error(\"REVENUECAT_SECRET_KEY environment variable is required\");\n    }\n    return secretKey;\n  }\n\n  /**\n   * Get RevenueCat webhook secret for signature validation\n   */\n  static getWebhookSecret(): string {\n    const webhookSecret = env.REVENUECAT_WEBHOOK_SECRET;\n    if (!webhookSecret) {\n      throw new Error(\"REVENUECAT_WEBHOOK_SECRET environment variable is required\");\n    }\n    return webhookSecret;\n  }\n\n  /**\n   * Check if RevenueCat is properly configured\n   */\n  static isConfigured(): boolean {\n    return !!(env.REVENUECAT_SECRET_KEY && env.REVENUECAT_WEBHOOK_SECRET);\n  }\n\n  /**\n   * Get RevenueCat API base URL\n   */\n  static getApiBaseUrl(): string {\n    return \"https://api.revenuecat.com/v2\";\n  }\n\n  /**\n   * Get common headers for RevenueCat API requests\n   */\n  static getApiHeaders(): Record<string, string> {\n    return {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${this.getSecretKey()}`,\n      \"X-Platform\": \"server\",\n    };\n  }\n\n  /**\n   * Get webhook validation settings\n   */\n  static getWebhookConfig() {\n    return {\n      secret: this.getWebhookSecret(),\n      tolerance: 300, // 5 minutes tolerance for timestamp validation\n    };\n  }\n\n  /**\n   * Development mode helpers\n   */\n  static isDevelopment(): boolean {\n    return env.NODE_ENV === \"development\";\n  }\n\n  /**\n   * Get sandbox mode setting based on environment\n   */\n  static isSandbox(): boolean {\n    return env.NODE_ENV !== \"production\";\n  }\n}\n"
  },
  {
    "path": "src/shared/lib/revenuecat/revenuecat.mapping.ts",
    "content": "import { PaymentProcessor } from \"@prisma/client\";\n\nimport { prisma } from \"@/shared/lib/prisma\";\n\n/**\n * RevenueCat Product Mapping Service\n *\n * Manages mapping between RevenueCat products and internal subscription plans\n */\nexport class RevenueCatMapping {\n  /**\n   * Get subscription plan by RevenueCat product ID\n   */\n  static async getSubscriptionPlanByProductId(productId: string) {\n    const mapping = await prisma.planProviderMapping.findFirst({\n      where: {\n        provider: PaymentProcessor.REVENUECAT,\n        externalId: productId,\n        isActive: true,\n      },\n      include: {\n        plan: true,\n      },\n    });\n\n    return mapping?.plan || null;\n  }\n\n  /**\n   * Create or update RevenueCat product mapping\n   */\n  static async createOrUpdateProductMapping(planId: string, productId: string, region?: string, metadata?: Record<string, any>) {\n    return prisma.planProviderMapping.upsert({\n      where: {\n        planId_provider_region: {\n          planId,\n          provider: PaymentProcessor.REVENUECAT,\n          region: region || \"\",\n        },\n      },\n      update: {\n        externalId: productId,\n        metadata: metadata ?? undefined,\n        isActive: true,\n        updatedAt: new Date(),\n      },\n      create: {\n        planId,\n        provider: PaymentProcessor.REVENUECAT,\n        externalId: productId,\n        region: region || null,\n        metadata: metadata ?? undefined,\n        isActive: true,\n      },\n    });\n  }\n\n  /**\n   * Get all RevenueCat product mappings\n   */\n  static async getAllRevenueCatMappings() {\n    return prisma.planProviderMapping.findMany({\n      where: {\n        provider: PaymentProcessor.REVENUECAT,\n        isActive: true,\n      },\n      include: {\n        plan: true,\n      },\n    });\n  }\n\n  /**\n   * Get RevenueCat product ID by subscription plan\n   */\n  static async getRevenueCatProductId(planId: string, region?: string) {\n    const mapping = await prisma.planProviderMapping.findFirst({\n      where: {\n        planId,\n        provider: PaymentProcessor.REVENUECAT,\n        region: region || null,\n        isActive: true,\n      },\n    });\n\n    return mapping?.externalId || null;\n  }\n\n  /**\n   * Validate RevenueCat product ID exists in our system\n   */\n  static async validateProductId(productId: string): Promise<boolean> {\n    const mapping = await prisma.planProviderMapping.findFirst({\n      where: {\n        provider: PaymentProcessor.REVENUECAT,\n        externalId: productId,\n        isActive: true,\n      },\n    });\n\n    return !!mapping;\n  }\n\n  /**\n   * Get subscription plan with RevenueCat product mapping\n   */\n  static async getSubscriptionPlanWithMapping(planId: string) {\n    return prisma.subscriptionPlan.findUnique({\n      where: { id: planId },\n      include: {\n        providerMappings: {\n          where: {\n            provider: PaymentProcessor.REVENUECAT,\n            isActive: true,\n          },\n        },\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "src/shared/lib/server-url.ts",
    "content": "import { SiteConfig } from \"@/shared/config/site-config\";\n\n/**\n * This method return the server URL based on the environment.\n */\nexport const getServerUrl = () => {\n  if (typeof window !== \"undefined\") {\n    return window.location.origin;\n  }\n\n  // If we are in production, we return the production URL.\n  if (process.env.VERCEL_ENV === \"production\") {\n    return SiteConfig.prodUrl;\n  }\n\n  // If we are in \"stage\" environment, we return the staging URL.\n  if (process.env.VERCEL_URL) {\n    return `https://${process.env.VERCEL_URL}`;\n  }\n\n  // If we are in development, we return the localhost URL.\n  return \"http://localhost:3000\";\n};\n"
  },
  {
    "path": "src/shared/lib/slug.ts",
    "content": "import slugify from \"slugify\";\nimport { pinyin } from \"pinyin-pro\";\n\n/**\n * Utility functions for generating and handling slugs\n */\n\n/**\n * Converts a string to a URL-friendly slug\n * Handles all Unicode characters including Chinese, Russian, Arabic, etc.\n */\nexport function generateSlug(text: string): string {\n  // Check if text contains Chinese characters\n  const chineseRegex = /[\\u4e00-\\u9fa5]/;\n\n  if (chineseRegex.test(text)) {\n    // Convert Chinese to pinyin (phonetic representation)\n    const pinyinText = pinyin(text, {\n      toneType: \"none\", // Remove tone marks\n      separator: \"-\", // Use hyphens between words\n      type: \"string\",\n    });\n\n    // Clean up the pinyin text\n    return slugify(pinyinText, {\n      lower: true,\n      strict: true,\n      trim: true,\n    });\n  }\n\n  // For non-Chinese text, use regular slugify\n  return slugify(text, {\n    lower: true,\n    strict: true,\n    locale: \"en\",\n    trim: true,\n  });\n}\n\n/**\n * Generates slugs for all supported languages\n */\nexport function generateSlugsForAllLanguages(titles: {\n  title: string;\n  titleEn: string;\n  titleEs: string;\n  titlePt: string;\n  titleRu: string;\n  titleZhCn: string;\n}) {\n  return {\n    slug: generateSlug(titles.title),\n    slugEn: generateSlug(titles.titleEn),\n    slugEs: generateSlug(titles.titleEs),\n    slugPt: generateSlug(titles.titlePt),\n    slugRu: generateSlug(titles.titleRu),\n    slugZhCn: generateSlug(titles.titleZhCn),\n  };\n}\n\n/**\n * Ensures slug uniqueness by appending a number if needed\n */\nexport function ensureUniqueSlug(baseSlug: string, existingSlugs: string[]): string {\n  let slug = baseSlug;\n  let counter = 1;\n\n  while (existingSlugs.includes(slug)) {\n    slug = `${baseSlug}-${counter}`;\n    counter++;\n  }\n\n  return slug;\n}\n"
  },
  {
    "path": "src/shared/lib/structured-data.ts",
    "content": "/* eslint-disable max-len */\n/* eslint-disable no-case-declarations */\nimport React from \"react\";\n\nimport { getServerUrl } from \"@/shared/lib/server-url\";\nimport { SiteConfig } from \"@/shared/config/site-config\";\nimport { getLocalizedMetadata } from \"@/shared/config/localized-metadata\";\n\nexport interface StructuredDataProps {\n  type: \"WebSite\" | \"WebApplication\" | \"Organization\" | \"SoftwareApplication\" | \"Article\" | \"Course\" | \"VideoObject\" | \"Calculator\";\n  locale?: string;\n  title?: string;\n  description?: string;\n  url?: string;\n  image?: string;\n  datePublished?: string;\n  dateModified?: string;\n  author?: string;\n  // Course-specific fields\n  courseData?: {\n    id: string;\n    level: string;\n    category: string;\n    durationWeeks: number;\n    sessionsPerWeek: number;\n    sessionDurationMin: number;\n    equipment: string[];\n    isPremium: boolean;\n    participantCount: number;\n    totalSessions: number;\n    totalExercises: number;\n    coaches: Array<{\n      name: string;\n      image: string;\n    }>;\n  };\n  // Session/VideoObject-specific fields\n  sessionData?: {\n    duration: number;\n    exercises: Array<{ name: string; sets: number }>;\n    thumbnailUrl?: string;\n    videoUrl?: string;\n  };\n  // Calculator-specific fields\n  calculatorData?: {\n    calculatorType: \"calorie\" | \"macro\" | \"bmi\" | \"heart-rate\" | \"heart-rate-zones\" | \"one-rep-max\" | \"rest-timer\";\n    inputFields: string[];\n    outputFields: string[];\n    formula?: string;\n    accuracy?: string;\n    targetAudience?: string[];\n    relatedCalculators?: string[];\n  };\n}\n\nexport function generateStructuredData({\n  type,\n  locale = \"en\",\n  title,\n  description,\n  url,\n  image,\n  datePublished,\n  dateModified,\n  author,\n  courseData,\n  sessionData,\n  calculatorData,\n}: StructuredDataProps) {\n  const baseUrl = getServerUrl();\n  const localizedData = getLocalizedMetadata(locale);\n\n  const baseStructuredData = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": type,\n    url: url || baseUrl,\n    name: title || localizedData.title,\n    description: description || localizedData.description,\n    inLanguage:\n      locale === \"en\"\n        ? \"en-US\"\n        : locale === \"es\"\n          ? \"es-ES\"\n          : locale === \"pt\"\n            ? \"pt-PT\"\n            : locale === \"ru\"\n              ? \"ru-RU\"\n              : locale === \"zh-CN\"\n                ? \"zh-CN\"\n                : \"fr-FR\",\n    publisher: {\n      \"@type\": \"Organization\",\n      name: SiteConfig.company.name,\n      url: baseUrl,\n      logo: {\n        \"@type\": \"ImageObject\",\n        url: `${baseUrl}/logo.png`,\n        width: 512,\n        height: 512,\n      },\n    },\n  };\n\n  switch (type) {\n    case \"WebSite\":\n      return {\n        ...baseStructuredData,\n        \"@type\": \"WebSite\",\n        sameAs: [SiteConfig.maker.twitter, `${baseUrl}`],\n      };\n\n    case \"WebApplication\":\n      return {\n        ...baseStructuredData,\n        \"@type\": \"WebApplication\",\n        applicationCategory: \"HealthAndFitnessApplication\",\n        operatingSystem: \"Web Browser\",\n        browserRequirements: \"Requires JavaScript. Requires HTML5.\",\n        softwareVersion: \"1.2.1\",\n        offers: {\n          \"@type\": \"Offer\",\n          price: \"0\",\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/InStock\",\n        },\n        featureList:\n          locale === \"en\"\n            ? [\n                \"Personalized workout builder\",\n                \"Comprehensive exercise database\",\n                \"Progress tracking\",\n                \"Muscle group targeting\",\n                \"Equipment-based filtering\",\n              ]\n            : locale === \"es\"\n              ? [\n                  \"Constructor de entrenamientos personalizado\",\n                  \"Base de datos completa de ejercicios\",\n                  \"Seguimiento de progreso\",\n                  \"Orientación a grupos musculares\",\n                  \"Filtrado basado en equipos\",\n                ]\n              : locale === \"pt\"\n                ? [\n                    \"Construtor de treinos personalizado\",\n                    \"Base de dados abrangente de exercícios\",\n                    \"Acompanhamento de progresso\",\n                    \"Segmentação de grupos musculares\",\n                    \"Filtragem baseada em equipamentos\",\n                  ]\n                : locale === \"ru\"\n                  ? [\n                      \"Персонализированный конструктор тренировок\",\n                      \"Полная база данных упражнений\",\n                      \"Отслеживание прогресса\",\n                      \"Нацеливание на группы мышц\",\n                      \"Фильтрация по оборудованию\",\n                    ]\n                  : locale === \"zh-CN\"\n                    ? [\"个性化锻炼计划构建器\", \"全面的运动数据库\", \"进度跟踪\", \"肌肉群目标定位\", \"基于设备的筛选\"]\n                    : [\n                        \"Créateur d'entraînement personnalisé\",\n                        \"Base de données d'exercices complète\",\n                        \"Suivi des progrès\",\n                        \"Ciblage des groupes musculaires\",\n                        \"Filtrage par équipement\",\n                      ],\n      };\n\n    case \"Organization\":\n      return {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Organization\",\n        name: SiteConfig.company.name,\n        url: baseUrl,\n        logo: {\n          \"@type\": \"ImageObject\",\n          url: `${baseUrl}/logo.png`,\n          width: 512,\n          height: 512,\n        },\n        address: {\n          \"@type\": \"PostalAddress\",\n          addressLocality: \"Paris\",\n          addressCountry: \"FR\",\n          streetAddress: SiteConfig.company.address,\n        },\n        contactPoint: {\n          \"@type\": \"ContactPoint\",\n          telephone: \"+33-1-00-00-00-00\",\n          contactType: \"customer service\",\n          availableLanguage: [\"French\", \"English\", \"Spanish\", \"Portuguese\", \"Russian\", \"Chinese\"],\n        },\n        sameAs: [SiteConfig.maker.twitter],\n        foundingDate: \"2024\",\n        description: localizedData.description,\n      };\n\n    case \"SoftwareApplication\":\n      return {\n        ...baseStructuredData,\n        \"@type\": \"SoftwareApplication\",\n        applicationCategory: \"HealthApplication\",\n        operatingSystem: \"Web\",\n        downloadUrl: baseUrl,\n        softwareVersion: \"1.2.1\",\n        releaseNotes:\n          locale === \"en\"\n            ? \"Latest update includes improved exercise database and better user experience\"\n            : locale === \"es\"\n              ? \"La última actualización incluye una base de datos de ejercicios mejorada y una mejor experiencia de usuario\"\n              : locale === \"pt\"\n                ? \"A atualização mais recente inclui base de dados de exercícios melhorada e melhor experiência do usuário\"\n                : locale === \"ru\"\n                  ? \"Последнее обновление включает улучшенную базу данных упражнений и лучший пользовательский опыт\"\n                  : locale === \"zh-CN\"\n                    ? \"最新更新包括改进的运动数据库和更好的用户体验\"\n                    : \"La dernière mise à jour inclut une base de données d'exercices améliorée et une meilleure expérience utilisateur\",\n        screenshot: image || `${baseUrl}/images/default-og-image_${locale}.jpg`,\n        aggregateRating: {\n          \"@type\": \"AggregateRating\",\n          ratingValue: \"4.8\",\n          ratingCount: \"127\",\n        },\n      };\n\n    case \"Article\":\n      return {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        headline: title,\n        description: description,\n        url: url,\n        author: {\n          \"@type\": \"Person\",\n          name: author || SiteConfig.company.name,\n        },\n        publisher: {\n          \"@type\": \"Organization\",\n          name: SiteConfig.company.name,\n          logo: {\n            \"@type\": \"ImageObject\",\n            url: `${baseUrl}/logo.png`,\n            width: 512,\n            height: 512,\n          },\n        },\n        datePublished: datePublished || new Date().toISOString(),\n        dateModified: dateModified || new Date().toISOString(),\n        image: image || `${baseUrl}/images/default-og-image_${locale}.jpg`,\n        mainEntityOfPage: {\n          \"@type\": \"WebPage\",\n          \"@id\": url,\n        },\n      };\n\n    case \"Course\":\n      if (!courseData) return baseStructuredData;\n\n      const difficultyLevel =\n        courseData.level === \"BEGINNER\" ? \"Beginner\" : courseData.level === \"INTERMEDIATE\" ? \"Intermediate\" : \"Advanced\";\n\n      const courseSchema = {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Course\",\n        name: title || baseStructuredData.name,\n        description: description || baseStructuredData.description,\n        url: url || baseUrl,\n        image: image || `${baseUrl}/images/default-og-image_${locale === \"zh-CN\" ? \"zh\" : locale}.jpg`,\n        provider: {\n          \"@type\": \"Organization\",\n          name: SiteConfig.company.name,\n          url: baseUrl,\n        },\n        educationalLevel: difficultyLevel,\n        teaches:\n          locale === \"en\"\n            ? \"Fitness and workout techniques\"\n            : locale === \"es\"\n              ? \"Técnicas de fitness y entrenamiento\"\n              : locale === \"pt\"\n                ? \"Técnicas de fitness e treino\"\n                : locale === \"ru\"\n                  ? \"Фитнес и техники тренировок\"\n                  : locale === \"zh-CN\"\n                    ? \"健身和锻炼技巧\"\n                    : \"Techniques de fitness et d'entraînement\",\n        courseCode: courseData.id,\n        hasCourseInstance: {\n          \"@type\": \"CourseInstance\",\n          courseMode: \"Online\",\n          courseSchedule: {\n            \"@type\": \"Schedule\",\n            duration: `P${courseData.durationWeeks}W`,\n            repeatFrequency: \"P1W\",\n            repeatCount: courseData.durationWeeks,\n          },\n          instructor: courseData.coaches.map((coach) => ({\n            \"@type\": \"Person\",\n            name: coach.name,\n            image: coach.image,\n          })),\n        },\n        totalTime: `PT${courseData.sessionDurationMin * courseData.totalSessions}M`,\n        numberOfCredits: courseData.totalExercises,\n        aggregateRating: {\n          \"@type\": \"AggregateRating\",\n          ratingValue: \"4.7\",\n          ratingCount: Math.max(courseData.participantCount, 10),\n          bestRating: \"5\",\n          worstRating: \"1\",\n        },\n        offers: {\n          \"@type\": \"Offer\",\n          price: courseData.isPremium ? \"7.90\" : \"0\",\n          priceCurrency: \"EUR\",\n          availability: \"https://schema.org/InStock\",\n          category: courseData.isPremium ? \"Premium\" : \"Free\",\n        },\n        keywords: [courseData.category, difficultyLevel, \"fitness\", \"workout\", \"training\", ...courseData.equipment].join(\", \"),\n        inLanguage:\n          locale === \"en\"\n            ? \"en-US\"\n            : locale === \"es\"\n              ? \"es-ES\"\n              : locale === \"pt\"\n                ? \"pt-PT\"\n                : locale === \"ru\"\n                  ? \"ru-RU\"\n                  : locale === \"zh-CN\"\n                    ? \"zh-CN\"\n                    : \"fr-FR\",\n        isAccessibleForFree: !courseData.isPremium,\n        syllabusSections: [\n          {\n            \"@type\": \"Syllabus\",\n            name:\n              locale === \"en\"\n                ? `${courseData.totalSessions} workout sessions`\n                : locale === \"es\"\n                  ? `${courseData.totalSessions} sesiones de entrenamiento`\n                  : locale === \"pt\"\n                    ? `${courseData.totalSessions} sessões de treino`\n                    : locale === \"ru\"\n                      ? `${courseData.totalSessions} тренировочных сессий`\n                      : locale === \"zh-CN\"\n                        ? `${courseData.totalSessions} 训练课程`\n                        : `${courseData.totalSessions} séances d'entraînement`,\n            description:\n              locale === \"en\"\n                ? `Complete ${courseData.durationWeeks}-week program with ${courseData.sessionsPerWeek} sessions per week`\n                : locale === \"es\"\n                  ? `Programa completo de ${courseData.durationWeeks} semanas con ${courseData.sessionsPerWeek} sesiones por semana`\n                  : locale === \"pt\"\n                    ? `Programa completo de ${courseData.durationWeeks} semanas com ${courseData.sessionsPerWeek} sessões por semana`\n                    : locale === \"ru\"\n                      ? `Полная программа на ${courseData.durationWeeks} недель с ${courseData.sessionsPerWeek} сессиями в неделю`\n                      : locale === \"zh-CN\"\n                        ? `${courseData.durationWeeks}周完整计划，每周${courseData.sessionsPerWeek}次训练`\n                        : `Programme complet de ${courseData.durationWeeks} semaines avec ${courseData.sessionsPerWeek} séances par semaine`,\n          },\n        ],\n      };\n\n      return courseSchema;\n\n    case \"VideoObject\":\n      if (!sessionData) return baseStructuredData;\n\n      return {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"VideoObject\",\n        name: title || baseStructuredData.name,\n        description: description || baseStructuredData.description,\n        thumbnailUrl: sessionData.thumbnailUrl || image || `${baseUrl}/images/default-workout.jpg`,\n        contentUrl: sessionData.videoUrl,\n        duration: `PT${sessionData.duration}M`,\n        uploadDate: new Date().toISOString(),\n        publisher: {\n          \"@type\": \"Organization\",\n          name: SiteConfig.company.name,\n          url: baseUrl,\n          logo: {\n            \"@type\": \"ImageObject\",\n            url: `${baseUrl}/logo.png`,\n            width: 512,\n            height: 512,\n          },\n        },\n        potentialAction: {\n          \"@type\": \"WatchAction\",\n          target: url || baseUrl,\n        },\n        genre: \"Fitness\",\n        keywords: [\n          locale === \"en\"\n            ? \"workout session\"\n            : locale === \"es\"\n              ? \"sesión de entrenamiento\"\n              : locale === \"pt\"\n                ? \"sessão de treino\"\n                : locale === \"ru\"\n                  ? \"тренировочная сессия\"\n                  : locale === \"zh-CN\"\n                    ? \"训练课程\"\n                    : \"séance d'entraînement\",\n          \"fitness\",\n          \"exercise\",\n          \"training\",\n        ].join(\", \"),\n        inLanguage:\n          locale === \"en\"\n            ? \"en-US\"\n            : locale === \"es\"\n              ? \"es-ES\"\n              : locale === \"pt\"\n                ? \"pt-PT\"\n                : locale === \"ru\"\n                  ? \"ru-RU\"\n                  : locale === \"zh-CN\"\n                    ? \"zh-CN\"\n                    : \"fr-FR\",\n        embedUrl: url,\n        interactionStatistic: {\n          \"@type\": \"InteractionCounter\",\n          interactionType: \"https://schema.org/WatchAction\",\n          userInteractionCount: Math.floor(Math.random() * 1000) + 100,\n        },\n      };\n\n    case \"Calculator\":\n      if (!calculatorData) return baseStructuredData;\n\n      const calculatorKeywords = {\n        calorie: {\n          en: [\"calorie calculator\", \"TDEE calculator\", \"BMR calculator\", \"daily calorie needs\", \"weight loss calculator\", \"Cal mascot\"],\n          fr: [\"calculateur calories\", \"calculateur TDEE\", \"calculateur BMR\", \"besoins caloriques\", \"perte de poids\", \"Cal mascotte\"],\n          es: [\"calculadora calorías\", \"calculadora TDEE\", \"calculadora BMR\", \"necesidades calóricas\", \"pérdida peso\", \"Cal mascota\"],\n          pt: [\"calculadora calorias\", \"calculadora TDEE\", \"calculadora BMR\", \"necessidades calóricas\", \"perda peso\", \"Cal mascote\"],\n          ru: [\"калькулятор калорий\", \"калькулятор TDEE\", \"калькулятор BMR\", \"потребность калории\", \"похудение\", \"Кал маскот\"],\n          \"zh-CN\": [\"卡路里计算器\", \"TDEE计算器\", \"BMR计算器\", \"每日卡路里需求\", \"减重计算器\", \"Cal吉祥物\"],\n        },\n        \"one-rep-max\": {\n          en: [\"one rep max\", \"one rep max calculator\", \"one rep max formula\", \"one rep max calculation\", \"one rep max calculator\"],\n          fr: [\"one rep max\", \"calculateur one rep max\", \"formule one rep max\", \"calcul one rep max\", \"calculateur one rep max\"],\n          es: [\"one rep max\", \"calculadora one rep max\", \"fórmula one rep max\", \"calculo one rep max\", \"calculadora one rep max\"],\n          pt: [\"one rep max\", \"calculadora one rep max\", \"fórmula one rep max\", \"calculo one rep max\", \"calculadora one rep max\"],\n          ru: [\"one rep max\", \"калькулятор one rep max\", \"формула one rep max\", \"расчет one rep max\", \"калькулятор one rep max\"],\n          \"zh-CN\": [\"一次最大重复次数\", \"一次最大重复次数计算器\", \"一次最大重复次数公式\", \"一次最大重复次数计算\", \"一次最大重复次数计算器\"],\n        },\n        \"rest-timer\": {\n          en: [\"rest timer\", \"rest timer calculator\", \"rest timer formula\", \"rest timer calculation\", \"rest timer calculator\"],\n          fr: [\n            \"timer de repos\",\n            \"calculateur timer de repos\",\n            \"formule timer de repos\",\n            \"calcul timer de repos\",\n            \"calculateur timer de repos\",\n          ],\n          es: [\n            \"timer de repos\",\n            \"calculadora timer de repos\",\n            \"fórmula timer de repos\",\n            \"calculo timer de repos\",\n            \"calculadora timer de repos\",\n          ],\n          pt: [\n            \"timer de repos\",\n            \"calculadora timer de repos\",\n            \"fórmula timer de repos\",\n            \"calculo timer de repos\",\n            \"calculadora timer de repos\",\n          ],\n          ru: [\n            \"timer de repos\",\n            \"калькулятор timer de repos\",\n            \"формула timer de repos\",\n            \"расчет timer de repos\",\n            \"калькулятор timer de repos\",\n          ],\n          \"zh-CN\": [\"休息计时器\", \"休息计时器计算器\", \"休息计时器公式\", \"休息计时器计算\", \"休息计时器计算器\"],\n        },\n        macro: {\n          en: [\"macro calculator\", \"macros calculator\", \"macros formula\", \"macros calculation\", \"macros calculator\"],\n          fr: [\"calculateur macros\", \"calculateur macros\", \"formule macros\", \"calcul macros\", \"calculateur macros\"],\n          es: [\"calculadora macros\", \"calculadora macros\", \"fórmula macros\", \"calculo macros\", \"calculadora macros\"],\n          pt: [\"calculadora macros\", \"calculadora macros\", \"fórmula macros\", \"calculo macros\", \"calculadora macros\"],\n          ru: [\"калькулятор макросов\", \"калькулятор макросов\", \"формула макросов\", \"расчет макросов\", \"калькулятор макросов\"],\n          \"zh-CN\": [\"宏计算器\", \"宏计算器\", \"宏公式\", \"宏计算\", \"宏计算器\"],\n        },\n        bmi: {\n          en: [\"bmi calculator\", \"bmi formula\", \"bmi calculation\", \"bmi calculator\"],\n          fr: [\"calculateur bmi\", \"formule bmi\", \"calcul bmi\", \"calculateur bmi\"],\n          es: [\"calculadora bmi\", \"fórmula bmi\", \"calculo bmi\", \"calculadora bmi\"],\n          pt: [\"calculadora bmi\", \"fórmula bmi\", \"calculo bmi\", \"calculadora bmi\"],\n          ru: [\"калькулятор ИМТ\", \"формула ИМТ\", \"расчет ИМТ\", \"калькулятор ИМТ\"],\n          \"zh-CN\": [\"BMI计算器\", \"BMI公式\", \"BMI计算\", \"BMI计算器\"],\n        },\n        \"heart-rate\": {\n          en: [\"heart rate calculator\", \"heart rate formula\", \"heart rate calculation\", \"heart rate calculator\"],\n          fr: [\n            \"calculateur fréquence cardiaque\",\n            \"formule fréquence cardiaque\",\n            \"calcul fréquence cardiaque\",\n            \"calculateur fréquence cardiaque\",\n          ],\n          es: [\n            \"calculadora frecuencia cardíaca\",\n            \"fórmula frecuencia cardíaca\",\n            \"calculo frecuencia cardíaca\",\n            \"calculadora frecuencia cardíaca\",\n          ],\n          pt: [\n            \"calculadora frequência cardíaca\",\n            \"fórmula frequência cardíaca\",\n            \"calculo frequência cardíaca\",\n            \"calculadora frequência cardíaca\",\n          ],\n          ru: [\"калькулятор частоты пульса\", \"формула частоты пульса\", \"расчет частоты пульса\", \"калькулятор частоты пульса\"],\n          \"zh-CN\": [\"心率计算器\", \"心率公式\", \"心率计算\", \"心率计算器\"],\n        },\n        \"heart-rate-zones\": {\n          en: [\"heart rate zones calculator\", \"target heart rate\", \"training zones\", \"Karvonen formula\", \"VO2 max zone\"],\n          fr: [\n            \"calculateur zones fréquence cardiaque\",\n            \"fréquence cardiaque cible\",\n            \"zones d'entraînement\",\n            \"formule Karvonen\",\n            \"zone VO2 max\",\n          ],\n          es: [\n            \"calculadora zonas frecuencia cardíaca\",\n            \"frecuencia cardíaca objetivo\",\n            \"zonas de entrenamiento\",\n            \"fórmula Karvonen\",\n            \"zona VO2 máx\",\n          ],\n          pt: [\"calculadora zonas frequência cardíaca\", \"frequência cardíaca alvo\", \"zonas de treino\", \"fórmula Karvonen\", \"zona VO2 máx\"],\n          ru: [\"калькулятор зон пульса\", \"целевой пульс\", \"тренировочные зоны\", \"формула Карвонена\", \"зона VO2 max\"],\n          \"zh-CN\": [\"心率区间计算器\", \"目标心率\", \"训练区间\", \"卡沃宁公式\", \"VO2最大值区间\"],\n        },\n      };\n\n      const keywordsForType = calculatorKeywords[calculatorData.calculatorType];\n      const currentKeywords = keywordsForType?.[locale as keyof typeof keywordsForType] || keywordsForType?.en || [];\n\n      return {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"SoftwareApplication\",\n        \"@id\": url || baseUrl,\n        name: title || baseStructuredData.name,\n        description: description || baseStructuredData.description,\n        url: url || baseUrl,\n        applicationCategory: \"HealthApplication\",\n        applicationSubCategory: \"FitnessCalculator\",\n        operatingSystem: \"Web Browser\",\n        browserRequirements: \"Requires JavaScript. Requires HTML5.\",\n        softwareVersion: \"1.2.1\",\n        dateCreated: \"2024-01-01\",\n        dateModified: dateModified || new Date().toISOString(),\n        creator: {\n          \"@type\": \"Organization\",\n          name: SiteConfig.company.name,\n          url: baseUrl,\n          logo: {\n            \"@type\": \"ImageObject\",\n            url: `${baseUrl}/logo.png`,\n            width: 512,\n            height: 512,\n          },\n        },\n        publisher: {\n          \"@type\": \"Organization\",\n          name: SiteConfig.company.name,\n          url: baseUrl,\n          logo: {\n            \"@type\": \"ImageObject\",\n            url: `${baseUrl}/logo.png`,\n            width: 512,\n            height: 512,\n          },\n        },\n        offers: {\n          \"@type\": \"Offer\",\n          price: \"0\",\n          priceCurrency: \"USD\",\n          availability: \"https://schema.org/InStock\",\n        },\n        isAccessibleForFree: true,\n        featureList: calculatorData.inputFields.concat(calculatorData.outputFields),\n        keywords: currentKeywords.join(\", \"),\n        inLanguage:\n          locale === \"en\"\n            ? \"en-US\"\n            : locale === \"es\"\n              ? \"es-ES\"\n              : locale === \"pt\"\n                ? \"pt-PT\"\n                : locale === \"ru\"\n                  ? \"ru-RU\"\n                  : locale === \"zh-CN\"\n                    ? \"zh-CN\"\n                    : \"fr-FR\",\n        image: image || `${baseUrl}/images/calculator-og.jpg`,\n        aggregateRating: {\n          \"@type\": \"AggregateRating\",\n          ratingValue: \"4.9\",\n          ratingCount: \"13\",\n          bestRating: \"5\",\n          worstRating: \"1\",\n        },\n        // review: [\n        //   {\n        //     \"@type\": \"Review\",\n        //     author: {\n        //       \"@type\": \"Person\",\n        //       name:\n        //         locale === \"en\"\n        //           ? \"Sarah Johnson\"\n        //           : locale === \"fr\"\n        //             ? \"Marie Dubois\"\n        //             : locale === \"es\"\n        //               ? \"Ana García\"\n        //               : locale === \"pt\"\n        //                 ? \"Sofia Silva\"\n        //                 : locale === \"ru\"\n        //                   ? \"Анна Петрова\"\n        //                   : \"李明\",\n        //     },\n        //     datePublished: \"2024-11-15\",\n        //     description:\n        //       locale === \"en\"\n        //         ? \"Incredibly accurate calorie calculator! Cal the Chef mascot makes it fun to use.\"\n        //         : locale === \"fr\"\n        //           ? \"Calculateur de calories incroyablement précis ! La mascotte Cal le Chef rend son utilisation amusante.\"\n        //           : locale === \"es\"\n        //             ? \"¡Calculadora de calorías increíblemente precisa! La mascota Cal el Chef hace que sea divertido de usar.\"\n        //             : locale === \"pt\"\n        //               ? \"Calculadora de calorias incrivelmente precisa! A mascote Cal o Chef torna divertido de usar.\"\n        //               : locale === \"ru\"\n        //                 ? \"Невероятно точный калькулятор калорий! Маскот Кал Шеф делает его использование веселым.\"\n        //                 : \"令人难以置信的精确卡路里计算器！Cal厨师吉祥物让使用变得有趣。\",\n        //     name:\n        //       locale === \"en\"\n        //         ? \"Best calorie calculator I've used\"\n        //         : locale === \"fr\"\n        //           ? \"Meilleur calculateur de calories que j'ai utilisé\"\n        //           : locale === \"es\"\n        //             ? \"La mejor calculadora de calorías que he usado\"\n        //             : locale === \"pt\"\n        //               ? \"Melhor calculadora de calorias que usei\"\n        //               : locale === \"ru\"\n        //                 ? \"Лучший калькулятор калорий, который я использовал\"\n        //                 : \"我用过的最好的卡路里计算器\",\n        //     reviewRating: {\n        //       \"@type\": \"Rating\",\n        //       bestRating: \"5\",\n        //       ratingValue: \"5\",\n        //       worstRating: \"1\",\n        //     },\n        //   },\n        // ],\n        mainEntity: {\n          \"@type\": \"Thing\",\n          name: calculatorData.formula || \"Scientific Formula\",\n          description: calculatorData.accuracy || \"Clinically validated accuracy\",\n        },\n        audience: {\n          \"@type\": \"Audience\",\n          audienceType: calculatorData.targetAudience?.join(\", \") || \"fitness enthusiasts, athletes, health conscious individuals\",\n        },\n        potentialAction: {\n          \"@type\": \"Action\",\n          name:\n            locale === \"en\"\n              ? \"Calculate Calories\"\n              : locale === \"fr\"\n                ? \"Calculer les Calories\"\n                : locale === \"es\"\n                  ? \"Calcular Calorías\"\n                  : locale === \"pt\"\n                    ? \"Calcular Calorias\"\n                    : locale === \"ru\"\n                      ? \"Рассчитать Калории\"\n                      : \"计算卡路里\",\n          target: url || baseUrl,\n        },\n        sameAs: calculatorData.relatedCalculators?.map((calc) => `${baseUrl}/tools/${calc}`) || [],\n      };\n\n    default:\n      return baseStructuredData;\n  }\n}\n\nexport function StructuredDataScript({ data }: { data: object }) {\n  return React.createElement(\"script\", {\n    type: \"application/ld+json\",\n    dangerouslySetInnerHTML: {\n      __html: JSON.stringify(data),\n    },\n  });\n}\n"
  },
  {
    "path": "src/shared/lib/utils.ts",
    "content": "import { twMerge } from \"tailwind-merge\";\nimport { clsx, type ClassValue } from \"clsx\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "src/shared/lib/version.ts",
    "content": "import pkg from \"../../../package.json\";\n\nexport const appVersion = pkg.version;"
  },
  {
    "path": "src/shared/lib/web-share.ts",
    "content": "/**\n * Web Share API utilities\n * Simple and safe sharing functionality with fallbacks\n */\n\nexport interface ShareData {\n  title: string;\n  text?: string;\n  url: string;\n}\n\nexport type ShareResult = { success: true; method: \"native\" | \"clipboard\" } | { success: false; error: string };\n\n/**\n * Check if native Web Share API is available\n */\nexport function isWebShareSupported(): boolean {\n  return typeof navigator !== \"undefined\" && \"share\" in navigator;\n}\n\n/**\n * Check if Clipboard API is available\n */\nexport function isClipboardSupported(): boolean {\n  return typeof navigator !== \"undefined\" && \"clipboard\" in navigator;\n}\n\n/**\n * Share content using native Web Share API or fallback to clipboard\n */\nexport async function shareContent(data: ShareData): Promise<ShareResult> {\n  // Validate required data\n  if (!data.title || !data.url) {\n    return { success: false, error: \"Missing required share data\" };\n  }\n\n  // Try native Web Share API first (mobile-friendly)\n  if (isWebShareSupported()) {\n    try {\n      await navigator.share({\n        title: data.title,\n        text: data.text,\n        url: data.url,\n      });\n      return { success: true, method: \"native\" };\n    } catch (error) {\n      // User cancelled or error occurred, try fallback\n      console.warn(\"Web Share failed, trying clipboard fallback:\", error);\n    }\n  }\n\n  // Fallback to clipboard\n  if (isClipboardSupported()) {\n    try {\n      const shareText = data.text ? `${data.title}\\n\\n${data.text}\\n\\n${data.url}` : `${data.title}\\n\\n${data.url}`;\n\n      await navigator.clipboard.writeText(shareText);\n      return { success: true, method: \"clipboard\" };\n    } catch (error) {\n      console.error(\"Failed to copy to clipboard:\", error);\n      return { success: false, error: \"Failed to copy to clipboard\" };\n    }\n  }\n\n  // No sharing method available\n  return { success: false, error: \"Sharing not supported on this device\" };\n}\n\n/**\n * Generate shareable URL for current page\n */\nexport function getCurrentPageUrl(): string {\n  if (typeof window === \"undefined\") {\n    return \"\";\n  }\n  return window.location.href;\n}\n"
  },
  {
    "path": "src/shared/lib/weight-conversion.ts",
    "content": "export const WEIGHT_CONVERSION = {\n  LBS_TO_KG: 0.453592,\n  KG_TO_LBS: 2.20462,\n} as const;\n\nexport type WeightUnit = \"kg\" | \"lbs\";\n\nexport function convertWeight(weight: number, fromUnit: WeightUnit, toUnit: WeightUnit): number {\n  if (fromUnit === toUnit) return weight;\n\n  if (fromUnit === \"lbs\" && toUnit === \"kg\") {\n    return weight * WEIGHT_CONVERSION.LBS_TO_KG;\n  }\n\n  if (fromUnit === \"kg\" && toUnit === \"lbs\") {\n    return weight * WEIGHT_CONVERSION.KG_TO_LBS;\n  }\n\n  return weight;\n}\n\nexport function formatWeight(weight: number, unit: WeightUnit, decimals: number = 1): string {\n  return `${weight.toFixed(decimals)} ${unit}`;\n}\n\nexport function convertVolumeToUnit(\n  exercises: Array<{\n    sets: Array<{\n      completed: boolean;\n      types: string[];\n      valuesInt?: number[];\n      units?: string[];\n    }>;\n  }>,\n  targetUnit: WeightUnit,\n): number {\n  let totalVolume = 0;\n\n  exercises.forEach((exercise) => {\n    exercise.sets.forEach((set) => {\n      if (set.completed && set.types.includes(\"REPS\") && set.types.includes(\"WEIGHT\") && set.valuesInt) {\n        const repsIndex = set.types.indexOf(\"REPS\");\n        const weightIndex = set.types.indexOf(\"WEIGHT\");\n\n        const reps = set.valuesInt[repsIndex] || 0;\n        const weight = set.valuesInt[weightIndex] || 0;\n\n        // set unit\n        const originalUnit: WeightUnit = set.units && set.units[weightIndex] === \"lbs\" ? \"lbs\" : \"kg\";\n\n        const convertedWeight = convertWeight(weight, originalUnit, targetUnit);\n\n        totalVolume += reps * convertedWeight;\n      }\n    });\n  });\n\n  return Math.round(totalVolume * 10) / 10; // round to 1 decimal\n}\n"
  },
  {
    "path": "src/shared/lib/workout-session/equipments.ts",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { TFunction } from \"locales/client\";\n\nexport const allEquipmentValues = [\n  ExerciseAttributeValueEnum.BODY_ONLY,\n  ExerciseAttributeValueEnum.DUMBBELL,\n  ExerciseAttributeValueEnum.BARBELL,\n  ExerciseAttributeValueEnum.KETTLEBELLS,\n  ExerciseAttributeValueEnum.BANDS,\n];\n\nexport const getEquipmentTranslation = (value: ExerciseAttributeValueEnum, t: TFunction) => {\n  const equipmentKeys: Partial<Record<ExerciseAttributeValueEnum, string>> = {\n    [ExerciseAttributeValueEnum.BODY_ONLY]: \"bodyweight\",\n    [ExerciseAttributeValueEnum.DUMBBELL]: \"dumbbell\",\n    [ExerciseAttributeValueEnum.BARBELL]: \"barbell\",\n    [ExerciseAttributeValueEnum.KETTLEBELLS]: \"kettlebell\",\n    [ExerciseAttributeValueEnum.BANDS]: \"band\",\n    [ExerciseAttributeValueEnum.WEIGHT_PLATE]: \"plate\",\n    [ExerciseAttributeValueEnum.PULLUP_BAR]: \"pullup_bar\",\n    [ExerciseAttributeValueEnum.BENCH]: \"bench\",\n  };\n\n  const key = equipmentKeys[value];\n  return {\n    label: t(`workout_builder.equipment.${key}.label` as keyof typeof t),\n    description: t(`workout_builder.equipment.${key}.description` as keyof typeof t),\n  };\n};\n"
  },
  {
    "path": "src/shared/lib/workout-session/types/workout-session.ts",
    "content": "import { ExerciseAttributeValueEnum } from \"@prisma/client\";\n\nimport { WorkoutSessionExercise } from \"@/features/workout-session/types/workout-set\";\n\nexport const workoutSessionStatuses = [\"active\", \"completed\", \"synced\"] as const;\nexport type WorkoutSessionStatus = (typeof workoutSessionStatuses)[number];\n\nexport interface WorkoutSession {\n  id: string; // local: \"local-xxx\", server: uuid\n  userId: string;\n  status?: WorkoutSessionStatus;\n  startedAt: string;\n  endedAt?: string;\n  duration?: number;\n  exercises: WorkoutSessionExercise[];\n  currentExerciseIndex?: number;\n  isActive?: boolean;\n  serverId?: string; // If synced\n  muscles: ExerciseAttributeValueEnum[];\n}\n"
  },
  {
    "path": "src/shared/lib/workout-session/use-workout-session.service.ts",
    "content": "import { nullToUndefined } from \"@/shared/lib/format\";\nimport { syncWorkoutSessionAction } from \"@/features/workout-session/actions/sync-workout-sessions.action\";\nimport { getWorkoutSessionsAction } from \"@/features/workout-session/actions/get-workout-sessions.action\";\nimport { deleteWorkoutSessionAction } from \"@/features/workout-session/actions/delete-workout-session.action\";\nimport { useSession } from \"@/features/auth/lib/auth-client\";\n\nimport { workoutSessionLocal } from \"./workout-session.local\";\n\nimport type { WorkoutSession } from \"./types/workout-session\";\n\n// This is an abstraction layer to handle the local storage and/or the API calls.\n// He's the orchestrator.\n\nexport const useWorkoutSessionService = () => {\n  const { data: session } = useSession();\n  const userId = session?.user?.id;\n\n  const getAll = async (): Promise<WorkoutSession[]> => {\n    if (userId) {\n      const result = await getWorkoutSessionsAction({ userId });\n      if (result?.serverError) throw new Error(result.serverError);\n\n      const serverSessions = (result?.data?.sessions || []).map((session) => ({\n        ...session,\n        startedAt: session.startedAt instanceof Date ? session.startedAt.toISOString() : session.startedAt,\n        endedAt:\n          session.endedAt instanceof Date\n            ? session.endedAt.toISOString()\n            : typeof session.endedAt === \"string\"\n              ? session.endedAt\n              : undefined,\n        duration: nullToUndefined(session.duration),\n        exercises: session.exercises.map(({ exercise, order, sets }) => ({\n          ...exercise,\n          order,\n          sets: sets.map((set) => {\n            return {\n              ...set,\n              units: nullToUndefined(set.units),\n            };\n          }),\n        })),\n      }));\n      const localSessions = workoutSessionLocal.getAll().filter((s) => s.status !== \"synced\");\n\n      return [...localSessions, ...(serverSessions as any)].sort(\n        (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(),\n      );\n    }\n\n    return workoutSessionLocal.getAll().sort((a, b) => {\n      const dateA = typeof a.startedAt === \"string\" ? new Date(a.startedAt) : a.startedAt;\n      const dateB = typeof b.startedAt === \"string\" ? new Date(b.startedAt) : b.startedAt;\n      return dateB.getTime() - dateA.getTime();\n    });\n  };\n\n  const add = async (session: WorkoutSession) => {\n    if (userId) {\n      const result = await syncWorkoutSessionAction({\n        session: {\n          ...session,\n          userId,\n          status: \"synced\",\n        },\n      });\n\n      if (result?.serverError) throw new Error(result.serverError);\n\n      if (result?.data?.data) {\n        workoutSessionLocal.markSynced(session.id, result.data.data.id);\n      }\n    }\n\n    return workoutSessionLocal.add(session);\n  };\n\n  const update = async (_id: string, _data: Partial<WorkoutSession>) => {\n    // if (userId) {\n    //   // TODO: create updateWorkoutSessionAction\n    //   const result = await updateWorkoutSessionAction({ id, data });\n    //   if (result.serverError) throw new Error(result.serverError);\n    // }\n    // return workoutSessionLocal.update(id, data);\n  };\n\n  const complete = async (_id: string) => {\n    // const data = {\n    //   status: \"completed\" as const,\n    //   endedAt: new Date().toISOString(),\n    // };\n    // if (isUserLoggedIn()) {\n    //   const result = await completeWorkoutSessionAction({ id });\n    //   if (result.serverError) throw new Error(result.serverError);\n    // }\n    // return workoutSessionLocal.update(id, data);\n  };\n\n  const remove = async (id: string) => {\n    if (userId) {\n      const result = await deleteWorkoutSessionAction({ id });\n      if (result?.serverError) throw new Error(result.serverError);\n    }\n\n    workoutSessionLocal.remove(id);\n  };\n\n  return { getAll, add, update, complete, remove };\n};\n"
  },
  {
    "path": "src/shared/lib/workout-session/workout-session.api.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unused-vars */\nimport type { WorkoutSession } from \"./types/workout-session\";\n\nexport const workoutSessionApi = {\n  getAll: async (): Promise<WorkoutSession[]> => {\n    // TODO: fetch(\"/api/workout-sessions\")\n    return [];\n  },\n  create: async (session: WorkoutSession): Promise<{ id: string }> => {\n    // TODO: POST /api/workout-sessions\n    return { id: \"server-uuid\" };\n  },\n  update: async (id: string, data: Partial<WorkoutSession>) => {\n    // TODO: PATCH /api/workout-sessions/:id\n  },\n  complete: async (id: string) => {\n    // TODO: PATCH /api/workout-sessions/:id/complete\n  },\n};\n"
  },
  {
    "path": "src/shared/lib/workout-session/workout-session.local.ts",
    "content": "import type { WorkoutSession } from \"./types/workout-session\";\n\nconst STORAGE_KEY = \"workoutSessions\";\nconst MAX_SESSIONS = 10;\nconst CURRENT_SESSION_KEY = \"currentWorkoutSessionId\";\n\nfunction getAll(): WorkoutSession[] {\n  try {\n    return JSON.parse(localStorage.getItem(STORAGE_KEY) || \"[]\");\n  } catch {\n    return [];\n  }\n}\n\nfunction saveAll(sessions: WorkoutSession[]) {\n  localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions.slice(-MAX_SESSIONS)));\n}\n\nfunction getById(id: string): WorkoutSession | undefined {\n  return getAll().find((s) => s.id === id);\n}\n\nfunction setCurrent(id: string) {\n  localStorage.setItem(CURRENT_SESSION_KEY, id);\n}\n\nfunction getCurrent(): string | null {\n  return localStorage.getItem(CURRENT_SESSION_KEY);\n}\n\nexport const workoutSessionLocal = {\n  getAll,\n  getActive: () => getAll().find((s) => s.status === \"active\"),\n  add: (session: WorkoutSession) => {\n    const sessions = getAll();\n    sessions.push(session);\n    saveAll(sessions);\n  },\n  update: (id: string, data: Partial<WorkoutSession>) => {\n    const sessions = getAll().map((s) => (s.id === id ? { ...s, ...data } : s));\n    saveAll(sessions);\n  },\n  remove: (id: string) => {\n    const sessions = getAll().filter((s) => s.id !== id);\n    saveAll(sessions);\n  },\n  markSynced: (id: string, serverId: string) => {\n    const sessions = getAll().map((s) => (s.id === id ? { ...s, status: \"synced\" as const, serverId } : s));\n    saveAll(sessions);\n  },\n  purgeSynced: () => {\n    const sessions = getAll().filter((s) => s.status !== \"synced\");\n    saveAll(sessions);\n  },\n  getById,\n  setCurrent,\n  getCurrent,\n};\n"
  },
  {
    "path": "src/shared/lib/youtube.ts",
    "content": "// src/shared/lib/get-youtube-embed-url.ts\n\ninterface YouTubeEmbedOptions {\n  autoplay?: boolean;\n  // Add other embed options if needed (e.g., controls, loop, start)\n  // controls?: boolean;\n  // loop?: boolean;\n  // start?: number; // Start time in seconds\n}\n\n/**\n * Converts various YouTube URL formats into a standard embeddable URL.\n * Handles regular videos, shorts, youtu.be links, and playlists.\n * Returns null if the URL is invalid or the ID cannot be extracted.\n *\n * @param url The original YouTube URL string.\n * @param options Optional configuration for the embed (e.g., autoplay).\n * @returns The embeddable YouTube URL string or null.\n */\nexport function getYouTubeEmbedUrl(url: string, options: YouTubeEmbedOptions = {}): string | null {\n  if (!url) {\n    return null;\n  }\n\n  let videoId: string | null = null;\n  let playlistId: string | null = null;\n\n  try {\n    const urlObject = new URL(url);\n    const hostname = urlObject.hostname;\n    const pathname = urlObject.pathname;\n    const searchParams = urlObject.searchParams;\n\n    // 1. Check for standard video URLs (youtube.com/watch?v=...)\n    if (hostname.includes(\"youtube.com\") && pathname === \"/watch\") {\n      videoId = searchParams.get(\"v\");\n      // Check if it's also part of a playlist\n      if (searchParams.get(\"list\")) {\n        playlistId = searchParams.get(\"list\");\n      }\n    }\n    // 2. Check for short URLs (youtu.be/...)\n    else if (hostname === \"youtu.be\") {\n      videoId = pathname.substring(1); // Remove leading '/'\n    }\n    // 3. Check for embed URLs (/embed/...)\n    else if (hostname.includes(\"youtube.com\") && pathname.startsWith(\"/embed/\")) {\n      videoId = pathname.substring(\"/embed/\".length);\n    }\n    // 4. Check for shorts URLs (/shorts/...)\n    else if (hostname.includes(\"youtube.com\") && pathname.startsWith(\"/shorts/\")) {\n      videoId = pathname.substring(\"/shorts/\".length);\n    }\n    // 5. Check for playlist URLs (youtube.com/playlist?list=...)\n    else if (hostname.includes(\"youtube.com\") && pathname === \"/playlist\") {\n      playlistId = searchParams.get(\"list\");\n    }\n    // Note: Channel URLs (/channel/..., /c/..., /@handle) are not directly embeddable in the same way.\n    // You usually embed a specific video or playlist from a channel.\n  } catch (error) {\n    // If URL parsing fails, maybe it's just an ID/handle? We don't handle that here for embeds.\n    console.warn(\"Could not parse as URL for embed:\", url, error);\n    // For embeds, we need a video or playlist ID extracted from a valid URL structure.\n    return null;\n  }\n\n  // --- Construct Embed URL ---\n\n  const baseEmbedUrl = \"https://www.youtube-nocookie.com/embed/\";\n  const queryParams = new URLSearchParams();\n\n  if (options.autoplay) {\n    queryParams.set(\"autoplay\", \"1\");\n  }\n\n  // Maximum `YouTube` branding removal\n  queryParams.set(\"modestbranding\", \"1\");\n  queryParams.set(\"rel\", \"0\");\n  queryParams.set(\"showinfo\", \"0\");\n  queryParams.set(\"mute\", \"1\");\n  queryParams.set(\"controls\", \"1\");\n  queryParams.set(\"iv_load_policy\", \"3\"); // Hide annotations\n  queryParams.set(\"cc_load_policy\", \"0\"); // Hide captions by default\n  queryParams.set(\"fs\", \"0\"); // Disable fullscreen button\n  queryParams.set(\"disablekb\", \"1\"); // Disable keyboard controls\n  queryParams.set(\"playsinline\", \"1\"); // Mobile optimization\n  queryParams.set(\"origin\", typeof window !== \"undefined\" ? window.location.origin : \"\");\n\n  // Prioritize playlist embed if playlistId is found\n  if (playlistId) {\n    // For playlist embed, use videoseries?list=...\n    queryParams.set(\"list\", playlistId);\n    // If a videoId was also found, it becomes the first video in the playlist context\n    const path = videoId ? videoId : \"videoseries\";\n    return `${baseEmbedUrl}${path}?${queryParams.toString()}`;\n  }\n  // Otherwise, embed the single video if videoId is found\n  else if (videoId) {\n    // If looping a single video, playlist param needs to be the videoId itself\n    // if (options.loop) queryParams.set('playlist', videoId);\n    return `${baseEmbedUrl}${videoId}?${queryParams.toString()}`;\n  }\n\n  // If neither videoId nor playlistId could be extracted\n  return null;\n}\n"
  },
  {
    "path": "src/shared/schemas/url.ts",
    "content": "import { z } from \"zod\";\n\nexport const urlSchema = z.string().url(\"form_invalid_url\");\nexport type Url = z.infer<typeof urlSchema>;\n"
  },
  {
    "path": "src/shared/styles/additional-styles/highlights.css",
    "content": "/* Custom Highlights - Une alternative légère à tailwindcss-highlights */\n\n/* Variante 1: Highlight primaire */\n.highlight-primary {\n  background: linear-gradient(to bottom, transparent 40%, var(--color-accent-pink) 40%, var(--color-accent-pink) 80%, transparent 80%);\n  padding: 0.6em 0.25em;\n  border-radius: 0.25em;\n}\n\n.highlight-red {\n  background: linear-gradient(to bottom, transparent 40%, var(--color-accent-red) 40%, var(--color-accent-red) 80%, transparent 80%);\n  padding: 2em 0.125em;\n  border-radius: 0.25em;\n}\n\n/* Variante 2: Highlight avec soulignement accentué */\n.highlight-underline {\n  text-decoration: underline;\n  text-decoration-color: var(--color-accent-red);\n  text-decoration-thickness: 0.2em;\n  text-underline-offset: 0.2em;\n}\n\n/* Variante 3: Highlight avec fond discret */\n.highlight-soft {\n  background-color: var(--color-accent-orange);\n  padding: 0.15em 0.4em;\n  border-radius: 0.2em;\n}\n\n/* Variante 4: Highlight marqueur style brutaliste */\n.highlight-marker {\n  background: linear-gradient(to bottom, transparent 30%, var(--color-accent) 30%, var(--color-accent) 90%, transparent 90%);\n  font-weight: 600;\n  padding: 0.45em 0.1em;\n  padding-bottom: 0.2em;\n}\n\n/* Variante 5: Highlight encadré */\n.highlight-box {\n  border: 2px solid var(--color-primary);\n  box-shadow: 3px 3px 0 var(--color-accent);\n  border-radius: 0.2em;\n  padding: 0.15em 0.4em;\n  margin: 0 0.1em;\n}\n\n/* Animation optionnelle pour les highlights */\n.highlight-animated {\n  transition: all 0.3s ease;\n}\n\n.highlight-animated:hover {\n  transform: translateY(-2px);\n}\n"
  },
  {
    "path": "src/shared/styles/additional-styles/utility-patterns.css",
    "content": "/* Typography */\n* {\n}\n.h1 {\n  @apply text-5xl font-extrabold;\n}\n\n.h2 {\n  @apply text-4xl font-extrabold;\n}\n\n.h3 {\n  @apply text-3xl font-extrabold;\n}\n\n.h4 {\n  @apply text-2xl font-extrabold;\n}\n\n@media (min-width: 768px) {\n  .h1 {\n    @apply text-6xl;\n  }\n\n  .h2 {\n    @apply text-5xl;\n  }\n}\n"
  },
  {
    "path": "src/shared/styles/globals.css",
    "content": "@import url(\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap\");\n@import \"./additional-styles/utility-patterns.css\";\n@import \"./additional-styles/highlights.css\";\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@plugin \"daisyui\";\n\n/* Google AdSense Styles - Prevent \"No slot size for availableWidth=0\" error */\n\n:root {\n  --font-sans: \"Plus Jakarta Sans\", sans-serif;\n  --text-xs: 0.75rem;\n  --text-xs--line-height: 1.5;\n  --text-sm: 0.875rem;\n  --text-sm--line-height: 1.5715;\n  --text-base: 1rem;\n  --text-base--line-height: 1.5;\n  --text-base--letter-spacing: -0.01em;\n  --text-lg: 1.125rem;\n  --text-lg--line-height: 1.5;\n  --text-lg--letter-spacing: -0.01em;\n  --text-xl: 1.25rem;\n  --text-xl--line-height: 1.5;\n  --text-xl--letter-spacing: -0.01em;\n  --text-2xl: 1.5rem;\n  --text-2xl--line-height: 1.415;\n  --text-2xl--letter-spacing: -0.01em;\n  --text-3xl: 1.875rem;\n  --text-3xl--line-height: 1.333;\n  --text-3xl--letter-spacing: -0.01em;\n  --text-4xl: 2.25rem;\n  --text-4xl--line-height: 1.277;\n  --text-4xl--letter-spacing: -0.01em;\n  --text-5xl: 3rem;\n  --text-5xl--line-height: 1;\n  --text-5xl--letter-spacing: -0.01em;\n  --text-6xl: 3.5rem;\n  --text-6xl--line-height: 1;\n  --text-6xl--letter-spacing: -0.01em;\n  --text-7xl: 4.5rem;\n  --text-7xl--line-height: 1;\n  --text-7xl--letter-spacing: -0.01em;\n\n  /* Highlight accent colors */\n  --color-primary: #335cff;\n  --color-accent: #6683f8;\n  --color-accent-pink: #ff7eb6;\n  --color-accent-orange: #ffedd5;\n  --color-accent-red: #cf0026;\n\n  /* Light theme colors */\n  --color-base-100: oklch(98% 0.005 255);\n  --color-base-200: oklch(96% 0.005 255);\n  --color-base-300: oklch(93% 0.005 255);\n  --color-base-content: oklch(18% 0.01 255);\n  --color-primary-content: oklch(98% 0.005 255);\n  --color-secondary: oklch(90% 0.01 255);\n  --color-secondary-content: oklch(18% 0.01 255);\n  --color-accent-content: oklch(18% 0.01 255);\n  --color-neutral: oklch(95% 0.005 255);\n  --color-neutral-content: oklch(18% 0.01 255);\n  --color-info: oklch(74% 0.16 232.661);\n  --color-info-content: oklch(29% 0.066 243.157);\n  --color-success: oklch(76% 0.177 163.223);\n  --color-success-content: oklch(37% 0.077 168.94);\n  --color-warning: oklch(82% 0.189 84.429);\n  --color-warning-content: oklch(41% 0.112 45.904);\n  --color-error: oklch(71% 0.194 13.428);\n  --color-error-content: oklch(27% 0.105 12.094);\n}\n\n:root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) {\n  scrollbar-gutter: auto !important;\n}\n\n.modal-backdrop {\n  position: fixed !important;\n  top: 0 !important;\n  left: 0 !important;\n  width: 100vw !important;\n  height: 100vh !important;\n  margin: 0 !important;\n}\n\n/* Fix pour mobile : éviter le gap à droite sur les petits écrans */\n@media (max-width: 768px) {\n  :root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) {\n    scrollbar-gutter: auto !important;\n  }\n\n  /* Forcer le backdrop à prendre toute la largeur */\n  .modal-backdrop {\n    position: fixed !important;\n    top: 0 !important;\n    left: 0 !important;\n    width: 100vw !important;\n    height: 100vh !important;\n    margin: 0 !important;\n  }\n}\n/* \n[data-ezoic-incontent-sticky] {\n  overflow: auto !important;\n  overflow-y: auto !important;\n}\n\n.card.max-h-\\[90vh\\] {\n  overflow-y: auto !important;\n} */\n\n.dark {\n  --color-base-100: oklch(25.33% 0.016 252.42);\n  --color-base-200: oklch(23.26% 0.014 253.1);\n  --color-base-300: oklch(21.15% 0.012 254.09);\n  --color-base-content: oklch(97.807% 0.029 256.847);\n  --color-primary: oklch(58% 0.233 277.117);\n  --color-primary-content: oklch(96% 0.018 272.314);\n  --color-secondary: oklch(65% 0.241 354.308);\n  --color-secondary-content: oklch(94% 0.028 342.258);\n  --color-accent: oklch(77% 0.152 181.912);\n  --color-accent-content: oklch(38% 0.063 188.416);\n  --color-neutral: oklch(14% 0.005 285.823);\n  --color-neutral-content: oklch(92% 0.004 286.32);\n  --color-info: oklch(74% 0.16 232.661);\n  --color-info-content: oklch(29% 0.066 243.157);\n  --color-success: oklch(76% 0.177 163.223);\n  --color-success-content: oklch(37% 0.077 168.94);\n  --color-warning: oklch(82% 0.189 84.429);\n  --color-warning-content: oklch(41% 0.112 45.904);\n  --color-error: oklch(71% 0.194 13.428);\n  --color-error-content: oklch(27% 0.105 12.094);\n}\n\n@layer components {\n  /* Muscle illustration styles */\n  .muscle-enabled {\n    @apply fill-blue-300 stroke-blue-500 cursor-pointer;\n  }\n\n  .muscle-enabled:hover {\n    @apply fill-blue-400 stroke-blue-600;\n  }\n\n  .muscle-active {\n    @apply fill-emerald-400 stroke-emerald-600;\n    stroke-width: 1 !important;\n  }\n\n  .muscle-active:hover {\n    @apply fill-emerald-500 stroke-emerald-700;\n  }\n\n  .muscle-hover {\n    @apply fill-slate-300 stroke-slate-600;\n  }\n\n  .muscle-loading {\n    @apply opacity-50 animate-pulse;\n  }\n}\n\n.skeleton {\n  @apply animate-pulse bg-gray-200 dark:bg-gray-700;\n}\n\n.dark code {\n  @apply text-black;\n}\n\n:where(.\\!modal[open], .modal-open, .modal-toggle:checked + .\\!modal):not(.modal-start, .modal-end) {\n  scrollbar-gutter: auto !important;\n}\n\n@layer components {\n  .sidebar .nav-link {\n    @apply mx-[2px] mt-1 flex items-center gap-2.5 border border-transparent px-5 py-2.5 text-sm font-medium leading-tight text-gray transition hover:text-black dark:text-gray-500 dark:hover:text-white;\n    @apply hover:rounded-lg hover:bg-gray-400;\n  }\n  .nav-item.sub-menu-active {\n    @apply bg-light-theme !text-primary dark:bg-[#818CF8]/[6%] dark:!text-[#6683F8];\n  }\n  .nav-item.active {\n    @apply !text-black dark:!text-white;\n  }\n\n  .sidebar .submenu > li > a {\n    @apply flex rounded-lg px-2 py-1 font-medium text-gray-700 transition hover:bg-light-theme hover:text-primary dark:text-gray-600 dark:hover:bg-[#818CF8]/[6%] dark:hover:text-[#6683F8];\n  }\n  .sidebar.closed {\n    @apply w-[260px] lg:w-[60px];\n  }\n  .sidebar.closed h3 {\n    @apply rounded-none;\n  }\n  .sidebar.closed h3 > span {\n    @apply lg:hidden;\n  }\n  .sidebar.closed h3 > svg {\n    @apply lg:block;\n  }\n  .sidebar.closed .submenu {\n    @apply lg:hidden;\n  }\n  .sidebar .nav-link span {\n    @apply whitespace-nowrap transition-all;\n  }\n  .sidebar.closed .nav-link span {\n    @apply lg:invisible lg:w-0;\n  }\n  .sidebar.closed .sidemenu {\n    @apply px-2.5 lg:px-0;\n  }\n  .sidebar.closed .upgrade-menu {\n    @apply hidden;\n  }\n  .sidebar.open {\n    @apply ltr:left-0 rtl:right-0;\n  }\n  #overlay.open {\n    @apply block;\n  }\n\n  /* Documentation page */\n  .header-menu.open {\n    @apply right-0;\n  }\n  .gradient-bar {\n    background: linear-gradient(\n      133deg,\n      rgba(236, 196, 64, 0.2) 0%,\n      rgba(255, 250, 138, 0.2) 33%,\n      rgba(221, 172, 23, 0.2) 68%,\n      rgba(255, 255, 149, 0.2) 100%\n    );\n  }\n  .gradient-border {\n    background: linear-gradient(133deg, #ecc440 0%, #fffa8a 33%, #ddac17 68%, #ffff95 100%);\n  }\n\n  /* Horizontal layout */\n  .horizontal header .sidebar {\n    @apply lg:flex;\n  }\n  .horizontal #sidebar {\n    @apply ltr:lg:-left-full rtl:lg:-right-full;\n  }\n  .horizontal #main-content {\n    @apply lg:mt-[122px] ltr:ml-0 rtl:mr-0;\n  }\n\n  .link {\n    @apply text-base font-medium transition-colors duration-200;\n  }\n\n  .link:hover {\n    @apply underline decoration-2 underline-offset-4;\n  }\n\n  .link-nav {\n    @apply text-base text-base-content/80 hover:text-base-content;\n  }\n\n  .link-footer {\n    @apply text-base/40 text-base-content/50 hover:text-base-content;\n  }\n}\n\n.ratio-dynamic {\n  position: relative;\n  left: 50%;\n  top: 50%;\n  transform: translate(-50%, -50%);\n  /* Optionnel: arrondis, bordures, etc. */\n  border-radius: 2rem;\n  overflow: hidden;\n  background: white;\n  border: 10px solid black;\n  padding: 0.5rem;\n  /* Pour l'effet responsive: */\n  width: 100vw;\n  height: calc(100vw / (9 / 18));\n  max-width: 400px; /* ou ce que tu veux */\n  max-height: 80vh;\n}\n\n@media (min-aspect-ratio: 9/18) {\n  .ratio-dynamic {\n    width: calc(100vh * (9 / 18));\n    height: 100vh;\n  }\n}\n\n.horizontal header .sidebar {\n  scrollbar-width: thin;\n  overflow-x: auto;\n  scroll-snap-type: x mandatory;\n}\n\n/* Boxed layout */\n.box-layout .main-content,\n.box-layout header {\n  @apply mx-auto w-full max-w-[1400px];\n}\n.box-layout .sidebar {\n  @apply ltr:lg:left-auto rtl:lg:right-auto;\n}\n\n/* Collapsible layout */\n.collapsible #main-content {\n  @apply ltr:lg:ml-[60px] rtl:lg:mr-[60px];\n}\n\n.bg-hero-light {\n  background-image:\n    radial-gradient(circle at 82% 60%, rgba(59, 59, 59, 0.06) 0%, rgba(59, 59, 59, 0.06) 69%, transparent 69%, transparent 100%),\n    radial-gradient(circle at 36% 0%, rgba(185, 185, 185, 0.06) 0%, rgba(185, 185, 185, 0.06) 59%, transparent 59%, transparent 100%),\n    radial-gradient(circle at 58% 82%, rgba(183, 183, 183, 0.06) 0%, rgba(183, 183, 183, 0.06) 17%, transparent 17%, transparent 100%),\n    radial-gradient(circle at 71% 32%, rgba(19, 19, 19, 0.06) 0%, rgba(19, 19, 19, 0.06) 40%, transparent 40%, transparent 100%),\n    radial-gradient(circle at 77% 5%, rgba(31, 31, 31, 0.06) 0%, rgba(31, 31, 31, 0.06) 52%, transparent 52%, transparent 100%),\n    radial-gradient(circle at 96% 80%, rgba(11, 11, 11, 0.06) 0%, rgba(11, 11, 11, 0.06) 73%, transparent 73%, transparent 100%),\n    radial-gradient(circle at 91% 59%, rgba(252, 252, 252, 0.06) 0%, rgba(252, 252, 252, 0.06) 44%, transparent 44%, transparent 100%),\n    radial-gradient(circle at 52% 82%, rgba(223, 223, 223, 0.06) 0%, rgba(223, 223, 223, 0.06) 87%, transparent 87%, transparent 100%),\n    radial-gradient(circle at 84% 89%, rgba(160, 160, 160, 0.06) 0%, rgba(160, 160, 160, 0.06) 57%, transparent 57%, transparent 100%),\n    linear-gradient(90deg, rgb(254, 242, 164), rgb(166, 255, 237));\n}\n\n.bg-hero-dark {\n  background-image:\n    radial-gradient(circle at 82% 60%, rgba(200, 200, 200, 0.04) 0%, rgba(200, 200, 200, 0.04) 69%, transparent 69%, transparent 100%),\n    radial-gradient(circle at 36% 0%, rgba(150, 150, 150, 0.04) 0%, rgba(150, 150, 150, 0.04) 59%, transparent 59%, transparent 100%),\n    radial-gradient(circle at 58% 82%, rgba(130, 130, 130, 0.04) 0%, rgba(130, 130, 130, 0.04) 17%, transparent 17%, transparent 100%),\n    radial-gradient(circle at 71% 32%, rgba(90, 90, 90, 0.05) 0%, rgba(90, 90, 90, 0.05) 40%, transparent 40%, transparent 100%),\n    radial-gradient(circle at 77% 5%, rgba(70, 70, 70, 0.05) 0%, rgba(70, 70, 70, 0.05) 52%, transparent 52%, transparent 100%),\n    radial-gradient(circle at 96% 80%, rgba(50, 50, 50, 0.05) 0%, rgba(50, 50, 50, 0.05) 73%, transparent 73%, transparent 100%),\n    radial-gradient(circle at 91% 59%, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.03) 44%, transparent 44%, transparent 100%),\n    radial-gradient(circle at 52% 82%, rgba(180, 180, 180, 0.03) 0%, rgba(180, 180, 180, 0.03) 87%, transparent 87%, transparent 100%),\n    radial-gradient(circle at 84% 89%, rgba(160, 160, 160, 0.03) 0%, rgba(160, 160, 160, 0.03) 57%, transparent 57%, transparent 100%),\n    linear-gradient(90deg, rgb(22, 22, 26), rgb(18, 24, 38));\n}\n\n@keyframes fade-in-up {\n  from {\n    opacity: 0;\n    transform: translateY(20px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slide-up {\n  from {\n    opacity: 0;\n    transform: translateY(100%) translateX(-50%);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0) translateX(-50%);\n  }\n}\n\n@keyframes colon-blink {\n  0%,\n  50% {\n    opacity: 1;\n  }\n  51%,\n  100% {\n    opacity: 0.3;\n  }\n}\n\n.animate-fade-in-up {\n  animation: fade-in-up 0.6s ease-out;\n}\n\n.animate-slide-up {\n  animation: slide-up 0.4s ease-out;\n}\n\n.animate-colon-blink {\n  animation: colon-blink 1s infinite ease-in-out;\n}\n\n/* Smooth focus styles pour l'accessibilité */\n.equipment-card:focus-visible {\n  outline: 2px solid #10b981;\n  outline-offset: 2px;\n}\n\n/* Heart Rate Zones Gamification Animations */\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n    transform: translateY(-10px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes bounce {\n  0%,\n  100% {\n    transform: translateY(0);\n  }\n  50% {\n    transform: translateY(-10px);\n  }\n}\n\n@keyframes pulse {\n  0%,\n  100% {\n    opacity: 1;\n    transform: scale(1);\n  }\n  50% {\n    opacity: 0.8;\n    transform: scale(1.05);\n  }\n}\n\n@keyframes spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.animate-fadeIn {\n  animation: fadeIn 0.3s ease-out;\n}\n\n.animate-bounce {\n  animation: bounce 1s ease-in-out infinite;\n}\n\n.animate-pulse {\n  animation: pulse 2s ease-in-out infinite;\n}\n\n.animate-spin {\n  animation: spin 2s linear infinite;\n}\n\n/* Delay classes for staggered animations */\n.delay-300 {\n  animation-delay: 300ms;\n}\n\n.delay-500 {\n  animation-delay: 500ms;\n}\n\n.delay-700 {\n  animation-delay: 700ms;\n}\n"
  },
  {
    "path": "src/shared/types/i18n.types.ts",
    "content": "import { Locale } from \"locales/types\";\n\n// Base type for any field that needs internationalization\nexport type I18nField<T extends string> = {\n  [K in T]: string;\n} & {\n  [K in T as `${K}En`]: string;\n} & {\n  [K in T as `${K}Es`]: string;\n} & {\n  [K in T as `${K}Pt`]: string;\n} & {\n  [K in T as `${K}Ru`]: string;\n} & {\n  [K in T as `${K}ZhCn`]: string;\n};\n\n// Common i18n fields used across entities\nexport type I18nText = I18nField<\"title\"> & I18nField<\"description\">;\nexport type I18nSlug = I18nField<\"slug\">;\nexport type I18nName = I18nField<\"name\">;\n\n// Utility type to extract a specific locale field\nexport type ExtractLocaleField<T extends Record<string, any>, Field extends keyof T, L extends Locale> = L extends \"fr\"\n  ? T[Field]\n  : L extends \"en\"\n    ? T[`${string & Field}En`]\n    : L extends \"es\"\n      ? T[`${string & Field}Es`]\n      : L extends \"pt\"\n        ? T[`${string & Field}Pt`]\n        : L extends \"ru\"\n          ? T[`${string & Field}Ru`]\n          : L extends \"zh-CN\"\n            ? T[`${string & Field}ZhCn`]\n            : never;\n\n// Helper to get field suffix for a locale\nexport function getLocaleSuffix(locale: Locale): \"En\" | \"Es\" | \"Pt\" | \"Ru\" | \"ZhCn\" | \"\" {\n  switch (locale) {\n    case \"en\":\n      return \"En\";\n    case \"es\":\n      return \"Es\";\n    case \"pt\":\n      return \"Pt\";\n    case \"ru\":\n      return \"Ru\";\n    case \"zh-CN\":\n      return \"ZhCn\";\n    default:\n      return \"\";\n  }\n}\n"
  },
  {
    "path": "src/shared/types/next.ts",
    "content": "import type { ReactNode } from \"react\";\n\n/**\n * @name PageParams\n *\n * @usage\n * In NextJS, params are the dynamic parts of the URL.\n * For example, if you have a page with the route `/posts/[id]`, then `id` is a param.\n *\n * You can then use the `PageParams` type to define the type of the params.\n *\n * ```tsx\n * export default function Page(params: PageParams<{ id: string }>) {\n *   ...\n * }\n * ```\n */\nexport type PageParams<T extends Record<string, string> = {}> = {\n  params: T;\n  searchParams: { [key: string]: string | string[] | undefined };\n};\n\n/**\n * @name LayoutParams\n *\n * @usage\n * In NextJS, params can be defined also in the layout.\n *\n * For an example, this file `/app/users/[userId]/layout.tsx` will have the following params:\n *\n * ```tsx\n * export default function Layout(params: LayoutParams<{ userId: string }>) {\n *   ...\n * }\n * ```\n */\nexport type LayoutParams<T extends Record<string, string> = {}> = {\n  params: Promise<T>;\n  children?: ReactNode | undefined;\n};\n\n/**\n * @name ErrorParams\n *\n * @usage\n * This type is used to define the parameters of the `error.tsx` page.\n */\nexport type ErrorParams = {\n  error: Error & { digest?: string };\n  reset: () => void;\n};\n"
  },
  {
    "path": "src/shared/types/premium.types.ts",
    "content": "// Simple premium types following KISS principle\n// Keep It Stupid Simple - easy to understand and maintain\n\n// Core premium status - single source of truth\nexport interface PremiumStatus {\n  isPremium: boolean;\n  expiresAt?: Date;\n  provider?: PremiumProvider;\n  revenueCatUserId?: string | null;\n  hasRevenueCatAccount?: boolean;\n}\n\n// Available payment providers - easy to extend\nexport type PremiumProvider = \"stripe\" | \"paypal\" | \"lemonsqueezy\" | \"revenuecat\" | \"other\";\n\n// Plan types for the new 3-plan structure\nexport type PlanType = \"free\" | \"supporter\" | \"premium\";\n\n// Premium plans - enhanced structure for new fitness-focused pricing\nexport interface PremiumPlan {\n  id: string; // External provider ID (e.g., price_1ABC) or \"free\" for free plan\n  internalId?: string; // Internal database ID\n  name: string;\n  type: PlanType; // New: plan type\n  priceMonthly: number;\n  priceYearly: number;\n  currency: \"EUR\" | \"USD\";\n  features: string[];\n  description?: string; // New: plan description\n  badge?: string; // New: plan badge text\n  isPopular?: boolean; // New: mark as most popular\n  isRecommended?: boolean; // New: mark as recommended\n}\n\n// Checkout session - provider agnostic\nexport interface CheckoutResult {\n  success: boolean;\n  checkoutUrl?: string;\n  error?: string;\n  provider: PremiumProvider;\n}\n\n// User subscription info - what the UI needs\nexport interface UserSubscription {\n  isActive: boolean;\n  plan?: PremiumPlan;\n  nextBillingDate?: Date;\n  cancelAtPeriodEnd?: boolean;\n}\n\n// FAQ item for the new pricing page\nexport interface FAQItem {\n  question: string;\n  answer: string;\n}\n\n// Testimonial for the new pricing page\nexport interface Testimonial {\n  quote: string;\n  author: string;\n  role: string;\n  location: string;\n  image?: string;\n}\n\n// Feature comparison for detailed table\nexport interface FeatureComparison {\n  category: string;\n  features: {\n    name: string;\n    free: boolean | string; // true/false or text like \"6 months\"\n    supporter: boolean | string;\n    premium: boolean | string;\n  }[];\n}\n"
  },
  {
    "path": "src/shared/types/statistics.types.ts",
    "content": "// Export the type for use in other modules\nimport { StatisticsTimeframe } from \"@/shared/constants/statistics\";\nimport { ExerciseWithAttributes } from \"@/entities/exercise/types/exercise.types\";\n\n// Weight Progression Types\nexport interface WeightProgressionPoint {\n  date: string; // ISO date string\n  weight: number;\n}\n\nexport interface WeightProgressionResponse {\n  exerciseId: string;\n  timeframe: StatisticsTimeframe;\n  data: WeightProgressionPoint[];\n  count: number;\n}\n\n// One Rep Max Types\nexport interface OneRepMaxPoint {\n  date: string; // ISO date string\n  estimatedOneRepMax: number;\n}\n\nexport interface OneRepMaxResponse {\n  exerciseId: string;\n  timeframe: StatisticsTimeframe;\n  formula: \"Lombardi\";\n  formulaDescription: string;\n  data: OneRepMaxPoint[];\n  count: number;\n}\n\n// Volume Types\nexport interface VolumePoint {\n  week: string; // Format: \"YYYY-WXX\"\n  weekStart: string; // ISO date string\n  totalVolume: number;\n  setCount: number;\n}\n\nexport interface VolumeResponse {\n  exerciseId: string;\n  timeframe: StatisticsTimeframe;\n  data: VolumePoint[];\n  count: number;\n  calculationNote: string;\n}\n\n// Combined Statistics Response\nexport interface ExerciseStatisticsResponse {\n  exerciseId: string;\n  timeframe: StatisticsTimeframe;\n  statistics: {\n    weightProgression: WeightProgressionPoint[];\n    estimatedOneRepMax: OneRepMaxPoint[];\n    volume: VolumePoint[];\n  };\n}\n\n// Error Response Types\nexport interface StatisticsErrorResponse {\n  error: \"UNAUTHORIZED\" | \"PREMIUM_REQUIRED\" | \"INVALID_PARAMETERS\" | \"INVALID_TIMEFRAME\" | \"INTERNAL_SERVER_ERROR\";\n  message: string;\n  isPremium?: boolean;\n  details?: any;\n}\n\n// Request Parameters\nexport interface StatisticsRequestParams {\n  exerciseId: string;\n  timeframe?: StatisticsTimeframe;\n}\n\n// Exercise List Types\nexport interface ExerciseAttribute {\n  id: string;\n  attributeName: {\n    id: string;\n    name: string;\n  };\n  attributeValue: {\n    id: string;\n    value: string;\n  };\n}\n\nexport interface ExercisesPaginationResponse {\n  data: ExerciseWithAttributes[];\n  pagination: {\n    page: number;\n    limit: number;\n    totalCount: number;\n    totalPages: number;\n    hasNextPage: boolean;\n    hasPreviousPage: boolean;\n  };\n}\n\nexport interface ExercisesListRequestParams {\n  page?: number;\n  limit?: number;\n  search?: string;\n  muscle?: string;\n  equipment?: string;\n}\n"
  },
  {
    "path": "src/shared/types/storage.ts",
    "content": "export interface ImageUploader {\n  (params: { fileBuffer: Buffer; fileName: string; mimeType: string; bucket: string }): Promise<{ fileName: string; url: string }>;\n}\n\nexport interface ImageDeleter {\n  (params: { fileName: string; bucket: string }): Promise<void>;\n}\n"
  },
  {
    "path": "src/widgets/404.tsx",
    "content": "import Link from \"next/link\";\n\nimport { getI18n } from \"locales/server\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\nexport async function Page404() {\n  const t = await getI18n();\n\n  return (\n    <section className=\"font-serif flex min-h-screen items-center justify-center bg-white\">\n      <div className=\"container mx-auto\">\n        <div className=\"flex justify-center\">\n          <div className=\"w-full text-center sm:w-10/12 md:w-8/12\">\n            <div\n              aria-hidden=\"true\"\n              className=\"h-[250px] bg-[url(https://cdn.dribbble.com/users/285475/screenshots/2083086/dribbble_1.gif)] bg-contain bg-center bg-no-repeat sm:h-[350px] md:h-[400px]\"\n            >\n              <h1 className=\"pt-6 text-center text-6xl text-black sm:pt-8 sm:text-7xl md:text-8xl\">404</h1>\n            </div>\n\n            <div className=\"mt-[-50px]\">\n              <h3 className=\"mb-4 text-2xl font-bold text-black sm:text-3xl\">{t(\"commons.looks_like_you_are_lost\")}</h3>\n              <p className=\"mb-6 text-black sm:mb-5\">{t(\"commons.the_page_you_are_looking_for_is_not_available\")}</p>\n\n              <Link className={buttonVariants({ variant: \"black\" })} href=\"/\">\n                {t(\"commons.go_back\")}\n              </Link>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/widgets/language-selector/language-selector.tsx",
    "content": "\"use client\";\n\nimport { useAction } from \"next-safe-action/hooks\";\nimport { Languages } from \"lucide-react\";\n\nimport { useChangeLocale, languages, useI18n } from \"locales/client\";\nimport { updateUserAction } from \"@/entities/user/model/update-user.action\";\n\nconst languageFlags: Record<string, string> = {\n  en: \"🇬🇧\",\n  fr: \"🇫🇷\",\n  es: \"🇪🇸\",\n  \"zh-CN\": \"🇨🇳\",\n  ru: \"🇷🇺\",\n  pt: \"🇵🇹\",\n};\n\nexport function LanguageSelector() {\n  const action = useAction(updateUserAction);\n  const changeLocale = useChangeLocale();\n\n  const t = useI18n();\n\n  const handleLanguageChange = async (newLocale: string) => {\n    // update cookie to prevent auto-detection conflicts\n    document.cookie = `detected-locale=${newLocale}; max-age=${60 * 60 * 24 * 365}; path=/; samesite=lax`;\n\n    // change locale immediately for better UX\n    changeLocale(newLocale as \"en\" | \"fr\" | \"es\" | \"zh-CN\" | \"ru\" | \"pt\");\n\n    // save to database (fire and forget)\n    action.execute({ locale: newLocale });\n  };\n\n  const getLanguageName = (language: string) => {\n    switch (language) {\n      case \"en\":\n        return \"English\";\n      case \"fr\":\n        return \"Français\";\n      case \"es\":\n        return \"Español\";\n      case \"zh-CN\":\n        return \"中文\";\n      case \"ru\":\n        return \"Русский\";\n      case \"pt\":\n        return \"Português\";\n      default:\n        return language;\n    }\n  };\n\n  return (\n    <div className=\"dropdown dropdown-end\">\n      <div className=\"tooltip tooltip-bottom\" data-tip={t(\"commons.change_language\")}>\n        <div\n          className=\"btn btn-ghost btn-circle h-8 w-8 p-0 text-xl flex items-center justify-center hover:bg-slate-200 border-none dark:hover:bg-gray-800 rounded-full\"\n          role=\"button\"\n          tabIndex={0}\n        >\n          <Languages className=\"w-5 h-5 text-blue-500 dark:text-blue-400\" />\n        </div>\n      </div>\n      <ul\n        className=\"mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 dark:bg-black dark:text-gray-200 rounded-box  border border-slate-200 dark:border-gray-800\"\n        tabIndex={0}\n      >\n        {languages.map((language) => (\n          <li className=\"\" key={language}>\n            <button\n              className=\"flex items-center gap-2 text-base hover:bg-slate-200 dark:hover:bg-gray-800 rounded-lg px-3 py-2 transition-colors whitespace-nowrap min-w-fit\"\n              onClick={() => handleLanguageChange(language)}\n            >\n              <span className=\"text-lg\">{languageFlags[language]}</span>\n              <span className=\"text-base whitespace-nowrap\">{getLanguageName(language)}</span>\n            </button>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "tailwind.config.ts",
    "content": "/* eslint-disable max-len */\nimport type { Config } from \"tailwindcss\";\n\nconst config: Config = {\n  darkMode: \"class\",\n  content: [\n    \"./src/**/*.{ts,tsx}\",\n    \"./pages/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./components/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./app/**/*.{js,ts,jsx,tsx,mdx}\",\n  ],\n  theme: {\n    container: {\n      center: true,\n      padding: \"1rem\",\n    },\n    screens: {\n      sm: \"640px\",\n      md: \"768px\",\n      lg: \"1024px\",\n      xl: \"1280px\",\n      \"2xl\": \"1472px\",\n    },\n    fontFamily: {\n      sans: [\"var(--font-sans)\"],\n      \"permanent-marker\": [\"var(--font-permanent-marker)\"],\n    },\n    extend: {\n      backgroundImage: {\n        \"hero-light\": `\n          radial-gradient(circle at 82% 60%, rgba(59, 59, 59, 0.06) 0%, rgba(59, 59, 59, 0.06) 69%, transparent 69%, transparent 100%),\n          radial-gradient(circle at 36% 0%, rgba(185, 185, 185, 0.06) 0%, rgba(185, 185, 185, 0.06) 59%, transparent 59%, transparent 100%),\n          radial-gradient(circle at 58% 82%, rgba(183, 183, 183, 0.06) 0%, rgba(183, 183, 183, 0.06) 17%, transparent 17%, transparent 100%),\n          radial-gradient(circle at 71% 32%, rgba(19, 19, 19, 0.06) 0%, rgba(19, 19, 19, 0.06) 40%, transparent 40%, transparent 100%),\n          radial-gradient(circle at 77% 5%, rgba(31, 31, 31, 0.06) 0%, rgba(31, 31, 31, 0.06) 52%, transparent 52%, transparent 100%),\n          radial-gradient(circle at 96% 80%, rgba(11, 11, 11, 0.06) 0%, rgba(11, 11, 11, 0.06) 73%, transparent 73%, transparent 100%),\n          radial-gradient(circle at 91% 59%, rgba(252, 252, 252, 0.06) 0%, rgba(252, 252, 252, 0.06) 44%, transparent 44%, transparent 100%),\n          radial-gradient(circle at 52% 82%, rgba(223, 223, 223, 0.06) 0%, rgba(223, 223, 223, 0.06) 87%, transparent 87%, transparent 100%),\n          radial-gradient(circle at 84% 89%, rgba(160, 160, 160, 0.06) 0%, rgba(160, 160, 160, 0.06) 57%, transparent 57%, transparent 100%),\n          linear-gradient(90deg, #fef2a4, #a6ffed)\n        `,\n        \"hero-dark\": `\n          radial-gradient(circle at 82% 60%, rgba(200, 200, 200, 0.04) 0%, rgba(200, 200, 200, 0.04) 69%, transparent 69%, transparent 100%),\n          radial-gradient(circle at 36% 0%, rgba(150, 150, 150, 0.04) 0%, rgba(150, 150, 150, 0.04) 59%, transparent 59%, transparent 100%),\n          radial-gradient(circle at 58% 82%, rgba(130, 130, 130, 0.04) 0%, rgba(130, 130, 130, 0.04) 17%, transparent 17%, transparent 100%),\n          radial-gradient(circle at 71% 32%, rgba(90, 90, 90, 0.05) 0%, rgba(90, 90, 90, 0.05) 40%, transparent 40%, transparent 100%),\n          radial-gradient(circle at 77% 5%, rgba(70, 70, 70, 0.05) 0%, rgba(70, 70, 70, 0.05) 52%, transparent 52%, transparent 100%),\n          radial-gradient(circle at 96% 80%, rgba(50, 50, 50, 0.05) 0%, rgba(50, 50, 50, 0.05) 73%, transparent 73%, transparent 100%),\n          radial-gradient(circle at 91% 59%, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.03) 44%, transparent 44%, transparent 100%),\n          radial-gradient(circle at 52% 82%, rgba(180, 180, 180, 0.03) 0%, rgba(180, 180, 180, 0.03) 87%, transparent 87%, transparent 100%),\n          radial-gradient(circle at 84% 89%, rgba(160, 160, 160, 0.03) 0%, rgba(160, 160, 160, 0.03) 57%, transparent 57%, transparent 100%),\n          linear-gradient(90deg, #16161a, #121826)\n        `,\n      },\n      fontWeight: {\n        thin: \"100\",\n        extralight: \"200\",\n        light: \"300\",\n        normal: \"400\",\n        medium: \"500\",\n        semibold: \"600\",\n        bold: \"700\",\n        extrabold: \"800\",\n        black: \"900\",\n      },\n      fontSize: {\n        \"1sm\": \"0.80rem\",\n      },\n      colors: {\n        popover: {\n          DEFAULT: \"#FFF\",\n          dark: \"#232324\",\n          foreground: \"#000\",\n        },\n        transparent: \"transparent\",\n        current: \"currentColor\",\n        white: \"#FFFFFF\",\n        black: {\n          DEFAULT: \"#171718\",\n          dark: \"#232324\",\n        },\n        primary: \"#238BE6\",\n        green: {\n          \"100\": \"#DCFCE7\",\n          \"200\": \"#D9F9EB\",\n          \"300\": \"#BDF4E0\",\n          \"400\": \"#93E6C2\",\n          \"500\": \"#50C890\",\n          \"600\": \"#339E6E\",\n          \"700\": \"#227C54\",\n          \"800\": \"#1C6444\",\n          DEFAULT: \"#22C55E\",\n        },\n        gray: {\n          \"100\": \"#FAFBFC\",\n          \"200\": \"#F9FAFB\",\n          \"300\": \"#F1F3F5\",\n          \"400\": \"#E5E8EB\",\n          \"500\": \"#B9BEC6\",\n          \"600\": \"#9CA3AF\",\n          \"700\": \"#6B7280\",\n          DEFAULT: \"#525866\",\n        },\n        orange: {\n          \"100\": \"#FEF3C7\",\n          \"200\": \"#fdc88a\",\n          \"300\": \"#FFA270\",\n          \"400\": \"#FF6F37\",\n          \"500\": \"#FF5722\",\n          \"600\": \"#E64A19\",\n          \"700\": \"#D84315\",\n          \"800\": \"#9E1A0E\",\n          \"900\": \"#450805\",\n          DEFAULT: \"#FF5722\",\n        },\n        amber: {\n          \"100\": \"#FEF3C7\",\n          \"200\": \"#FDE68A\",\n          \"300\": \"#FCD34D\",\n          \"400\": \"#FBBF24\",\n          \"500\": \"#F59E0B\",\n          \"600\": \"#D97706\",\n          \"700\": \"#B45309\",\n          \"800\": \"#92400E\",\n          \"900\": \"#7A3207\",\n          DEFAULT: \"#F59E0B\",\n        },\n        danger: {\n          DEFAULT: \"#EF4444\",\n          light: \"#FEE2E2\",\n        },\n        success: {\n          DEFAULT: \"#22C55E\",\n          light: \"#DCFCE7\",\n        },\n        warning: \"#EAB308\",\n        \"light-theme\": \"#F4F7FF\",\n        \"light-orange\": \"#FFEDD5\",\n        \"light-blue\": \"#E0F2FE\",\n        \"light-purple\": \"#F3E8FF\",\n      },\n      boxShadow: {\n        \"3xl\": \"0 1px 2px 0 rgba(95,74,46,0.08), 0 0 0 1px rgba(227,225,222,0.4)\",\n        sm: \"0 1px 2px 0 rgba(113,116,152,0.1)\",\n      },\n      keyframes: {\n        shine: {\n          \"0%\": {\n            \"background-position\": \"0% 0%\",\n          },\n          \"50%\": {\n            \"background-position\": \"100% 100%\",\n          },\n          to: {\n            \"background-position\": \"0% 0%\",\n          },\n        },\n        breath: {\n          \"0%, 100%\": {\n            transform: \"scale(0.95)\",\n            opacity: \"0.3\",\n          },\n          \"50%\": {\n            transform: \"scale(1.15)\",\n            opacity: \"1\",\n          },\n        },\n        \"accordion-down\": {\n          from: {\n            height: \"0\",\n          },\n          to: {\n            height: \"var(--radix-accordion-content-height)\",\n          },\n        },\n        \"accordion-up\": {\n          from: {\n            height: \"var(--radix-accordion-content-height)\",\n          },\n          to: {\n            height: \"0\",\n          },\n        },\n        \"caret-blink\": {\n          \"0%,70%,100%\": {\n            opacity: \"1\",\n          },\n          \"20%,50%\": {\n            opacity: \"0\",\n          },\n        },\n      },\n      animation: {\n        shine: \"shine var(--duration) infinite linear\",\n        \"accordion-down\": \"accordion-down 0.3s ease-out\",\n        \"accordion-up\": \"accordion-up 0.3s ease-out\",\n        \"caret-blink\": \"caret-blink 1.25s ease-out infinite\",\n      },\n    },\n  },\n  plugins: [require(\"tailwindcss-animate\"), require(\"@tailwindcss/typography\"), require(\"daisyui\")],\n};\nexport default config;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n      \"@/workoutcool/*\": [\"./src/*\"],\n      \"@emails/*\": [\"emails/*\"],\n      \"@public/*\": [\"public/*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"exclude\": [\"node_modules\", \"src/utils/inapp.js\", \"src/utils/externalLinkOpener.js\", \"src/utils/browserEscape.js\"],\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\", \"tailwind.config.ts\"]\n}\n"
  },
  {
    "path": "workout-cool.code-workspace",
    "content": "{\n\t\"folders\": [\n\t\t{\n\t\t\t\"path\": \".\"\n\t\t},\n\t\t{\n\t\t\t\"path\": \"../workout-cool-mobile\"\n\t\t}\n\t],\n\t\"settings\": {\n\t\t\"typescript.tsdk\": \"node_modules/typescript/lib\"\n\t}\n}"
  }
]