main 5c24b6a9272d cached
72 files
519.1 KB
134.3k tokens
140 symbols
1 requests
Download .txt
Showing preview only (544K chars total). Download the full file or copy to clipboard to get everything.
Repository: seanprashad/leetcode-patterns
Branch: main
Commit: 5c24b6a9272d
Files: 72
Total size: 519.1 KB

Directory structure:
gitextract_d7fndcjj/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── feature_request.yml
│   │   └── question_request.yml
│   └── workflows/
│       ├── ci.yml
│       ├── deploy.yml
│       └── update-questions.yml
├── .gitignore
├── .husky/
│   └── pre-push
├── .npmrc
├── LICENSE
├── README.md
├── cron/
│   ├── leetcode/
│   │   ├── __init__.py
│   │   ├── auth.py
│   │   ├── models.py
│   │   └── rest.py
│   └── update_questions.py
├── eslint.config.mjs
├── next.config.ts
├── package.json
├── postcss.config.mjs
├── public/
│   ├── .nojekyll
│   ├── manifest.json
│   ├── robots.txt
│   └── sw.js
├── scripts/
│   └── generate-sw-precache.mjs
├── src/
│   ├── app/
│   │   ├── globals.css
│   │   ├── layout.tsx
│   │   ├── not-found.tsx
│   │   └── page.tsx
│   ├── components/
│   │   ├── layout/
│   │   │   ├── AuthContext.test.tsx
│   │   │   ├── AuthContext.tsx
│   │   │   ├── GitHubLink.tsx
│   │   │   ├── Logo.tsx
│   │   │   ├── ServiceWorkerRegistrar.tsx
│   │   │   ├── ThemeToggle.test.tsx
│   │   │   ├── ThemeToggle.tsx
│   │   │   ├── UserMenu.test.tsx
│   │   │   ├── UserMenu.tsx
│   │   │   └── ViewSwitcher.tsx
│   │   ├── panels/
│   │   │   ├── AboutPanel.tsx
│   │   │   ├── AcknowledgementsPanel.tsx
│   │   │   ├── TipsPanel.tsx
│   │   │   └── panels.test.tsx
│   │   ├── questions/
│   │   │   ├── ConfirmModal.tsx
│   │   │   ├── FilterToolbar.tsx
│   │   │   ├── GroupHeaderRow.tsx
│   │   │   ├── NoteModal.tsx
│   │   │   ├── ProgressBar.tsx
│   │   │   ├── QuestionRow.tsx
│   │   │   ├── QuestionsTable.test.tsx
│   │   │   ├── QuestionsTable.tsx
│   │   │   └── ReviewDateModal.tsx
│   │   └── roadmaps/
│   │       ├── RoadmapView.test.tsx
│   │       └── RoadmapView.tsx
│   ├── data/
│   │   ├── questions.json
│   │   └── roadmaps.ts
│   ├── lib/
│   │   ├── analytics.test.ts
│   │   ├── analytics.ts
│   │   ├── register-sw.test.ts
│   │   ├── register-sw.ts
│   │   ├── reminders.test.ts
│   │   ├── reminders.ts
│   │   ├── storage.test.ts
│   │   ├── storage.ts
│   │   ├── supabase.ts
│   │   ├── sw.test.ts
│   │   ├── sync.test.ts
│   │   └── sync.ts
│   ├── test/
│   │   └── setup.ts
│   └── types/
│       └── question.ts
├── tsconfig.json
└── vitest.config.mts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: Report a bug or issue with Leetcode Patterns
labels: ["bug"]
body:
  - type: textarea
    id: description
    attributes:
      label: Description
      description: A clear description of the bug.
      placeholder: Describe the issue...
    validations:
      required: true
  - type: textarea
    id: steps
    attributes:
      label: Steps to Reproduce
      description: How can we reproduce this issue?
      placeholder: |
        1. Go to '...'
        2. Click on '...'
        3. See error
    validations:
      required: true
  - type: textarea
    id: expected
    attributes:
      label: Expected Behavior
      description: What did you expect to happen?
    validations:
      required: true
  - type: textarea
    id: screenshots
    attributes:
      label: Screenshots
      description: If applicable, add screenshots to help explain the issue.
    validations:
      required: false
  - type: dropdown
    id: browser
    attributes:
      label: Browser
      options:
        - Chrome
        - Firefox
        - Safari
        - Edge
        - Other
    validations:
      required: false


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Suggest a new feature or improvement
labels: ["enhancement"]
body:
  - type: textarea
    id: description
    attributes:
      label: Description
      description: A clear description of the feature you'd like.
      placeholder: I'd like to see...
    validations:
      required: true
  - type: textarea
    id: motivation
    attributes:
      label: Motivation
      description: Why would this feature be useful?
    validations:
      required: false
  - type: textarea
    id: alternatives
    attributes:
      label: Alternatives Considered
      description: Any alternative solutions or features you've considered.
    validations:
      required: false


================================================
FILE: .github/ISSUE_TEMPLATE/question_request.yml
================================================
name: Question Request
description: Request a new question to be added to the list
labels: ["question-request"]
body:
  - type: input
    id: title
    attributes:
      label: Question Title
      description: The title of the LeetCode question.
      placeholder: e.g. Two Sum
    validations:
      required: true
  - type: input
    id: url
    attributes:
      label: LeetCode URL
      description: Link to the question on LeetCode.
      placeholder: https://leetcode.com/problems/two-sum/
    validations:
      required: true
  - type: dropdown
    id: difficulty
    attributes:
      label: Difficulty
      options:
        - Easy
        - Medium
        - Hard
    validations:
      required: true
  - type: input
    id: pattern
    attributes:
      label: Pattern(s)
      description: Which pattern(s) does this question belong to?
      placeholder: e.g. Arrays, Two Pointers
    validations:
      required: true
  - type: textarea
    id: reason
    attributes:
      label: Why should this be added?
      description: Why is this question a good fit for the list?
    validations:
      required: false


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  pull_request:
    types: [opened, synchronize, reopened, closed, labeled]
    branches: [main]

permissions:
  contents: write
  pull-requests: write

jobs:
  lint:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci

      - run: npm run lint

  test:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci

      - run: npx vitest run --coverage

  preview:
    if: >-
      always() &&
      contains(github.event.pull_request.labels.*.name, 'preview') &&
      (github.event.action == 'closed' || (needs.lint.result == 'success' && needs.test.result == 'success'))
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - if: github.event.action != 'closed'
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - if: github.event.action != 'closed'
        run: npm ci

      - if: github.event.action != 'closed'
        name: Build
        env:
          NEXT_PUBLIC_BASE_PATH: /leetcode-patterns/pr-preview/pr-${{ github.event.number }}
          NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
          NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
        run: npx next build

      - if: github.event.action != 'closed'
        name: Remove service worker from preview
        run: rm -f out/sw.js

      - uses: rossjrw/pr-preview-action@v1
        with:
          source-dir: out


================================================
FILE: .github/workflows/deploy.yml
================================================
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]

  workflow_run:
    workflows: [Update Questions]
    types:
      - completed

  workflow_dispatch:

permissions:
  contents: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci

      - name: Build
        env:
          NEXT_PUBLIC_BASE_PATH: /leetcode-patterns
          NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
          NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
        run: npm run build

      - uses: JamesIves/github-pages-deploy-action@v4
        with:
          folder: out
          branch: gh-pages
          clean-exclude: pr-preview
          force: false


================================================
FILE: .github/workflows/update-questions.yml
================================================
name: Update Questions

on:
  schedule:
    # Every Sunday at 8am EST (1pm UTC)
    - cron: "0 13 * * 0"
  workflow_dispatch:

permissions:
  contents: write

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Update question metadata
        working-directory: cron
        env:
          LEETCODE_SESSION_TOKEN: ${{ secrets.LEETCODE_SESSION_TOKEN }}
          LEETCODE_CSRF_TOKEN: ${{ secrets.LEETCODE_CSRF_TOKEN }}
          LEETCODE_CF_CLEARANCE: ${{ secrets.LEETCODE_CF_CLEARANCE }}
        run: python update_questions.py

      - name: Commit and push changes
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add src/data/questions.json
          git diff --cached --quiet || git commit --author="Sean Prashad <13009507+seanprashad@users.noreply.github.com>" -m "chore: update question metadata" && git push


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# python
__pycache__


================================================
FILE: .husky/pre-push
================================================
npm run lint -- --fix
npm test


================================================
FILE: .npmrc
================================================
registry=https://registry.npmjs.org


================================================
FILE: LICENSE
================================================
Creative Commons Attribution-NonCommercial 4.0 International

Copyright (c) 2026 Sean Prashad

This work is licensed under the Creative Commons
Attribution-NonCommercial 4.0 International License.

You are free to:

  Share — copy and redistribute the material in any medium or format
  Adapt — remix, transform, and build upon the material

Under the following terms:

  Attribution — You must give appropriate credit, provide a link to
  the license, and indicate if changes were made. You may do so in
  any reasonable manner, but not in any way that suggests the licensor
  endorses you or your use.

  NonCommercial — You may not use the material for commercial purposes.

  No additional restrictions — You may not apply legal terms or
  technological measures that legally restrict others from doing
  anything the license permits.

Notices:

  You do not have to comply with the license for elements of the
  material in the public domain or where your use is permitted by an
  applicable exception or limitation.

  No warranties are given. The license may not give you all of the
  permissions necessary for your intended use. For example, other
  rights such as publicity, privacy, or moral rights may limit how
  you use the material.

Full license text:
https://creativecommons.org/licenses/by-nc/4.0/legalcode


================================================
FILE: README.md
================================================
<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="public/images/logo-dark.png" />
    <source media="(prefers-color-scheme: light)" srcset="public/images/logo-light.png" />
    <img alt="Leetcode Patterns" src="public/images/logo-light.png" width="500" />
  </picture>
</p>

<p align="center">
  <a href="https://github.com/seanprashad/leetcode-patterns/actions/workflows/deploy.yml"><img src="https://github.com/seanprashad/leetcode-patterns/actions/workflows/deploy.yml/badge.svg" alt="Deploy to GitHub Pages" /></a>
  <a href="https://github.com/seanprashad/leetcode-patterns/actions/workflows/update-questions.yml"><img src="https://github.com/seanprashad/leetcode-patterns/actions/workflows/update-questions.yml/badge.svg" alt="Update Questions" /></a>
</p>

## Table of Contents

- [Background](#background)
- [Fundamentals](#fundamentals)
- [Notes](#notes)
- [Question List](#question-list)
- [Solutions](#solutions)
- [Contributing](#contributing)
- [Suggestions](#suggestions)
- [Acknowledgements](#acknowledgements)

## Background

This repo is intended for any individual wanting to improve their problem
solving skills for software engineering interviews.

Problems are grouped under their respective subtopic, in order to focus on
repeatedly applying common patterns rather than randomly tackling questions.

All questions are available on [leetcode.com] with some requiring [leetcode premium].

## Fundamentals

To find the greatest amount of success when practicing, it is highly recommended
to know the methods and runtimes of the following data structures and their
operations:

- Arrays
- Maps
- Linked Lists
- Queues
- Heaps
- Stacks
- Trees
- Graphs

In addition, you should have a good grasp on common algorithms such as:

- Breadth-first search
- Depth-first search
- Binary search
- Recursion

## Notes

[This pdf] contains information for the main data structures in Java.

Other useful methods to know include [`substring()`](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#substring-int-int-), [`toCharArray()`](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#toCharArray--), [`Math.max()`](https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#max-int-int-),
[`Math.min()`](https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#min-int-int-), and [`Arrays.fill()`](https://docs.oracle.com/javase/8/docs/api/java/util/Arrays.html#fill-int:A-int-).

## Question List

The entire question list can be found here:
https://seanprashad.com/leetcode-patterns/.

## Solutions

Solutions written in Java can be found in the [solutions] branch.

## Contributing

The app is built with [Next.js] (App Router), [React] 19, [TypeScript], [Tailwind CSS] v4, [TanStack Table] v8, [Lucide React] for icons, and Google Analytics via `@next/third-parties`. Tests use [Vitest] + [React Testing Library].

```bash
npm install
npm run dev         # http://localhost:3000
npm test            # single run
npm run test:watch  # watch mode
```

A [Husky] `pre-push` hook runs `npm test` automatically before every push. This is set up for every clone via the `prepare` script.

## Acknowledgements

This list is heavily inspired from [Grokking the Coding Interview] with
additional problems extracted from the [Blind 75 list] and this hackernoon article
on [14 patterns to ace any coding interview question].

[leetcode.com]: https://leetcode.com
[leetcode premium]: https://leetcode.com/subscribe/
[next.js]: https://nextjs.org
[react]: https://react.dev
[typescript]: https://www.typescriptlang.org
[tailwind css]: https://tailwindcss.com
[tanstack table]: https://tanstack.com/table
[lucide react]: https://lucide.dev
[vitest]: https://vitest.dev
[react testing library]: https://testing-library.com/docs/react-testing-library/intro
[husky]: https://typicode.github.io/husky
[this pdf]: https://drive.google.com/open?id=1ao4ZA28zzBttDkuS6MLQI52gDs_CJZEm
[solutions]: https://github.com/SeanPrashad/leetcode-patterns/tree/solutions
[grokking the coding interview]: https://www.educative.io/courses/grokking-the-coding-interview
[issue]: https://github.com/SeanPrashad/leetcode-patterns/issues/new
[blind 75 list]: https://www.teamblind.com/article/New-Year-Gift---Curated-List-of-Top-100-LeetCode-Questions-to-Save-Your-Time-OaM1orEU?utm_source=share&utm_medium=ios_app
[14 patterns to ace any coding interview question]: https://hackernoon.com/14-patterns-to-ace-any-coding-interview-question-c5bb3357f6ed


================================================
FILE: cron/leetcode/__init__.py
================================================
from leetcode.models import (
    Configuration,
    ApiClient,
    DefaultApi,
    GraphqlQuery,
    GraphqlQueryGetQuestionDetailVariables,
    GraphqlQuestionDetail,
    GraphqlData,
    GraphqlResponse,
)

from leetcode import auth
from leetcode import rest

__all__ = [
    "Configuration",
    "ApiClient",
    "DefaultApi",
    "GraphqlQuery",
    "GraphqlQueryGetQuestionDetailVariables",
    "GraphqlQuestionDetail",
    "GraphqlData",
    "GraphqlResponse",
    "auth",
    "rest",
]


================================================
FILE: cron/leetcode/auth.py
================================================
# Placeholder for leetcode.auth compatibility.
# Authentication is handled via Configuration.api_key in the main module.


================================================
FILE: cron/leetcode/models.py
================================================
import json
import urllib.request

from leetcode.rest import ApiException


class Configuration:
    def __init__(self):
        self.host = "https://leetcode.com"
        self.api_key = {}
        self.debug = False


class GraphqlQuery:
    def __init__(self, query=None, variables=None, operation_name=None):
        self.query = query
        self.variables = variables
        self.operation_name = operation_name


class GraphqlQueryGetQuestionDetailVariables:
    def __init__(self, title_slug=None):
        self.title_slug = title_slug


class GraphqlQuestionDetail:
    def __init__(self, question_id=None, title=None, difficulty=None,
                 company_tag_stats_v2=None, is_paid_only=None, topic_tags=None):
        self.question_id = question_id
        self.title = title
        self.difficulty = difficulty
        self.company_tag_stats_v2 = company_tag_stats_v2
        self.is_paid_only = is_paid_only
        self.topic_tags = topic_tags


class GraphqlData:
    def __init__(self, question=None):
        self.question = question


class GraphqlResponse:
    def __init__(self, data=None):
        self.data = data


class ApiClient:
    def __init__(self, configuration=None):
        self.configuration = configuration or Configuration()


class DefaultApi:
    def __init__(self, api_client=None):
        self.api_client = api_client or ApiClient()

    def graphql_post(self, body=None):
        config = self.api_client.configuration

        variables = {}
        if body.variables and hasattr(body.variables, "title_slug"):
            variables["titleSlug"] = body.variables.title_slug

        payload = {"query": body.query, "variables": variables}
        if body.operation_name:
            payload["operationName"] = body.operation_name

        data = json.dumps(payload).encode("utf-8")

        cookie = (
            f"csrftoken={config.api_key.get('csrftoken') or ''}; "
            f"LEETCODE_SESSION={config.api_key.get('LEETCODE_SESSION') or ''}; "
            f"cf_clearance={config.api_key.get('cf_clearance') or ''}"
        )

        url = config.host + "/graphql"
        req = urllib.request.Request(url, data=data, method="POST")
        req.add_header("Content-Type", "application/json")
        req.add_header("Accept", "application/json")
        req.add_header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
        req.add_header("x-csrftoken", config.api_key.get("x-csrftoken") or "")
        req.add_header("Referer", config.api_key.get("Referer") or "https://leetcode.com")
        req.add_header("Cookie", cookie)

        try:
            with urllib.request.urlopen(req) as resp:
                resp_data = json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            raise ApiException(
                status=e.code,
                reason=e.reason,
                body=e.read().decode("utf-8"),
            )

        question_data = resp_data.get("data", {}).get("question", {})

        topic_tags_raw = question_data.get("topicTags")
        topic_tags = [type("Tag", (), {"name": t["name"]}) for t in topic_tags_raw]

        question = GraphqlQuestionDetail(
            question_id=question_data.get("questionId"),
            title=question_data.get("title"),
            difficulty=question_data.get("difficulty"),
            company_tag_stats_v2=question_data.get("companyTagStatsV2"),
            is_paid_only=question_data.get("isPaidOnly"),
            topic_tags=topic_tags,
        )

        return GraphqlResponse(data=GraphqlData(question=question))


================================================
FILE: cron/leetcode/rest.py
================================================
class ApiException(Exception):
    def __init__(self, status=None, reason=None, body=None):
        self.status = status
        self.reason = reason
        self.body = body

    def __str__(self):
        msg = f"({self.status})\nReason: {self.reason}\n"
        if self.body:
            msg += f"HTTP response body: {self.body}\n"
        return msg


================================================
FILE: cron/update_questions.py
================================================
import os
import json
import leetcode
import leetcode.auth
from datetime import datetime
from leetcode.rest import ApiException


def create_leetcode_api():
    leetcode_session_token = os.environ.get("LEETCODE_SESSION_TOKEN")
    csrf_token = os.environ.get("LEETCODE_CSRF_TOKEN")
    cf_clearance = os.environ.get("LEETCODE_CF_CLEARANCE")

    if not leetcode_session_token:
        print("❌ LEETCODE_SESSION_TOKEN environment variable is required")
        exit(1)

    if not csrf_token:
        print("❌ LEETCODE_CSRF_TOKEN environment variable is required")
        exit(1)

    if not cf_clearance:
        print("❌ LEETCODE_CF_CLEARANCE environment variable is required")
        exit(1)

    configuration = leetcode.Configuration()

    configuration.api_key["x-csrftoken"] = csrf_token
    configuration.api_key["csrftoken"] = csrf_token
    configuration.api_key["LEETCODE_SESSION"] = leetcode_session_token
    configuration.api_key["cf_clearance"] = cf_clearance
    configuration.api_key["Referer"] = "https://leetcode.com"
    configuration.debug = False

    return leetcode.DefaultApi(leetcode.ApiClient(configuration))


def get_question_metadata(api, title_slug):
    graphql_request = leetcode.GraphqlQuery(
        query='''query questionData($titleSlug: String!) {
            question(titleSlug: $titleSlug) {
                questionId
                title
                difficulty
                companyTagStatsV2
                isPaidOnly
                topicTags {
                    name
                }
            }
        }
        ''',
        variables=leetcode.GraphqlQueryGetQuestionDetailVariables(
            title_slug=title_slug)
    )

    try:
        response = api.graphql_post(body=graphql_request)
        if not response.data.question:
            print(f'❌ Empty response body for question: {title_slug}')
            exit(1)
        return response
    except ApiException as e:
        print(
            f'Exception occurred when contacting the Leetcode GraphQL API: ${e}')
        exit()


def construct_company_tag_list(company_tag_stats_v2):
    companies = []

    tag_stats = json.loads(company_tag_stats_v2)
    # "three_months" = 0-3 months, "six_months" = 3-6 months, "more_than_six_months" = 6+ months
    for tag in tag_stats["three_months"] + tag_stats["six_months"]:
        companies.append({
            "name": tag["name"],
            "slug": tag["slug"],
            "frequency": tag["timesEncountered"]
        })

    return sorted(companies, key=lambda d: d['frequency'], reverse=True)


def update_question_metadata(question, response):
    print(f'''🔄 Updating question metadata for {question["title"]}''')

    question_id = response.data.question.question_id
    question_title = response.data.question.title
    question_difficulty = response.data.question.difficulty
    question_company_tag_stats_v2 = response.data.question.company_tag_stats_v2
    question_is_premium = response.data.question.is_paid_only
    question_topic_tags = response.data.question.topic_tags

    companies = construct_company_tag_list(question_company_tag_stats_v2)
    patterns = [tag.name for tag in question_topic_tags]

    question["id"] = int(question_id)
    question["title"] = question_title
    question["difficulty"] = question_difficulty
    question["pattern"] = patterns
    question["companies"] = companies
    question["premium"] = question_is_premium


def read_questions(file_name):
    print(f"💾 Loading {file_name}")

    try:
        with open(file_name, "r") as file:
            questions = json.load(file)
            print(f"✅ Finished loading {file_name}")
            return questions
    except Exception as e:
        print(
            f"❌ Exception occurred when reading {file_name}: {e}")
        exit()


def write_questions(file_name, questions):
    print(f"💾 Updating {file_name}")

    try:
        with open(file_name, "w") as file:
            questions["updated"] = str(datetime.now().isoformat())
            json.dump(questions, file, indent=2)
            print(f"✅ Finished updating {file_name}")
    except Exception as e:
        print(
            f"❌ Exception occurred when writing {file_name}: {e}")
        exit()


def main(file_name):
    api = create_leetcode_api()
    questions = read_questions(file_name)

    for question in questions["data"]:
        title_slug = question["slug"]

        response = get_question_metadata(api, title_slug)

        update_question_metadata(question, response)

    write_questions(file_name, questions)


if __name__ == "__main__":
    file_name = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src", "data", "questions.json")
    startTime = datetime.now()

    main(file_name)

    print(f"⏱️  Data updated in {datetime.now() - startTime} seconds")


================================================
FILE: eslint.config.mjs
================================================
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

const eslintConfig = defineConfig([
  ...nextVitals,
  ...nextTs,
  // Override default ignores of eslint-config-next.
  globalIgnores([
    // Default ignores of eslint-config-next:
    ".next/**",
    "out/**",
    "build/**",
    "next-env.d.ts",
  ]),
]);

export default eslintConfig;


================================================
FILE: next.config.ts
================================================
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  reactStrictMode: false,
  output: "export",
  trailingSlash: false,
  basePath: process.env.NEXT_PUBLIC_BASE_PATH ?? "",
  images: { unoptimized: true },
};

export default nextConfig;


================================================
FILE: package.json
================================================
{
  "name": "leetcode-patterns-v2",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build && node scripts/generate-sw-precache.mjs",
    "start": "next start",
    "lint": "eslint",
    "test": "vitest run",
    "test:watch": "vitest",
    "prepare": "husky"
  },
  "dependencies": {
    "@next/third-parties": "^16.1.6",
    "@supabase/supabase-js": "^2.99.0",
    "@tanstack/react-table": "^8.21.3",
    "@tanstack/react-virtual": "^3.13.21",
    "lucide-react": "^0.577.0",
    "next": "16.1.7",
    "react": "19.2.3",
    "react-dom": "19.2.3"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@testing-library/jest-dom": "^6.9.1",
    "@testing-library/react": "^16.3.2",
    "@testing-library/user-event": "^14.6.1",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@vitejs/plugin-react": "^5.1.4",
    "@vitest/coverage-v8": "^4.0.18",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "happy-dom": "^20.8.3",
    "husky": "^9.1.7",
    "jsdom": "^28.1.0",
    "tailwindcss": "^4",
    "typescript": "^5",
    "vitest": "^4.0.18"
  }
}


================================================
FILE: postcss.config.mjs
================================================
const config = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

export default config;


================================================
FILE: public/.nojekyll
================================================



================================================
FILE: public/manifest.json
================================================
{
  "short_name": "leetcode-patterns",
  "name": "Leetcode Patterns",
  "description": "A curated list of LeetCode questions grouped by patterns.",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#09090b",
  "background_color": "#ffffff"
}


================================================
FILE: public/robots.txt
================================================
User-agent: *
Allow: /


================================================
FILE: public/sw.js
================================================
// Service Worker — cache-first for static assets, network-first for navigation
const CACHE_NAME = "lc-patterns-v2";

const PRECACHE_URLS = [
  "./",
  "./manifest.json",
];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
  );
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
    )
  );
  self.clients.claim();
});

self.addEventListener("fetch", (event) => {
  const { request } = event;

  // Skip non-GET and chrome-extension requests
  if (request.method !== "GET" || !request.url.startsWith("http")) return;

  // Skip analytics and external API calls
  if (
    request.url.includes("google-analytics.com") ||
    request.url.includes("googletagmanager.com") ||
    request.url.includes("s2/favicons")
  ) return;

  // Navigation requests: network-first with cache fallback
  if (request.mode === "navigate") {
    event.respondWith(
      fetch(request)
        .then((response) => {
          const clone = response.clone();
          caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
          return response;
        })
        .catch(() => caches.match(request).then((cached) => cached || caches.match("./")))
    );
    return;
  }

  // Static assets: cache-first with network fallback
  event.respondWith(
    caches.match(request).then(
      (cached) =>
        cached ||
        fetch(request).then((response) => {
          // Only cache successful same-origin responses
          if (response.ok && request.url.startsWith(self.location.origin)) {
            const clone = response.clone();
            caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
          }
          return response;
        })
    )
  );
});


================================================
FILE: scripts/generate-sw-precache.mjs
================================================
import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
import { join, relative } from "path";

const OUT_DIR = "out";
const SW_PATH = join(OUT_DIR, "sw.js");

const PRECACHE_EXTENSIONS = new Set([
  ".html", ".js", ".css", ".json", ".txt", ".woff", ".woff2",
]);

function collectFiles(dir, base = dir) {
  const entries = [];
  for (const entry of readdirSync(dir)) {
    const full = join(dir, entry);
    if (statSync(full).isDirectory()) {
      entries.push(...collectFiles(full, base));
    } else {
      const rel = "./" + relative(base, full);
      if (rel === "./sw.js") continue;
      if (rel.endsWith(".map")) continue;
      const ext = rel.slice(rel.lastIndexOf("."));
      if (!PRECACHE_EXTENSIONS.has(ext)) continue;
      entries.push(rel);
    }
  }
  return entries;
}

const files = collectFiles(OUT_DIR);
const sw = readFileSync(SW_PATH, "utf-8");

// Replace the placeholder PRECACHE_URLS array with the full file list
const updated = sw.replace(
  /const PRECACHE_URLS = \[[\s\S]*?\];/,
  `const PRECACHE_URLS = ${JSON.stringify(files, null, 2)};`
);

writeFileSync(SW_PATH, updated);
console.log(`Precache manifest: ${files.length} files injected into sw.js`);


================================================
FILE: src/app/globals.css
================================================
@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));

:root {
  --background: #ffffff;
  --foreground: #171717;
}

.dark {
  --background: #0a0a0a;
  --foreground: #ededed;
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

body {
  background: var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(1rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}


================================================
FILE: src/app/layout.tsx
================================================
import type { Metadata } from "next";
import { GoogleAnalytics } from "@next/third-parties/google";
import { Geist, Geist_Mono, Dancing_Script } from "next/font/google";
import "./globals.css";
import ServiceWorkerRegistrar from "@/components/layout/ServiceWorkerRegistrar";
import { AuthProvider } from "@/components/layout/AuthContext";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

const dancingScript = Dancing_Script({
  variable: "--font-dancing-script",
  subsets: ["latin"],
});

const siteUrl = "https://seanprashad.com/leetcode-patterns";
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";

export const metadata: Metadata = {
  title: "Leetcode Patterns",
  description:
    "A curated list of LeetCode questions grouped by pattern to help you ace coding interviews. Filter by difficulty, company, and topic.",
  manifest: `${basePath}/manifest.json`,
  metadataBase: new URL(siteUrl),
  alternates: { canonical: "/" },
  openGraph: {
    title: "Leetcode Patterns",
    description:
      "A curated list of LeetCode questions grouped by pattern to help you ace coding interviews.",
    url: siteUrl,
    siteName: "Leetcode Patterns",
    images: [
      {
        url: `${basePath}/images/og-image.png`,
        width: 1200,
        height: 630,
        alt: "Leetcode Patterns – A curated list of LeetCode questions grouped by pattern",
      },
    ],
    type: "website",
  },
  twitter: {
    card: "summary_large_image",
    title: "Leetcode Patterns",
    description:
      "A curated list of LeetCode questions grouped by pattern to help you ace coding interviews.",
    images: [`${basePath}/images/og-image.png`],
  },
  keywords: [
    "leetcode",
    "coding interview",
    "data structures",
    "algorithms",
    "interview prep",
    "blind 75",
    "leetcode patterns",
  ],
  authors: [{ name: "Sean Prashad", url: "https://github.com/SeanPrashad" }],
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `(function(){try{var t=localStorage.getItem("theme");if(t==="dark"||(t!=="light"&&matchMedia("(prefers-color-scheme:dark)").matches))document.documentElement.classList.add("dark")}catch(e){}})();if(location.hash&&location.hash.indexOf("access_token")>-1)window.__SUPABASE_AUTH_HASH__=location.hash`,
          }}
        />
      </head>
      <body
        className={`${geistSans.variable} ${geistMono.variable} ${dancingScript.variable} antialiased`}
      >
        <AuthProvider>
          {children}
        </AuthProvider>
        <ServiceWorkerRegistrar />
      </body>
      <GoogleAnalytics gaId="G-J7FBQPGZTW" />
    </html>
  );
}


================================================
FILE: src/app/not-found.tsx
================================================
import Link from "next/link";

export default function NotFound() {
  return (
    <div className="flex min-h-screen items-center justify-center px-4">
      <div className="text-center">
        <div
          className="mx-auto mb-6 w-fit rounded-lg px-6 py-2 text-5xl font-bold text-amber-950 dark:text-amber-200"
          style={{ background: "linear-gradient(90deg, #FFD200, #FFA500, #FF7800)" }}
        >
          404
        </div>
        <h1 className="mb-2 text-2xl font-bold">Page not found</h1>
        <p className="mb-6 text-zinc-500">
          The page you&apos;re looking for doesn&apos;t exist.
        </p>
        <Link
          href="/"
          className="inline-block rounded-lg border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
        >
          ← Back to questions
        </Link>
      </div>
    </div>
  );
}


================================================
FILE: src/app/page.tsx
================================================
import { Suspense } from "react";
import { MessageSquarePlus } from "lucide-react";
import ThemeToggle from "@/components/layout/ThemeToggle";
import GitHubLink from "@/components/layout/GitHubLink";
import UserMenu from "@/components/layout/UserMenu";
import Logo from "@/components/layout/Logo";
import ViewSwitcher from "@/components/layout/ViewSwitcher";
import AboutPanel from "@/components/panels/AboutPanel";
import AcknowledgementsPanel from "@/components/panels/AcknowledgementsPanel";
import TipsPanel from "@/components/panels/TipsPanel";
import questionsJson from "@/data/questions.json";
import { QuestionsData } from "@/types/question";

const { data: questions } = questionsJson as QuestionsData;

export default function Home() {
  return (
    <div className="mx-auto max-w-6xl px-4 py-6 sm:py-10">
      <div className="mb-4 flex items-start justify-between sm:mb-6">
        <div>
          <h1>
            <Logo />
          </h1>
          <p className="text-base italic text-zinc-500">
            by{" "}
            <a
              href="https://github.com/SeanPrashad"
              target="_blank"
              rel="noopener noreferrer"
              className="text-blue-600 hover:underline dark:text-blue-400"
            >
              Sean Prashad
            </a>
            {" · Est. 2019"}
          </p>
          <p className="mt-1 text-sm text-zinc-500 sm:mt-2 sm:text-base">
            A collection of {questions.length} questions grouped by pattern to help you prepare for coding interviews.
          </p>
        </div>
        <div className="flex shrink-0 items-center gap-2">
          <GitHubLink />
          <span className="group/fb relative">
            <a
              href="https://github.com/SeanPrashad/leetcode-patterns/issues/new/choose"
              target="_blank"
              rel="noopener noreferrer"
              className="block rounded-lg border border-zinc-300 p-2 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
            >
              <MessageSquarePlus className="h-4 w-4" />
            </a>
            <span className="pointer-events-none absolute left-1/2 top-full z-10 mt-1.5 -translate-x-1/2 whitespace-nowrap rounded bg-zinc-800 px-2 py-1 text-xs text-white opacity-0 shadow transition-opacity group-hover/fb:opacity-100 dark:bg-zinc-200 dark:text-zinc-900">
              Feedback
            </span>
          </span>
          <ThemeToggle />
          <UserMenu />
        </div>
      </div>
      {/* Side panel tabs – stacked flush in a fixed column */}
      <div className="fixed left-0 top-0 bottom-0 z-30 flex flex-col items-start justify-center max-[1439px]:hidden">
        <AboutPanel />
        <TipsPanel />
        <AcknowledgementsPanel />
      </div>
      <Suspense>
        <ViewSwitcher
          questions={questions}
          updatedDate={questionsJson.updated}
        />
      </Suspense>
    </div>
  );
}


================================================
FILE: src/components/layout/AuthContext.test.tsx
================================================
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, cleanup, act } from "@testing-library/react";

type AuthCallback = (event: string, session: { user: Record<string, unknown> } | null) => void;

const { mockGetSession, mockOnAuthStateChange, mockTrackEvent, mockDownloadAndMerge, mockChannel } = vi.hoisted(() => {
  const mockChannel = {
    on: vi.fn().mockReturnThis(),
    subscribe: vi.fn().mockReturnThis(),
  };
  return {
    mockGetSession: vi.fn(),
    mockOnAuthStateChange: vi.fn(),
    mockTrackEvent: vi.fn(),
    mockDownloadAndMerge: vi.fn(() => Promise.resolve()),
    mockChannel,
  };
});

vi.mock("@/lib/supabase", () => ({
  supabase: {
    auth: {
      getSession: mockGetSession,
      onAuthStateChange: mockOnAuthStateChange,
      setSession: vi.fn(),
    },
    channel: () => mockChannel,
    removeChannel: vi.fn(),
  },
}));

vi.mock("@/lib/sync", () => ({
  downloadAndMerge: mockDownloadAndMerge,
  scheduleUpload: vi.fn(),
  mergeFromRealtimePayload: vi.fn(),
}));

vi.mock("@/lib/analytics", () => ({
  trackEvent: mockTrackEvent,
}));

import { AuthProvider, useAuth } from "@/components/layout/AuthContext";

const fakeUser = {
  id: "user-1",
  user_metadata: { user_name: "testuser", avatar_url: "https://example.com/avatar.png" },
};

function TestConsumer() {
  const { user } = useAuth();
  return <div>{user ? `signed-in:${(user as unknown as typeof fakeUser).user_metadata.user_name}` : "signed-out"}</div>;
}

describe("AuthProvider sign-in toast suppression", () => {
  let authCallback: AuthCallback;
  const unsubscribe = vi.fn();

  beforeEach(() => {
    mockTrackEvent.mockClear();
    mockDownloadAndMerge.mockClear();
    mockOnAuthStateChange.mockImplementation((cb: AuthCallback) => {
      authCallback = cb;
      return { data: { subscription: { unsubscribe } } };
    });
  });

  afterEach(() => {
    cleanup();
    vi.restoreAllMocks();
  });

  it("shows toast on fresh sign-in when no existing session", async () => {
    mockGetSession.mockResolvedValue({ data: { session: null } });

    await act(async () => {
      render(
        <AuthProvider>
          <TestConsumer />
        </AuthProvider>
      );
    });

    expect(screen.getByText("signed-out")).toBeInTheDocument();

    await act(async () => {
      authCallback("SIGNED_IN", { user: fakeUser });
    });

    expect(screen.getByText(/Signed in as testuser/)).toBeInTheDocument();
    expect(mockTrackEvent).toHaveBeenCalledWith("sign_in", { provider: "github" });
  });

  it("does NOT show toast when session already exists and SIGNED_IN fires again", async () => {
    mockGetSession.mockResolvedValue({ data: { session: { user: fakeUser } } });

    await act(async () => {
      render(
        <AuthProvider>
          <TestConsumer />
        </AuthProvider>
      );
    });

    expect(screen.getByText("signed-in:testuser")).toBeInTheDocument();
    mockTrackEvent.mockClear();

    await act(async () => {
      authCallback("SIGNED_IN", { user: fakeUser });
    });

    expect(screen.queryByText(/Signed in as/)).not.toBeInTheDocument();
    expect(mockTrackEvent).not.toHaveBeenCalledWith("sign_in", expect.anything());
  });

  it("does NOT show toast when INITIAL_SESSION fires before getSession resolves", async () => {
    // Simulate getSession that never resolves before INITIAL_SESSION fires
    let resolveGetSession!: (v: { data: { session: { user: typeof fakeUser } } }) => void;
    mockGetSession.mockReturnValue(new Promise((r) => { resolveGetSession = r; }));

    await act(async () => {
      render(
        <AuthProvider>
          <TestConsumer />
        </AuthProvider>
      );
    });

    // Supabase fires INITIAL_SESSION before getSession resolves
    await act(async () => {
      authCallback("INITIAL_SESSION", { user: fakeUser });
    });

    // Then SIGNED_IN fires (common Supabase behaviour on page load)
    await act(async () => {
      authCallback("SIGNED_IN", { user: fakeUser });
    });

    expect(screen.queryByText(/Signed in as/)).not.toBeInTheDocument();
    expect(mockTrackEvent).not.toHaveBeenCalledWith("sign_in", expect.anything());

    // Let getSession resolve to avoid dangling promise
    await act(async () => {
      resolveGetSession({ data: { session: { user: fakeUser } } });
    });
  });

  it("shows toast again after sign-out then fresh sign-in", async () => {
    mockGetSession.mockResolvedValue({ data: { session: { user: fakeUser } } });

    await act(async () => {
      render(
        <AuthProvider>
          <TestConsumer />
        </AuthProvider>
      );
    });

    mockTrackEvent.mockClear();

    // Simulate sign-out
    await act(async () => {
      authCallback("SIGNED_OUT", null);
    });

    // Simulate fresh sign-in
    await act(async () => {
      authCallback("SIGNED_IN", { user: fakeUser });
    });

    expect(screen.getByText(/Signed in as testuser/)).toBeInTheDocument();
    expect(mockTrackEvent).toHaveBeenCalledWith("sign_in", { provider: "github" });
  });
});


================================================
FILE: src/components/layout/AuthContext.tsx
================================================
"use client";

import { createContext, useContext, useEffect, useState, useCallback, useRef, type ReactNode } from "react";
import { supabase } from "@/lib/supabase";
import { downloadAndMerge, scheduleUpload, flushPendingUpload, mergeFromRealtimePayload } from "@/lib/sync";
import { trackEvent } from "@/lib/analytics";
import type { User } from "@supabase/supabase-js";

interface AuthContextValue {
  user: User | null;
  loading: boolean;
  signIn: () => Promise<void>;
  signOut: () => Promise<void>;
  syncNow: () => void;
  syncVersion: number;
}

const AuthContext = createContext<AuthContextValue>({
  user: null,
  loading: true,
  signIn: async () => {},
  signOut: async () => {},
  syncNow: () => {},
  syncVersion: 0,
});

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [syncVersion, setSyncVersion] = useState(0);
  const [toast, setToast] = useState<{ message: string; type: "error" | "success" } | null>(null);
  const [toastFading, setToastFading] = useState(false);
  const realtimeChannelRef = useRef<ReturnType<typeof supabase.channel> | null>(null);
  const hasSessionRef = useRef(false);

  useEffect(() => {
    if (!toast) return;
    const fadeTimer = setTimeout(() => setToastFading(true), 3000);
    const removeTimer = setTimeout(() => { setToast(null); setToastFading(false); }, 3700);
    return () => { clearTimeout(fadeTimer); clearTimeout(removeTimer); };
  }, [toast]);

  // Subscribe to realtime changes when user is signed in
  useEffect(() => {
    if (!user) {
      // Clean up any existing subscription
      if (realtimeChannelRef.current) {
        supabase.removeChannel(realtimeChannelRef.current);
        realtimeChannelRef.current = null;
      }
      return;
    }

    const channel = supabase
      .channel("user_progress_sync")
      .on(
        "postgres_changes",
        {
          event: "UPDATE",
          schema: "public",
          table: "user_progress",
          filter: `user_id=eq.${user.id}`,
        },
        (payload) => {
          const changed = mergeFromRealtimePayload(payload.new);
          if (changed) {
            trackEvent("realtime_sync");
            setSyncVersion((v) => v + 1);
          }
        }
      )
      .subscribe();

    realtimeChannelRef.current = channel;

    return () => {
      supabase.removeChannel(channel);
      realtimeChannelRef.current = null;
    };
  }, [user]);

  useEffect(() => {
    // Next.js App Router clears the URL hash during hydration before
    // Supabase can detect it. We capture it early in an inline <script>
    // and fall back to setSession if auto-detection missed it.
    const savedHash = (window as Record<string, unknown>).__SUPABASE_AUTH_HASH__ as string | undefined;
    if (savedHash) {
      delete (window as Record<string, unknown>).__SUPABASE_AUTH_HASH__;
      const params = new URLSearchParams(savedHash.substring(1));
      const accessToken = params.get("access_token");
      const refreshToken = params.get("refresh_token");
      if (accessToken && refreshToken) {
        supabase.auth.setSession({ access_token: accessToken, refresh_token: refreshToken });
      }
    }

    supabase.auth.getSession().then(({ data: { session } }) => {
      const u = session?.user ?? null;
      setUser(u);
      setLoading(false);
      if (u) {
        hasSessionRef.current = true;
        downloadAndMerge(u.id).then(() => setSyncVersion((v) => v + 1));
      }
    });

    const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
      const u = session?.user ?? null;
      setUser(u);
      if (u) downloadAndMerge(u.id).then(() => setSyncVersion((v) => v + 1));
      if (event === "INITIAL_SESSION") {
        // Supabase fires INITIAL_SESSION on load for existing sessions;
        // mark it so the subsequent SIGNED_IN event doesn't show a toast.
        if (u) hasSessionRef.current = true;
      } else if (event === "SIGNED_IN") {
        if (!hasSessionRef.current) {
          trackEvent("sign_in", { provider: "github" });
          setToast({ message: `Signed in as ${u?.user_metadata?.user_name ?? "user"}`, type: "success" });
        }
        hasSessionRef.current = true;
      }
      if (event === "SIGNED_OUT") {
        hasSessionRef.current = false;
        trackEvent("sign_out");
      }
    });

    return () => subscription.unsubscribe();
  }, []);

  // Flush any pending debounced upload before the page unloads so that
  // a refresh always sees the latest state in Supabase.
  useEffect(() => {
    const flush = () => flushPendingUpload();
    const onVisChange = () => { if (document.visibilityState === "hidden") flush(); };
    window.addEventListener("beforeunload", flush);
    document.addEventListener("visibilitychange", onVisChange);
    return () => {
      window.removeEventListener("beforeunload", flush);
      document.removeEventListener("visibilitychange", onVisChange);
    };
  }, []);

  const signIn = useCallback(async () => {
    const redirectTo = typeof window !== "undefined"
      ? window.location.origin + window.location.pathname.replace(/\/?$/, "/")
      : undefined;
    const { error } = await supabase.auth.signInWithOAuth({
      provider: "github",
      options: { redirectTo, scopes: "" },
    });
    if (error) {
      trackEvent("sign_in_error", { error: error.message });
      setToast({ message: `Sign in failed: ${error.message}`, type: "error" });
    }
  }, []);

  const signOut = useCallback(async () => {
    const { error } = await supabase.auth.signOut();
    if (error) {
      setToast({ message: `Sign out failed: ${error.message}`, type: "error" });
    } else {
      setUser(null);
      setToast({ message: "Signed out", type: "success" });
    }
  }, []);

  const syncNow = useCallback(() => {
    if (user) scheduleUpload(user.id);
  }, [user]);

  return (
    <AuthContext.Provider value={{ user, loading, signIn, signOut, syncNow, syncVersion }}>
      {children}
      {toast && (
        <div
          className={`fixed inset-x-0 bottom-6 z-50 mx-auto w-fit animate-[fadeInUp_0.3s_ease-out] rounded-lg border px-4 py-3 text-sm font-medium shadow-lg transition-opacity duration-700 ease-in-out ${
            toast.type === "error"
              ? "border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200"
              : "border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200"
          } ${toastFading ? "opacity-0" : "opacity-100"}`}
        >
          {toast.type === "error" ? "✕" : "✓"} {toast.message}
        </div>
      )}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}


================================================
FILE: src/components/layout/GitHubLink.tsx
================================================
const REPO = "SeanPrashad/leetcode-patterns";

async function getStarCount(): Promise<number | null> {
  try {
    const res = await fetch(`https://api.github.com/repos/${REPO}`, {
      cache: "force-cache",
    });
    if (!res.ok) return null;
    const data = await res.json();
    return data.stargazers_count;
  } catch {
    return null;
  }
}

function formatCount(n: number): string {
  if (n >= 1000) return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k`;
  return String(n);
}

export default async function GitHubLink() {
  const stars = await getStarCount();

  return (
    <a
      href={`https://github.com/${REPO}`}
      target="_blank"
      rel="noopener noreferrer"
      title="Star on GitHub"
      className="flex items-center gap-1.5 rounded-lg border border-zinc-300 py-2 pl-2 pr-2.5 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
    >
      <svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
        <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
      </svg>
      {stars !== null && (
        <span className="hidden text-xs font-medium text-zinc-600 sm:inline dark:text-zinc-400">
          ⭐ {formatCount(stars)}
        </span>
      )}
    </a>
  );
}


================================================
FILE: src/components/layout/Logo.tsx
================================================
export default function Logo() {
  const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";

  return (
    <>
      {/* eslint-disable-next-line @next/next/no-img-element */}
      <img
        src={`${basePath}/images/logo-light.png`}
        alt="Leetcode Patterns"
        className="h-8 dark:hidden sm:h-10"
      />
      {/* eslint-disable-next-line @next/next/no-img-element */}
      <img
        src={`${basePath}/images/logo-dark.png`}
        alt="Leetcode Patterns"
        className="hidden h-8 dark:block sm:h-10"
      />
    </>
  );
}


================================================
FILE: src/components/layout/ServiceWorkerRegistrar.tsx
================================================
"use client";

import { useEffect } from "react";
import { registerServiceWorker } from "@/lib/register-sw";

export default function ServiceWorkerRegistrar() {
  useEffect(() => {
    registerServiceWorker(process.env.NEXT_PUBLIC_BASE_PATH ?? "");
  }, []);

  return null;
}


================================================
FILE: src/components/layout/ThemeToggle.test.tsx
================================================
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

const { mockTrackEvent } = vi.hoisted(() => ({
  mockTrackEvent: vi.fn(),
}));

vi.mock("@/lib/analytics", () => ({
  trackEvent: mockTrackEvent,
}));

import ThemeToggle from "@/components/layout/ThemeToggle";

describe("ThemeToggle analytics", () => {
  beforeEach(() => {
    mockTrackEvent.mockClear();
    document.documentElement.classList.remove("dark");
  });

  it("tracks theme_toggle with 'dark' when switching to dark mode", async () => {
    const user = userEvent.setup();
    render(<ThemeToggle />);
    const button = screen.getByRole("button", { name: /switch to dark mode/i });
    await user.click(button);
    expect(mockTrackEvent).toHaveBeenCalledWith("theme_toggle", { theme: "dark" });
  });

  it("tracks theme_toggle with 'light' when switching to light mode", async () => {
    const user = userEvent.setup();
    document.documentElement.classList.add("dark");
    render(<ThemeToggle />);
    const button = screen.getByRole("button", { name: /switch to light mode/i });
    await user.click(button);
    expect(mockTrackEvent).toHaveBeenCalledWith("theme_toggle", { theme: "light" });
  });
});


================================================
FILE: src/components/layout/ThemeToggle.tsx
================================================
"use client";

import { useCallback, useSyncExternalStore } from "react";
import { Sun, Moon } from "lucide-react";
import { trackEvent } from "@/lib/analytics";

function subscribe(callback: () => void) {
  const observer = new MutationObserver(callback);
  observer.observe(document.documentElement, {
    attributes: true,
    attributeFilter: ["class"],
  });
  return () => observer.disconnect();
}

function getSnapshot() {
  return document.documentElement.classList.contains("dark");
}

function getServerSnapshot() {
  return false;
}

export default function ThemeToggle() {
  const dark = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);

  const toggle = useCallback(() => {
    const next = !dark;
    document.documentElement.classList.toggle("dark", next);
    localStorage.setItem("theme", next ? "dark" : "light");
    trackEvent("theme_toggle", { theme: next ? "dark" : "light" });
  }, [dark]);

  return (
    <button
      onClick={toggle}
      aria-label={dark ? "Switch to light mode" : "Switch to dark mode"}
      title={dark ? "Switch to light mode" : "Switch to dark mode"}
      className="rounded-lg border border-zinc-300 p-2 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
    >
      {dark ? (
        <Sun className="h-4 w-4 text-yellow-500" />
      ) : (
        <Moon className="h-4 w-4 text-blue-600" />
      )}
    </button>
  );
}


================================================
FILE: src/components/layout/UserMenu.test.tsx
================================================
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

const { mockUseAuth } = vi.hoisted(() => ({
  mockUseAuth: { current: { user: null, loading: false, signIn: vi.fn(), signOut: vi.fn(), syncNow: vi.fn(), syncVersion: 0 } },
}));

vi.mock("@/components/layout/AuthContext", () => ({
  useAuth: () => mockUseAuth.current,
}));

vi.mock("@/lib/supabase", () => ({
  supabase: {},
}));

import UserMenu from "@/components/layout/UserMenu";

describe("UserMenu", () => {
  beforeEach(() => {
    mockUseAuth.current = {
      user: null,
      loading: false,
      signIn: vi.fn(),
      signOut: vi.fn(),
      syncNow: vi.fn(),
      syncVersion: 0,
    };
  });

  afterEach(() => {
    cleanup();
    vi.restoreAllMocks();
  });

  it("renders nothing when loading", () => {
    mockUseAuth.current = { ...mockUseAuth.current, loading: true };
    const { container } = render(<UserMenu />);
    expect(container.innerHTML).toBe("");
  });

  it("renders Sign in with GitHub button when no user", () => {
    render(<UserMenu />);
    expect(screen.getByRole("button", { name: "Sign in with GitHub" })).toBeInTheDocument();
  });

  it("renders user avatar when signed in", () => {
    mockUseAuth.current = {
      ...mockUseAuth.current,
      user: { user_metadata: { avatar_url: "https://example.com/avatar.png", user_name: "testuser" } } as never,
    };
    render(<UserMenu />);
    const avatar = screen.getByAltText("testuser");
    expect(avatar).toBeInTheDocument();
    expect(avatar).toHaveAttribute("src", "https://example.com/avatar.png");
  });

  it("shows dropdown with Signed in as and username when avatar clicked", async () => {
    mockUseAuth.current = {
      ...mockUseAuth.current,
      user: { user_metadata: { avatar_url: "https://example.com/avatar.png", user_name: "testuser" } } as never,
    };
    const user = userEvent.setup();
    render(<UserMenu />);
    await user.click(screen.getByAltText("testuser"));
    expect(screen.getByText("Signed in as")).toBeInTheDocument();
    expect(screen.getByText("testuser")).toBeInTheDocument();
  });

  it("calls signOut when Sign out is clicked", async () => {
    const mockSignOut = vi.fn();
    mockUseAuth.current = {
      ...mockUseAuth.current,
      user: { user_metadata: { avatar_url: "https://example.com/avatar.png", user_name: "testuser" } } as never,
      signOut: mockSignOut,
    };
    const user = userEvent.setup();
    render(<UserMenu />);
    await user.click(screen.getByAltText("testuser"));
    await user.click(screen.getByText("Sign out"));
    expect(mockSignOut).toHaveBeenCalled();
  });

  it("closes dropdown when clicking outside", async () => {
    mockUseAuth.current = {
      ...mockUseAuth.current,
      user: { user_metadata: { avatar_url: "https://example.com/avatar.png", user_name: "testuser" } } as never,
    };
    const user = userEvent.setup();
    render(<UserMenu />);
    await user.click(screen.getByAltText("testuser"));
    expect(screen.getByText("Signed in as")).toBeInTheDocument();
    await user.click(document.body);
    expect(screen.queryByText("Signed in as")).not.toBeInTheDocument();
  });
});


================================================
FILE: src/components/layout/UserMenu.tsx
================================================
"use client";

import { useState, useRef, useEffect } from "react";
import { useAuth } from "./AuthContext";

export default function UserMenu() {
  const { user, loading, signIn, signOut } = useAuth();
  const [open, setOpen] = useState(false);
  const menuRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;
    const handleClick = (e: MouseEvent) => {
      if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
        setOpen(false);
      }
    };
    document.addEventListener("mousedown", handleClick);
    return () => document.removeEventListener("mousedown", handleClick);
  }, [open]);

  if (loading) return null;

  if (!user) {
    return (
      <button
        onClick={signIn}
        className="rounded-lg border border-zinc-300 px-3 py-1.5 text-sm font-medium transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
      >
        Sign in with GitHub
      </button>
    );
  }

  return (
    <div className="relative" ref={menuRef}>
      <button
        onClick={() => setOpen((prev) => !prev)}
        className="flex items-center gap-2 rounded-lg border border-zinc-300 p-1 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
      >
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          src={user.user_metadata.avatar_url}
          alt={user.user_metadata.user_name}
          className="h-6 w-6 rounded-full"
        />
      </button>
      {open && (
        <div className="absolute right-0 top-full z-10 mt-1.5 w-48 rounded-lg border border-zinc-200 bg-white p-2 shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
          <p className="px-2 py-1 text-xs text-zinc-500">Signed in as</p>
          <p className="truncate px-2 pb-1 text-sm font-medium">
            {user.user_metadata.user_name}
          </p>
          <button
            onClick={() => { setOpen(false); signOut(); }}
            className="w-full rounded px-2 py-1 text-left text-sm text-red-600 hover:bg-zinc-100 dark:hover:bg-zinc-800"
          >
            Sign out
          </button>
        </div>
      )}
    </div>
  );
}


================================================
FILE: src/components/layout/ViewSwitcher.tsx
================================================
"use client";

import { useState, useEffect, useSyncExternalStore, useCallback, useRef } from "react";
import { useSearchParams } from "next/navigation";
import { TableProperties, Map, Trophy } from "lucide-react";
import QuestionsTable from "@/components/questions/QuestionsTable";
import RoadmapView from "@/components/roadmaps/RoadmapView";
import { Question } from "@/types/question";
import { beginnerRoadmap, experiencedRoadmap } from "@/data/roadmaps";
import { trackEvent } from "@/lib/analytics";

type View = "table" | "beginner" | "experienced";

const VIEW_KEY = "leetcode-patterns-view";

const views: { id: View; label: string; icon: typeof TableProperties; description: string }[] = [
  { id: "table", label: "All Questions", icon: TableProperties, description: "Browse all questions with filters" },
  { id: "beginner", label: "Beginner Roadmap", icon: Map, description: "Structured path for newcomers" },
  { id: "experienced", label: "Experienced Roadmap", icon: Trophy, description: "Must-know problems for experienced engineers" },
];

function isValidView(v: string | null): v is View {
  return v !== null && views.some((view) => view.id === v);
}

export default function ViewSwitcher({
  questions,
  updatedDate,
}: {
  questions: Question[];
  updatedDate: string;
}) {
  const searchParams = useSearchParams();

  const paramView = searchParams.get("view");

  const storedView = useSyncExternalStore(
    () => () => {},
    () => {
      if (isValidView(paramView)) return paramView;
      const stored = localStorage.getItem(VIEW_KEY) as View | null;
      return stored && isValidView(stored) ? stored : "table";
    },
    () => (isValidView(paramView) ? paramView : "table"),
  );

  const [activeView, setActiveView] = useState<View>(storedView);
  const [displayedView, setDisplayedView] = useState<View>(storedView);
  const [fading, setFading] = useState(false);
  const pendingView = useRef<View | null>(null);

  const switchView = useCallback((view: View) => {
    setActiveView(view);
    localStorage.setItem(VIEW_KEY, view);
    const params = new URLSearchParams(window.location.search);
    if (view === "table") {
      params.delete("view");
    } else {
      params.set("view", view);
    }
    const qs = params.toString();
    window.history.replaceState(null, "", qs ? `?${qs}` : window.location.pathname);
    trackEvent("switch_view", { view });

    pendingView.current = view;
    setFading(true);
  }, []);

  useEffect(() => {
    if (!fading || pendingView.current === null) return;
    const timeout = setTimeout(() => {
      if (pendingView.current !== null) {
        setDisplayedView(pendingView.current);
        pendingView.current = null;
        setFading(false);
      }
    }, 200);
    return () => clearTimeout(timeout);
  }, [fading]);

  const handleTransitionEnd = useCallback(() => {
    if (fading && pendingView.current !== null) {
      setDisplayedView(pendingView.current);
      pendingView.current = null;
      setFading(false);
    }
  }, [fading]);

  return (
    <>
      {/* View tabs */}
      <div className="relative mb-4 flex gap-1 overflow-hidden rounded-lg p-1 dark:bg-zinc-900"
        style={{ background: "linear-gradient(90deg, #FFD200, #FFA500, #FF7800)" }}
      >
        <div className="pointer-events-none absolute inset-0 hidden dark:block"
          style={{ background: "linear-gradient(90deg, rgba(255,210,0,0.08), rgba(255,165,0,0.08), rgba(255,120,0,0.08))", backgroundColor: "rgba(24,24,27,0.60)" }}
        />
        {views.map((v) => {
          const Icon = v.icon;
          const isActive = activeView === v.id;
          return (
            <button
              key={v.id}
              onClick={() => switchView(v.id)}
              className={`relative flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-bold transition-all ${
                isActive
                  ? "bg-white/80 text-zinc-900 shadow-sm dark:bg-zinc-800/80 dark:text-amber-200 dark:shadow-amber-900/20"
                  : "text-amber-950 hover:bg-white/40 hover:text-zinc-900 dark:text-amber-200/70 dark:hover:bg-zinc-800/40 dark:hover:text-amber-200"
              }`}
            >
              <Icon className="h-4 w-4" />
              <span className="hidden sm:inline">{v.label}</span>
            </button>
          );
        })}
      </div>

      {/* View content */}
      <div
        className={`transition-opacity duration-150 ${fading ? "opacity-0" : "opacity-100"}`}
        onTransitionEnd={handleTransitionEnd}
      >
        {displayedView === "table" && (
          <QuestionsTable data={questions} updatedDate={updatedDate} />
        )}
        {displayedView === "beginner" && (
          <RoadmapView roadmap={beginnerRoadmap} questions={questions} />
        )}
        {displayedView === "experienced" && (
          <RoadmapView roadmap={experiencedRoadmap} questions={questions} />
        )}
      </div>
    </>
  );
}


================================================
FILE: src/components/panels/AboutPanel.tsx
================================================
"use client";

import { useState, useEffect } from "react";
import { Info, X } from "lucide-react";
import { trackEvent } from "@/lib/analytics";

export default function AboutPanel() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    if (!open) return;
    const handleKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") { setOpen(false); trackEvent("panel_close", { panel: "about" }); }
    };
    window.addEventListener("keydown", handleKey);
    return () => window.removeEventListener("keydown", handleKey);
  }, [open]);

  return (
    <>
      {/* Tab button – rendered inline inside the fixed flex wrapper in page.tsx */}
      <button
        onClick={() => { setOpen(true); trackEvent("panel_open", { panel: "about" }); }}
        className="rounded-r-xl bg-emerald-600 px-2.5 py-4 text-white shadow-lg transition-colors hover:bg-emerald-700"
        aria-label="Open about"
      >
        <span className="flex items-center gap-2 text-sm font-semibold [writing-mode:vertical-lr]">
          <Info className="h-4 w-4 rotate-90" />
          About
        </span>
      </button>

      {/* Panel */}
      <div
        className={`fixed left-0 top-0 z-50 h-full w-80 transform border-r border-zinc-200 bg-white shadow-xl transition-transform duration-300 ease-in-out dark:border-zinc-700 dark:bg-zinc-900 ${
          open ? "translate-x-0" : "-translate-x-full"
        }`}
      >
        <div className="flex items-center justify-between bg-emerald-600 px-4 py-3 text-white">
          <h2 className="flex items-center gap-2 text-base font-semibold">
            <Info className="h-4 w-4" />
            About
          </h2>
          <button
            onClick={() => { setOpen(false); trackEvent("panel_close", { panel: "about" }); }}
            className="rounded p-1 transition-colors hover:bg-emerald-700"
          >
            <X className="h-4 w-4" />
          </button>
        </div>
        <div className="h-[calc(100%-49px)] overflow-y-auto px-5 py-5">
          <div className="space-y-5 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
            <p>
              In <span className="font-semibold text-zinc-900 dark:text-zinc-100">2019</span>, as a broke college
              student who couldn&apos;t afford premium interview resources, I spent countless hours searching
              for free materials and teaching myself React to build{" "}
              <span className="font-semibold text-zinc-900 dark:text-zinc-100">Leetcode Patterns</span>.
            </p>
            <p>
              I believe <span className="font-semibold text-zinc-900 dark:text-zinc-100">everyone</span> deserves
              access to high-quality interview material - regardless of their financial situation. It&apos;s why I chose to make this website{" "}
              <a
                href="https://github.com/seanprashad/leetcode-patterns/blob/main/LICENSE"
                target="_blank"
                rel="noopener noreferrer"
                className="font-semibold text-blue-600 hover:underline dark:text-blue-400"
              >free and open source</a>.
            </p>
            <p>
              Best of luck on your journey!
            </p>
            <p className="text-2xl text-zinc-900 dark:text-zinc-100" style={{ fontFamily: "var(--font-dancing-script)" }}>
              <a
                href="https://github.com/SeanPrashad"
                target="_blank"
                rel="noopener noreferrer"
                className="underline decoration-dotted underline-offset-4 hover:text-blue-600 hover:decoration-solid dark:hover:text-blue-400"
              >Sean</a>
            </p>
          </div>
        </div>
      </div>
    </>
  );
}


================================================
FILE: src/components/panels/AcknowledgementsPanel.tsx
================================================
"use client";

import { useState, useEffect } from "react";
import { Heart, X } from "lucide-react";
import { trackEvent } from "@/lib/analytics";

const sources = [
  {
    title: "Blind Curated 75 Question List",
    url: "https://www.teamblind.com/post/New-Year-Gift---Curated-List-of-Top-100-LeetCode-Questions-to-Save-Your-Time-OaM1orEU",
    image: "/images/Blind.png",
  },
  {
    title: "Grokking the Coding Interview: Patterns for Coding Questions",
    url: "https://www.designgurus.io/course/grokking-the-coding-interview",
    image: "/images/DesignGurus.png",
  },
  {
    title: "14 Patterns to Ace Any Coding Interview Question",
    url: "https://hackernoon.com/14-patterns-to-ace-any-coding-interview-question-c5bb3357f6ed",
    image: "/images/Hackernoon.png",
  },
];

export default function AcknowledgementsPanel() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    if (!open) return;
    const handleKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") { setOpen(false); trackEvent("panel_close", { panel: "acknowledgements" }); }
    };
    window.addEventListener("keydown", handleKey);
    return () => window.removeEventListener("keydown", handleKey);
  }, [open]);

  return (
    <>
      {/* Tab button – rendered inline inside the fixed flex wrapper in page.tsx */}
      <button
        onClick={() => { setOpen(true); trackEvent("panel_open", { panel: "acknowledgements" }); }}
        className="rounded-r-xl bg-amber-600 px-2.5 py-4 text-white shadow-lg transition-colors hover:bg-amber-700"
        aria-label="Open acknowledgements"
      >
        <span className="flex items-center gap-2 text-sm font-semibold [writing-mode:vertical-lr]">
          <Heart className="h-4 w-4 rotate-90" />
          Acknowledgements
        </span>
      </button>

      {/* Panel */}
      <div
        className={`fixed left-0 top-0 z-50 h-full w-80 transform border-r border-zinc-200 bg-white shadow-xl transition-transform duration-300 ease-in-out dark:border-zinc-700 dark:bg-zinc-900 ${
          open ? "translate-x-0" : "-translate-x-full"
        }`}
      >
        <div className="flex items-center justify-between bg-amber-600 px-4 py-3 text-white">
          <h2 className="flex items-center gap-2 text-base font-semibold">
            <Heart className="h-4 w-4" />
            Acknowledgements
          </h2>
          <button
            onClick={() => { setOpen(false); trackEvent("panel_close", { panel: "acknowledgements" }); }}
            className="rounded p-1 transition-colors hover:bg-amber-700"
          >
            <X className="h-4 w-4" />
          </button>
        </div>
        <div className="h-[calc(100%-49px)] overflow-y-auto px-4 py-4">
          <p className="mb-4 text-xs text-zinc-500">
            Leetcode Patterns wouldn&apos;t exist without the following resources:
          </p>
          <div className="space-y-4">
            {sources.map((source) => (
              <a
                key={source.title}
                href={source.url}
                target="_blank"
                rel="noopener noreferrer"
                className="block overflow-hidden rounded-lg border border-zinc-200 transition-colors hover:border-blue-300 dark:border-zinc-800 dark:hover:border-blue-700"
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                  src={`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}${source.image}`}
                  alt={source.title}
                  className="h-32 w-full object-cover"
                />
                <div className="p-3">
                  <p className="text-sm font-medium text-blue-600 dark:text-blue-400">
                    {source.title}
                  </p>
                </div>
              </a>
            ))}
          </div>
        </div>
      </div>
    </>
  );
}


================================================
FILE: src/components/panels/TipsPanel.tsx
================================================
"use client";

import { useState, useCallback, useEffect, Fragment } from "react";
import { createPortal } from "react-dom";
import { Lightbulb, X, Copy, Check } from "lucide-react";
import { trackEvent } from "@/lib/analytics";

function formatApproach(text: string) {
  const parts = text.split(/(O\([^)]*\)|\bK\b)/g);
  return parts.map((part, i) =>
    /^O\(/.test(part) || /^\bK\b$/.test(part) ? (
      <code key={i} className="rounded bg-zinc-100 px-1 py-0.5 font-mono text-xs dark:bg-zinc-800">{part}</code>
    ) : (
      part
    )
  );
}

const tipGroups = [
  { label: "Arrays & Strings", tips: [
    { condition: "If input array is sorted", approaches: ["Binary search", "Two pointers"] },
    { condition: "If need O(1) lookup", approaches: ["Hash table", "Hash set"] },
    { condition: "If must solve in-place", approaches: ["Swap corresponding values", "Store multiple values in the same pointer"] },
    { condition: "If asked for common strings", approaches: ["Map", "Trie"] },
    { condition: "If asked to count bits or use XOR", approaches: ["Bit manipulation"] },
  ]},
  { label: "Subarrays & Sequences", tips: [
    { condition: "If asked for max/min subarray/subset", approaches: ["Dynamic programming", "Sliding window"] },
    { condition: "If asked for sliding window max/min", approaches: ["Monotonic queue"] },
    { condition: "If asked for next greater/smaller element", approaches: ["Monotonic stack"] },
    { condition: "If need range sum/frequency queries", approaches: ["Prefix sum", "Binary indexed tree", "Segment tree"] },
  ]},
  { label: "Trees & Graphs", tips: [
    { condition: "If given a tree", approaches: ["DFS", "BFS"] },
    { condition: "If given a graph", approaches: ["DFS", "BFS", "Union-Find"] },
    { condition: "If given a matrix", approaches: ["BFS", "DFS", "Dynamic programming"] },
    { condition: "If asked for connectivity/grouping", approaches: ["Union-Find", "DFS"] },
    { condition: "If asked for ordering/scheduling", approaches: ["Topological sort"] },
  ]},
  { label: "Linked Lists & Stacks", tips: [
    { condition: "If given a linked list", approaches: ["Two pointers"] },
    { condition: "If recursion is banned", approaches: ["Stack"] },
  ]},
  { label: "Sorting & Intervals", tips: [
    { condition: "If asked for top/least K items", approaches: ["Heap", "Quickselect", "Bucket sort"] },
    { condition: "If asked to merge sorted lists/intervals", approaches: ["Merge sort", "Heap"] },
    { condition: "If asked for overlapping intervals", approaches: ["Sorting", "Sweep line"] },
    { condition: "If given a stream of data", approaches: ["Heap", "Design"] },
  ]},
  { label: "Optimization", tips: [
    { condition: "If asked for all permutations/subsets", approaches: ["Backtracking"] },
    { condition: "If need to count/divide optimally", approaches: ["Greedy", "Dynamic programming"] },
  ]},
  { label: "General", tips: [
    { condition: "Else", approaches: ["Map/Set for O(1) time & O(n) space", "Sort input for O(nlogn) time and O(1) space"] },
  ]},
];

export default function TipsPanel() {
  const [open, setOpen] = useState(false);
  const [copied, setCopied] = useState(false);
  const [toastFading, setToastFading] = useState(false);

  useEffect(() => {
    if (!open) return;
    const handleKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") { setOpen(false); trackEvent("panel_close", { panel: "tips" }); }
    };
    window.addEventListener("keydown", handleKey);
    return () => window.removeEventListener("keydown", handleKey);
  }, [open]);

  const copyToClipboard = useCallback(() => {
    const text = tipGroups
      .map((group) => {
        const rows = group.tips
          .map((tip) => `  ${tip.condition} → ${tip.approaches.join(", ")}`)
          .join("\n");
        return `${group.label}\n${rows}`;
      })
      .join("\n\n");
    navigator.clipboard.writeText(text);
    setCopied(true);
    setToastFading(false);
    trackEvent("copy_tips");
  }, []);

  useEffect(() => {
    if (!copied) return;
    const fadeTimer = setTimeout(() => setToastFading(true), 1500);
    const removeTimer = setTimeout(() => { setCopied(false); setToastFading(false); }, 2200);
    return () => { clearTimeout(fadeTimer); clearTimeout(removeTimer); };
  }, [copied]);

  return (
    <>
      {/* Tab button – rendered inline inside the fixed flex wrapper in page.tsx */}
      <button
        onClick={() => { setOpen(true); trackEvent("panel_open", { panel: "tips" }); }}
        className="rounded-r-xl bg-blue-600 px-2.5 py-4 text-white shadow-lg transition-colors hover:bg-blue-700"
        aria-label="Open tips"
      >
        <span className="flex items-center gap-2 text-sm font-semibold [writing-mode:vertical-lr]">
          <Lightbulb className="h-4 w-4 rotate-90" />
          Helpful Tips
        </span>
      </button>

      {/* Panel */}
      <div
        className={`fixed left-0 top-0 z-50 h-full w-80 transform border-r border-zinc-200 bg-white shadow-xl transition-transform duration-300 ease-in-out dark:border-zinc-700 dark:bg-zinc-900 ${
          open ? "translate-x-0" : "-translate-x-full"
        }`}
      >
        <div className="flex items-center justify-between bg-blue-600 px-4 py-3 text-white">
          <h2 className="flex items-center gap-2 text-base font-semibold">
            <Lightbulb className="h-4 w-4" />
            Helpful Tips
          </h2>
          <div className="flex items-center gap-1">
            <button
              onClick={copyToClipboard}
              className="rounded p-1 transition-colors hover:bg-blue-700"
              title="Copy to clipboard"
            >
              {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
            </button>
            <button
              onClick={() => { setOpen(false); trackEvent("panel_close", { panel: "tips" }); }}
              className="rounded p-1 transition-colors hover:bg-blue-700"
            >
              <X className="h-4 w-4" />
            </button>
          </div>
        </div>
        <div className="h-[calc(100%-49px)] overflow-y-auto px-4 py-4">
          <p className="mb-4 text-xs text-zinc-500">
            Based on the problem constraints, use these heuristics to identify possible approaches when unsure.
          </p>
          <table className="w-full text-sm">
            <thead>
              <tr className="border-b border-zinc-200 dark:border-zinc-700">
                <th className="pb-2 text-left font-semibold">Condition</th>
                <th className="pb-2 text-left font-semibold">Approach</th>
              </tr>
            </thead>
            <tbody>
              {tipGroups.map((group) => (
                <Fragment key={group.label}>
                  <tr>
                    <td colSpan={2} className="pt-4 pb-1 text-xs font-semibold uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
                      {group.label}
                    </td>
                  </tr>
                  {group.tips.map((tip) => (
                    <tr key={tip.condition} className="border-b border-zinc-100 dark:border-zinc-800">
                      <td className="py-2 pr-3 align-top text-zinc-700 dark:text-zinc-300">{formatApproach(tip.condition)}</td>
                      <td className="py-2 align-top text-zinc-600 dark:text-zinc-400">
                        {tip.approaches.map((a, i) => (
                          <span key={i}>{i > 0 && ", "}{formatApproach(a)}</span>
                        ))}
                      </td>
                    </tr>
                  ))}
                </Fragment>
              ))}
            </tbody>
          </table>
        </div>
      </div>

      {/* Copy toast – portalled to body so it centres on the viewport */}
      {copied && createPortal(
        <div
          className={`fixed inset-x-0 bottom-6 z-50 mx-auto w-fit animate-[fadeInUp_0.3s_ease-out] rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm font-medium text-blue-800 shadow-lg transition-opacity duration-700 ease-in-out dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200 ${toastFading ? "opacity-0" : "opacity-100"}`}
        >
          ✓ Tips copied to clipboard
        </div>,
        document.body
      )}
    </>
  );
}


================================================
FILE: src/components/panels/panels.test.tsx
================================================
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

const { mockTrackEvent } = vi.hoisted(() => ({
  mockTrackEvent: vi.fn(),
}));

vi.mock("@/lib/analytics", () => ({
  trackEvent: mockTrackEvent,
}));

import AboutPanel from "@/components/panels/AboutPanel";
import TipsPanel from "@/components/panels/TipsPanel";
import AcknowledgementsPanel from "@/components/panels/AcknowledgementsPanel";

describe("AboutPanel analytics", () => {
  beforeEach(() => {
    mockTrackEvent.mockClear();
  });

  it("tracks panel_open when About tab is clicked", async () => {
    const user = userEvent.setup();
    render(<AboutPanel />);
    await user.click(screen.getByLabelText("Open about"));
    expect(mockTrackEvent).toHaveBeenCalledWith("panel_open", { panel: "about" });
  });

  it("tracks panel_close when close button is clicked", async () => {
    const user = userEvent.setup();
    render(<AboutPanel />);
    await user.click(screen.getByLabelText("Open about"));
    mockTrackEvent.mockClear();
    const heading = screen.getByRole("heading", { name: /about/i });
    const closeBtn = heading.closest("div")!.querySelector("button")!;
    await user.click(closeBtn);
    expect(mockTrackEvent).toHaveBeenCalledWith("panel_close", { panel: "about" });
  });
});

describe("TipsPanel analytics", () => {
  beforeEach(() => {
    mockTrackEvent.mockClear();
  });

  it("tracks panel_open when Tips tab is clicked", async () => {
    const user = userEvent.setup();
    render(<TipsPanel />);
    await user.click(screen.getByLabelText("Open tips"));
    expect(mockTrackEvent).toHaveBeenCalledWith("panel_open", { panel: "tips" });
  });

  it("tracks panel_close when close button is clicked", async () => {
    const user = userEvent.setup();
    render(<TipsPanel />);
    await user.click(screen.getByLabelText("Open tips"));
    mockTrackEvent.mockClear();
    const heading = screen.getByRole("heading", { name: /helpful tips/i });
    const buttons = heading.closest("div")!.querySelectorAll("button");
    const closeBtn = buttons[buttons.length - 1];
    await user.click(closeBtn);
    expect(mockTrackEvent).toHaveBeenCalledWith("panel_close", { panel: "tips" });
  });
});

describe("AcknowledgementsPanel analytics", () => {
  beforeEach(() => {
    mockTrackEvent.mockClear();
  });

  it("tracks panel_open when Acknowledgements tab is clicked", async () => {
    const user = userEvent.setup();
    render(<AcknowledgementsPanel />);
    await user.click(screen.getByLabelText("Open acknowledgements"));
    expect(mockTrackEvent).toHaveBeenCalledWith("panel_open", { panel: "acknowledgements" });
  });

  it("tracks panel_close when close button is clicked", async () => {
    const user = userEvent.setup();
    render(<AcknowledgementsPanel />);
    await user.click(screen.getByLabelText("Open acknowledgements"));
    mockTrackEvent.mockClear();
    const heading = screen.getByRole("heading", { name: /acknowledgements/i });
    const closeBtn = heading.closest("div")!.querySelector("button")!;
    await user.click(closeBtn);
    expect(mockTrackEvent).toHaveBeenCalledWith("panel_close", { panel: "acknowledgements" });
  });
});


================================================
FILE: src/components/questions/ConfirmModal.tsx
================================================
export default function ConfirmModal({
  title,
  message,
  confirmLabel,
  onConfirm,
  onCancel,
}: {
  title: string;
  message: React.ReactNode;
  confirmLabel: string;
  onConfirm: () => void;
  onCancel: () => void;
}) {
  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
      onClick={onCancel}
      role="dialog"
      aria-modal="true"
      aria-label={title}
    >
      <div
        className="mx-4 w-full max-w-sm rounded-xl border border-zinc-200 bg-white p-6 shadow-xl dark:border-zinc-700 dark:bg-zinc-900"
        onClick={(e) => e.stopPropagation()}
      >
        <h2 className="mb-2 text-lg font-semibold">
          {title}
        </h2>
        <p className="mb-4 text-sm text-zinc-500">
          {message}
        </p>
        <div className="flex justify-end gap-2">
          <button
            onClick={onCancel}
            className="rounded-lg border border-zinc-300 px-4 py-2 text-sm font-medium hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
          >
            Cancel
          </button>
          <button
            onClick={onConfirm}
            className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
          >
            {confirmLabel}
          </button>
        </div>
      </div>
    </div>
  );
}


================================================
FILE: src/components/questions/FilterToolbar.tsx
================================================
import { useState, useMemo, useEffect, useRef } from "react";
import { type Table } from "@tanstack/react-table";
import { Question } from "@/types/question";
import { RotateCcw, Shuffle, Download, Upload, Trash2, StarOff, Dices, ListOrdered, CalendarOff } from "lucide-react";
import type { Reminder } from "@/lib/reminders";
import { trackEvent } from "@/lib/analytics";

const difficultyColor: Record<string, string> = {
  Easy: "text-green-700 dark:text-green-400",
  Medium: "text-yellow-700 dark:text-yellow-400",
  Hard: "text-red-700 dark:text-red-400",
};

interface FilterToolbarProps {
  table: Table<Question>;
  globalFilter: string;
  setGlobalFilter: (value: string) => void;
  patterns: string[];
  companies: [string, string][];
  showStarredOnly: boolean;
  setShowStarredOnly: (value: boolean) => void;
  hideCompleted: boolean;
  setHideCompleted: (value: boolean) => void;
  hidePatterns: boolean;
  setHidePatterns: (value: boolean) => void;
  showDueOnly: boolean;
  setShowDueOnly: (value: boolean) => void;
  pickRandom: () => void;
  shuffleOrder: number[] | null;
  toggleShuffle: () => void;
  exportProgress: () => void;
  fileInputRef: React.RefObject<HTMLInputElement | null>;
  importProgress: (file: File) => void;
  starred: Set<number>;
  notes: Record<number, string>;
  completed: Set<number>;
  reminders: Record<number, Reminder>;
  setClearConfirm: (value: "notes" | "questions" | "starred" | "reminders" | null) => void;
  searchRef: React.RefObject<HTMLInputElement | null>;
  columnFilters: { id: string; value: unknown }[];
}

export default function FilterToolbar({
  table,
  globalFilter,
  setGlobalFilter,
  patterns,
  companies,
  showStarredOnly,
  setShowStarredOnly,
  hideCompleted,
  setHideCompleted,
  hidePatterns,
  setHidePatterns,
  showDueOnly,
  setShowDueOnly,
  pickRandom,
  shuffleOrder,
  toggleShuffle,
  exportProgress,
  fileInputRef,
  importProgress,
  starred,
  notes,
  completed,
  reminders,
  setClearConfirm,
  searchRef,
  columnFilters,
}: FilterToolbarProps) {
  const difficultyFilter = useMemo(
    () => (table.getColumn("difficulty")?.getFilterValue() as string[]) ?? [],
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [table, columnFilters]
  );
  const [difficultyDropdownOpen, setDifficultyDropdownOpen] = useState(false);
  const difficultyDropdownRef = useRef<HTMLDivElement>(null);

  const patternFilter = useMemo(
    () => (table.getColumn("pattern")?.getFilterValue() as string[]) ?? [],
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [table, columnFilters]
  );
  const [patternDropdownOpen, setPatternDropdownOpen] = useState(false);
  const [patternSearch, setPatternSearch] = useState("");
  const patternDropdownRef = useRef<HTMLDivElement>(null);

  const filteredPatterns = useMemo(() => {
    const list = patternSearch
      ? patterns.filter((p) =>
          p.toLowerCase().includes(patternSearch.toLowerCase())
        )
      : patterns;
    return [...list].sort((a, b) => {
      const aChecked = patternFilter.includes(a);
      const bChecked = patternFilter.includes(b);
      if (aChecked !== bChecked) return aChecked ? -1 : 1;
      return 0;
    });
  }, [patterns, patternSearch, patternFilter]);

  const companyFilter = useMemo(
    () => (table.getColumn("companies")?.getFilterValue() as string[]) ?? [],
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [table, columnFilters]
  );
  const [companyDropdownOpen, setCompanyDropdownOpen] = useState(false);
  const [companySearch, setCompanySearch] = useState("");
  const companyDropdownRef = useRef<HTMLDivElement>(null);

  const filteredCompanies = useMemo(() => {
    const list = companySearch
      ? companies.filter(([, name]) =>
          name.toLowerCase().includes(companySearch.toLowerCase())
        )
      : companies;
    return [...list].sort((a, b) => {
      const aChecked = companyFilter.includes(a[0]);
      const bChecked = companyFilter.includes(b[0]);
      if (aChecked !== bChecked) return aChecked ? -1 : 1;
      return 0;
    });
  }, [companies, companySearch, companyFilter]);

  useEffect(() => {
    const handleClick = (e: MouseEvent | TouchEvent) => {
      if (difficultyDropdownRef.current && !difficultyDropdownRef.current.contains(e.target as Node)) {
        setDifficultyDropdownOpen(false);
      }
      if (patternDropdownRef.current && !patternDropdownRef.current.contains(e.target as Node)) {
        setPatternDropdownOpen(false);
        setPatternSearch("");
      }
      if (companyDropdownRef.current && !companyDropdownRef.current.contains(e.target as Node)) {
        setCompanyDropdownOpen(false);
        setCompanySearch("");
      }
    };
    document.addEventListener("mousedown", handleClick);
    document.addEventListener("touchstart", handleClick);
    return () => {
      document.removeEventListener("mousedown", handleClick);
      document.removeEventListener("touchstart", handleClick);
    };
  }, []);

  return (
    <div className="flex flex-wrap items-center justify-center gap-2 text-sm">
      <div className="relative">
        <input
          ref={searchRef}
          type="text"
          placeholder="Search"
          value={globalFilter}
          onChange={(e) => setGlobalFilter(e.target.value)}
          aria-label="Search questions"
          className="w-36 rounded border border-zinc-300 bg-white px-2 py-1.5 pr-7 shadow-sm focus:border-blue-500 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900"
        />
        <kbd className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded bg-zinc-200 px-1 py-0.5 text-[10px] font-mono leading-none text-zinc-500 dark:bg-zinc-700 dark:text-zinc-400">/</kbd>
      </div>
      <div ref={difficultyDropdownRef} className="relative">
        <button
          onClick={() => setDifficultyDropdownOpen((o) => !o)}
          aria-expanded={difficultyDropdownOpen}
          aria-haspopup="listbox"
          className="flex items-center gap-1 whitespace-nowrap rounded border border-zinc-300 bg-white px-2 py-1.5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
        >
          <span>
            {difficultyFilter.length === 0
              ? "All Difficulties"
              : `Difficulty (${difficultyFilter.length})`}
          </span>
          <svg className="h-3 w-3 shrink-0 opacity-50" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2">
            <path d="M3 5l3 3 3-3" />
          </svg>
        </button>
        {difficultyDropdownOpen && (
          <div className="absolute left-0 top-full z-20 mt-1 w-48 rounded-lg border border-zinc-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
            {difficultyFilter.length > 0 && (
              <button
                onClick={() => {
                  table.getColumn("difficulty")?.setFilterValue(undefined);
                }}
                className="w-full border-b border-zinc-200 px-3 py-1.5 text-left text-xs text-blue-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-blue-400 dark:hover:bg-zinc-800"
              >
                Clear all ({difficultyFilter.length})
              </button>
            )}
            <div className="py-1">
              {(["Easy", "Medium", "Hard"] as const).map((d) => (
                <label
                  key={d}
                  className="flex cursor-pointer items-center gap-2 px-3 py-1.5 hover:bg-zinc-50 dark:hover:bg-zinc-800"
                >
                  <input
                    type="checkbox"
                    checked={difficultyFilter.includes(d)}
                    onChange={() => {
                      const next = difficultyFilter.includes(d)
                        ? difficultyFilter.filter((x) => x !== d)
                        : [...difficultyFilter, d];
                      table
                        .getColumn("difficulty")
                        ?.setFilterValue(next.length ? next : undefined);
                    }}
                    className="h-3.5 w-3.5 accent-blue-600"
                  />
                  <span className={`font-medium ${difficultyColor[d]}`}>{d}</span>
                </label>
              ))}
            </div>
          </div>
        )}
      </div>
      <div ref={patternDropdownRef} className="relative">
        <button
          onClick={() => setPatternDropdownOpen((o) => !o)}
          aria-expanded={patternDropdownOpen}
          aria-haspopup="listbox"
          className="flex items-center gap-1 whitespace-nowrap rounded border border-zinc-300 bg-white px-2 py-1.5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
        >
          <span>
            {patternFilter.length === 0
              ? "All Patterns"
              : `Patterns (${patternFilter.length})`}
          </span>
          <svg className="h-3 w-3 shrink-0 opacity-50" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2">
            <path d="M3 5l3 3 3-3" />
          </svg>
        </button>
        {patternDropdownOpen && (
          <div className="absolute left-0 top-full z-20 mt-1 w-64 rounded-lg border border-zinc-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
            <div className="border-b border-zinc-200 p-2 dark:border-zinc-700">
              <input
                type="text"
                placeholder="Search patterns..."
                value={patternSearch}
                onChange={(e) => setPatternSearch(e.target.value)}
                className="w-full rounded border border-zinc-300 bg-transparent px-2 py-1 text-sm focus:border-blue-500 focus:outline-none dark:border-zinc-700"
                autoFocus
              />
            </div>
            {patternFilter.length > 0 && (
              <button
                onClick={() => {
                  table.getColumn("pattern")?.setFilterValue(undefined);
                  setPatternSearch("");
                }}
                className="w-full border-b border-zinc-200 px-3 py-1.5 text-left text-xs text-blue-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-blue-400 dark:hover:bg-zinc-800"
              >
                Clear all ({patternFilter.length})
              </button>
            )}
            <div className="max-h-52 overflow-y-auto py-1">
              {filteredPatterns.length === 0 ? (
                <p className="px-3 py-2 text-xs text-zinc-400">No patterns found</p>
              ) : (
                filteredPatterns.map((p) => (
                  <label
                    key={p}
                    className="flex cursor-pointer items-center gap-2 px-3 py-1.5 hover:bg-zinc-50 dark:hover:bg-zinc-800"
                  >
                    <input
                      type="checkbox"
                      checked={patternFilter.includes(p)}
                      onChange={() => {
                        const next = patternFilter.includes(p)
                          ? patternFilter.filter((x) => x !== p)
                          : [...patternFilter, p];
                        table
                          .getColumn("pattern")
                          ?.setFilterValue(next.length ? next : undefined);
                      }}
                      className="h-3.5 w-3.5 accent-blue-600"
                    />
                    {p}
                  </label>
                ))
              )}
            </div>
          </div>
        )}
      </div>
      <div ref={companyDropdownRef} className="relative">
        <button
          onClick={() => setCompanyDropdownOpen((o) => !o)}
          aria-expanded={companyDropdownOpen}
          aria-haspopup="listbox"
          className="flex items-center gap-1 whitespace-nowrap rounded border border-zinc-300 bg-white px-2 py-1.5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
        >
          <span>
            {companyFilter.length === 0
              ? "All Companies"
              : `Companies (${companyFilter.length})`}
          </span>
          <svg className="h-3 w-3 shrink-0 opacity-50" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2">
            <path d="M3 5l3 3 3-3" />
          </svg>
        </button>
        {companyDropdownOpen && (
          <div className="absolute left-0 top-full z-20 mt-1 w-64 rounded-lg border border-zinc-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
            <div className="border-b border-zinc-200 p-2 dark:border-zinc-700">
              <input
                type="text"
                placeholder="Search companies..."
                value={companySearch}
                onChange={(e) => setCompanySearch(e.target.value)}
                className="w-full rounded border border-zinc-300 bg-transparent px-2 py-1 text-sm focus:border-blue-500 focus:outline-none dark:border-zinc-700"
                autoFocus
              />
            </div>
            {companyFilter.length > 0 && (
              <button
                onClick={() => {
                  table.getColumn("companies")?.setFilterValue(undefined);
                  setCompanySearch("");
                }}
                className="w-full border-b border-zinc-200 px-3 py-1.5 text-left text-xs text-blue-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-blue-400 dark:hover:bg-zinc-800"
              >
                Clear all ({companyFilter.length})
              </button>
            )}
            <div className="max-h-52 overflow-y-auto py-1">
              {filteredCompanies.length === 0 ? (
                <p className="px-3 py-2 text-xs text-zinc-400">No companies found</p>
              ) : (
                filteredCompanies.map(([slug, name]) => (
                  <label
                    key={slug}
                    className="flex cursor-pointer items-center gap-2 px-3 py-1.5 hover:bg-zinc-50 dark:hover:bg-zinc-800"
                  >
                    <input
                      type="checkbox"
                      checked={companyFilter.includes(slug)}
                      onChange={() => {
                        const next = companyFilter.includes(slug)
                          ? companyFilter.filter((s) => s !== slug)
                          : [...companyFilter, slug];
                        table
                          .getColumn("companies")
                          ?.setFilterValue(next.length ? next : undefined);
                      }}
                      className="h-3.5 w-3.5 accent-blue-600"
                    />
                    {name}
                  </label>
                ))
              )}
            </div>
          </div>
        )}
      </div>
      <div className="flex flex-wrap items-center justify-center gap-1 px-1 py-0.5 sm:flex-nowrap sm:rounded-lg sm:border sm:border-zinc-200 sm:dark:border-zinc-800">
        {([
          { label: "Starred only", checked: showStarredOnly, onChange: (v: boolean) => { setShowStarredOnly(v); trackEvent("show_starred_only", { enabled: v }); } },
          { label: "Due for review", checked: showDueOnly, onChange: (v: boolean) => { setShowDueOnly(v); trackEvent("show_due_only", { enabled: v }); } },
          { label: "Hide completed", checked: hideCompleted, onChange: (v: boolean) => { setHideCompleted(v); trackEvent("hide_completed", { enabled: v }); } },
          { label: "Hide patterns", checked: hidePatterns, onChange: (v: boolean) => { setHidePatterns(v); trackEvent("hide_patterns", { enabled: v }); } },
        ] as const).map((opt) => (
          <label key={opt.label} className={`flex cursor-pointer items-center gap-1.5 whitespace-nowrap rounded px-2 py-1.5 transition-colors select-none ${opt.checked ? "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400" : "text-zinc-600 hover:bg-zinc-50 dark:text-zinc-400 dark:hover:bg-zinc-800"}`}>
            <input
              type="checkbox"
              checked={opt.checked}
              onChange={(e) => opt.onChange(e.target.checked)}
              className="h-3.5 w-3.5 accent-blue-600"
            />
            {opt.label}
          </label>
        ))}
      </div>
      {/* Random & Shuffle */}
      <div className="flex items-center gap-1 rounded-lg border border-zinc-200 px-1 py-0.5 dark:border-zinc-800">
        <button
          onClick={pickRandom}
          className="inline-flex items-center gap-1 whitespace-nowrap rounded px-2 py-1.5 transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
        >
          <Shuffle className="h-3.5 w-3.5" />
          Random
          <kbd className="rounded bg-zinc-200 px-1 py-0.5 text-[10px] font-mono leading-none text-zinc-500 dark:bg-zinc-700 dark:text-zinc-400">r</kbd>
        </button>
        <button
          onClick={toggleShuffle}
          className={`inline-flex items-center gap-1 whitespace-nowrap rounded px-2 py-1.5 transition-colors ${
            shuffleOrder
              ? "bg-violet-50 text-violet-600 hover:bg-violet-100 dark:bg-violet-900/30 dark:text-violet-400 dark:hover:bg-violet-900/50"
              : "hover:bg-zinc-100 dark:hover:bg-zinc-800"
          }`}
        >
          {shuffleOrder ? <ListOrdered className="h-3.5 w-3.5" /> : <Dices className="h-3.5 w-3.5" />}
          {shuffleOrder ? "Unshuffle" : "Shuffle"}
        </button>
      </div>
      {/* Import & Export */}
      <div className="flex items-center gap-1 rounded-lg border border-zinc-200 px-1 py-0.5 dark:border-zinc-800">
        <button
          onClick={exportProgress}
          className="inline-flex items-center gap-1 whitespace-nowrap rounded px-2 py-1.5 transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
        >
          <Download className="h-3.5 w-3.5" />
          Export
        </button>
        <button
          onClick={() => fileInputRef.current?.click()}
          className="inline-flex items-center gap-1 whitespace-nowrap rounded px-2 py-1.5 transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
        >
          <Upload className="h-3.5 w-3.5" />
          Import
        </button>
        <input
          ref={fileInputRef}
          type="file"
          accept=".json"
          className="hidden"
          onChange={(e) => {
            const file = e.target.files?.[0];
            if (file) importProgress(file);
            e.target.value = "";
          }}
        />
      </div>

      {/* Clear */}
      {(starred.size > 0 || Object.keys(notes).length > 0 || completed.size > 0 || Object.keys(reminders).length > 0) && (
        <div className="flex items-center gap-1 rounded-lg border border-red-200 px-1 py-0.5 dark:border-red-900/40">
          {starred.size > 0 && (
            <button
              onClick={() => setClearConfirm("starred")}
              className="inline-flex items-center gap-1 whitespace-nowrap rounded px-2 py-1.5 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/30 dark:hover:text-red-400"
            >
              <StarOff className="h-3.5 w-3.5" />
              Stars
            </button>
          )}
          {Object.keys(notes).length > 0 && (
            <button
              onClick={() => setClearConfirm("notes")}
              className="inline-flex items-center gap-1 whitespace-nowrap rounded px-2 py-1.5 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/30 dark:hover:text-red-400"
            >
              <Trash2 className="h-3.5 w-3.5" />
              Notes
            </button>
          )}
          {Object.keys(reminders).length > 0 && (
            <button
              onClick={() => setClearConfirm("reminders")}
              className="inline-flex items-center gap-1 whitespace-nowrap rounded px-2 py-1.5 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/30 dark:hover:text-red-400"
            >
              <CalendarOff className="h-3.5 w-3.5" />
              Reminders
            </button>
          )}
          {completed.size > 0 && (
            <button
              onClick={() => setClearConfirm("questions")}
              className="inline-flex items-center gap-1 whitespace-nowrap rounded px-2 py-1.5 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/30 dark:hover:text-red-400"
            >
              <RotateCcw className="h-3.5 w-3.5" />
              Progress
            </button>
          )}
        </div>
      )}
    </div>
  );
}


================================================
FILE: src/components/questions/GroupHeaderRow.tsx
================================================
import { forwardRef } from "react";
import { ChevronRight, ChevronDown, RotateCcw } from "lucide-react";

const difficultyColor: Record<string, string> = {
  Easy: "text-green-700 dark:text-green-400",
  Medium: "text-yellow-700 dark:text-yellow-400",
  Hard: "text-red-700 dark:text-red-400",
};

const groupRowStyles: Record<string, string> = {
  Easy: "border-l-green-500 bg-green-50 hover:bg-green-100 dark:bg-green-900/30 dark:hover:bg-green-900/50",
  Medium: "border-l-yellow-500 bg-yellow-50 hover:bg-yellow-100 dark:bg-yellow-900/30 dark:hover:bg-yellow-900/50",
  Hard: "border-l-red-500 bg-red-50 hover:bg-red-100 dark:bg-red-900/30 dark:hover:bg-red-900/50",
};

interface GroupHeaderRowProps {
  groupKey: string;
  groupDone: number;
  total: number;
  isCollapsed: boolean;
  toggleGroup: (group: string) => void;
  setResetConfirmGroup: (group: string) => void;
  colSpan: number;
  dataIndex: number;
}

const GroupHeaderRow = forwardRef<HTMLTableRowElement, GroupHeaderRowProps>(
  function GroupHeaderRow({ groupKey, groupDone, total, isCollapsed, toggleGroup, setResetConfirmGroup, colSpan, dataIndex }, ref) {
    return (
      <tr
        key={`header-${groupKey}`}
        data-index={dataIndex}
        ref={ref}
        className={`cursor-pointer select-none border-l-4 ${groupRowStyles[groupKey] ?? ""}`}
        onClick={() => toggleGroup(groupKey)}
        onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleGroup(groupKey); } }}
        tabIndex={0}
        role="button"
        aria-expanded={!isCollapsed}
        aria-label={`${groupKey} group, ${groupDone} of ${total} completed`}
      >
        <td
          colSpan={colSpan}
          className="px-2 py-2.5 sm:px-4 sm:py-3"
        >
          <span className="flex items-center gap-2">
            {isCollapsed ? (
              <ChevronRight className="h-4 w-4" />
            ) : (
              <ChevronDown className="h-4 w-4" />
            )}
            <span className={`text-base font-bold ${difficultyColor[groupKey] ?? ""}`}>
              {groupKey}
            </span>
            <span className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
              {groupDone}/{total} completed
            </span>
            {groupDone > 0 && (
              <button
                onClick={(e) => {
                  e.stopPropagation();
                  setResetConfirmGroup(groupKey);
                }}
                className="ml-auto flex items-center gap-1 rounded px-1.5 py-1 text-xs text-zinc-400 transition-colors hover:bg-zinc-200 hover:text-zinc-600 dark:hover:bg-zinc-600 dark:hover:text-zinc-300"
              >
                <RotateCcw className="h-3 w-3" />
                <span className="hidden sm:inline">Reset</span>
              </button>
            )}
          </span>
        </td>
      </tr>
    );
  }
);

export default GroupHeaderRow;


================================================
FILE: src/components/questions/NoteModal.tsx
================================================
import { MAX_NOTE_LENGTH } from "@/lib/storage";

export interface EditingNote {
  id: number;
  title: string;
  draft: string;
  confirmDiscard: boolean;
}

export default function NoteModal({
  editingNote,
  setEditingNote,
  updateNote,
  notes,
}: {
  editingNote: EditingNote;
  setEditingNote: (note: EditingNote | null) => void;
  updateNote: (id: number, value: string) => void;
  notes: Record<number, string>;
}) {
  const saved = notes[editingNote.id] ?? "";
  const hasChanges = editingNote.draft !== saved;
  const tryDismiss = () => {
    if (hasChanges) {
      setEditingNote({ ...editingNote, confirmDiscard: true });
    } else {
      setEditingNote(null);
    }
  };
  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
      onClick={tryDismiss}
      role="dialog"
      aria-modal="true"
      aria-label={`Edit note for ${editingNote.title}`}
    >
      <div
        className="mx-4 w-full max-w-lg rounded-xl border border-zinc-200 bg-white p-6 shadow-xl dark:border-zinc-700 dark:bg-zinc-900"
        onClick={(e) => e.stopPropagation()}
      >
        {editingNote.confirmDiscard ? (
          <>
            <h2 className="mb-2 text-lg font-semibold">Unsaved changes</h2>
            <p className="mb-3 text-sm text-zinc-500">
              Your note for <span className="font-medium text-foreground">{editingNote.title}</span> has
              been modified but not saved. Would you like to go back and save
              your changes, or discard them?
            </p>
            <div className="mb-4 rounded-lg border border-zinc-200 bg-zinc-50 p-3 text-sm dark:border-zinc-700 dark:bg-zinc-800">
              <p className="mb-1 text-xs font-medium text-zinc-400">Your unsaved note:</p>
              <p className="whitespace-pre-wrap break-words text-zinc-600 dark:text-zinc-300">
                {editingNote.draft || <span className="italic text-zinc-400">(empty)</span>}
              </p>
            </div>
            <div className="flex justify-end gap-2">
              <button
                onClick={() =>
                  setEditingNote({ ...editingNote, confirmDiscard: false })
                }
                className="rounded-lg border border-zinc-300 px-4 py-2 text-sm font-medium hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
              >
                Keep editing
              </button>
              <button
                onClick={() => setEditingNote(null)}
                className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
              >
                Discard
              </button>
            </div>
          </>
        ) : (
          <>
            <h2 className="mb-1 text-lg font-semibold">{editingNote.title}</h2>
            <p className="mb-4 text-sm text-zinc-500">Add your notes below</p>
            <textarea
              autoFocus
              rows={4}
              value={editingNote.draft}
              onChange={(e) =>
                setEditingNote({ ...editingNote, draft: e.target.value })
              }
              onKeyDown={(e) => {
                if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
                  e.preventDefault();
                  updateNote(editingNote.id, editingNote.draft);
                  setEditingNote(null);
                }
              }}
              maxLength={MAX_NOTE_LENGTH}
              placeholder="Write your notes here..."
              className="w-full resize-y rounded-lg border border-zinc-300 bg-transparent px-3 py-2 text-sm break-words focus:border-blue-500 focus:outline-none dark:border-zinc-700"
            />
            <div className="mt-2 flex items-center justify-between">
              <span className={`text-xs ${editingNote.draft.length >= MAX_NOTE_LENGTH ? "text-red-500" : "text-zinc-400"}`}>
                {editingNote.draft.length.toLocaleString()} / {MAX_NOTE_LENGTH.toLocaleString()} characters
              </span>
              {hasChanges ? (
                <span className="text-xs text-amber-600 dark:text-amber-400">
                  ⚠ Unsaved changes · {navigator.platform?.includes("Mac") ? "⌘" : "Ctrl"}+Enter to save
                </span>
              ) : (
                <span className="text-xs text-zinc-400">
                  {navigator.platform?.includes("Mac") ? "⌘" : "Ctrl"}+Enter to save
                </span>
              )}
            </div>
            <div className="mt-4 flex justify-end gap-2">
              <button
                onClick={tryDismiss}
                className="rounded-lg border border-zinc-300 px-4 py-2 text-sm font-medium hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
              >
                Cancel
              </button>
              <button
                onClick={() => {
                  updateNote(editingNote.id, editingNote.draft);
                  setEditingNote(null);
                }}
                className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
              >
                Done
              </button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}


================================================
FILE: src/components/questions/ProgressBar.tsx
================================================
export interface ProgressStats {
  totals: { Easy: number; Medium: number; Hard: number };
  done: { Easy: number; Medium: number; Hard: number };
  total: number;
  totalDone: number;
}

export default function ProgressBar({ stats, pct }: { stats: ProgressStats; pct: number }) {
  return (
    <div className="group relative rounded-lg border border-zinc-200 bg-zinc-50 p-3 sm:p-4 dark:border-zinc-800 dark:bg-zinc-900">
      <div className="mb-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm font-medium">
        <span>{stats.totalDone}/{stats.total} completed ({pct}%)</span>
        <div className="flex gap-4 sm:opacity-0 sm:transition-opacity sm:duration-500 sm:ease-in-out sm:group-hover:opacity-100">
          {stats.totals.Easy > 0 && (
            <span className="text-green-700 dark:text-green-400">
              Easy: {stats.done.Easy}/{stats.totals.Easy}
            </span>
          )}
          {stats.totals.Medium > 0 && (
            <span className="text-yellow-700 dark:text-yellow-400">
              Medium: {stats.done.Medium}/{stats.totals.Medium}
            </span>
          )}
          {stats.totals.Hard > 0 && (
            <span className="text-red-700 dark:text-red-400">
              Hard: {stats.done.Hard}/{stats.totals.Hard}
            </span>
          )}
        </div>
      </div>
      <div className="relative h-2 overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-700" role="progressbar" aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100} aria-label="Completion progress">
        {/* Default: solid blue bar */}
        <div
          className="absolute inset-0 h-full bg-blue-500 transition-opacity duration-500 ease-in-out sm:group-hover:opacity-0 max-sm:hidden"
          style={{ width: `${pct}%` }}
        />
        {/* Hover: blended difficulty gradient */}
        <div
          className="absolute inset-0 h-full transition-opacity duration-500 ease-in-out max-sm:opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
          style={{
            width: `${pct}%`,
            background: (() => {
              if (!stats.totalDone) return "var(--color-green-500)";
              const easyPct = (stats.done.Easy / stats.totalDone) * 100;
              const medPct = ((stats.done.Easy + stats.done.Medium) / stats.totalDone) * 100;
              return `linear-gradient(90deg, var(--color-green-500) ${Math.max(easyPct - 3, 0)}%, var(--color-yellow-500) ${Math.min(easyPct + 3, medPct - 3)}%, var(--color-yellow-500) ${Math.max(medPct - 3, easyPct + 3)}%, var(--color-red-500) ${medPct + 3}%)`;
            })(),
          }}
        />
      </div>

    </div>
  );
}


================================================
FILE: src/components/questions/QuestionRow.tsx
================================================
import { forwardRef } from "react";
import { flexRender, type Row } from "@tanstack/react-table";
import { Question } from "@/types/question";

const completedRowStyles: Record<string, string> = {
  Easy: "bg-green-100/60 hover:bg-green-100 dark:bg-green-900/30 dark:hover:bg-green-900/40",
  Medium: "bg-yellow-100/60 hover:bg-yellow-100 dark:bg-yellow-900/30 dark:hover:bg-yellow-900/40",
  Hard: "bg-red-100/60 hover:bg-red-100 dark:bg-red-900/30 dark:hover:bg-red-900/40",
};

interface QuestionRowProps {
  row: Row<Question>;
  completed: Set<number>;
  toggleCompleted: (id: number) => void;
  toggleStarred: (id: number) => void;
  dataIndex: number;
}

const QuestionRow = forwardRef<HTMLTableRowElement, QuestionRowProps>(
  function QuestionRow({ row, completed, toggleCompleted, toggleStarred, dataIndex }, ref) {
    const isDone = completed.has(row.original.id);
    return (
      <tr
        key={row.id}
        data-index={dataIndex}
        ref={ref}
        className={
          isDone
            ? `text-zinc-400 dark:text-zinc-500 ${completedRowStyles[row.original.difficulty]}`
            : "hover:bg-zinc-50 dark:hover:bg-zinc-900/50"
        }
      >
        {row.getVisibleCells().map((cell) => {
          const meta = cell.column.columnDef.meta as { clickable?: boolean; toggleFn?: string; noStrikethrough?: boolean } | undefined;
          const isClickable = meta?.clickable;
          const onClick = isClickable
            ? () => (meta?.toggleFn === "starred" ? toggleStarred : toggleCompleted)(row.original.id)
            : undefined;
          const strikethrough = isDone && !meta?.noStrikethrough ? "line-through decoration-zinc-300 dark:decoration-zinc-600" : "";
          return (
            <td
              key={cell.id}
              className={`px-2 py-2 sm:px-4 sm:py-3 ${strikethrough} ${isClickable ? "cursor-pointer select-none" : ""}`}
              onClick={onClick}
            >
              {flexRender(cell.column.columnDef.cell, cell.getContext())}
            </td>
          );
        })}
      </tr>
    );
  }
);

export default QuestionRow;


================================================
FILE: src/components/questions/QuestionsTable.test.tsx
================================================
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor, cleanup, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { Question } from "@/types/question";

const { mockTrackEvent } = vi.hoisted(() => ({
  mockTrackEvent: vi.fn(),
}));

vi.mock("@/lib/analytics", () => ({
  trackEvent: mockTrackEvent,
}));

vi.mock("next/navigation", () => ({
  useSearchParams: () => new URLSearchParams(),
  useRouter: () => ({ replace: vi.fn() }),
}));

vi.mock("@/components/layout/AuthContext", () => ({
  useAuth: () => ({ user: null, loading: false, signIn: vi.fn(), signOut: vi.fn(), syncNow: vi.fn(), syncVersion: 0 }),
}));

import QuestionsTable from "@/components/questions/QuestionsTable";

const testData: Question[] = [
  {
    id: 0,
    title: "Two Sum",
    slug: "two-sum",
    pattern: ["Arrays"],
    difficulty: "Easy",
    premium: false,
    companies: [{ name: "Google", slug: "google", frequency: 5 }],
  },
  {
    id: 1,
    title: "Add Two Numbers",
    slug: "add-two-numbers",
    pattern: ["Linked List"],
    difficulty: "Medium",
    premium: false,
    companies: [{ name: "Amazon", slug: "amazon", frequency: 3 }],
  },
  {
    id: 2,
    title: "Median of Two Sorted Arrays",
    slug: "median-of-two-sorted-arrays",
    pattern: ["Binary Search"],
    difficulty: "Hard",
    premium: false,
    companies: [],
  },
];

describe("QuestionsTable analytics", () => {
  beforeEach(() => {
    mockTrackEvent.mockClear();
    localStorage.clear();
    window.open = vi.fn();
    URL.createObjectURL = vi.fn(() => "blob:test");
    URL.revokeObjectURL = vi.fn();
  });

  afterEach(() => {
    cleanup();
    vi.restoreAllMocks();
  });

  it("tracks question_toggle when checking a question", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const checkboxes = screen.getAllByRole("checkbox", { name: /^Mark .+ as (complete|incomplete)$/ });
    const cell = checkboxes[0].closest("td")!;
    await user.click(cell);
    expect(mockTrackEvent).toHaveBeenCalledWith("question_toggle", {
      question_id: 0,
      completed: true,
    });
  });

  it("tracks question_toggle with completed=false when unchecking", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const checkboxes = screen.getAllByRole("checkbox", { name: /^Mark .+ as (complete|incomplete)$/ });
    const cell = checkboxes[0].closest("td")!;
    await user.click(cell);
    expect(mockTrackEvent).toHaveBeenCalledWith("question_toggle", {
      question_id: 0,
      completed: false,
    });
  });

  it("tracks note_save when saving a note", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Add note for Two Sum/ }));
    const textarea = screen.getByPlaceholderText("Write your notes here...");
    await user.type(textarea, "my test note");
    await user.click(screen.getByText("Done"));
    expect(mockTrackEvent).toHaveBeenCalledWith("note_save", {
      question_id: 0,
      has_content: true,
    });
  });

  it("tracks search with debounce", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const searchInput = screen.getByPlaceholderText("Search");
    await user.type(searchInput, "two");
    await waitFor(() => {
      expect(mockTrackEvent).toHaveBeenCalledWith("search", { query: "two" });
    }, { timeout: 2000 });
  });

  it("tracks filter_difficulty when selecting a difficulty filter", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByText("All Difficulties"));
    await user.click(screen.getByLabelText("Easy"));
    expect(mockTrackEvent).toHaveBeenCalledWith("filter_difficulty", { values: "Easy" });
  });

  it("tracks filter_pattern when selecting a pattern filter", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByText("All Patterns"));
    await user.click(screen.getByLabelText("Arrays"));
    expect(mockTrackEvent).toHaveBeenCalledWith("filter_pattern", { values: "Arrays" });
  });

  it("tracks filter_company when selecting a company filter", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByText("All Companies"));
    await user.click(screen.getByLabelText("Amazon"));
    expect(mockTrackEvent).toHaveBeenCalledWith("filter_company", { values: "amazon" });
  });

  it("tracks random_question when picking a random question", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Random/ }));
    expect(mockTrackEvent).toHaveBeenCalledWith(
      "random_question",
      expect.objectContaining({ question_id: expect.any(Number), slug: expect.any(String) })
    );
  });

  it("tracks export_progress when exporting", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Export/ }));
    expect(mockTrackEvent).toHaveBeenCalledWith("export_progress", {
      completed_count: 0,
      notes_count: 0,
    });
  });

  it("tracks hide_completed when toggling", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

await user.click(screen.getByLabelText("Hide completed"));
    expect(mockTrackEvent).toHaveBeenCalledWith("hide_completed", { enabled: true });
  });

  it("tracks hide_patterns when toggling", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    await user.click(screen.getByLabelText("Hide patterns"));
    expect(mockTrackEvent).toHaveBeenCalledWith("hide_patterns", { enabled: true });
  });

  it("tracks sort_column when clicking a sortable header", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByText("Title"));
    expect(mockTrackEvent).toHaveBeenCalledWith("sort_column", {
      column: "title",
      direction: "asc",
    });
  });

  it("tracks clear_all_progress when clearing all questions", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Progress/ }));
    await user.click(screen.getByRole("button", { name: "Clear" }));
    expect(mockTrackEvent).toHaveBeenCalledWith("clear_all_progress");
  });

  it("tracks clear_all_notes when clearing all notes", async () => {
    localStorage.setItem("leetcode-patterns-notes", JSON.stringify({ 0: "test note" }));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Notes/ }));
    await user.click(screen.getByRole("button", { name: "Clear" }));
    expect(mockTrackEvent).toHaveBeenCalledWith("clear_all_notes");
  });

  it("tracks reset_group when resetting a difficulty group", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const resetBtns = screen.getAllByText("Reset");
    await user.click(resetBtns[0]);
    // Modal has heading "Reset Easy progress" - find the confirm button inside it
    const modal = screen.getByText("Reset Easy progress").closest("div[class*='rounded-xl']")! as HTMLElement;
    const confirmBtn = within(modal).getByRole("button", { name: "Reset" });
    await user.click(confirmBtn);
    expect(mockTrackEvent).toHaveBeenCalledWith("reset_group", { difficulty: "Easy" });
  });

  it("truncates long notes in the notes column", async () => {
    const longNote = "a".repeat(300);
    localStorage.setItem("leetcode-patterns-notes", JSON.stringify({ 0: longNote }));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const noteBtn = screen.getByText(longNote);
    expect(noteBtn).toHaveClass("truncate", "max-w-[100px]");
  });

  it("shows progress stats scoped to filtered questions", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0, 1]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    // Unfiltered: 2 of 3 completed
    expect(screen.getByText("2/3 completed (67%)")).toBeInTheDocument();

    // Filter to Easy only - 1 of 1 completed, only Easy breakdown shown
    await user.click(screen.getByText("All Difficulties"));
    await user.click(screen.getByLabelText("Easy"));
    expect(screen.getByText("1/1 completed (100%)")).toBeInTheDocument();
    expect(screen.getByText(/Easy: 1\/1/)).toBeInTheDocument();
    expect(screen.queryByText(/Medium:/)).not.toBeInTheDocument();
    expect(screen.queryByText(/Hard:/)).not.toBeInTheDocument();
  });

  it("progress bar is not affected by hide completed toggle", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    // Before hiding: 1 of 3 completed
    expect(screen.getByText("1/3 completed (33%)")).toBeInTheDocument();

    // Toggle hide completed

await user.click(screen.getByLabelText("Hide completed"));

    // Row is hidden but progress bar still shows 1/3
    await waitFor(() => expect(screen.queryByText("Two Sum")).not.toBeInTheDocument());
    expect(screen.getByText("1/3 completed (33%)")).toBeInTheDocument();
  });

  it("progress bar reflects all difficulties when hide completed is on", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0, 1]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    // 2 of 3 completed
    expect(screen.getByText("2/3 completed (67%)")).toBeInTheDocument();


await user.click(screen.getByLabelText("Hide completed"));

    // Progress bar still shows 2/3 with all difficulty breakdowns
    expect(screen.getByText("2/3 completed (67%)")).toBeInTheDocument();
    expect(screen.getByText(/Easy: 1\/1/)).toBeInTheDocument();
    expect(screen.getByText(/Medium: 1\/1/)).toBeInTheDocument();
    expect(screen.getByText(/Hard: 0\/1/)).toBeInTheDocument();
  });

  it("progress bar respects difficulty filter even when hide completed is on", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    // Filter to Easy + hide completed
    await user.click(screen.getByText("All Difficulties"));
    await user.click(screen.getByLabelText("Easy"));

await user.click(screen.getByLabelText("Hide completed"));

    // Two Sum (Easy, completed) is hidden from table but still counted in progress
    await waitFor(() => expect(screen.queryByText("Two Sum")).not.toBeInTheDocument());
    expect(screen.getByText("1/1 completed (100%)")).toBeInTheDocument();
  });

  it("progress bar respects pattern filter even when hide completed is on", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    // Filter to Arrays pattern + hide completed
    await user.click(screen.getByText("All Patterns"));
    await user.click(screen.getByLabelText("Arrays"));

await user.click(screen.getByLabelText("Hide completed"));

    // Two Sum is the only Arrays question, completed and hidden, but still in progress
    await waitFor(() => expect(screen.queryByText("Two Sum")).not.toBeInTheDocument());
    expect(screen.getByText("1/1 completed (100%)")).toBeInTheDocument();
  });

  it("progress bar excludes completed items that don't match active filters", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0, 1]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    // Filter to Hard only + hide completed
    await user.click(screen.getByText("All Difficulties"));
    await user.click(screen.getByLabelText("Hard"));

await user.click(screen.getByLabelText("Hide completed"));

    // Only the Hard question (id=2, not completed) should count - Easy/Medium are filtered out
    expect(screen.getByText("0/1 completed (0%)")).toBeInTheDocument();
  });

  it("tracks star_toggle when starring a question", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const starCheckboxes = screen.getAllByRole("checkbox", { name: /^Star / });
    await user.click(starCheckboxes[0].closest("td")!);
    expect(mockTrackEvent).toHaveBeenCalledWith("star_toggle", {
      question_id: 0,
      starred: true,
    });
  });

  it("tracks star_toggle with starred=false when unstarring", async () => {
    localStorage.setItem("leetcode-patterns-starred", JSON.stringify([0]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const starCheckboxes = screen.getAllByRole("checkbox", { name: /^Star / });
    await user.click(starCheckboxes[0].closest("td")!);
    expect(mockTrackEvent).toHaveBeenCalledWith("star_toggle", {
      question_id: 0,
      starred: false,
    });
  });

  it("filters to starred only when toggling starred only checkbox", async () => {
    localStorage.setItem("leetcode-patterns-starred", JSON.stringify([0]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    // All 3 questions visible
    expect(screen.getByText("Two Sum")).toBeInTheDocument();
    expect(screen.getByText("Add Two Numbers")).toBeInTheDocument();


    await user.click(screen.getByLabelText("Starred only"));
    expect(screen.getByText("Two Sum")).toBeInTheDocument();
    await waitFor(() => expect(screen.queryByText("Add Two Numbers")).not.toBeInTheDocument());
    expect(screen.queryByText("Median of Two Sorted Arrays")).not.toBeInTheDocument();
  });

  it("tracks clear_all_starred when clearing all stars", async () => {
    localStorage.setItem("leetcode-patterns-starred", JSON.stringify([0, 1]));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Stars/ }));
    await user.click(screen.getByRole("button", { name: "Clear" }));
    expect(mockTrackEvent).toHaveBeenCalledWith("clear_all_starred");
  });

  it("persists starred only checkbox to localStorage", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    await user.click(screen.getByLabelText("Starred only"));
    expect(localStorage.getItem("leetcode-patterns-starred-only")).toBe("true");
  });

  it("restores starred only checkbox from localStorage", () => {
    localStorage.setItem("leetcode-patterns-starred-only", "true");
    localStorage.setItem("leetcode-patterns-starred", JSON.stringify([0]));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    expect(screen.getByLabelText("Starred only")).toBeChecked();
    expect(screen.getByText("Two Sum")).toBeInTheDocument();
    expect(screen.queryByText("Add Two Numbers")).not.toBeInTheDocument();
  });

  it("persists hide completed checkbox to localStorage", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

await user.click(screen.getByLabelText("Hide completed"));
    expect(localStorage.getItem("leetcode-patterns-hide-completed")).toBe("true");
  });

  it("restores hide completed checkbox from localStorage", () => {
    localStorage.setItem("leetcode-patterns-hide-completed", "true");
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    expect(screen.getByLabelText("Hide completed")).toBeChecked();
    expect(screen.queryByText("Two Sum")).not.toBeInTheDocument();
  });

  it("persists hide patterns checkbox to localStorage", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    await user.click(screen.getByLabelText("Hide patterns"));
    expect(localStorage.getItem("leetcode-patterns-hide-patterns")).toBe("true");
  });

  it("restores hide patterns checkbox from localStorage", () => {
    localStorage.setItem("leetcode-patterns-hide-patterns", "true");
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    expect(screen.getByLabelText("Hide patterns")).toBeChecked();
  });

  it("tracks shuffle_questions when clicking shuffle", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Shuffle/ }));
    expect(mockTrackEvent).toHaveBeenCalledWith("shuffle_questions");
  });

  it("persists shuffle order to localStorage", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Shuffle/ }));
    const stored = localStorage.getItem("leetcode-patterns-shuffle-order");
    expect(stored).not.toBeNull();
    const order = JSON.parse(stored!);
    expect(order).toHaveLength(3);
    expect(order.sort()).toEqual([0, 1, 2]);
  });

  it("removes difficulty group headers when shuffled", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const getGroupHeaders = () =>
      screen.getAllByRole("button", { name: /group/ });
    expect(getGroupHeaders()).toHaveLength(3);

    await user.click(screen.getByRole("button", { name: /Shuffle/ }));
    expect(screen.queryAllByRole("button", { name: /group/ })).toHaveLength(0);
  });

  it("toggles between shuffle and restore on the same button", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const getGroupHeaders = () =>
      screen.queryAllByRole("button", { name: /group/ });

    // Initially shows "Shuffle"
    expect(screen.getByRole("button", { name: /Shuffle/ })).toBeInTheDocument();

    // Click to shuffle
    await user.click(screen.getByRole("button", { name: /Shuffle/ }));
    expect(mockTrackEvent).toHaveBeenCalledWith("shuffle_questions");
    expect(getGroupHeaders()).toHaveLength(0);

    // Button now shows "Unshuffle"
    expect(screen.getByRole("button", { name: /Unshuffle/ })).toBeInTheDocument();

    // Click to restore
    await user.click(screen.getByRole("button", { name: /Unshuffle/ }));
    expect(mockTrackEvent).toHaveBeenCalledWith("restore_order");
    expect(getGroupHeaders().length).toBeGreaterThan(0);

    // Button shows "Shuffle" again
    expect(screen.getByRole("button", { name: /Shuffle/ })).toBeInTheDocument();
  });

  it("clears shuffle order from localStorage on restore", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Shuffle/ }));
    expect(localStorage.getItem("leetcode-patterns-shuffle-order")).not.toBeNull();

    await user.click(screen.getByRole("button", { name: /Unshuffle/ }));
    expect(localStorage.getItem("leetcode-patterns-shuffle-order")).toBeNull();
  });

  it("restores shuffle order from localStorage on mount", () => {
    localStorage.setItem("leetcode-patterns-shuffle-order", JSON.stringify([2, 0, 1]));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const groupHeaders = screen.queryAllByRole("button", { name: /group/ });
    expect(groupHeaders).toHaveLength(0);
    // Button shows "Unshuffle" since shuffle is active
    expect(screen.getByRole("button", { name: /Unshuffle/ })).toBeInTheDocument();
    const rows = screen.getAllByRole("row").filter((row) => row.querySelector("td") && row.querySelectorAll("td").length > 1);
    expect(rows).toHaveLength(3);
  });

  it("tracks import_progress when importing a file", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const importContainer = screen.getByRole("button", { name: /Import/ }).closest("div")!;
    const fileInput = importContainer.querySelector("input[type='file']")! as HTMLInputElement;
    const importData = JSON.stringify({ completed: [0, 1], notes: { 0: "note" } });
    const file = new File([importData], "progress.json", { type: "application/json" });
    await user.upload(fileInput, file);
    await waitFor(() => {
      expect(mockTrackEvent).toHaveBeenCalledWith("import_progress", {
        completed_count: 2,
        notes_count: 1,
      });
    });
  });

  it("collapses and expands a difficulty group", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

    // Easy group visible with its question
    expect(screen.getByText("Two Sum")).toBeInTheDocument();
    const easyGroup = screen.getByRole("button", { name: /Easy group/ });

    // Collapse
    await user.click(easyGroup);
    expect(screen.queryByText("Two Sum")).not.toBeInTheDocument();

    // Expand
    await user.click(easyGroup);
    expect(screen.getByText("Two Sum")).toBeInTheDocument();
  });

  it("focuses search input when pressing /", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const searchInput = screen.getByPlaceholderText("Search");
    expect(searchInput).not.toHaveFocus();
    await user.keyboard("/");
    expect(searchInput).toHaveFocus();
  });

  it("picks a random question when pressing r", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.keyboard("r");
    expect(mockTrackEvent).toHaveBeenCalledWith(
      "random_question",
      expect.objectContaining({ question_id: expect.any(Number), slug: expect.any(String) })
    );
  });

  it("shows discard confirmation when closing note modal with unsaved changes", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Add note for Two Sum/ }));
    const textarea = screen.getByPlaceholderText("Write your notes here...");
    await user.type(textarea, "unsaved text");
    await user.click(screen.getByText("Cancel"));
    expect(screen.getByText("Unsaved changes")).toBeInTheDocument();

    // Click "Keep editing" to go back
    await user.click(screen.getByText("Keep editing"));
    expect(screen.getByPlaceholderText("Write your notes here...")).toBeInTheDocument();
  });

  it("discards note changes when clicking Discard", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Add note for Two Sum/ }));
    const textarea = screen.getByPlaceholderText("Write your notes here...");
    await user.type(textarea, "unsaved text");
    await user.click(screen.getByText("Cancel"));
    await user.click(screen.getByText("Discard"));
    // Modal is closed, note not saved
    expect(screen.queryByText("Unsaved changes")).not.toBeInTheDocument();
    expect(screen.getAllByRole("button", { name: /Add note for/ })).toHaveLength(3);
  });

  it("saves note with Cmd+Enter keyboard shortcut", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Add note for Two Sum/ }));
    const textarea = screen.getByPlaceholderText("Write your notes here...");
    await user.type(textarea, "keyboard note");
    await user.keyboard("{Meta>}{Enter}{/Meta}");
    expect(mockTrackEvent).toHaveBeenCalledWith("note_save", {
      question_id: 0,
      has_content: true,
    });
    // Modal closed
    expect(screen.queryByPlaceholderText("Write your notes here...")).not.toBeInTheDocument();
  });

  it("filters questions when typing in search", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const searchInput = screen.getByPlaceholderText("Search");
    await user.type(searchInput, "Two Sum");
    await waitFor(() => {
      expect(screen.getByText("Two Sum")).toBeInTheDocument();
      expect(screen.queryByText("Median of Two Sorted Arrays")).not.toBeInTheDocument();
    });
  });

  it("records solved date when completing a question", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const checkboxes = screen.getAllByRole("checkbox", { name: /^Mark .+ as (complete|incomplete)$/ });
    await user.click(checkboxes[0].closest("td")!);
    const stored = JSON.parse(localStorage.getItem("leetcode-patterns-solved-dates")!);
    expect(stored).toHaveProperty("0");
    expect(new Date(stored["0"]).getTime()).not.toBeNaN();
  });

  it("shows relative solved date and set-review placeholder after completing a question", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const checkboxes = screen.getAllByRole("checkbox", { name: /^Mark .+ as (complete|incomplete)$/ });
    await user.click(checkboxes[0].closest("td")!);
    await waitFor(() => {
      expect(screen.getByText("Solved today")).toBeInTheDocument();
      expect(screen.getByText("+ Set review")).toBeInTheDocument();
    });
  });

  it("shows relative solved date for imported past dates", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    vi.setSystemTime(new Date("2026-03-09T12:00:00Z"));
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-06T10:00:00.000Z" }));
    localStorage.setItem("leetcode-patterns-reminders", JSON.stringify({ "0": { nextReview: "2026-03-09", interval: 3 } }));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    expect(screen.getByText("Solved 3d ago")).toBeInTheDocument();
    expect(screen.getByText("Due today")).toBeInTheDocument();
    vi.useRealTimers();
  });

  it("shows overdue pill for past review dates", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    vi.setSystemTime(new Date("2026-03-12T12:00:00Z"));
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-06T10:00:00.000Z" }));
    localStorage.setItem("leetcode-patterns-reminders", JSON.stringify({ "0": { nextReview: "2026-03-09", interval: 3 } }));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    expect(screen.getByText("Overdue 3d")).toBeInTheDocument();
    vi.useRealTimers();
  });

  it("shows future review pill for upcoming dates", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    vi.setSystemTime(new Date("2026-03-09T12:00:00Z"));
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    localStorage.setItem("leetcode-patterns-reminders", JSON.stringify({ "0": { nextReview: "2026-03-16", interval: 7 } }));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    expect(screen.getByText("Review in 7d")).toBeInTheDocument();
    vi.useRealTimers();
  });

  it("review pill tooltip contains the actual date and click hint", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    vi.setSystemTime(new Date("2026-03-09T12:00:00Z"));
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    localStorage.setItem("leetcode-patterns-reminders", JSON.stringify({ "0": { nextReview: "2026-03-16", interval: 7 } }));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const pill = screen.getByText("Review in 7d");
    expect(pill.getAttribute("title")).toContain("Click to change");
    vi.useRealTimers();
  });

  it("shows set review placeholder when solved but no reminder", () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    expect(screen.getByText("+ Set review")).toBeInTheDocument();
  });

  it("opens review modal when clicking set review placeholder", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByText("+ Set review"));
    expect(screen.getByRole("dialog", { name: /review date/i })).toBeInTheDocument();
  });

  it("note column shows full note as tooltip", () => {
    const note = "This is a long test note for tooltip";
    localStorage.setItem("leetcode-patterns-notes", JSON.stringify({ 0: note }));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const noteBtn = screen.getByText(note);
    expect(noteBtn.getAttribute("title")).toBe(note);
  });

  it("note column has no tooltip when empty", () => {
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const noteBtn = screen.getByRole("button", { name: /Add note for Two Sum/ });
    expect(noteBtn.getAttribute("title")).toBeNull();
  });

  it("clears reminder when clicking completed button on review pill", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    vi.setSystemTime(new Date("2026-03-09T12:00:00Z"));
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    localStorage.setItem("leetcode-patterns-reminders", JSON.stringify({ "0": { nextReview: "2026-03-16", interval: 7 } }));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    expect(screen.getByText("Review in 7d")).toBeInTheDocument();
    const completedBtn = screen.getByRole("button", { name: /mark review as completed/i });
    await user.click(completedBtn);
    await waitFor(() => {
      expect(screen.queryByText("Review in 7d")).not.toBeInTheDocument();
      expect(screen.getByText("+ Set review")).toBeInTheDocument();
    });
    const stored = JSON.parse(localStorage.getItem("leetcode-patterns-reminders")!);
    expect(stored).not.toHaveProperty("0");
    vi.useRealTimers();
  });

  it("opens review modal when clicking review pill", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    vi.setSystemTime(new Date("2026-03-09T12:00:00Z"));
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    localStorage.setItem("leetcode-patterns-reminders", JSON.stringify({ "0": { nextReview: "2026-03-16", interval: 7 } }));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByText("Review in 7d"));
    expect(screen.getByRole("dialog", { name: /review date/i })).toBeInTheDocument();
    vi.useRealTimers();
  });

  it("closes review modal when clicking close button", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    vi.setSystemTime(new Date("2026-03-09T12:00:00Z"));
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    localStorage.setItem("leetcode-patterns-reminders", JSON.stringify({ "0": { nextReview: "2026-03-16", interval: 7 } }));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByText("Review in 7d"));
    expect(screen.getByRole("dialog")).toBeInTheDocument();
    await user.click(screen.getByRole("button", { name: "Close" }));
    await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
    vi.useRealTimers();
  });

  it("shows clear review date button in modal when reminder exists", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    vi.setSystemTime(new Date("2026-03-09T12:00:00Z"));
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    localStorage.setItem("leetcode-patterns-reminders", JSON.stringify({ "0": { nextReview: "2026-03-16", interval: 7 } }));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByText("Review in 7d"));
    expect(screen.getByRole("button", { name: /clear review date/i })).toBeInTheDocument();
    vi.useRealTimers();
  });

  it("does not show clear review date button when no reminder exists", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByText("+ Set review"));
    expect(screen.getByRole("dialog")).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: /clear review date/i })).not.toBeInTheDocument();
  });

  it("clears review date and closes modal when clicking clear button", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    vi.setSystemTime(new Date("2026-03-09T12:00:00Z"));
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    localStorage.setItem("leetcode-patterns-reminders", JSON.stringify({ "0": { nextReview: "2026-03-16", interval: 7 } }));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByText("Review in 7d"));
    await user.click(screen.getByRole("button", { name: /clear review date/i }));
    await waitFor(() => {
      expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
      expect(screen.getByText("+ Set review")).toBeInTheDocument();
    });
    const stored = JSON.parse(localStorage.getItem("leetcode-patterns-reminders")!);
    expect(stored).not.toHaveProperty("0");
    expect(mockTrackEvent).toHaveBeenCalledWith("clear_review_date", { question_id: 0 });
    vi.useRealTimers();
  });

  it("solved date pill shows tooltip with actual date", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    vi.setSystemTime(new Date("2026-03-12T12:00:00Z"));
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-solved-dates", JSON.stringify({ "0": "2026-03-09T10:00:00.000Z" }));
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const solvedPill = screen.getByText("Solved 3d ago");
    expect(solvedPill.getAttribute("title")).toContain("Mar");
    expect(solvedPill.getAttribute("title")).toContain("2026");
    vi.useRealTimers();
  });

  it("tracks clear_all_reminders when clearing all reminders", async () => {
    localStorage.setItem("leetcode-patterns-completed", JSON.stringify([0]));
    localStorage.setItem("leetcode-patterns-reminders", JSON.stringify({ "0": { nextReview: "2026-03-16", interval: 7 } }));
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    await user.click(screen.getByRole("button", { name: /Reminders/ }));
    await user.click(screen.getByRole("button", { name: "Clear" }));
    expect(mockTrackEvent).toHaveBeenCalledWith("clear_all_reminders");
  });

  it("filter checkboxes are visible without opening a dropdown", () => {
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    expect(screen.getByLabelText("Starred only")).toBeInTheDocument();
    expect(screen.getByLabelText("Due for review")).toBeInTheDocument();
    expect(screen.getByLabelText("Hide completed")).toBeInTheDocument();
    expect(screen.getByLabelText("Hide patterns")).toBeInTheDocument();
  });

  it("imports starred and solvedDates from file", async () => {
    const user = userEvent.setup();
    render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
    const importContainer = screen.getByRole("button", { name: /Import/ }).closest("div")!;
    const fileInput = importContainer.querySelector("input[type='file']")! as HTMLInputElement;
    const importData = JSON.stringify({
      completed: [0],
      starred: [1],
      notes: {},
      solvedDates: { "0": "2025-06-01T00:00:00.000Z" },
    });
    const file = new File([importData], "progress.json", { type: "application/json" });
    await user.upload(fileInput, file);
    await waitFor(() => {
      expect(JSON.parse(localStorage.getItem("leetcode-patterns-starred")!)).toEqual([1]);
      expect(JSON.parse(localStorage.getItem("leetcode-patterns-solved-dates")!)).toEqual({ "0": "2025-06-01T00:00:00.000Z" });
    });
  });

  describe("legacy V1 migration", () => {
    it("migrates old checked array to new completed format via slug matching", () => {
      // Old V1 format: boolean array indexed by old 0-based id
      // two-sum was at legacy index 147, add-two-numbers at 57
      const checked = new Array(175).fill(false);
      checked[147] = true; // two-sum
      checked[57] = true;  // add-two-numbers
      localStorage.setItem("checked", JSON.stringify(checked));
      render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

      // Both questions should be checked
      const checkboxes = screen.getAllByRole("checkbox", { name: /^Mark .+ as (complete|incomplete)$/ });
      expect(checkboxes[0]).toBeChecked(); // Two Sum (id: 0)
      expect(checkboxes[1]).toBeChecked(); // Add Two Numbers (id: 1)
      expect(checkboxes[2]).not.toBeChecked(); // Median of Two Sorted Arrays

      // Old keys should be cleaned up
      expect(localStorage.getItem("checked")).toBeNull();

      // New format should be saved
      const stored = JSON.parse(localStorage.getItem("leetcode-patterns-completed")!);
      expect(stored.sort()).toEqual([0, 1]);
    });

    it("shows a toast after migration", () => {
      const checked = new Array(175).fill(false);
      checked[147] = true;
      localStorage.setItem("checked", JSON.stringify(checked));
      render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
      expect(screen.getByText(/Migrated 1 completed question from V1/)).toBeInTheDocument();
    });

    it("only runs migration once", () => {
      const checked = new Array(175).fill(false);
      checked[147] = true;
      localStorage.setItem("checked", JSON.stringify(checked));
      const { unmount } = render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
      unmount();

      // Re-add old key to simulate a second visit
      localStorage.setItem("checked", JSON.stringify(checked));
      render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

      // Migration flag prevents re-running - old key should still be there
      expect(localStorage.getItem("checked")).not.toBeNull();
    });

    it("merges legacy progress with existing new progress", () => {
      localStorage.setItem("leetcode-patterns-completed", JSON.stringify([2]));
      const checked = new Array(175).fill(false);
      checked[147] = true; // two-sum -> id 0
      localStorage.setItem("checked", JSON.stringify(checked));
      render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);

      const stored = JSON.parse(localStorage.getItem("leetcode-patterns-completed")!);
      expect(stored.sort()).toEqual([0, 2]);
    });

    it("does nothing when no legacy data exists", () => {
      render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
      expect(screen.queryByText(/Migrated/)).not.toBeInTheDocument();
      expect(screen.getByText("0/3 completed (0%)")).toBeInTheDocument();
    });

    it("cleans up old showPatterns and hidePatterns keys", () => {
      const checked = new Array(175).fill(false);
      checked[147] = true;
      localStorage.setItem("checked", JSON.stringify(checked));
      localStorage.setItem("showPatterns", JSON.stringify([true]));
      localStorage.setItem("hidePatterns", JSON.stringify([false]));
      render(<QuestionsTable data={testData} updatedDate="2025-01-01" />);
      expect(localStorage.getItem("showPatterns")).toBeNull();
      expect(localStorage.getItem("hidePatterns")).toBeNull();
    });
  });
});


================================================
FILE: src/components/questions/QuestionsTable.tsx
================================================
"use client";

import { useState, useMemo, useCallback, useEffect, useRef, useSyncExternalStore } from "react";
import { useSearchParams } from "next/navigation";
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  createColumnHelper,
  flexRender,
  type SortingState,
  type ColumnFiltersState,
} from "@tanstack/react-table";
import { Question } from "@/types/question";
import { ExternalLink, Star, Check } from "lucide-react";
import { trackEvent } from "@/lib/analytics";
import { loadCompleted, saveCompleted, loadStarred, saveStarred, loadNotes, saveNotes, loadSolvedDates, saveSolvedDates, loadShuffleOrder, saveShuffleOrder, migrateLegacyProgress, loadReminders, saveReminders } from "@/lib/storage";
import { useAuth } from "@/components/layout/AuthContext";
import { type Reminder, isDue, setCustomDate as setCustomReviewDate } from "@/lib/reminders";
import ProgressBar, { type ProgressStats } from "./ProgressBar";
import FilterToolbar from "./FilterToolbar";
import NoteModal, { type EditingNote } from "./NoteModal";
import ConfirmModal from "./ConfirmModal";
import GroupHeaderRow from "./GroupHeaderRow";
import QuestionRow from "./QuestionRow";
import ReviewDateModal, { type ReviewDateTarget } from "./ReviewDateModal";

const columnHelper = createColumnHelper<Question>();

const difficultyColor: Record<string, string> = {
  Easy: "text-green-700 dark:text-green-400",
  Medium: "text-yellow-700 dark:text-yellow-400",
  Hard: "text-red-700 dark:text-red-400",
};

const difficultyPill: Record<string, string> = {
  Easy: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400",
  Medium: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-400",
  Hard: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400",
};

const makeColumns = (
  completed: Set<number>,
  toggleCompleted: (id: number) => void,
  starred: Set<number>,
  toggleStarred: (id: number) => void,
  notes: Record<number, string>,
  openNoteModal: (id: number, title: string) => void,
  hidePatterns: boolean,
  companyFilter: string[],
  updatedDate: string,
  solvedDates: Record<number, string>,
  reminders: Record<number, Reminder>,
  openReviewModal: (id: number, title: string) => void,
  completeReminder: (id: number) => void
) => [
  columnHelper.display({
    id: "completed",
    header: () => <Check className="h-4 w-4" />,
    size: 40,
    cell: (info) => (
      <input
        type="checkbox"
        checked={completed.has(info.row.original.id)}
        onChange={() => toggleCompleted(info.row.original.id)}
        className="h-4 w-4 pointer-events-none accent-blue-600"
        aria-label={`Mark ${info.row.original.title} as ${completed.has(info.row.original.id) ? "incomplete" : "complete"}`}
      />
    ),
    meta: { clickable: true },
  }),
  columnHelper.display({
    id: "starred",
    header: "★",
    size: 40,
    cell: (info) => (
      <span
        role="checkbox"
        aria-checked={starred.has(info.row.original.id)}
        aria-label={`Star ${info.row.original.title}`}
      >
        <Star
          className={`h-4 w-4 pointer-events-none ${
            starred.has(info.row.original.id)
              ? "fill-amber-400 text-amber-400"
              : "text-zinc-300 dark:text-zinc-600"
          }`}
        />
      </span>
    ),
    meta: { clickable: true, toggleFn: "starred" },
    enableSorting: false,
  }),
  columnHelper.accessor("title", {
    header: "Title",
    cell: (info) => (
      <a
        href={`https://leetcode.com/problems/${info.row.original.slug}/`}
        target="_blank"
        rel="noopener noreferrer"
        className="text-blue-600 hover:underline dark:text-blue-400"
      >
        {info.getValue()}
        {info.row.original.premium && (
          <span className="ml-1 text-xs text-amber-500">🔒</span>
        )}
      </a>
    ),
  }),
  columnHelper.display({
    id: "solutions",
    header: "Solutions",
    size: 75,
    cell: (info) => (
      <a
        href={`https://leetcode.com/problems/${info.row.original.slug}/solutions/`}
        target="_blank"
        rel="noopener noreferrer"
        title="View solutions"
        aria-label={`View solutions for ${info.row.original.title}`}
        className="inline-flex items-center text-zinc-400 hover:text-blue-600 dark:text-zinc-500 dark:hover:text-blue-400"
      >
        <ExternalLink className="h-4 w-4" />
      </a>
    ),
    enableSorting: false,
    meta: { hideOnMobile: true },
  }),
  columnHelper.accessor("difficulty", {
    header: "Difficulty",
    cell: (info) => (
      <span className={`inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold ${difficultyPill[info.getValue()]} ${completed.has(info.row.original.id) ? "line-through opacity-60" : ""}`}>
        {info.getValue()}
      </span>
    ),
    filterFn: (row, _columnId, filterValue: string[]) => {
      if (!filterValue || filterValue.length === 0) return true;
      return filterValue.includes(row.original.difficulty);
    },
    meta: { hideOnMobile: true },
  }),
  columnHelper.accessor("pattern", {
    header: "Pattern(s)",
    cell: (info) => (
      <div className="flex flex-wrap gap-1">
        {info.getValue().map((p) => (
          <span
            key={p}
            className="whitespace-nowrap rounded-full bg-zinc-100 px-2 py-0.5 text-xs dark:bg-zinc-800"
          >
            {hidePatterns ? "•".repeat(p.length) : p}
          </span>
        ))}
      </div>
    ),
    filterFn: (row, _columnId, filterValue: string[]) => {
      if (!filterValue || filterValue.length === 0) return true;
      return row.original.pattern.some((p) =>
        filterValue.some((f) => p.toLowerCase() === f.toLowerCase())
      );
    },
  }),
  columnHelper.accessor("companies", {
    header: () => (
      <div>
        <span>Companies</span>
        <div className="text-[10px] font-normal text-zinc-400 dark:text-zinc-500">
          0–6 months, via{" "}
          <a href="https://leetcode.com/subscribe/" target="_blank" rel="noopener noreferrer" className="underline decoration-dotted">
            LC Premium
          </a>
          {", "}
          {new Date(updatedDate).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
        </div>
      </div>
    ),
    meta: { hideOnMobile: true },
    cell: (info) => (
      <div className="flex w-[156px] flex-wrap gap-1">
        {info.getValue().map((c) => (
          <span key={c.slug} className="group/icon relative">
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/icons/${c.slug}.png`}
              alt={c.name}
              className="h-5 w-5 rounded-sm object-contain bg-white/0 p-px dark:bg-white/90 dark:rounded"
              onError={(e) => {
                const img = e.target as HTMLImageElement;
                const fallback = `https://www.google.com/s2/favicons?sz=64&domain_url=https://${c.slug}.com`;
                if (!img.dataset.triedFallback) {
                  img.dataset.triedFallback = "1";
                  img.src = fallback;
                } else {
                  img.style.display = "none";
                  img.nextElementSibling?.classList.remove("hidden");
                }
              }}
            />
            <span className="hidden rounded-full bg-zinc-100 px-2 py-0.5 text-xs dark:bg-zinc-800">
              {c.name}
            </span>
            <span className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded bg-zinc-800 px-2 py-1 text-xs text-white opacity-0 shadow transition-opacity group-hover/icon:opacity-100 dark:bg-zinc-200 dark:text-zinc-900">
              {c.name} - asked {c.frequency} {c.frequency === 1 ? "time" : "times"} in the last 6 months
            </span>
          </span>
        ))}
      </div>
    ),
    enableSorting: companyFilter.length === 1,
    sortingFn: (rowA, rowB) => {
      const slug = companyFilter[0];
      const freqA = rowA.original.companies.find((c) => c.slug === slug)?.frequency ?? 0;
      const freqB = rowB.original.companies.find((c) => c.slug === slug)?.frequency ?? 0;
      return freqA - freqB;
    },
    filterFn: (row, _columnId, filterValue: string[]) => {
      if (!filterValue || filterValue.length === 0) return true;
      return row.original.companies.some(
        (c) => filterValue.includes(c.slug)
      );
    },
  }),
  columnHelper.display({
    id: "notes",
    header: "Notes",
    size: 100,
    meta: { hideOnMobile: true, noStrikethrough: true },
    cell: (info) => {
      const note = notes[info.row.original.id];
      return (
        <button
          onClick={() =>
            openNoteModal(info.row.original.id, info.row.original.title)
          }
          title={note || undefined}
          aria-label={`${note ? "Edit" : "Add"} note for ${info.row.original.title}`}
          className="block max-w-[100px] cursor-pointer truncate text-left text-sm text-zinc-400 hover:text-zinc-600 dark:text-zinc-600 dark:hover:text-zinc-400"
        >
          {note || <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>}
        </button>
      );
    },
    enableSorting: false,
  }),
  columnHelper.display({
    id: "review",
    header: "Review",
    size: 160,
    meta: { hideOnMobile: true, noStrikethrough: true },
    cell: (info) => {
      const solvedDate = solvedDates[info.row.original.id];
      const reminder = reminders[info.row.original.id];
      if (!solvedDate && !reminder) return <span className="text-zinc-300 dark:text-zinc-700">—</span>;
      const dateFmt = { month: "short" as const, day: "numeric" as const, year: "numeric" as const };
      return (
        <div className="flex flex-col items-start gap-0.5">
          {solvedDate && (
            <span
              className="whitespace-nowrap rounded-full px-2 py-0.5 text-xs font-semibold ring-1 ring-inset bg-zinc-100 text-zinc-600 ring-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:ring-zinc-700"
              title={new Date(solvedDate).toLocaleDateString("en-US", dateFmt)}
            >
              Solved {relativeDate(solvedDate, "past")}
            </span>
          )}
          {reminder ? (
            <span className={`inline-flex items-center whitespace-nowrap rounded-full text-xs font-semibold ring-1 ring-inset ${reviewPillStyle(reminder.nextReview)}`}>
              <button
                className="inline-flex items-center gap-1 cursor-pointer pl-2 pr-1.5 py-0.5 rounded-l-full transition-opacity hover:opacity-80"
                title={`${new Date(reminder.nextReview + "T00:00:00").toLocaleDateString("en-US", dateFmt)} · Click to change`}
                onClick={(e) => { e.stopPropagation(); openReviewModal(info.row.original.id, info.row.original.title); }}
              >
                {relativeDate(reminder.nextReview, "future")}
              </button>
              <span className="w-px self-stretch bg-current opacity-20" />
              <button
                className="inline-flex items-center justify-center cursor-pointer px-1.5 py-0.5 rounded-r-full transition-opacity hover:opacity-80"
                title="Mark review as completed"
                onClick={(e) => { e.stopPropagation(); completeReminder(info.row.original.id); }}
              >
                <Check className="h-3 w-3" strokeWidth={2.5} />
              </button>
            </span>
          ) : solvedDate && (
            <button
              className="inline-flex items-center gap-1 whitespace-nowrap cursor-pointer rounded-full px-2 py-0.5 text-xs ring-1 ring-inset ring-dashed transition-colors text-zinc-400 ring-zinc-300 hover:text-zinc-600 hover:ring-zinc-400 dark:text-zinc-500 dark:ring-zinc-600 dark:hover:text-zinc-400 dark:hover:ring-zinc-500"
              title="Set a review date"
              onClick={(e) => { e.stopPropagation(); openReviewModal(info.row.original.id, info.row.original.title); }}
            >
              + Set review
            </button>
          )}
        </div>
      );
    },
    enableSorting: false,
  }),
];

function daysDiff(isoDate: string): number {
  const todayStr = new Date().toISOString().slice(0, 10);
  const target = isoDate.slice(0, 10);
  const todayMs = new Date(todayStr + "T00:00:00Z").getTime();
  const targetMs = new Date(target + "T00:00:00Z").getTime();
  return Math.round((targetMs - todayMs) / 86_400_000);
}

function reviewPillStyle(isoDate: string): string {
  const diff = daysDiff(isoDate);
  if (diff < 0) return "bg-red-100 text-red-700 ring-red-200 hover:bg-red-200 dark:bg-red-900/40 dark:text-red-400 dark:ring-red-800 dark:hover:bg-red-900/60";
  if (diff === 0) return "bg-orange-100 text-orange-700 ring-orange-200 hover:bg-orange-200 dark:bg-orange-900/40 dark:text-orange-400 dark:ring-orange-800 dark:hover:bg-orange-900/60";
  if (diff === 1) return "bg-amber-100 text-amber-700 ring-amber-200 hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-400 dark:ring-amber-800 dark:hover:bg-amber-900/60";
  if (diff <= 3) return "bg-yellow-100 text-yellow-700 ring-yellow-200 hover:bg-yellow-200 dark:bg-yellow-900/40 dark:text-yellow-400 dark:ring-yellow-800 dark:hover:bg-yellow-900/60";
  if (diff <= 7) return "bg-lime-100 text-lime-700 ring-lime-200 hover:bg-lime-200 dark:bg-lime-900/40 dark:text-lime-400 dark:ring-lime-800 dark:hover:bg-lime-900/60";
  if (diff <= 14) return "bg-emerald-100 text-emerald-700 ring-emerald-200 hover:bg-emerald-200 dark:bg-emerald-900/40 dark:text-emerald-400 dark:ring-emerald-800 dark:hover:bg-emerald-900/60";
  if (diff <= 30) return "bg-cyan-100 text-cyan-700 ring-cyan-200 hover:bg-cyan-200 dark:bg-cyan-900/40 dark:text-cyan-400 dark:ring-cyan-800 dark:hover:bg-cyan-900/60";
  return "bg-zinc-100 text-zinc-600 ring-zinc-200 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:ring-zinc-700 dark:hover:bg-zinc-700";
}

function relativeDate(isoDate: string, mode: "past" | "future"): string {
  const diffDays = daysDiff(isoDate);
  if (mode === "past") {
    const ago = -diffDays;
    if (ago <= 0) return "today";
    if (ago === 1) return "yesterday";
    return `${ago}d ago`;
  }
  if (diffDays < 0) return `Overdue ${-diffDays}d`;
  if (diffDays === 0) return "Due today";
  if (diffDays === 1) return "Review tomorrow";
  return `Review in ${diffDays}d`;
}

const mobileQuery = "(max-width: 639px)";

function subscribeMobile(callback: () => void) {
  const mq = window.matchMedia(mobileQuery);
  mq.addEventListener("change", callback);
  return () => mq.removeEventListener("change", callback);
}

function getMobileSnapshot() {
  return window.matchMedia(mobileQuery).matches;
}

function getMobileServerSnapshot() {
  return false;
}

function useIsMobile() {
  return useSyncExternalStore(subscribeMobile, getMobileSnapshot, getMobileServerSnapshot);
}

function parseInitialFilters(searchParams: URLSearchParams) {
  const filters: ColumnFiltersState = [];
  const difficulty = searchParams.get("difficulty");
  if (difficulty) filters.push({ id: "difficulty", value: difficulty.split(",") });
  const pattern = searchParams.get("pattern");
  if (pattern) filters.push({ id: "pattern", value: pattern.split(",") });
  const companies = searchParams.get("companies");
  if (companies) filters.push({ id: "companies", value: companies.split(",") });
  return filters;
}

export default function QuestionsTable({ data, updatedDate }: { data: Question[]; updatedDate: string }) {
  const isMobile = useIsMobile();
  const searchParams = useSearchParams();
  const { syncNow, syncVersion } = useAuth();

  const [sorting, setSorting] = useState<SortingState>([]);
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(() =>
    parseInitialFilters(searchParams)
  );
  const [globalFilter, setGlobalFilter] = useState(
    () => searchParams.get("q") ?? ""
  );
  const [completed, setCompleted] = useState<Set<number>>(new Set());
  const [starred, setStarred] = useState<Set<number>>(new Set());
  const [shuffleOrder, setShuffleOrder] = useState<number[] | null>(null);
  const [notes, setNotes] = useState<Record<number, string>>({});
  const [solvedDates, setSolvedDates] = useState<Record<number, string>>({});
  const [reminders, setReminders] = useState<Record<number, Reminder>>({});
  const [migrationToast, setMigrationToast] = useState<string | null>(null);
  const [toastFading, setToastFading] = useState(false);
  const [hydrated, setHydrated] = useState(false);

  useEffect(() => {
    const migrated = migrateLegacyProgress(data);
    setCompleted(migrated ?? loadCompleted());
    if (migrated) {
      setMigrationToast(`Migrated ${migrated.size} completed question${migrated.size === 1 ? "" : "s"} from V1`);
    }
    setStarred(loadStarred());
    setShuffleOrder(loadShuffleOrder());
    setNotes(loadNotes());
    setSolvedDates(loadSolvedDates());
    setReminders(loadReminders());
    setHydrated(true);
  }, [data]);

  // Reload from localStorage when remote sync arrives
  useEffect(() => {
    if (!hydrated || syncVersion === 0) return;
    setCompleted(loadCompleted());
    setStarred(loadStarred());
    setNotes(loadNotes());
    setSolvedDates(loadSolvedDates());
    setReminders(loadReminders());
  }, [syncVersion, hydrated]);

  useEffect(() => {
    if (!migrationToast) return;
    const fadeTimer = setTimeout(() => setToastFading(true), 2500);
    const removeTimer = setTimeout(() => { setMigrationToast(null); setToastFading(false); }, 3200);
    return () => { clearTimeout(fadeTimer); clearTimeout(removeTimer); };
  }, [migrationToast]);

  useEffect(() => {
    const params = new URLSearchParams();
    if (globalFilter) params.set("q", globalFilter);
    const difficulty = columnFilters.find((f) => f.id === "difficulty")?.value as string[] | undefined;
    if (difficulty?.length) params.set("difficulty", difficulty.join(","));
    const pattern = columnFilters.find((f) => f.id === "pattern")?.value as string[] | undefined;
    if (pattern?.length) params.set("pattern", pattern.join(","));
    const companies = columnFilters.find((f) => f.id === "companies")?.value as string[] | undefined;
    if (companies?.length) params.set("companies", companies.join(","));
    const qs = params.toString();
    window.history.replaceState(null, "", qs ? `?${qs}` : window.location.pathname);
  }, [globalFilter, columnFilters]);

  const toggleCompleted = useCallback((id: number) => {
    let completing = false;
    setCompleted((prev) => {
      const next = new Set(prev);
      completing = !next.has(id);
      if (completing) next.add(id);
      else next.delete(id);
      saveCompleted(next);
      trackEvent("question_toggle", { question_id: id, completed: completing });
      return next;
    });
    setSolvedDates((prev) => {
      const next = { ...prev };
      next[id] = new Date().toISOString();
      saveSolvedDates(next);
      return next;
    });
    if (!completing) {
      setReminders((prev) => {
        const next = { ...prev };
        delete next[id];
        saveReminders(next);
        return next;
      });
    }
    syncNow();
  }, [syncNow]);

  const [reviewTarget, setReviewTarget] = useState<ReviewDateTarget | null>(null);

  const openReviewModal = useCallback((id: number, title: string) => {
    setReviewTarget({ id, title });
  }, []);

  const onReviewDateChange = useCallback((id: number, date: string) => {
    setReminders((prev) => {
      const existing = prev[id];
      const updated = existing
        ? setCustomReviewDate(existing, date)
        : { nextReview: date.slice(0, 10), interval: 1 };
      const next = { ...prev, [id]: updated };
      saveReminders(next);
      trackEvent("custom_review_date", { question_id: id });
      return next;
    });
    setReviewTarget(null);
    syncNow();
  }, [syncNow]);

  const onReviewDateClear = useCallback((id: number) => {
    setReminders((prev) => {
      const next = { ...prev };
      delete next[id];
      saveReminders(next);
      trackEvent("clear_review_date", { question_id: id });
      return next;
    });
    setReviewTarget(null);
    syncNow();
  }, [syncNow]);

  const toggleStarred = useCallback((id: number) => {
    setStarred((prev) => {
      const next = new Set(prev);
      const starring = !next.has(id);
      if (starring) next.add(id);
      else next.delete(id);
      saveStarred(next);
      trackEvent("star_toggle", { question_id: id, starred: starring });
      return next;
    });
    syncNow();
  }, [syncNow]);

  const patterns = useMemo(() => {
    const set = new Set<string>();
    data.forEach((q) => q.pattern.forEach((p) => set.add(p)));
    return Array.from(set).sort();
  }, [data]);

  const companies = useMemo(() => {
    const map = new Map<string, string>();
    data.forEach((q) =>
      q.companies.forEach((c) => {
        if (!map.has(c.slug)) map.set(c.slug, c.name);
      })
    );
    return Array.from(map.entries())
      .sort(([, a], [, b]) => a.localeCompare(b));
  }, [data]);

  const updateNote = useCallback((id: number, value: string) => {
    setNotes((prev) => {
      const next = { ...prev };
      if (value) next[id] = value;
      else delete next[id];
      saveNotes(next);
      trackEvent("note_save", { question_id: id, has_content: !!value });
      return next;
    });
    syncNow();
  }, [syncNow]);

  const [editingNote, setEditingNote] = useState<EditingNote | null>(null);

  const openNoteModal = useCallback((id: number, title: string) => {
    setEditingNote({ id, title, draft: notes[id] ?? "", confirmDiscard: false });
  }, [notes]);

  const [hideCompleted, setHideCompleted] = useState(false);
  const [showStarredOnly, setShowStarredOnly] = useState(false);
  const [hidePatterns, setHidePatterns] = useState(false);
  const [showDueOnly, setShowDueOnly] = useState(false);

  useEffect(() => {
    setHideCompleted(localStorage.getItem("leetcode-patterns-hide-completed") === "true");
    setShowStarredOnly(localStorage.getItem("leetcode-patterns-starred-only") === "true");
    setHidePatterns(localStorage.getItem("leetcode-patterns-hide-patterns") === "true");
    setShowDueOnly(localStorage.getItem("leetcode-patterns-due-only") === "true");
  }, []);

  useEffect(() => { if (hydrated) localStorage.setItem("leetcode-patterns-hide-completed", String(hideCompleted)); }, [hideCompleted, hydrated]);
  useEffect(() => { if (hydrated) localStorage.setItem("leetcode-patterns-starred-only", String(showStarredOnly)); }, [showStarredOnly, hydrated]);
  useEffect(() => { if (hydrated) localStorage.setItem("leetcode-patterns-hide-patterns", String(hidePatterns)); }, [hidePatterns, hydrated]);
  useEffect(() => { if (hydrated) localStorage.setItem("leetcode-patterns-due-only", String(showDueOnly)); }, [showDueOnly, hydrated]);

  const activeCompanyFilter = useMemo(
    () => (columnFilters.find((f) => f.id === "companies")?.value as string[]) ?? [],
    [columnFilters]
  );

  const columns = useMemo(
    () => makeColumns(completed, toggleCompleted, starred, toggleStarred, notes, openNoteModal, hidePatterns, activeCompanyFilter, updatedDate, solvedDates, reminders, openReviewModal, onReviewDateClear),
    [completed, toggleCompleted, starred, toggleStarred, notes, openNoteModal, hidePatterns, activeCompanyFilter, updatedDate, solvedDates, reminders, openReviewModal, onReviewDateClear]
  );

  useEffect(() => {
    if (activeCompanyFilter.length === 1) {
      setSorting([{ id: "companies", desc: true }]);
    } else {
      setSorting((prev) =>
        prev.some((s) => s.id === "companies")
          ? prev.filter((s) => s.id !== "companies")
          : prev
      );
    }
  }, [activeCompanyFilter]);

  const filteredData = useMemo(() => {
    let result = data;
    if (hideCompleted) result = result.filter((q) => !completed.has(q.id));
    if (showStarredOnly) result = result.filter((q) => starred.has(q.id));
    if (showDueOnly) result = result.filter((q) => reminders[q.id] && isDue(reminders[q.id]));
    return result;
  }, [data, completed, starred, hideCompleted, showStarredOnly, showDueOnly, reminders]);

  const columnVisibility = useMemo(() => {
    if (!isMobile) return {};
    const vis: Record<string, boolean> = {};
    columns.forEach((col) => {
      const id = "accessorKey" in col ? (col.accessorKey as string) : (col as { id?: string }).id;
      if (id && (col.meta as { hideOnMobile?: boolean })?.hideOnMobile) {
        vis[id] = false;
      }
    });
    return vis;
  }, [isMobile, columns]);

  // eslint-disable-next-line react-hooks/incompatible-library
  const table = useReactTable({
    data: filteredData,
    columns,
    state: { sorting, columnFilters, globalFilter, columnVisibility },
    globalFilterFn: (row, _columnId, filterValue: string) => {
      const q = row.original;
      const search = filterValue.toLowerCase();
      return (
        q.title.toLowerCase().includes(search) ||
        q.difficulty.toLowerCase().includes(search) ||
        q.pattern.some((p) => p.toLowerCase().includes(search)) ||
        q.companies.some((c) => c.name.toLowerCase().includes(search))
      );
    },
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onGlobalFilterChange: setGlobalFilter,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
  });

  const stats = useMemo<ProgressStats>(() => {
    const totals = { Easy: 0, Medium: 0, Hard: 0 };
    const done = { Easy: 0, Medium: 0, Hard: 0 };
    const unfilteredData = showStarredOnly ? data.filter((q) => starred.has(q.id)) : data;
    const filteredRows = table.getFilteredRowModel().rows;
    const visibleIds = new Set(filteredRows.map((r) => r.original.id));
    const baseRows = unfilteredData.filter(
      (q) => visibleIds.has(q.id) || (hideCompleted && completed.has(q.id) && (() => {
        const patternFilter = columnFilters.find((f) => f.id === "pattern");
        const diffFilter = columnFilters.find((f) => f.id === "difficulty");
        const companyFilter = columnFilters.find((f) => f.id === "companies");
        const patternOk = !patternFilter || q.pattern.some((p) =>
          (patternFilter.value as string[]).some((f) => p.toLowerCase() === f.toLowerCase())
        );
        const diffOk = !diffFilter || (diffFilter.value as string[]).includes(q.difficulty);
        const companyOk = !companyFilter || q.companies.some((c) =>
          (companyFilter.value as string[]).includes(c.slug)
        );
        const searchOk = !globalFilter || q.title.toLowerCase().includes(globalFilter.toLowerCase()) ||
          q.difficulty.toLowerCase().includes(globalFilter.toLowerCase()) ||
          q.pattern.some((p) => p.toLowerCase().includes(globalFilter.toLowerCase())) ||
          q.companies.some((c) => c.name.toLowerCase().includes(globalFilter.toLowerCase()));
        return patternOk && diffOk && companyOk && searchOk;
      })())
    );
    baseRows.forEach((q) => {
      totals[q.difficulty]++;
      if (completed.has(q.id)) done[q.difficulty]++;
    });
    const total = baseRows.length;
    const totalDone = done.Easy + done.Medium + done.Hard;
    return { totals, done, total, totalDone };
  }, [table, data, completed, starred, columnFilters, globalFilter, hideCompleted, showStarredOnly]);

  const pickRandom = useCallback(() => {
    const unsolved = table
      .getFilteredRowModel()
      .rows.filter((r) => !completed.has(r.original.id));
    if (unsolved.length === 0) return;
    const pick = unsolved[Math.floor(Math.random() * unsolved.length)];
    trackEvent("random_question", { question_id: pick.original.id, slug: pick.original.slug });
    window.open(
      `https://leetcode.com/problems/${pick.original.slug}/description/`,
      "_blank"
    );
  }, [table, completed]);

  const toggleShuffle = useCallback(() => {
    if (shuffleOrder) {
      setShuffleOrder(null);
      saveShuffleOrder(null);
      trackEvent("restore_order");
    } else {
      const rows = table.getFilteredRowModel().rows;
      const ids = rows.map((r) => r.original.id);
      for (let i = ids.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [ids[i], ids[j]] = [ids[j], ids[i]];
      }
      setShuffleOrder(ids);
      saveShuffleOrder(ids);
      trackEvent("shuffle_questions");
    }
  }, [table, shuffleOrder]);

  const [resetConfirmGroup, setResetConfirmGroup] = useState<string | null>(null);
  const [clearConfirm, setClearConfirm] = useState<"notes" | "questions" | "starred" | "reminders" | null>(null);
  const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());

  const toggleGroup = useCallback((group: string) => {
    setCollapsedGroups((prev) => {
      const next = new Set(prev);
      if (next.has(group)) next.delete(group);
      else next.add(group);
      return next;
    });
  }, []);

  const resetGroupProgress = useCallback((difficulty: string) => {
    const ids = data.filter((q) => q.difficulty === difficulty).map((q) => q.id);
    setCompleted((prev) => {
      const next = new Set(prev);
      ids.forEach((id) => next.delete(id));
      saveCompleted(next);
      return next;
    });
    setNotes((prev) => {
      const next = { ...prev };
      ids.forEach((id) => delete next[id]);
      saveNotes(next);
      return next;
    });
    setSolvedDates((prev) => {
      const next = { ...prev };
      ids.forEach((id) => delete next[id]);
      saveSolvedDates(next);
      return next;
    });
    setReminders((prev) => {
      const next = { ...prev };
      ids.forEach((id) => delete next[id]);
      saveReminders(next);
      return next;
    });
    trackEvent("reset_group", { difficulty });
    setResetConfirmGroup(null);
    syncNow();
  }, [data, syncNow]);

  const clearAllNotes = useCallback(() => {
    setNotes({});
    saveNotes({});
    trackEvent("clear_all_notes");
    setClearConfirm(null);
    syncNow();
  }, [syncNow]);

  const clearAllQuestions = useCallback(() => {
    setCompleted(new Set());
    saveCompleted(new Set());
    setSolvedDates({});
    saveSolvedDates({});
    setReminders({});
    saveReminders({});
    trackEvent("clear_all_progress");
    setClearConfirm(null);
    syncNow();
  }, [syncNow]);

  const clearAllStarred = useCallback(() => {
    setStarred(new Set());
    saveStarred(new Set());
    trackEvent("clear_all_starred");
    setClearConfirm(null);
    syncNow();
  }, [syncNow]);

  const clearAllReminders = useCallback(() => {
    setReminders({});
    saveReminders({});
    trackEvent("clear_all_reminders");
    setClearConfirm(null);
    syncNow();
  }, [syncNow]);

  const exportProgress = useCallback(() => {
    const payload = { completed: [...completed], starred: [...starred], notes, solvedDates, reminders };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `leetcode-patterns-progress-${new Date().toISOString().slice(0, 10)}.json`;
    a.click();
    URL.revokeObjectURL(url);
    trackEvent("export_progress", { completed_count: completed.size, notes_count: Object.keys(notes).length });
  }, [completed, starred, notes, solvedDates, reminders]);

  const fileInputRef = useRef<HTMLInputElement>(null);

  const importProgress = useCallback((file: File) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const parsed = JSON.parse(e.target?.result as string);
        if (Array.isArray(parsed.completed)) {
          const imported = new Set<number>(parsed.completed);
          setCompleted(imported);
          saveCompleted(imported);
        }
        if (Array.isArray(parsed.starred)) {
          const imported = new Set<number>(parsed.starred);
          setStarred(imported);
          saveStarred(imported);
        }
        if (parsed.notes && typeof parsed.notes === "object") {
          setNotes(parsed.notes);
          saveNotes(parsed.notes);
        }
        if (parsed.solvedDates && typeof parsed.solvedDates === "object") {
          setSolvedDates(parsed.solvedDates);
          saveSolvedDates(parsed.solvedDates);
        }
        if (parsed.reminders && typeof parsed.reminders === "object") {
          setReminders(parsed.reminders);
          saveReminders(parsed.reminders);
        }
        trackEvent("import_progress", { completed_count: parsed.completed?.length ?? 0, notes_count: parsed.notes ? Object.keys(parsed.notes).length : 0 });
        syncNow();
      } catch {}
    };
    reader.readAsText(file);
  }, [syncNow]);

  const search
Download .txt
gitextract_d7fndcjj/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── feature_request.yml
│   │   └── question_request.yml
│   └── workflows/
│       ├── ci.yml
│       ├── deploy.yml
│       └── update-questions.yml
├── .gitignore
├── .husky/
│   └── pre-push
├── .npmrc
├── LICENSE
├── README.md
├── cron/
│   ├── leetcode/
│   │   ├── __init__.py
│   │   ├── auth.py
│   │   ├── models.py
│   │   └── rest.py
│   └── update_questions.py
├── eslint.config.mjs
├── next.config.ts
├── package.json
├── postcss.config.mjs
├── public/
│   ├── .nojekyll
│   ├── manifest.json
│   ├── robots.txt
│   └── sw.js
├── scripts/
│   └── generate-sw-precache.mjs
├── src/
│   ├── app/
│   │   ├── globals.css
│   │   ├── layout.tsx
│   │   ├── not-found.tsx
│   │   └── page.tsx
│   ├── components/
│   │   ├── layout/
│   │   │   ├── AuthContext.test.tsx
│   │   │   ├── AuthContext.tsx
│   │   │   ├── GitHubLink.tsx
│   │   │   ├── Logo.tsx
│   │   │   ├── ServiceWorkerRegistrar.tsx
│   │   │   ├── ThemeToggle.test.tsx
│   │   │   ├── ThemeToggle.tsx
│   │   │   ├── UserMenu.test.tsx
│   │   │   ├── UserMenu.tsx
│   │   │   └── ViewSwitcher.tsx
│   │   ├── panels/
│   │   │   ├── AboutPanel.tsx
│   │   │   ├── AcknowledgementsPanel.tsx
│   │   │   ├── TipsPanel.tsx
│   │   │   └── panels.test.tsx
│   │   ├── questions/
│   │   │   ├── ConfirmModal.tsx
│   │   │   ├── FilterToolbar.tsx
│   │   │   ├── GroupHeaderRow.tsx
│   │   │   ├── NoteModal.tsx
│   │   │   ├── ProgressBar.tsx
│   │   │   ├── QuestionRow.tsx
│   │   │   ├── QuestionsTable.test.tsx
│   │   │   ├── QuestionsTable.tsx
│   │   │   └── ReviewDateModal.tsx
│   │   └── roadmaps/
│   │       ├── RoadmapView.test.tsx
│   │       └── RoadmapView.tsx
│   ├── data/
│   │   ├── questions.json
│   │   └── roadmaps.ts
│   ├── lib/
│   │   ├── analytics.test.ts
│   │   ├── analytics.ts
│   │   ├── register-sw.test.ts
│   │   ├── register-sw.ts
│   │   ├── reminders.test.ts
│   │   ├── reminders.ts
│   │   ├── storage.test.ts
│   │   ├── storage.ts
│   │   ├── supabase.ts
│   │   ├── sw.test.ts
│   │   ├── sync.test.ts
│   │   └── sync.ts
│   ├── test/
│   │   └── setup.ts
│   └── types/
│       └── question.ts
├── tsconfig.json
└── vitest.config.mts
Download .txt
SYMBOL INDEX (140 symbols across 36 files)

FILE: cron/leetcode/models.py
  class Configuration (line 7) | class Configuration:
    method __init__ (line 8) | def __init__(self):
  class GraphqlQuery (line 14) | class GraphqlQuery:
    method __init__ (line 15) | def __init__(self, query=None, variables=None, operation_name=None):
  class GraphqlQueryGetQuestionDetailVariables (line 21) | class GraphqlQueryGetQuestionDetailVariables:
    method __init__ (line 22) | def __init__(self, title_slug=None):
  class GraphqlQuestionDetail (line 26) | class GraphqlQuestionDetail:
    method __init__ (line 27) | def __init__(self, question_id=None, title=None, difficulty=None,
  class GraphqlData (line 37) | class GraphqlData:
    method __init__ (line 38) | def __init__(self, question=None):
  class GraphqlResponse (line 42) | class GraphqlResponse:
    method __init__ (line 43) | def __init__(self, data=None):
  class ApiClient (line 47) | class ApiClient:
    method __init__ (line 48) | def __init__(self, configuration=None):
  class DefaultApi (line 52) | class DefaultApi:
    method __init__ (line 53) | def __init__(self, api_client=None):
    method graphql_post (line 56) | def graphql_post(self, body=None):

FILE: cron/leetcode/rest.py
  class ApiException (line 1) | class ApiException(Exception):
    method __init__ (line 2) | def __init__(self, status=None, reason=None, body=None):
    method __str__ (line 7) | def __str__(self):

FILE: cron/update_questions.py
  function create_leetcode_api (line 9) | def create_leetcode_api():
  function get_question_metadata (line 38) | def get_question_metadata(api, title_slug):
  function construct_company_tag_list (line 69) | def construct_company_tag_list(company_tag_stats_v2):
  function update_question_metadata (line 84) | def update_question_metadata(question, response):
  function read_questions (line 105) | def read_questions(file_name):
  function write_questions (line 119) | def write_questions(file_name, questions):
  function main (line 133) | def main(file_name):

FILE: public/sw.js
  constant CACHE_NAME (line 2) | const CACHE_NAME = "lc-patterns-v2";
  constant PRECACHE_URLS (line 4) | const PRECACHE_URLS = [

FILE: scripts/generate-sw-precache.mjs
  constant OUT_DIR (line 4) | const OUT_DIR = "out";
  constant SW_PATH (line 5) | const SW_PATH = join(OUT_DIR, "sw.js");
  constant PRECACHE_EXTENSIONS (line 7) | const PRECACHE_EXTENSIONS = new Set([
  function collectFiles (line 11) | function collectFiles(dir, base = dir) {

FILE: src/app/layout.tsx
  function RootLayout (line 68) | function RootLayout({

FILE: src/app/not-found.tsx
  function NotFound (line 3) | function NotFound() {

FILE: src/app/page.tsx
  function Home (line 16) | function Home() {

FILE: src/components/layout/AuthContext.test.tsx
  type AuthCallback (line 4) | type AuthCallback = (event: string, session: { user: Record<string, unkn...
  function TestConsumer (line 49) | function TestConsumer() {

FILE: src/components/layout/AuthContext.tsx
  type AuthContextValue (line 9) | interface AuthContextValue {
  function AuthProvider (line 27) | function AuthProvider({ children }: { children: ReactNode }) {
  function useAuth (line 190) | function useAuth() {

FILE: src/components/layout/GitHubLink.tsx
  constant REPO (line 1) | const REPO = "SeanPrashad/leetcode-patterns";
  function getStarCount (line 3) | async function getStarCount(): Promise<number | null> {
  function formatCount (line 16) | function formatCount(n: number): string {
  function GitHubLink (line 21) | async function GitHubLink() {

FILE: src/components/layout/Logo.tsx
  function Logo (line 1) | function Logo() {

FILE: src/components/layout/ServiceWorkerRegistrar.tsx
  function ServiceWorkerRegistrar (line 6) | function ServiceWorkerRegistrar() {

FILE: src/components/layout/ThemeToggle.tsx
  function subscribe (line 7) | function subscribe(callback: () => void) {
  function getSnapshot (line 16) | function getSnapshot() {
  function getServerSnapshot (line 20) | function getServerSnapshot() {
  function ThemeToggle (line 24) | function ThemeToggle() {

FILE: src/components/layout/UserMenu.tsx
  function UserMenu (line 6) | function UserMenu() {

FILE: src/components/layout/ViewSwitcher.tsx
  type View (line 12) | type View = "table" | "beginner" | "experienced";
  constant VIEW_KEY (line 14) | const VIEW_KEY = "leetcode-patterns-view";
  function isValidView (line 22) | function isValidView(v: string | null): v is View {
  function ViewSwitcher (line 26) | function ViewSwitcher({

FILE: src/components/panels/AboutPanel.tsx
  function AboutPanel (line 7) | function AboutPanel() {

FILE: src/components/panels/AcknowledgementsPanel.tsx
  function AcknowledgementsPanel (line 25) | function AcknowledgementsPanel() {

FILE: src/components/panels/TipsPanel.tsx
  function formatApproach (line 8) | function formatApproach(text: string) {
  function TipsPanel (line 59) | function TipsPanel() {

FILE: src/components/questions/ConfirmModal.tsx
  function ConfirmModal (line 1) | function ConfirmModal({

FILE: src/components/questions/FilterToolbar.tsx
  type FilterToolbarProps (line 14) | interface FilterToolbarProps {
  function FilterToolbar (line 43) | function FilterToolbar({

FILE: src/components/questions/GroupHeaderRow.tsx
  type GroupHeaderRowProps (line 16) | interface GroupHeaderRowProps {

FILE: src/components/questions/NoteModal.tsx
  type EditingNote (line 3) | interface EditingNote {
  function NoteModal (line 10) | function NoteModal({

FILE: src/components/questions/ProgressBar.tsx
  type ProgressStats (line 1) | interface ProgressStats {
  function ProgressBar (line 8) | function ProgressBar({ stats, pct }: { stats: ProgressStats; pct: number...

FILE: src/components/questions/QuestionRow.tsx
  type QuestionRowProps (line 11) | interface QuestionRowProps {

FILE: src/components/questions/QuestionsTable.tsx
  function daysDiff (line 300) | function daysDiff(isoDate: string): number {
  function reviewPillStyle (line 308) | function reviewPillStyle(isoDate: string): string {
  function relativeDate (line 320) | function relativeDate(isoDate: string, mode: "past" | "future"): string {
  function subscribeMobile (line 336) | function subscribeMobile(callback: () => void) {
  function getMobileSnapshot (line 342) | function getMobileSnapshot() {
  function getMobileServerSnapshot (line 346) | function getMobileServerSnapshot() {
  function useIsMobile (line 350) | function useIsMobile() {
  function parseInitialFilters (line 354) | function parseInitialFilters(searchParams: URLSearchParams) {
  function QuestionsTable (line 365) | function QuestionsTable({ data, updatedDate }: { data: Question[]; updat...

FILE: src/components/questions/ReviewDateModal.tsx
  constant PRESETS (line 4) | const PRESETS = [
  function addDays (line 12) | function addDays(dateStr: string, days: number): string {
  type ReviewDateTarget (line 18) | interface ReviewDateTarget {
  function ReviewDateModal (line 23) | function ReviewDateModal({

FILE: src/components/roadmaps/RoadmapView.tsx
  function InlineMarkdown (line 20) | function InlineMarkdown({ text }: { text: string }) {
  type Props (line 56) | interface Props {
  function RoadmapView (line 61) | function RoadmapView({ roadmap, questions }: Props) {

FILE: src/data/roadmaps.ts
  type RoadmapQuestion (line 1) | interface RoadmapQuestion {
  type RoadmapPhase (line 6) | interface RoadmapPhase {
  type Roadmap (line 13) | interface Roadmap {

FILE: src/lib/analytics.ts
  function trackEvent (line 3) | function trackEvent(eventName: string, params?: Record<string, string | ...

FILE: src/lib/register-sw.ts
  function registerServiceWorker (line 1) | function registerServiceWorker(basePath = ""): void {

FILE: src/lib/reminders.ts
  type Reminder (line 1) | interface Reminder {
  constant SCHEDULE (line 6) | const SCHEDULE = [1, 3, 7, 14, 30];
  function addDays (line 9) | function addDays(isoDate: string, days: number): string {
  function today (line 16) | function today(): string {
  function initReminder (line 21) | function initReminder(solvedDate: string): Reminder {
  function advanceReminder (line 27) | function advanceReminder(current: Reminder): Reminder | null {
  function isDue (line 38) | function isDue(reminder: Reminder): boolean {
  function setCustomDate (line 43) | function setCustomDate(reminder: Reminder, date: string): Reminder {
  function getDueReminders (line 48) | function getDueReminders(reminders: Record<number, Reminder>): number[] {

FILE: src/lib/storage.ts
  constant MAX_NOTE_LENGTH (line 4) | const MAX_NOTE_LENGTH = 10_000;
  constant STORAGE_KEY (line 6) | const STORAGE_KEY = "leetcode-patterns-completed";
  constant STARRED_KEY (line 7) | const STARRED_KEY = "leetcode-patterns-starred";
  constant NOTES_KEY (line 8) | const NOTES_KEY = "leetcode-patterns-notes";
  constant SHUFFLE_KEY (line 9) | const SHUFFLE_KEY = "leetcode-patterns-shuffle-order";
  constant SOLVED_DATES_KEY (line 10) | const SOLVED_DATES_KEY = "leetcode-patterns-solved-dates";
  constant REMINDERS_KEY (line 11) | const REMINDERS_KEY = "leetcode-patterns-reminders";
  constant LEGACY_SLUGS (line 14) | const LEGACY_SLUGS = ["contains-duplicate","missing-number","find-all-nu...
  function loadJson (line 16) | function loadJson<T>(key: string, fallback: T): T {
  function saveJson (line 25) | function saveJson(key: string, value: unknown): void {
  function loadCompleted (line 29) | function loadCompleted(): Set<number> {
  function saveCompleted (line 33) | function saveCompleted(ids: Set<number>): void {
  function loadStarred (line 37) | function loadStarred(): Set<number> {
  function saveStarred (line 41) | function saveStarred(ids: Set<number>): void {
  function loadNotes (line 45) | function loadNotes(): Record<number, string> {
  function saveNotes (line 49) | function saveNotes(notes: Record<number, string>): void {
  function loadSolvedDates (line 53) | function loadSolvedDates(): Record<number, string> {
  function saveSolvedDates (line 57) | function saveSolvedDates(dates: Record<number, string>): void {
  function loadReminders (line 61) | function loadReminders(): Record<number, Reminder> {
  function saveReminders (line 65) | function saveReminders(reminders: Record<number, Reminder>): void {
  function loadShuffleOrder (line 69) | function loadShuffleOrder(): number[] | null {
  function saveShuffleOrder (line 73) | function saveShuffleOrder(order: number[] | null): void {
  function migrateLegacyProgress (line 78) | function migrateLegacyProgress(data: Question[]): Set<number> | null {

FILE: src/lib/sw.test.ts
  function createMockCache (line 7) | function createMockCache() {
  type MockCache (line 25) | type MockCache = ReturnType<typeof createMockCache>;
  function createMockCaches (line 27) | function createMockCaches() {
  type FetchEvent (line 50) | interface FetchEvent {
  function loadSW (line 56) | function loadSW(mockSelf: Record<string, unknown>) {
  function makeFetchEvent (line 145) | function makeFetchEvent(url: string, overrides: Partial<RequestInit & { ...

FILE: src/lib/sync.ts
  type ProgressPayload (line 11) | interface ProgressPayload {
  function uploadProgress (line 21) | async function uploadProgress(userId: string): Promise<void> {
  function downloadAndMerge (line 36) | async function downloadAndMerge(userId: string): Promise<boolean> {
  function doUpload (line 63) | async function doUpload(userId: string): Promise<void> {
  function scheduleUpload (line 72) | function scheduleUpload(userId: string): void {
  function flushPendingUpload (line 82) | function flushPendingUpload(): void {
  function mergeFromRealtimePayload (line 94) | function mergeFromRealtimePayload(data: Record<string, unknown>): boolean {
  function setsEqual (line 138) | function setsEqual(a: Set<number>, b: Set<number>): boolean {
  function recordsEqual (line 144) | function recordsEqual(a: Record<number, string>, b: Record<number, strin...

FILE: src/types/question.ts
  type Company (line 1) | interface Company {
  type Question (line 7) | interface Question {
  type QuestionsData (line 17) | interface QuestionsData {
Condensed preview — 72 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (577K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 1145,
    "preview": "name: Bug Report\ndescription: Report a bug or issue with Leetcode Patterns\nlabels: [\"bug\"]\nbody:\n  - type: textarea\n    "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 702,
    "preview": "name: Feature Request\ndescription: Suggest a new feature or improvement\nlabels: [\"enhancement\"]\nbody:\n  - type: textarea"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question_request.yml",
    "chars": 1128,
    "preview": "name: Question Request\ndescription: Request a new question to be added to the list\nlabels: [\"question-request\"]\nbody:\n  "
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1814,
    "preview": "name: CI\n\non:\n  pull_request:\n    types: [opened, synchronize, reopened, closed, labeled]\n    branches: [main]\n\npermissi"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "chars": 858,
    "preview": "name: Deploy to GitHub Pages\n\non:\n  push:\n    branches: [main]\n\n  workflow_run:\n    workflows: [Update Questions]\n    ty"
  },
  {
    "path": ".github/workflows/update-questions.yml",
    "chars": 1065,
    "preview": "name: Update Questions\n\non:\n  schedule:\n    # Every Sunday at 8am EST (1pm UTC)\n    - cron: \"0 13 * * 0\"\n  workflow_disp"
  },
  {
    "path": ".gitignore",
    "chars": 502,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": ".husky/pre-push",
    "chars": 31,
    "preview": "npm run lint -- --fix\nnpm test\n"
  },
  {
    "path": ".npmrc",
    "chars": 36,
    "preview": "registry=https://registry.npmjs.org\n"
  },
  {
    "path": "LICENSE",
    "chars": 1324,
    "preview": "Creative Commons Attribution-NonCommercial 4.0 International\n\nCopyright (c) 2026 Sean Prashad\n\nThis work is licensed und"
  },
  {
    "path": "README.md",
    "chars": 4498,
    "preview": "<p align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"public/images/logo-dark.png\" />\n"
  },
  {
    "path": "cron/leetcode/__init__.py",
    "chars": 494,
    "preview": "from leetcode.models import (\n    Configuration,\n    ApiClient,\n    DefaultApi,\n    GraphqlQuery,\n    GraphqlQueryGetQue"
  },
  {
    "path": "cron/leetcode/auth.py",
    "chars": 121,
    "preview": "# Placeholder for leetcode.auth compatibility.\n# Authentication is handled via Configuration.api_key in the main module."
  },
  {
    "path": "cron/leetcode/models.py",
    "chars": 3666,
    "preview": "import json\nimport urllib.request\n\nfrom leetcode.rest import ApiException\n\n\nclass Configuration:\n    def __init__(self):"
  },
  {
    "path": "cron/leetcode/rest.py",
    "chars": 354,
    "preview": "class ApiException(Exception):\n    def __init__(self, status=None, reason=None, body=None):\n        self.status = status"
  },
  {
    "path": "cron/update_questions.py",
    "chars": 4828,
    "preview": "import os\nimport json\nimport leetcode\nimport leetcode.auth\nfrom datetime import datetime\nfrom leetcode.rest import ApiEx"
  },
  {
    "path": "eslint.config.mjs",
    "chars": 465,
    "preview": "import { defineConfig, globalIgnores } from \"eslint/config\";\nimport nextVitals from \"eslint-config-next/core-web-vitals\""
  },
  {
    "path": "next.config.ts",
    "chars": 261,
    "preview": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  reactStrictMode: false,\n  output: \"export\",\n"
  },
  {
    "path": "package.json",
    "chars": 1160,
    "preview": "{\n  \"name\": \"leetcode-patterns-v2\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \""
  },
  {
    "path": "postcss.config.mjs",
    "chars": 94,
    "preview": "const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "public/.nojekyll",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "public/manifest.json",
    "chars": 386,
    "preview": "{\n  \"short_name\": \"leetcode-patterns\",\n  \"name\": \"Leetcode Patterns\",\n  \"description\": \"A curated list of LeetCode quest"
  },
  {
    "path": "public/robots.txt",
    "chars": 23,
    "preview": "User-agent: *\nAllow: /\n"
  },
  {
    "path": "public/sw.js",
    "chars": 1928,
    "preview": "// Service Worker — cache-first for static assets, network-first for navigation\nconst CACHE_NAME = \"lc-patterns-v2\";\n\nco"
  },
  {
    "path": "scripts/generate-sw-precache.mjs",
    "chars": 1206,
    "preview": "import { readFileSync, writeFileSync, readdirSync, statSync } from \"fs\";\nimport { join, relative } from \"path\";\n\nconst O"
  },
  {
    "path": "src/app/globals.css",
    "chars": 633,
    "preview": "@import \"tailwindcss\";\n\n@custom-variant dark (&:where(.dark, .dark *));\n\n:root {\n  --background: #ffffff;\n  --foreground"
  },
  {
    "path": "src/app/layout.tsx",
    "chars": 2912,
    "preview": "import type { Metadata } from \"next\";\nimport { GoogleAnalytics } from \"@next/third-parties/google\";\nimport { Geist, Geis"
  },
  {
    "path": "src/app/not-found.tsx",
    "chars": 928,
    "preview": "import Link from \"next/link\";\n\nexport default function NotFound() {\n  return (\n    <div className=\"flex min-h-screen ite"
  },
  {
    "path": "src/app/page.tsx",
    "chars": 2956,
    "preview": "import { Suspense } from \"react\";\nimport { MessageSquarePlus } from \"lucide-react\";\nimport ThemeToggle from \"@/component"
  },
  {
    "path": "src/components/layout/AuthContext.test.tsx",
    "chars": 5061,
    "preview": "import { describe, it, expect, vi, beforeEach, afterEach } from \"vitest\";\nimport { render, screen, cleanup, act } from \""
  },
  {
    "path": "src/components/layout/AuthContext.tsx",
    "chars": 6847,
    "preview": "\"use client\";\n\nimport { createContext, useContext, useEffect, useState, useCallback, useRef, type ReactNode } from \"reac"
  },
  {
    "path": "src/components/layout/GitHubLink.tsx",
    "chars": 1915,
    "preview": "const REPO = \"SeanPrashad/leetcode-patterns\";\n\nasync function getStarCount(): Promise<number | null> {\n  try {\n    const"
  },
  {
    "path": "src/components/layout/Logo.tsx",
    "chars": 554,
    "preview": "export default function Logo() {\n  const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? \"\";\n\n  return (\n    <>\n      {/"
  },
  {
    "path": "src/components/layout/ServiceWorkerRegistrar.tsx",
    "chars": 277,
    "preview": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { registerServiceWorker } from \"@/lib/register-sw\";\n\nexport def"
  },
  {
    "path": "src/components/layout/ThemeToggle.test.tsx",
    "chars": 1300,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\n"
  },
  {
    "path": "src/components/layout/ThemeToggle.tsx",
    "chars": 1421,
    "preview": "\"use client\";\n\nimport { useCallback, useSyncExternalStore } from \"react\";\nimport { Sun, Moon } from \"lucide-react\";\nimpo"
  },
  {
    "path": "src/components/layout/UserMenu.test.tsx",
    "chars": 3283,
    "preview": "import { describe, it, expect, vi, beforeEach, afterEach } from \"vitest\";\nimport { render, screen, cleanup } from \"@test"
  },
  {
    "path": "src/components/layout/UserMenu.tsx",
    "chars": 2170,
    "preview": "\"use client\";\n\nimport { useState, useRef, useEffect } from \"react\";\nimport { useAuth } from \"./AuthContext\";\n\nexport def"
  },
  {
    "path": "src/components/layout/ViewSwitcher.tsx",
    "chars": 4977,
    "preview": "\"use client\";\n\nimport { useState, useEffect, useSyncExternalStore, useCallback, useRef } from \"react\";\nimport { useSearc"
  },
  {
    "path": "src/components/panels/AboutPanel.tsx",
    "chars": 3740,
    "preview": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { Info, X } from \"lucide-react\";\nimport { trackEvent "
  },
  {
    "path": "src/components/panels/AcknowledgementsPanel.tsx",
    "chars": 3893,
    "preview": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { Heart, X } from \"lucide-react\";\nimport { trackEvent"
  },
  {
    "path": "src/components/panels/TipsPanel.tsx",
    "chars": 8299,
    "preview": "\"use client\";\n\nimport { useState, useCallback, useEffect, Fragment } from \"react\";\nimport { createPortal } from \"react-d"
  },
  {
    "path": "src/components/panels/panels.test.tsx",
    "chars": 3282,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\n"
  },
  {
    "path": "src/components/questions/ConfirmModal.tsx",
    "chars": 1356,
    "preview": "export default function ConfirmModal({\n  title,\n  message,\n  confirmLabel,\n  onConfirm,\n  onCancel,\n}: {\n  title: string"
  },
  {
    "path": "src/components/questions/FilterToolbar.tsx",
    "chars": 20417,
    "preview": "import { useState, useMemo, useEffect, useRef } from \"react\";\nimport { type Table } from \"@tanstack/react-table\";\nimport"
  },
  {
    "path": "src/components/questions/GroupHeaderRow.tsx",
    "chars": 2919,
    "preview": "import { forwardRef } from \"react\";\nimport { ChevronRight, ChevronDown, RotateCcw } from \"lucide-react\";\n\nconst difficul"
  },
  {
    "path": "src/components/questions/NoteModal.tsx",
    "chars": 5235,
    "preview": "import { MAX_NOTE_LENGTH } from \"@/lib/storage\";\n\nexport interface EditingNote {\n  id: number;\n  title: string;\n  draft:"
  },
  {
    "path": "src/components/questions/ProgressBar.tsx",
    "chars": 2654,
    "preview": "export interface ProgressStats {\n  totals: { Easy: number; Medium: number; Hard: number };\n  done: { Easy: number; Mediu"
  },
  {
    "path": "src/components/questions/QuestionRow.tsx",
    "chars": 2111,
    "preview": "import { forwardRef } from \"react\";\nimport { flexRender, type Row } from \"@tanstack/react-table\";\nimport { Question } fr"
  },
  {
    "path": "src/components/questions/QuestionsTable.test.tsx",
    "chars": 42815,
    "preview": "import { describe, it, expect, vi, beforeEach, afterEach } from \"vitest\";\nimport { render, screen, waitFor, cleanup, wit"
  },
  {
    "path": "src/components/questions/QuestionsTable.tsx",
    "chars": 45353,
    "preview": "\"use client\";\n\nimport { useState, useMemo, useCallback, useEffect, useRef, useSyncExternalStore } from \"react\";\nimport {"
  },
  {
    "path": "src/components/questions/ReviewDateModal.tsx",
    "chars": 3981,
    "preview": "import { useState } from \"react\";\nimport { today } from \"@/lib/reminders\";\n\nconst PRESETS = [\n  { label: \"Tomorrow\", day"
  },
  {
    "path": "src/components/roadmaps/RoadmapView.test.tsx",
    "chars": 19202,
    "preview": "import { describe, it, expect, vi, beforeEach, afterEach } from \"vitest\";\nimport { render, screen, cleanup } from \"@test"
  },
  {
    "path": "src/components/roadmaps/RoadmapView.tsx",
    "chars": 37434,
    "preview": "\"use client\";\n\nimport { useState, useMemo, useCallback, useEffect, Fragment } from \"react\";\nimport {\n  ChevronDown,\n  Ch"
  },
  {
    "path": "src/data/questions.json",
    "chars": 176496,
    "preview": "{\n  \"updated\": \"2026-03-15T14:07:51.507813\",\n  \"data\": [\n    {\n      \"id\": 1,\n      \"title\": \"Two Sum\",\n      \"slug\": \"t"
  },
  {
    "path": "src/data/roadmaps.ts",
    "chars": 37786,
    "preview": "export interface RoadmapQuestion {\n  slug: string;\n  note: string;\n}\n\nexport interface RoadmapPhase {\n  title: string;\n "
  },
  {
    "path": "src/lib/analytics.test.ts",
    "chars": 1126,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\n\nconst { mockSendGAEvent } = vi.hoisted(() => ({\n  mockSe"
  },
  {
    "path": "src/lib/analytics.ts",
    "chars": 210,
    "preview": "import { sendGAEvent } from \"@next/third-parties/google\";\n\nexport function trackEvent(eventName: string, params?: Record"
  },
  {
    "path": "src/lib/register-sw.test.ts",
    "chars": 1604,
    "preview": "import { describe, it, expect, vi, beforeEach, afterEach } from \"vitest\";\nimport { registerServiceWorker } from \"@/lib/r"
  },
  {
    "path": "src/lib/register-sw.ts",
    "chars": 331,
    "preview": "export function registerServiceWorker(basePath = \"\"): void {\n  if (typeof window === \"undefined\" || !(\"serviceWorker\" in"
  },
  {
    "path": "src/lib/reminders.test.ts",
    "chars": 3821,
    "preview": "import { describe, it, expect, vi, afterEach } from \"vitest\";\nimport {\n  initReminder,\n  advanceReminder,\n  isDue,\n  set"
  },
  {
    "path": "src/lib/reminders.ts",
    "chars": 1800,
    "preview": "export interface Reminder {\n  nextReview: string; // ISO date string (date only, e.g. \"2026-03-09\")\n  interval: number; "
  },
  {
    "path": "src/lib/storage.test.ts",
    "chars": 6896,
    "preview": "import { describe, it, expect, beforeEach } from \"vitest\";\nimport type { Question } from \"@/types/question\";\nimport {\n  "
  },
  {
    "path": "src/lib/storage.ts",
    "chars": 7912,
    "preview": "import { Question } from \"@/types/question\";\nimport type { Reminder } from \"./reminders\";\n\nexport const MAX_NOTE_LENGTH "
  },
  {
    "path": "src/lib/supabase.ts",
    "chars": 251,
    "preview": "import { createClient } from \"@supabase/supabase-js\";\n\nconst supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;\nconst "
  },
  {
    "path": "src/lib/sw.test.ts",
    "chars": 9288,
    "preview": "import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { readFileSync } from \"fs\";\nimport { join } from \""
  },
  {
    "path": "src/lib/sync.test.ts",
    "chars": 11879,
    "preview": "import { describe, it, expect, beforeEach, vi } from \"vitest\";\nimport {\n  saveCompleted,\n  saveStarred,\n  saveNotes,\n  s"
  },
  {
    "path": "src/lib/sync.ts",
    "chars": 4572,
    "preview": "import { supabase } from \"./supabase\";\nimport {\n  loadCompleted, saveCompleted,\n  loadStarred, saveStarred,\n  loadNotes,"
  },
  {
    "path": "src/test/setup.ts",
    "chars": 43,
    "preview": "import \"@testing-library/jest-dom/vitest\";\n"
  },
  {
    "path": "src/types/question.ts",
    "chars": 342,
    "preview": "export interface Company {\n  name: string;\n  slug: string;\n  frequency: number;\n}\n\nexport interface Question {\n  id: num"
  },
  {
    "path": "tsconfig.json",
    "chars": 670,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  },
  {
    "path": "vitest.config.mts",
    "chars": 541,
    "preview": "import { defineConfig } from \"vitest/config\";\nimport react from \"@vitejs/plugin-react\";\nimport path from \"path\";\n\nexport"
  }
]

About this extraction

This page contains the full source code of the seanprashad/leetcode-patterns GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 72 files (519.1 KB), approximately 134.3k tokens, and a symbol index with 140 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!