diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cd87de..85ee6cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,22 +19,23 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.14.3" + python-version: "3.14" cache: "pip" cache-dependency-path: backend/requirements.txt - name: Install dependencies run: | python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -r requirements.txt - - name: Run tests (if present) - run: | - if [ -f api/tests.py ] || [ -d api/tests ]; then - python manage.py test - else - echo "No backend tests found in api/; skipping test step." - fi + - name: Lint Python + run: ruff check . + + - name: Security audit + run: safety check + + - name: Run tests + run: pytest -v frontend: runs-on: ubuntu-latest @@ -57,6 +58,7 @@ jobs: - name: Build run: npm run build + docker-build: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index eb427db..7de6f16 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ build/ *.whl venv/ .venv/ -env/ .env db.sqlite3 *.log @@ -35,3 +34,31 @@ frontend/dist/ # OS .DS_Store Thumbs.db + +# Agent/Session files +AGENTS.md +session-*.md + +# Testing/Coverage +.pytest_cache/ + +# Expanded environment file ignores (protects accidental secrets) +.env.* +frontend/.env +backend/.env + +# Ignore all SQLite DBs, not just the default +*.sqlite3 + +# Local coverage/test artifacts (frontend & backend) +frontend/coverage/ +backend/coverage/ + +# LaTeX/TeX build artifacts (if any contributor compiles PDFs locally) +*.pdf +*.aux +*.log +*.out + +# Backup and temp files +#*# diff --git a/README.md b/README.md index c0bfb77..f9f2c51 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,129 @@ -# cheat-sheet +# Cheat Sheet Generator [![CI](https://github.com/ChicoState/cheat-sheet/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ChicoState/cheat-sheet/actions/workflows/ci.yml) -A full-stack web application built with **React** (frontend) and **Django** (backend). +A full-stack web application for generating LaTeX-based cheat sheets. Users select math classes and formula categories, then view the generated LaTeX code alongside a live PDF preview. + +## Features + +- **Class Selection**: Choose from PRE-ALGEBRA, ALGEBRA I, ALGEBRA II, and GEOMETRY +- **Category Selection**: Select categories with checkboxes (no Ctrl/Cmd needed) +- **Live Preview**: Split-view interface with LaTeX code and PDF preview +- **Auto-compile**: PDF generates automatically when you generate a cheat sheet +- **PDF Export**: Compile to PDF using Tectonic LaTeX engine on the backend +- **Download Options**: Download as .tex source or .pdf + + +## To-Do / Known Issues + +- Fix bugs: + - Dark mode (complete variable coverage, remove hardcoded colors) + - Compile button not working after changes +- Implement or migrate database (currently MariaDB; consider migration if needed) +- Increase formatting options (columns, margins, text sizing, etc.) + + +## Planned Features + +> These features are not yet implemented + +- **Formatting Options**: + - Column layout (single, two, three columns) + - Text sizes (font scaling) + - Margin adjustments +- **Image Insertion**: + - Allow users to insert images, store them in the database + - Embed images in PDF and keep code reference in exported .tex +- **User Accounts**: Register and log in with username and password +- **Database Storage**: Save and manage cheat sheets in database +- **Autosave & Version History**: Every compile is saved automatically; revert to any previous version +- **Custom LaTeX Syntax Shortcuts**: Allow users to use shortcuts for custom LaTeX commands + +## Tech Stack + +| Layer | Technology | +|-------|------------| +| Frontend | React 18 + Vite + npm | +| Backend | Django 6 + Django REST Framework | +| LaTeX Engine | Tectonic | +| Database | SQLite (dev) / MariaDB (prod) | +| Container | Docker Compose | + +### Backend Dependencies +- django>=6.0 +- djangorestframework>=3.15 +- django-cors-headers>=4.4 +- python-dotenv>=1.0 +- dj-database-url>=2.1 +- pymysql>=1.1 +- pytest>=8.0 +- pytest-django>=4.8 +- ruff>=0.4.0 (linting) +- safety>=2.0 (security) + +### Frontend Dependencies +- react, react-dom +- vite ## Project Structure ``` -├── backend/ # Django REST API -│ ├── cheat_sheet/ # Django project settings -│ ├── api/ # Main API app -│ ├── manage.py -│ └── requirements.txt -├── frontend/ # React + Vite +├── backend/ # Django REST API +│ ├── cheat_sheet/ # Django project settings +│ ├── api/ # API app +│ │ ├── views.py # API endpoints +│ │ ├── models.py # Database models +│ │ ├── formula_data/ # Hardcoded formula data +│ │ │ ├── pre_algebra.py +│ │ │ ├── algebra_i.py +│ │ │ ├── algebra_ii.py +│ │ │ └── geometry.py +│ ├── requirements.txt # Python dependencies +│ ├── Dockerfile # Backend container +│ └── manage.py +├── frontend/ # React + Vite │ ├── src/ +│ │ ├── App.jsx # Main app component +│ │ ├── components/ +│ │ │ └── CreateCheatSheet.jsx # Main UI +│ │ └── App.css │ ├── package.json -│ └── vite.config.js +│ ├── vite.config.js +│ └── Dockerfile +├── docker-compose.yml # Container orchestration └── README.md ``` +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/health/` | Health check | +| GET | `/api/classes/` | List available classes with categories and formulas | +| POST | `/api/generate-sheet/` | Generate LaTeX for selected formulas | +| POST | `/api/compile/` | Compile LaTeX to PDF | + +### Available Formula Classes + +- **PRE-ALGEBRA** - Order of Operations, Fractions, Ratios, Properties, Area/Perimeter, Solving Equations +- **ALGEBRA I** - Linear Equations, Inequalities, Integer Rules, Decimals/Percents, Mean/Median/Mode, Quadratics, Polynomials, Exponents, Radicals, Functions, Absolute Value, Rational Expressions +- **ALGEBRA II** - Complex Numbers, Logarithms, Exponential Functions, Polynomial Theorems, Conic Sections, Sequences/Series, Matrices, Binomial Theorem +- **GEOMETRY** - Angle Relationships, Parallel Lines, Triangles, Pythagorean Theorem, Similar/Congruent Triangles, Quadrilaterals, Polygons, Circles, Circle Theorems, Coordinate Geometry, Surface Area/Volume, Transformations + ## Getting Started +### Prerequisites + +- Python 3.13+ +- Node.js 20+ +- Tectonic (for PDF compilation) + ### Backend ```bash cd backend python -m venv venv -source venv/bin/activate +source venv/bin/activate # On Windows: venv\Scripts\activate pip install -r requirements.txt python manage.py migrate python manage.py runserver @@ -42,12 +139,48 @@ npm install npm run dev ``` +The frontend will be available at `http://localhost:5173/`. + ### Docker ```bash docker compose up --build ``` -This will build and start both the Django backend and the React frontend services using Docker. +This builds and starts the Django backend, React frontend, and MariaDB database. The app will be available at `http://localhost:5173/`. API requests are proxied to the Django backend. + +## Running Tests + +### Backend (pytest) + +```bash +cd backend +pytest -v # Run with verbose output +pytest -k "test_name" # Run tests matching pattern +``` + +### Frontend (ESLint) + +```bash +cd frontend +npx eslint src/ # Lint source files +npx eslint --fix src/ # Auto-fix lint issues +``` + +### CI Pipeline + +The project includes GitHub Actions CI that runs: +- Backend: Ruff linting, Safety security scan, pytest +- Frontend: ESLint, build verification +- Docker: Image build verification + +## User Flow + +1. **Enter Title**: Give your cheat sheet a name +2. **Select Class**: Click on a class (PRE-ALGEBRA, ALGEBRA I, ALGEBRA II, GEOMETRY) +3. **Select Categories**: Check the categories you want (no Ctrl/Cmd needed) +4. **Generate**: Click "Generate Cheat Sheet" - LaTeX generates and PDF compiles automatically +5. **Preview**: View the PDF in the preview pane, or click the circular button to recompile +6. **Download**: Download as .tex or .pdf diff --git a/backend/.env.docker b/backend/.env.docker index f91313c..813046c 100644 --- a/backend/.env.docker +++ b/backend/.env.docker @@ -1,6 +1,8 @@ -# Example Docker environment configuration for Django. -# Set a strong, unique value for DJANGO_SECRET_KEY via environment variables in each environment. +# Docker environment configuration for Django. DJANGO_SECRET_KEY= DJANGO_DEBUG=True -DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0 +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,backend CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173 + +# MariaDB connection (matches the db service in docker-compose.yml) +DATABASE_URL=mysql://cheatsheet_user:cheatsheet_pass@db:3306/cheatsheet_db diff --git a/backend/Dockerfile b/backend/Dockerfile index f7a746b..60f7893 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -11,6 +11,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ wget \ tar \ fontconfig \ + default-libmysqlclient-dev \ + pkg-config \ + gcc \ + default-mysql-client \ && rm -rf /var/lib/apt/lists/* # Download and extract the appropriate Tectonic binary @@ -31,4 +35,4 @@ COPY . . EXPOSE 8000 -CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] +CMD ["sh", "-c", "until mysqladmin ping -hdb -ucheatsheet_user -pcheatsheet_pass --silent; do sleep 2; done && python manage.py runserver 0.0.0.0:8000"] diff --git a/backend/api/admin.py b/backend/api/admin.py new file mode 100644 index 0000000..12a09b2 --- /dev/null +++ b/backend/api/admin.py @@ -0,0 +1,31 @@ + + + +#from django.contrib import admin +#from .models import Template, CheatSheet, PracticeProblem +# +# +#@admin.register(Template) +#class TemplateAdmin(admin.ModelAdmin): +# list_display = ("name", "subject", "default_columns", "default_margins", "updated_at") +# list_filter = ("subject",) +# search_fields = ("name", "description") +# +# +#class PracticeProblemInline(admin.TabularInline): +# model = PracticeProblem +# extra = 1 +# +# +#@admin.register(CheatSheet) +#class CheatSheetAdmin(admin.ModelAdmin): +# list_display = ("title", "template", "columns", "margins", "font_size", "updated_at") +# list_filter = ("template",) +# search_fields = ("title",) +# inlines = [PracticeProblemInline] +# +# +#@admin.register(PracticeProblem) +#class PracticeProblemAdmin(admin.ModelAdmin): +# list_display = ("cheat_sheet", "order", "question_latex") +# list_filter = ("cheat_sheet",) diff --git a/backend/api/apps.py b/backend/api/apps.py new file mode 100644 index 0000000..878e7d5 --- /dev/null +++ b/backend/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "api" diff --git a/backend/api/formula_data/__init__.py b/backend/api/formula_data/__init__.py new file mode 100644 index 0000000..e2fbea5 --- /dev/null +++ b/backend/api/formula_data/__init__.py @@ -0,0 +1,42 @@ +""" +Formula data organized by class and category. +Each module exports FORMULAS dict and CLASS_NAME. +""" + +from .pre_algebra import FORMULAS as PRE_ALGEBRA, CLASS_NAME as PRE_ALGEBRA_NAME +from .algebra_i import FORMULAS as ALGEBRA_I, CLASS_NAME as ALGEBRA_I_NAME +from .algebra_ii import FORMULAS as ALGEBRA_II, CLASS_NAME as ALGEBRA_II_NAME +from .geometry import FORMULAS as GEOMETRY, CLASS_NAME as GEOMETRY_NAME + +AVAILABLE_CLASSES = [PRE_ALGEBRA_NAME, ALGEBRA_I_NAME, ALGEBRA_II_NAME, GEOMETRY_NAME] + +FORMULA_DATA = { + PRE_ALGEBRA_NAME: PRE_ALGEBRA, + ALGEBRA_I_NAME: ALGEBRA_I, + ALGEBRA_II_NAME: ALGEBRA_II, + GEOMETRY_NAME: GEOMETRY, +} + + +def get_formula_data(): + """Return the full formula data structure.""" + return FORMULA_DATA + + +def get_available_classes(): + """Return list of available class names.""" + return AVAILABLE_CLASSES + + +def get_classes_with_details(): + """Return full structure with classes, categories, and formulas.""" + return [ + { + "name": class_name, + "categories": [ + {"name": cat_name, "formulas": formulas} + for cat_name, formulas in categories.items() + ] + } + for class_name, categories in FORMULA_DATA.items() + ] diff --git a/backend/api/formula_data/algebra_i.py b/backend/api/formula_data/algebra_i.py new file mode 100644 index 0000000..06ce9fe --- /dev/null +++ b/backend/api/formula_data/algebra_i.py @@ -0,0 +1,80 @@ +""" +ALGEBRA I formulas +""" + +CLASS_NAME = "ALGEBRA I" + +FORMULAS = { + "Linear Equations": [ + {"name": "Slope Formula", "latex": r"m=\frac{y_2-y_1}{x_2-x_1}"}, + {"name": "Point-Slope Form", "latex": r"y-y_1=m(x-x_1)"}, + {"name": "Slope-Intercept Form", "latex": r"y=mx+b"}, + {"name": "Standard Form", "latex": r"Ax+By=C"}, + ], + "Inequalities": [ + {"name": "Multiply by Positive", "latex": r"\text{If } a>b \text{ and } c>0, \text{ then } ac>bc"}, + {"name": "Multiply by Negative", "latex": r"\text{If } a>b \text{ and } c<0, \text{ then } aca \implies x>a \text{ or } x<-a"}, + ], + "Integer Rules": [ + {"name": "Positive x Positive", "latex": r"(+)(+)=+"}, + {"name": "Negative x Negative", "latex": r"(-)(-)=+"}, + {"name": "Positive x Negative", "latex": r"(+)(-)=-"}, + {"name": "Negative x Positive", "latex": r"(-)(+)=-"}, + {"name": "Subtracting Negatives", "latex": r"a-(-b)=a+b"}, + ], + "Decimals and Percents": [ + {"name": "Percent Formula", "latex": r"\text{Percent}=\frac{\text{Part}}{\text{Whole}}\times 100"}, + {"name": "Part Formula", "latex": r"\text{Part}=\text{Whole}\times\frac{\text{Percent}}{100}"}, + {"name": "Decimal to Percent", "latex": r"\text{Decimal to Percent: move decimal 2 places right}"}, + {"name": "Percent to Decimal", "latex": r"\text{Percent to Decimal: move decimal 2 places left}"}, + ], + "Mean, Median, Mode": [ + {"name": "Mean", "latex": r"\text{Mean}=\frac{\text{Sum of values}}{\text{Number of values}}"}, + {"name": "Median", "latex": r"\text{Median: middle value when sorted}"}, + {"name": "Mode", "latex": r"\text{Mode: most frequently occurring value}"}, + ], + "Quadratic Equations": [ + {"name": "Standard Form", "latex": r"ax^2+bx+c=0"}, + {"name": "Quadratic Formula", "latex": r"x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}"}, + {"name": "Vertex Form", "latex": r"y=a(x-h)^2+k"}, + {"name": "Axis of Symmetry", "latex": r"x=-\frac{b}{2a}"}, + ], + "Polynomials": [ + {"name": "Perfect Square (Addition)", "latex": r"(a+b)^2=a^2+2ab+b^2"}, + {"name": "Perfect Square (Subtraction)", "latex": r"(a-b)^2=a^2-2ab+b^2"}, + {"name": "Difference of Squares", "latex": r"a^2-b^2=(a-b)(a+b)"}, + {"name": "FOIL", "latex": r"(a+b)(c+d)=ac+ad+bc+bd"}, + ], + "Exponents": [ + {"name": "Product Rule", "latex": r"a^m a^n = a^{m+n}"}, + {"name": "Quotient Rule", "latex": r"\frac{a^m}{a^n}=a^{m-n}"}, + {"name": "Power Rule", "latex": r"(a^m)^n=a^{mn}"}, + {"name": "Negative Exponent", "latex": r"a^{-n}=\frac{1}{a^n}"}, + {"name": "Zero Exponent", "latex": r"a^0=1"}, + {"name": "Radical to Exponent", "latex": r"\sqrt[n]{a^m}=a^{m/n}"}, + ], + "Radicals": [ + {"name": "Product Rule", "latex": r"\sqrt{ab}=\sqrt{a}\cdot\sqrt{b}"}, + {"name": "Quotient Rule", "latex": r"\sqrt{\frac{a}{b}}=\frac{\sqrt{a}}{\sqrt{b}}"}, + {"name": "Like Radicals", "latex": r"a\sqrt{b}+c\sqrt{b}=(a+c)\sqrt{b}"}, + {"name": "Square of Square Root", "latex": r"(\sqrt{a})^2=a"}, + ], + "Functions": [ + {"name": "Function Notation", "latex": r"f(x)=y"}, + {"name": "Domain", "latex": r"\text{Domain: all valid input } x \text{ values}"}, + {"name": "Range", "latex": r"\text{Range: all output } y \text{ values}"}, + {"name": "Vertical Line Test", "latex": r"\text{Vertical Line Test: each } x \to \text{one } y"}, + ], + "Absolute Value": [ + {"name": "Definition", "latex": r"|a| =\begin{cases} a & a \ge 0 \\ -a & a < 0 \end{cases}"}, + {"name": "Product", "latex": r"|a \cdot b| = |a||b|"}, + {"name": "Quotient", "latex": r"\left|\frac{a}{b}\right| = \frac{|a|}{|b|}"}, + {"name": "Triangle Inequality", "latex": r"|a+b| \le |a| + |b|"}, + ], + "Rational Expressions": [ + {"name": "Multiplication", "latex": r"\left(\frac{a}{b}\right)\left(\frac{c}{d}\right)=\frac{ac}{bd}"}, + {"name": "Division", "latex": r"\left(\frac{a}{b}\right)\div\left(\frac{c}{d}\right)=\left(\frac{a}{b}\right)\left(\frac{d}{c}\right)=\frac{ad}{bc}"}, + ], +} diff --git a/backend/api/formula_data/algebra_ii.py b/backend/api/formula_data/algebra_ii.py new file mode 100644 index 0000000..e785544 --- /dev/null +++ b/backend/api/formula_data/algebra_ii.py @@ -0,0 +1,59 @@ +""" +ALGEBRA II formulas +""" + +CLASS_NAME = "ALGEBRA II" + +FORMULAS = { + "Complex Numbers": [ + {"name": "Imaginary Unit", "latex": r"i=\sqrt{-1} \quad\quad i^2=-1"}, + {"name": "Addition", "latex": r"(a+bi)+(c+di)=(a+c)+(b+d)i"}, + {"name": "Multiplication", "latex": r"(a+bi)(c+di)=(ac-bd)+(ad+bc)i"}, + {"name": "Magnitude", "latex": r"|a+bi|=\sqrt{a^2+b^2}"}, + ], + "Logarithms": [ + {"name": "Definition", "latex": r"\log_b(a)=c \iff b^c=a"}, + {"name": "Product Rule", "latex": r"\log_b(xy)=\log_b(x)+\log_b(y)"}, + {"name": "Quotient Rule", "latex": r"\log_b\!\left(\frac{x}{y}\right)=\log_b(x)-\log_b(y)"}, + {"name": "Power Rule", "latex": r"\log_b(x^n)=n\log_b(x)"}, + {"name": "Change of Base", "latex": r"\log_b(a)=\frac{\ln(a)}{\ln(b)}"}, + {"name": "Special Values", "latex": r"\log_b(1)=0 \quad\quad \log_b(b)=1"}, + ], + "Exponential Functions": [ + {"name": "Basic Form", "latex": r"y=ab^x"}, + {"name": "Compound Interest", "latex": r"A=P\!\left(1+\frac{r}{n}\right)^{nt}"}, + {"name": "Continuous Growth", "latex": r"A=Pe^{rt}"}, + {"name": "Growth/Decay", "latex": r"y=ae^{kx} \quad (k>0 \text{ growth}, \; k<0 \text{ decay})"}, + ], + "Polynomial Theorems": [ + {"name": "Remainder Theorem", "latex": r"\text{Remainder Thm: } f(a)=\text{remainder of } f(x)\div(x-a)"}, + {"name": "Factor Theorem", "latex": r"\text{Factor Thm: } f(a)=0 \implies (x-a) \text{ is a factor}"}, + {"name": "Rational Root Theorem", "latex": r"\text{Rational Root Thm: } \frac{p}{q}, \; p\mid a_0, \; q\mid a_n"}, + {"name": "Descartes' Rule", "latex": r"\text{Descartes' Rule: sign changes} \to \text{positive roots}"}, + {"name": "Fundamental Theorem", "latex": r"\text{Degree } n \text{ polynomial has at most } n \text{ roots}"}, + ], + "Conic Sections": [ + {"name": "Circle", "latex": r"(x-h)^2+(y-k)^2=r^2"}, + {"name": "Ellipse", "latex": r"\frac{(x-h)^2}{a^2}+\frac{(y-k)^2}{b^2}=1"}, + {"name": "Hyperbola", "latex": r"\frac{(x-h)^2}{a^2}-\frac{(y-k)^2}{b^2}=1"}, + {"name": "Parabola", "latex": r"y=a(x-h)^2+k"}, + {"name": "Focal Distance", "latex": r"c^2=a^2-b^2 \;\text{(Ellipse)} \quad c^2=a^2+b^2 \;\text{(Hyperbola)}"}, + ], + "Sequences and Series": [ + {"name": "Arithmetic Sequence", "latex": r"a_n=a_1+(n-1)d"}, + {"name": "Arithmetic Sum", "latex": r"S_n=\frac{n}{2}(a_1+a_n)"}, + {"name": "Geometric Sequence", "latex": r"a_n=a_1 r^{n-1}"}, + {"name": "Geometric Sum", "latex": r"S_n=a_1\frac{1-r^n}{1-r}"}, + {"name": "Infinite Geometric Sum", "latex": r"S_\infty=\frac{a_1}{1-r} \quad |r|<1"}, + ], + "Matrices": [ + {"name": "2x2 Inverse", "latex": r"\begin{bmatrix}a&b\\c&d\end{bmatrix}^{-1}=\frac{1}{ad-bc}\begin{bmatrix}d&-b\\-c&a\end{bmatrix}"}, + {"name": "Determinant", "latex": r"\det\begin{bmatrix}a&b\\c&d\end{bmatrix}=ad-bc"}, + {"name": "Identity Property", "latex": r"A\cdot A^{-1}=I"}, + ], + "Binomial Theorem": [ + {"name": "Expansion", "latex": r"(a+b)^n=\sum_{k=0}^{n}\binom{n}{k}a^{n-k}b^k"}, + {"name": "Binomial Coefficient", "latex": r"\binom{n}{k}=\frac{n!}{k!(n-k)!}"}, + {"name": "Factorial", "latex": r"n!=n(n-1)(n-2)\cdots(2)(1) \quad\quad 0!=1"}, + ], +} diff --git a/backend/api/formula_data/geometry.py b/backend/api/formula_data/geometry.py new file mode 100644 index 0000000..3cefdfc --- /dev/null +++ b/backend/api/formula_data/geometry.py @@ -0,0 +1,88 @@ +""" +GEOMETRY formulas +""" + +CLASS_NAME = "GEOMETRY" + +FORMULAS = { + "Basic Angle Relationships": [ + {"name": "Supplementary", "latex": r"\text{Supplementary: } a+b=180^\circ"}, + {"name": "Complementary", "latex": r"\text{Complementary: } a+b=90^\circ"}, + {"name": "Vertical Angles", "latex": r"\text{Vertical angles are congruent}"}, + {"name": "Linear Pair", "latex": r"\text{Linear pair: supplementary and adjacent}"}, + ], + "Parallel Lines and Transversals": [ + {"name": "Corresponding Angles", "latex": r"\text{Corresponding angles are congruent}"}, + {"name": "Alternate Interior", "latex": r"\text{Alternate interior angles are congruent}"}, + {"name": "Alternate Exterior", "latex": r"\text{Alternate exterior angles are congruent}"}, + {"name": "Co-interior", "latex": r"\text{Co-interior (same-side interior) angles are supplementary}"}, + ], + "Triangles": [ + {"name": "Angle Sum", "latex": r"A+B+C=180^\circ"}, + {"name": "Exterior Angle", "latex": r"\text{Exterior angle} = \text{sum of two remote interior angles}"}, + {"name": "Area", "latex": r"A=\frac{1}{2}bh"}, + {"name": "Heron's Formula", "latex": r"A=\sqrt{s(s-a)(s-b)(s-c)} \quad s=\frac{a+b+c}{2}"}, + {"name": "Triangle Inequality", "latex": r"a+b>c"}, + ], + "Pythagorean Theorem": [ + {"name": "Theorem", "latex": r"a^2+b^2=c^2"}, + {"name": "Solve for c", "latex": r"c=\sqrt{a^2+b^2}"}, + {"name": "Common Triples", "latex": r"\text{Common triples: } (3,4,5),\;(5,12,13),\;(8,15,17),\;(7,24,25)"}, + ], + "Similar and Congruent Triangles": [ + {"name": "Congruence Rules", "latex": r"\text{Congruence: SSS, SAS, ASA, AAS, HL}"}, + {"name": "Similarity Rules", "latex": r"\text{Similarity: AA, SAS, SSS}"}, + {"name": "Proportional Sides", "latex": r"\frac{a_1}{a_2}=\frac{b_1}{b_2}=\frac{c_1}{c_2}"}, + {"name": "Area Ratio", "latex": r"\frac{A_1}{A_2}=\left(\frac{s_1}{s_2}\right)^2"}, + ], + "Quadrilaterals": [ + {"name": "Interior Angles", "latex": r"\text{Sum of interior angles} = 360^\circ"}, + {"name": "Rectangle/Square", "latex": r"A_{\text{rect}}=lw \quad\quad A_{\text{square}}=s^2"}, + {"name": "Parallelogram", "latex": r"A_{\text{parallelogram}}=bh"}, + {"name": "Trapezoid", "latex": r"A_{\text{trapezoid}}=\frac{1}{2}(b_1+b_2)h"}, + {"name": "Rhombus", "latex": r"A_{\text{rhombus}}=\frac{1}{2}d_1 d_2"}, + ], + "Polygons": [ + {"name": "Interior Sum", "latex": r"\text{Sum of interior angles}=(n-2)\cdot 180^\circ"}, + {"name": "Interior Angle", "latex": r"\text{Each interior angle (regular)}=\frac{(n-2)\cdot 180^\circ}{n}"}, + {"name": "Exterior Angle", "latex": r"\text{Each exterior angle (regular)}=\frac{360^\circ}{n}"}, + {"name": "Diagonals", "latex": r"\text{Number of diagonals}=\frac{n(n-3)}{2}"}, + ], + "Circles": [ + {"name": "Circumference", "latex": r"C=2\pi r=\pi d"}, + {"name": "Area", "latex": r"A=\pi r^2"}, + {"name": "Arc Length", "latex": r"\text{Arc length}=\frac{\theta}{360}\cdot 2\pi r"}, + {"name": "Sector Area", "latex": r"\text{Sector area}=\frac{\theta}{360}\cdot\pi r^2"}, + {"name": "Inscribed Angle", "latex": r"\text{Inscribed angle}=\frac{1}{2}\text{(intercepted arc)}"}, + {"name": "Central Angle", "latex": r"\text{Central angle}=\text{intercepted arc}"}, + ], + "Circle Theorems": [ + {"name": "Tangent-Radius", "latex": r"\text{Tangent} \perp \text{radius at point of tangency}"}, + {"name": "Equal Tangents", "latex": r"\text{Two tangents from external point are equal}"}, + {"name": "Intersecting Chords", "latex": r"\text{Intersecting chords: } (a)(b)=(c)(d)"}, + {"name": "Secant-Secant", "latex": r"\text{Secant-secant: } a(a+b)=c(c+d)"}, + {"name": "Secant-Tangent", "latex": r"\text{Secant-tangent: } t^2=a(a+b)"}, + ], + "Coordinate Geometry": [ + {"name": "Distance Formula", "latex": r"d=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}"}, + {"name": "Midpoint Formula", "latex": r"M=\left(\frac{x_1+x_2}{2},\;\frac{y_1+y_2}{2}\right)"}, + {"name": "Circle Equation", "latex": r"(x-h)^2+(y-k)^2=r^2"}, + ], + "Surface Area and Volume": [ + {"name": "Prism", "latex": r"V_{\text{prism}}=Bh \quad\quad SA_{\text{prism}}=2B+Ph"}, + {"name": "Cylinder", "latex": r"V_{\text{cylinder}}=\pi r^2 h \quad\quad SA_{\text{cylinder}}=2\pi r^2+2\pi rh"}, + {"name": "Pyramid", "latex": r"V_{\text{pyramid}}=\frac{1}{3}Bh"}, + {"name": "Cone", "latex": r"V_{\text{cone}}=\frac{1}{3}\pi r^2 h \quad\quad SA_{\text{cone}}=\pi r^2+\pi r l"}, + {"name": "Sphere", "latex": r"V_{\text{sphere}}=\frac{4}{3}\pi r^3 \quad\quad SA_{\text{sphere}}=4\pi r^2"}, + ], + "Transformations": [ + {"name": "Translation", "latex": r"\text{Translation: } (x,y)\to(x+a,\;y+b)"}, + {"name": "Reflection x-axis", "latex": r"\text{Reflection over } x\text{-axis: } (x,y)\to(x,-y)"}, + {"name": "Reflection y-axis", "latex": r"\text{Reflection over } y\text{-axis: } (x,y)\to(-x,y)"}, + {"name": "Reflection y=x", "latex": r"\text{Reflection over } y=x\text{: } (x,y)\to(y,x)"}, + {"name": "Rotation 90 CCW", "latex": r"\text{Rotation } 90^\circ \text{ CCW: } (x,y)\to(-y,x)"}, + {"name": "Rotation 180", "latex": r"\text{Rotation } 180^\circ\text{: } (x,y)\to(-x,-y)"}, + {"name": "Rotation 270 CCW", "latex": r"\text{Rotation } 270^\circ \text{ CCW: } (x,y)\to(y,-x)"}, + {"name": "Dilation", "latex": r"\text{Dilation: } (x,y)\to(kx,ky)"}, + ], +} diff --git a/backend/api/formula_data/pre_algebra.py b/backend/api/formula_data/pre_algebra.py new file mode 100644 index 0000000..76a63b9 --- /dev/null +++ b/backend/api/formula_data/pre_algebra.py @@ -0,0 +1,39 @@ +""" +PRE-ALGEBRA formulas +""" + +CLASS_NAME = "PRE-ALGEBRA" + +FORMULAS = { + "Order of Operations (PEMDAS)": [ + {"name": "PEMDAS Definition", "latex": r"\text{Parentheses} \to \text{Exponents} \to \text{Multiply/Divide} \to \text{Add/Subtract}"}, + ], + "Fractions": [ + {"name": "Addition", "latex": r"\frac{a}{b}+\frac{c}{d}=\frac{ad+bc}{bd}"}, + {"name": "Subtraction", "latex": r"\frac{a}{b}-\frac{c}{d}=\frac{ad-bc}{bd}"}, + {"name": "Multiplication", "latex": r"\frac{a}{b}\cdot\frac{c}{d}=\frac{ac}{bd}"}, + {"name": "Division", "latex": r"\frac{a}{b}\div\frac{c}{d}=\frac{a}{b}\cdot\frac{d}{c}=\frac{ad}{bc}"}, + ], + "Ratios and Proportions": [ + {"name": "Proportion", "latex": r"\frac{a}{b}=\frac{c}{d}\implies ad=bc"}, + {"name": "Unit Rate", "latex": r"\text{Unit Rate}=\frac{\text{Total}}{\text{Number of Units}}"}, + ], + "Properties of Numbers": [ + {"name": "Commutative", "latex": r"a+b=b+a"}, + {"name": "Associative", "latex": r"(a+b)+c=a+(b+c)"}, + {"name": "Distributive", "latex": r"a(b+c)=ab+ac"}, + {"name": "Identity", "latex": r"a+0=a"}, + {"name": "Inverse", "latex": r"a+(-a)=0"}, + ], + "Area and Perimeter": [ + {"name": "Rectangle Area/Perimeter", "latex": r"A_{\text{rect}}=lw \quad\quad P_{\text{rect}}=2l+2w"}, + {"name": "Triangle Area", "latex": r"A_{\text{tri}}=\frac{1}{2}bh"}, + {"name": "Circle Area/Circumference", "latex": r"A_{\text{circle}}=\pi r^2 \quad\quad C=2\pi r"}, + {"name": "Rectangular Prism Volume", "latex": r"V_{\text{rect prism}}=lwh"}, + ], + "Solving Equations": [ + {"name": "Linear Solution", "latex": r"ax+b=c \implies x=\frac{c-b}{a}"}, + {"name": "Absolute Value", "latex": r"|x|=a \implies x=a \text{ or } x=-a"}, + {"name": "Transitive Property", "latex": r"\text{If } a=b \text{ and } b=c, \text{ then } a=c"}, + ], +} diff --git a/backend/api/formula_loader.py.old b/backend/api/formula_loader.py.old new file mode 100644 index 0000000..677b214 --- /dev/null +++ b/backend/api/formula_loader.py.old @@ -0,0 +1,65 @@ +""" +Reads .tex formula files from the templates_data/ directory. +Returns a nested dict { subject: { category: [ {name, latex}, ... ] } } + +Each .tex file has lines in the format: + Formula Name | \\latex_code_here +""" + +from pathlib import Path + +TEMPLATES_DIR = Path(__file__).resolve().parent / "templates_data" + + +def _prettify_name(raw_name): + """Turn 'linear_eq' into 'Linear Eq' or 'linear_algebra' into 'Linear Algebra'.""" + return raw_name.replace("_", " ").title() + + +def load_all_formulas(): + formulas = {} + + if not TEMPLATES_DIR.is_dir(): + return formulas + + subject_dirs = sorted( + entry + for entry in TEMPLATES_DIR.iterdir() + if entry.is_dir() and not entry.name.startswith(".") + ) + + idx = 0 + while idx < len(subject_dirs): + subject_path = subject_dirs[idx] + subject_name = _prettify_name(subject_path.name) + formulas[subject_name] = {} + + tex_files = sorted(subject_path.glob("*.tex")) + file_idx = 0 + while file_idx < len(tex_files): + tex_file = tex_files[file_idx] + category_name = _prettify_name(tex_file.stem) + entries = [] + + with open(tex_file, "r", encoding="utf-8") as fh: + lines = fh.readlines() + + line_idx = 0 + while line_idx < len(lines): + line = lines[line_idx].strip() + if line and "|" in line: + parts = line.split("|", 1) + name = parts[0].strip() + latex = parts[1].strip() + if name and latex: + entries.append({"name": name, "latex": latex}) + line_idx += 1 + + if entries: + formulas[subject_name][category_name] = entries + + file_idx += 1 + + idx += 1 + + return formulas diff --git a/backend/api/migrations/0001_initial.py b/backend/api/migrations/0001_initial.py new file mode 100644 index 0000000..da86edf --- /dev/null +++ b/backend/api/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 6.0.3 on 2026-03-08 23:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CheatSheet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('latex_content', models.TextField(blank=True, default='')), + ('columns', models.IntegerField(default=2)), + ('margins', models.CharField(default='0.5in', max_length=20)), + ('font_size', models.CharField(default='10pt', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Template', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('subject', models.CharField(max_length=100)), + ('description', models.TextField(blank=True, default='')), + ('latex_content', models.TextField()), + ('default_columns', models.IntegerField(default=2)), + ('default_margins', models.CharField(default='0.5in', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='PracticeProblem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question_latex', models.TextField()), + ('answer_latex', models.TextField(blank=True, default='')), + ('order', models.IntegerField(default=0)), + ('cheat_sheet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problems', to='api.cheatsheet')), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.AddField( + model_name='cheatsheet', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.template'), + ), + ] diff --git a/backend/api/migrations/__init__.py b/backend/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/models.py b/backend/api/models.py index c8c4634..ad6058d 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -1 +1,108 @@ -# This app does not define any Django models yet. +from django.db import models + + +class Template(models.Model): + name = models.CharField(max_length=200) + subject = models.CharField(max_length=100) + description = models.TextField(blank=True, default="") + latex_content = models.TextField() + default_columns = models.IntegerField(default=2) + default_margins = models.CharField(max_length=20, default="0.5in") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + + +class CheatSheet(models.Model): + title = models.CharField(max_length=200) + latex_content = models.TextField(blank=True, default="") + template = models.ForeignKey( + Template, on_delete=models.SET_NULL, null=True, blank=True + ) + columns = models.IntegerField(default=2) + margins = models.CharField(max_length=20, default="0.5in") + font_size = models.CharField(max_length=10, default="10pt") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title + + def build_full_latex(self): + """ + Build a complete LaTeX document from the cheat sheet's content. + If the content already contains \\begin{document}, return it as-is. + Otherwise, wrap it in a proper document structure. + """ + content = self.latex_content or "" + + # If it's already a complete document, return as-is + if r"\begin{document}" in content: + return content + + # Build document header + header = [ + "\\documentclass{article}", + "\\usepackage[utf8]{inputenc}", + "\\usepackage{amsmath, amssymb}", + f"\\usepackage[a4paper, margin={self.margins}]{{geometry}}", + ] + + # Add font size if specified + if self.font_size and self.font_size != "10pt": + header[0] = f"\\documentclass[{self.font_size}]{{article}}" + + # Add multicolumn support if needed + if self.columns > 1: + header.append("\\usepackage{multicol}") + + # Start document + document_parts = header + ["\\begin{document}"] + + # Add title if exists + if self.title: + document_parts.append(f"\\title{{{self.title}}}") + document_parts.append("\\maketitle") + + # Add multicolumn environment if needed + if self.columns > 1: + document_parts.append(f"\\begin{{multicols}}{{{self.columns}}}") + + # Add main content + document_parts.append(content) + + # Add practice problems if they exist + problems = self.problems.all() + if problems: + document_parts.append("\\section*{Practice Problems}") + for problem in problems: + document_parts.append(f"\\textbf{{Problem {problem.order}:}} {problem.question_latex}") + if problem.answer_latex: + document_parts.append(f"\\textbf{{Answer:}} {problem.answer_latex}") + document_parts.append("") # Add spacing + + # Close multicolumn environment if needed + if self.columns > 1: + document_parts.append("\\end{multicols}") + + # End document + document_parts.append("\\end{document}") + + return "\n".join(document_parts) + + +class PracticeProblem(models.Model): + cheat_sheet = models.ForeignKey( + CheatSheet, on_delete=models.CASCADE, related_name="problems" + ) + question_latex = models.TextField() + answer_latex = models.TextField(blank=True, default="") + order = models.IntegerField(default=0) + + class Meta: + ordering = ["order"] + + def __str__(self): + return f"Problem {self.order} - {self.cheat_sheet.title}" diff --git a/backend/api/serializers.py b/backend/api/serializers.py index c7141ff..fbbc09c 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -1 +1,73 @@ # DRF serializers for the backend API will be added here. +from rest_framework import serializers +from .models import Template, CheatSheet, PracticeProblem + + +class TemplateSerializer(serializers.ModelSerializer): + class Meta: + model = Template + fields = [ + "id", + "name", + "subject", + "description", + "latex_content", + "default_margins", + "default_columns", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "created_at", "updated_at"] + + +class PracticeProblemSerializer(serializers.ModelSerializer): + class Meta: + model = PracticeProblem + fields = [ + "id", + "cheat_sheet", + "question_latex", + "answer_latex", + "order", + ] + read_only_fields = ["id"] + + +class CheatSheetSerializer(serializers.ModelSerializer): + problems = PracticeProblemSerializer(many=True, read_only=True) + full_latex = serializers.SerializerMethodField() + + class Meta: + model = CheatSheet + fields = [ + "id", + "title", + "template", + "latex_content", + "margins", + "columns", + "font_size", + "problems", + "full_latex", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "created_at", "updated_at", "full_latex"] + + def get_full_latex(self, obj): + """Return the fully-assembled LaTeX document string.""" + return obj.build_full_latex() + + +class CompileRequestSerializer(serializers.Serializer): + """Accepts either raw content OR a cheat_sheet id to compile.""" + + content = serializers.CharField(required=False, default="") + cheat_sheet_id = serializers.IntegerField(required=False, default=None) + + def validate(self, data): + if not data.get("content") and not data.get("cheat_sheet_id"): + raise serializers.ValidationError( + "Provide either 'content' or 'cheat_sheet_id'." + ) + return data diff --git a/backend/api/tests.py b/backend/api/tests.py new file mode 100644 index 0000000..cc752eb --- /dev/null +++ b/backend/api/tests.py @@ -0,0 +1,253 @@ +""" +Backend tests using pytest-django. +Run with: pytest (from the backend/ directory) +""" + +import pytest +from django.test import TestCase +from rest_framework.test import APIClient +from api.models import Template, CheatSheet, PracticeProblem + + +@pytest.fixture +def api_client(): + return APIClient() + + +@pytest.fixture +def sample_template(db): + return Template.objects.create( + name="Test Algebra", + subject="algebra", + description="A test template", + latex_content="\\section*{Test}\nHello World", + default_margins="0.5in", + default_columns=2, + ) + + +@pytest.fixture +def sample_sheet(db, sample_template): + return CheatSheet.objects.create( + title="My Test Sheet", + template=sample_template, + latex_content="Some content here", + margins="0.75in", + columns=2, + font_size="10pt", + ) + + +@pytest.fixture +def sample_problem(db, sample_sheet): + return PracticeProblem.objects.create( + cheat_sheet=sample_sheet, + question_latex="What is $2 + 2$?", + answer_latex="$4$", + order=1, + ) + + +# ── Model Tests ────────────────────────────────────────────────────── + + +class TestTemplateModel(TestCase): + def test_str_representation(self): + t = Template.objects.create( + name="Algebra Basics", + subject="algebra", + latex_content="\\section{Algebra}", + ) + assert "Algebra" in str(t) + assert "Algebra Basics" in str(t) + + +class TestCheatSheetModel(TestCase): + def test_build_full_latex_wraps_content(self): + sheet = CheatSheet.objects.create( + title="Test", + latex_content="Hello World", + margins="1in", + columns=1, + font_size="10pt", + ) + full = sheet.build_full_latex() + assert "\\begin{document}" in full + assert "\\end{document}" in full + assert "Hello World" in full + assert "margin=1in" in full + + def test_build_full_latex_multicolumn(self): + sheet = CheatSheet.objects.create( + title="Multi-col", + latex_content="Col content", + columns=3, + ) + full = sheet.build_full_latex() + assert "\\usepackage{multicol}" in full + assert "\\begin{multicols}{3}" in full + + def test_build_full_latex_passthrough(self): + raw = "\\documentclass{article}\n\\begin{document}\nCustom\n\\end{document}" + sheet = CheatSheet.objects.create( + title="Raw", + latex_content=raw, + ) + assert sheet.build_full_latex() == raw + + def test_build_full_latex_with_problems(self): + sheet = CheatSheet.objects.create( + title="With Problems", + latex_content="Content", + ) + PracticeProblem.objects.create( + cheat_sheet=sheet, + question_latex="What is $1+1$?", + answer_latex="$2$", + order=1, + ) + full = sheet.build_full_latex() + assert "Practice Problems" in full + assert "What is $1+1$?" in full + assert "$2$" in full + + +# ── API Tests ──────────────────────────────────────────────────────── + + +@pytest.mark.django_db +class TestHealthEndpoint: + def test_health_returns_ok(self, api_client): + resp = api_client.get("/api/health/") + assert resp.status_code == 200 + assert resp.json()["status"] == "ok" + + +@pytest.mark.django_db +class TestTemplateAPI: + def test_list_templates(self, api_client, sample_template): + resp = api_client.get("/api/templates/") + assert resp.status_code == 200 + data = resp.json() + assert len(data) >= 1 + assert data[0]["name"] == "Test Algebra" + + def test_filter_templates_by_subject(self, api_client, sample_template): + resp = api_client.get("/api/templates/?subject=algebra") + assert resp.status_code == 200 + data = resp.json() + assert len(data) >= 1 + + def test_create_template(self, api_client): + resp = api_client.post( + "/api/templates/", + { + "name": "New Template", + "subject": "calculus", + "latex_content": "\\section{Calc}", + }, + format="json", + ) + assert resp.status_code == 201 + + +@pytest.mark.django_db +class TestCheatSheetAPI: + def test_list_cheatsheets(self, api_client, sample_sheet): + resp = api_client.get("/api/cheatsheets/") + assert resp.status_code == 200 + + def test_create_cheatsheet(self, api_client): + resp = api_client.post( + "/api/cheatsheets/", + { + "title": "Brand New Sheet", + "latex_content": "Hello", + "margins": "1in", + "columns": 1, + "font_size": "12pt", + }, + format="json", + ) + assert resp.status_code == 201 + assert resp.json()["title"] == "Brand New Sheet" + assert "full_latex" in resp.json() + + def test_retrieve_cheatsheet_has_full_latex(self, api_client, sample_sheet): + resp = api_client.get(f"/api/cheatsheets/{sample_sheet.id}/") + assert resp.status_code == 200 + data = resp.json() + assert "\\begin{document}" in data["full_latex"] + + def test_update_cheatsheet(self, api_client, sample_sheet): + resp = api_client.patch( + f"/api/cheatsheets/{sample_sheet.id}/", + {"margins": "0.25in", "columns": 3}, + format="json", + ) + assert resp.status_code == 200 + assert resp.json()["margins"] == "0.25in" + assert resp.json()["columns"] == 3 + + def test_delete_cheatsheet(self, api_client, sample_sheet): + resp = api_client.delete(f"/api/cheatsheets/{sample_sheet.id}/") + assert resp.status_code == 204 + + +@pytest.mark.django_db +class TestCreateFromTemplate: + def test_create_from_template(self, api_client, sample_template): + resp = api_client.post( + "/api/cheatsheets/from-template/", + {"template_id": sample_template.id, "title": "My Copy"}, + format="json", + ) + assert resp.status_code == 201 + data = resp.json() + assert data["title"] == "My Copy" + assert data["template"] == sample_template.id + assert data["columns"] == sample_template.default_columns + + def test_create_from_template_missing_id(self, api_client): + resp = api_client.post( + "/api/cheatsheets/from-template/", + {"title": "Oops"}, + format="json", + ) + assert resp.status_code == 400 + + +@pytest.mark.django_db +class TestPracticeProblemAPI: + def test_create_problem(self, api_client, sample_sheet): + resp = api_client.post( + "/api/problems/", + { + "cheat_sheet": sample_sheet.id, + "question_latex": "What is $3+3$?", + "answer_latex": "$6$", + "order": 1, + }, + format="json", + ) + assert resp.status_code == 201 + + def test_filter_problems_by_sheet(self, api_client, sample_problem, sample_sheet): + resp = api_client.get(f"/api/problems/?cheat_sheet={sample_sheet.id}") + assert resp.status_code == 200 + assert len(resp.json()) >= 1 + + +@pytest.mark.django_db +class TestCompileEndpoint: + def test_compile_requires_content_or_id(self, api_client): + resp = api_client.post("/api/compile/", {}, format="json") + assert resp.status_code == 400 + + def test_compile_with_nonexistent_sheet(self, api_client): + resp = api_client.post( + "/api/compile/", + {"cheat_sheet_id": 99999}, + format="json", + ) + assert resp.status_code == 404 diff --git a/backend/api/urls.py b/backend/api/urls.py index 8b29267..222a17c 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -3,5 +3,16 @@ urlpatterns = [ path("health/", views.health_check, name="health-check"), + path("classes/", views.get_classes, name="get-classes"), + path("generate-sheet/", views.generate_sheet, name="generate-sheet"), path("compile/", views.compile_latex, name="compile-latex"), + + # CRUD endpoints + path("templates/", views.template_list, name="template-list"), + path("templates//", views.template_detail, name="template-detail"), + path("cheatsheets/", views.cheatsheet_list, name="cheatsheet-list"), + path("cheatsheets/from-template/", views.cheatsheet_from_template, name="cheatsheet-from-template"), + path("cheatsheets//", views.cheatsheet_detail, name="cheatsheet-detail"), + path("problems/", views.problem_list, name="problem-list"), + path("problems//", views.problem_detail, name="problem-detail"), ] diff --git a/backend/api/views.py b/backend/api/views.py index eb3b2d5..a5634e3 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,51 +1,347 @@ from rest_framework.decorators import api_view from rest_framework.response import Response +from rest_framework import status from django.http import FileResponse +from django.shortcuts import get_object_or_404 import subprocess import tempfile import os +from .models import Template, CheatSheet, PracticeProblem +from .serializers import TemplateSerializer, CheatSheetSerializer, PracticeProblemSerializer +from .formula_data import get_formula_data, get_classes_with_details + + +# ------------------------------------------------------------------ +# LaTeX document template with all packages +# ------------------------------------------------------------------ +LATEX_HEADER = r"""\documentclass[fleqn]{article} +\usepackage[margin=0.15in]{geometry} +\usepackage{amsmath, amssymb} +\usepackage{enumitem} +\usepackage{multicol} +\usepackage{titlesec} + +\setlength{\mathindent}{0pt} +\setlist[itemize]{noitemsep, topsep=0pt, leftmargin=*} +\pagestyle{empty} + +\titlespacing*{\subsection}{0pt}{2pt}{1pt} +\titlespacing*{\section}{0pt}{4pt}{2pt} + +\begin{document} +\scriptsize +""" + +LATEX_FOOTER = r""" +\end{document} +""" + + +def _build_latex_for_formulas(selected_formulas): + """ + Given a list of selected formulas (each with class_name, category, name, latex), + build a complete LaTeX document. + """ + body_lines = [] + + # Group formulas by class + by_class = {} + for formula in selected_formulas: + class_name = formula.get("class_name") or formula.get("class") + if class_name not in by_class: + by_class[class_name] = {} + + category = formula.get("category") + if category not in by_class[class_name]: + by_class[class_name][category] = [] + + by_class[class_name][category].append(formula) + + # Build LaTeX for each class + for class_name, categories in by_class.items(): + body_lines.append("\\section*{" + class_name + "}") + body_lines.append("") + + for category_name, formulas in categories.items(): + body_lines.append("\\subsection*{" + category_name + "}") + body_lines.append("") + body_lines.append(r"\begin{flushleft}") + + for formula in formulas: + name = formula.get("name", "") + latex = formula.get("latex", "") + body_lines.append("\\textbf{" + name + "}") + body_lines.append("\\[ " + latex + " \\]") + body_lines.append("\\\\[4pt]") + + body_lines.append(r"\end{flushleft}") + body_lines.append("") + + body = "\n".join(body_lines) + return LATEX_HEADER + body + LATEX_FOOTER + + +# ------------------------------------------------------------------ +# API endpoints +# ------------------------------------------------------------------ + @api_view(["GET"]) def health_check(request): return Response({"status": "ok"}) + +@api_view(["GET"]) +def get_classes(request): + """ + GET /api/classes/ + Returns full structure with classes, categories, and formulas. + Used by frontend to build 3-level selection UI. + """ + classes_with_details = get_classes_with_details() + return Response({"classes": classes_with_details}) + + +@api_view(["POST"]) +def generate_sheet(request): + """ + POST /api/generate-sheet/ + Accepts { "formulas": [...] } + Each formula: { "class": "ALGEBRA I", "category": "Linear Equations", "name": "Slope Formula" } + Or can also use: { "class_name", "category", "name" } + Returns { "tex_code": "..." } + """ + selected = request.data.get("formulas", []) + + if not selected: + return Response({"error": "No formulas selected"}, status=400) + + # Get formula details from formula_data + formula_data = get_formula_data() + selected_formulas = [] + + for sel in selected: + class_name = sel.get("class") or sel.get("class_name") + category = sel.get("category") + name = sel.get("name") + + if class_name in formula_data: + categories = formula_data[class_name] + if category in categories: + formulas = categories[category] + for f in formulas: + if f.get("name") == name: + selected_formulas.append({ + "class_name": class_name, + "category": category, + "name": f["name"], + "latex": f["latex"] + }) + + if not selected_formulas: + return Response({"error": "No valid formulas found"}, status=400) + + tex_code = _build_latex_for_formulas(selected_formulas) + return Response({"tex_code": tex_code}) + + @api_view(["POST"]) def compile_latex(request): + """ + POST /api/compile/ + Accepts either: + - { "content": "...full LaTeX code..." } + - { "cheat_sheet_id": 123 } + Compiles with Tectonic on the backend and returns the PDF. + """ content = request.data.get("content", "") + cheat_sheet_id = request.data.get("cheat_sheet_id") - # Simple boilerplate to ensure it's a valid document if user just provides text + # If cheat_sheet_id is provided, get content from the cheat sheet + if cheat_sheet_id: + cheatsheet = get_object_or_404(CheatSheet, pk=cheat_sheet_id) + content = cheatsheet.build_full_latex() + + if not content: + return Response({"error": "No LaTeX content provided"}, status=400) + + # Ensure document has proper structure if r"\begin{document}" not in content: - content = r"""\documentclass{article} -\usepackage[utf8]{inputenc} -\usepackage{amsmath, amssymb, geometry} -\geometry{a4paper, margin=1in} -\begin{document} -""" + content + r""" -\end{document}""" - - # Create a temporary directory to run tectonic + content = LATEX_HEADER + content + LATEX_FOOTER + with tempfile.TemporaryDirectory() as tempdir: tex_file_path = os.path.join(tempdir, "document.tex") with open(tex_file_path, "w", encoding="utf-8") as f: f.write(content) try: - # Run tectonic - # VScode will say result is unused but we need to capture it to check for errors, so ignore that warning - result = subprocess.run( + subprocess.run( ["tectonic", tex_file_path], cwd=tempdir, capture_output=True, text=True, - check=True + check=True, ) except subprocess.CalledProcessError as e: - return Response({"error": "Failed to compile LaTeX", "details": e.stderr}, status=400) - + return Response( + {"error": "Failed to compile LaTeX", "details": e.stderr}, + status=400, + ) + pdf_file_path = os.path.join(tempdir, "document.pdf") if os.path.exists(pdf_file_path): - response = FileResponse(open(pdf_file_path, "rb"), content_type="application/pdf") + response = FileResponse( + open(pdf_file_path, "rb"), content_type="application/pdf" + ) response["Content-Disposition"] = 'inline; filename="document.pdf"' return response else: return Response({"error": "PDF not generated"}, status=500) + + +# ------------------------------------------------------------------ +# CRUD API endpoints for Templates, CheatSheets, and Problems +# ------------------------------------------------------------------ + +@api_view(["GET", "POST"]) +def template_list(request): + """GET /api/templates/ - List all templates + POST /api/templates/ - Create a new template""" + if request.method == "GET": + subject = request.query_params.get("subject") + templates = Template.objects.all() + if subject: + templates = templates.filter(subject=subject) + serializer = TemplateSerializer(templates, many=True) + return Response(serializer.data) + + elif request.method == "POST": + serializer = TemplateSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(["GET", "PUT", "PATCH", "DELETE"]) +def template_detail(request, pk): + """GET/PUT/PATCH/DELETE /api/templates/{id}/""" + template = get_object_or_404(Template, pk=pk) + + if request.method == "GET": + serializer = TemplateSerializer(template) + return Response(serializer.data) + + elif request.method in ["PUT", "PATCH"]: + serializer = TemplateSerializer(template, data=request.data, partial=(request.method == "PATCH")) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + elif request.method == "DELETE": + template.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +@api_view(["GET", "POST"]) +def cheatsheet_list(request): + """GET /api/cheatsheets/ - List all cheat sheets + POST /api/cheatsheets/ - Create a new cheat sheet""" + if request.method == "GET": + cheatsheets = CheatSheet.objects.all() + serializer = CheatSheetSerializer(cheatsheets, many=True) + return Response(serializer.data) + + elif request.method == "POST": + serializer = CheatSheetSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(["GET", "POST"]) +def cheatsheet_from_template(request): + """POST /api/cheatsheets/from-template/ - Create cheat sheet from template""" + template_id = request.data.get("template_id") + title = request.data.get("title", "Untitled") + + if not template_id: + return Response({"error": "template_id is required"}, status=status.HTTP_400_BAD_REQUEST) + + template = get_object_or_404(Template, pk=template_id) + + cheatsheet = CheatSheet.objects.create( + title=title, + template=template, + latex_content=template.latex_content, + margins=template.default_margins, + columns=template.default_columns, + ) + + serializer = CheatSheetSerializer(cheatsheet) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +@api_view(["GET", "PUT", "PATCH", "DELETE"]) +def cheatsheet_detail(request, pk): + """GET/PUT/PATCH/DELETE /api/cheatsheets/{id}/""" + cheatsheet = get_object_or_404(CheatSheet, pk=pk) + + if request.method == "GET": + serializer = CheatSheetSerializer(cheatsheet) + return Response(serializer.data) + + elif request.method in ["PUT", "PATCH"]: + serializer = CheatSheetSerializer(cheatsheet, data=request.data, partial=(request.method == "PATCH")) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + elif request.method == "DELETE": + cheatsheet.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +@api_view(["GET", "POST"]) +def problem_list(request): + """GET /api/problems/ - List problems (optionally filtered by cheat_sheet) + POST /api/problems/ - Create a new problem""" + if request.method == "GET": + cheat_sheet_id = request.query_params.get("cheat_sheet") + if cheat_sheet_id: + problems = PracticeProblem.objects.filter(cheat_sheet=cheat_sheet_id) + else: + problems = PracticeProblem.objects.all() + serializer = PracticeProblemSerializer(problems, many=True) + return Response(serializer.data) + + elif request.method == "POST": + serializer = PracticeProblemSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(["GET", "PUT", "PATCH", "DELETE"]) +def problem_detail(request, pk): + """GET/PUT/PATCH/DELETE /api/problems/{id}/""" + problem = get_object_or_404(PracticeProblem, pk=pk) + + if request.method == "GET": + serializer = PracticeProblemSerializer(problem) + return Response(serializer.data) + + elif request.method in ["PUT", "PATCH"]: + serializer = PracticeProblemSerializer(problem, data=request.data, partial=(request.method == "PATCH")) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + elif request.method == "DELETE": + problem.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/backend/cheat_sheet/settings.py b/backend/cheat_sheet/settings.py index 4a74640..2e90c30 100644 --- a/backend/cheat_sheet/settings.py +++ b/backend/cheat_sheet/settings.py @@ -6,6 +6,7 @@ import os from dotenv import load_dotenv from django.core.exceptions import ImproperlyConfigured +import dj_database_url BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(BASE_DIR / ".env") @@ -16,7 +17,6 @@ if not SECRET_KEY: if DEBUG: - # Development-only secret key. Do NOT use this in production. SECRET_KEY = "django-insecure-dev-secret-key-change-me" else: raise ImproperlyConfigured( @@ -27,7 +27,10 @@ ALLOWED_HOSTS = [ host for host in ( - h.strip() for h in os.getenv("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,0.0.0.0").split(",") + h.strip() + for h in os.getenv( + "DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,0.0.0.0" + ).split(",") ) if host ] @@ -77,15 +80,12 @@ WSGI_APPLICATION = "cheat_sheet.wsgi.application" +# Database — uses DATABASE_URL env var, falls back to SQLite for local dev DATABASES = { - "default": { - "ENGINE": os.getenv("DB_ENGINE", "django.db.backends.sqlite3"), - "NAME": os.getenv("DB_NAME", str(BASE_DIR / "db.sqlite3")), - "USER": os.getenv("DB_USER", ""), - "PASSWORD": os.getenv("DB_PASSWORD", ""), - "HOST": os.getenv("DB_HOST", ""), - "PORT": os.getenv("DB_PORT", ""), - } + "default": dj_database_url.config( + default="sqlite:///" + str(BASE_DIR / "db.sqlite3"), + conn_max_age=600, + ) } AUTH_PASSWORD_VALIDATORS = [ diff --git a/backend/conftest.py b/backend/conftest.py new file mode 100644 index 0000000..d6b18eb --- /dev/null +++ b/backend/conftest.py @@ -0,0 +1,12 @@ +""" +Root conftest for pytest-django. +Fixtures defined here are available to all test files. +""" + +import pytest +from rest_framework.test import APIClient + + +@pytest.fixture +def api_client(): + return APIClient() diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..6a3f47d --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +DJANGO_SETTINGS_MODULE = cheat_sheet.settings +python_files = tests.py test_*.py *_tests.py diff --git a/backend/requirements.txt b/backend/requirements.txt index a2cad0f..2507cd0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -2,4 +2,13 @@ django>=6.0,<7.0 djangorestframework>=3.15 django-cors-headers>=4.4 python-dotenv>=1.0,<2.0 +dj-database-url>=2.1 +mysqlclient>=2.2 +# Testing +pytest>=8.0 +pytest-django>=4.8 + +# Development (linting, security) +ruff>=0.4.0 +safety>=2.0 diff --git a/docker-compose.yml b/docker-compose.yml index 7d304aa..95b760a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,42 @@ services: + db: + image: mariadb:11 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_DATABASE: cheatsheet_db + MYSQL_USER: cheatsheet_user + MYSQL_PASSWORD: cheatsheet_pass + # ports: comment out for now we'e not using atm + # - "3306:3306" + volumes: + - mariadb_data:/var/lib/mysql backend: build: ./backend ports: - "8000:8000" env_file: - ./backend/.env.docker + environment: + - DATABASE_URL=mysql://cheatsheet_user:cheatsheet_pass@db:3306/cheatsheet_db volumes: - ./backend:/app - + depends_on: + - db + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/health/"] + interval: 10s + timeout: 5s + retries: 10 frontend: build: ./frontend ports: - "5173:5173" - environment: - - BACKEND_URL=http://backend:8000 volumes: - ./frontend:/app - /app/node_modules depends_on: - - backend + backend: + condition: service_healthy +volumes: + mariadb_data: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4c2c8ad..c694654 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,10 +15,6 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", - "eslint": "^9.15.0", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.14", - "globals": "^15.12.0", "vite": "^6.0.0" } }, @@ -746,215 +742,6 @@ "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1013,9 +800,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -1027,9 +814,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -1041,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -1055,9 +842,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -1069,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -1083,9 +870,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -1097,9 +884,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -1111,9 +898,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -1125,9 +912,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -1139,9 +926,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -1153,9 +940,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -1167,9 +954,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -1181,9 +968,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -1195,9 +982,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -1209,9 +996,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -1223,9 +1010,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -1237,9 +1024,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -1251,9 +1038,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -1265,9 +1052,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -1279,9 +1066,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -1293,9 +1080,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -1307,9 +1094,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -1321,9 +1108,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -1335,9 +1122,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -1349,9 +1136,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1414,13 +1201,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1470,95 +1250,17 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/browserslist": { @@ -1595,20 +1297,10 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001770", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", - "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "version": "1.0.30001776", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", + "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", "dev": true, "funding": [ { @@ -1626,50 +1318,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1677,21 +1325,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1717,17 +1350,10 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "dev": true, "license": "ISC" }, @@ -1783,471 +1409,72 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": ">=12.0.0" }, "peerDependencies": { - "jiti": "*" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "jiti": { + "picomatch": { "optional": true } } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=6" } }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -2257,53 +1484,6 @@ "node": ">=6" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2326,19 +1506,6 @@ "yallist": "^3.0.2" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2365,103 +1532,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2483,9 +1560,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -2511,26 +1588,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -2566,20 +1623,10 @@ "node": ">=0.10.0" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -2593,31 +1640,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -2640,29 +1687,6 @@ "semver": "bin/semver.js" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2673,32 +1697,6 @@ "node": ">=0.10.0" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2716,19 +1714,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2760,16 +1745,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -2845,51 +1820,12 @@ } } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/frontend/package.json b/frontend/package.json index 1903c5f..a6d8bf9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,10 +16,6 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", - "eslint": "^9.15.0", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.14", - "globals": "^15.12.0", "vite": "^6.0.0" } } diff --git a/frontend/src/App.css b/frontend/src/App.css index b75905f..20884f3 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,120 +1,70 @@ +:root { + --bg: #18181c; + --text: #f2f2f2; + --panel-bg: #23232b; + --border: #333; + --primary: #3b82f6; + --card-bg: #23232b; + --box-bg: #222127; + --input-bg: #18181c; + --input-border: #444550; + --input-text: #dbeaff; + --btn-primary: #3b82f6; + --btn-primary-hover: #2563eb; + --btn-download: #22c55e; + --btn-download-hover: #16a34a; + --btn-clear: #ef4444; + --btn-clear-hover: #b91c1c; + --btn-preview: #a78bfa; + --btn-preview-hover: #7c3aed; +} + .App { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; + max-width: 100%; + margin: 0; + padding: 0.5rem; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } .create-cheat-sheet { - background: white; - padding: 2rem; + background: var(--panel-bg); + padding: 0.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 2rem; } -.subjects-container { - margin-bottom: 20px; -} - -.subject-tabs { - display: flex; - margin-bottom: 10px; - border-bottom: 1px solid #ddd; -} - -.subject-tab { - padding: 8px 16px; - background: #f0f2f5; - border: none; - border-radius: 4px 4px 0 0; - margin-right: 5px; - cursor: pointer; - font-weight: 600; - color: #333; -} - -.subject-tab.active { - background: #e2e8f0; -} - -.category-tabs { - display: flex; - gap: 10px; - margin-bottom: 15px; - flex-wrap: wrap; -} - -.category-tab { - padding: 6px 12px; - background: white; - border: 1px solid #ddd; - border-radius: 4px; - cursor: pointer; - color: #666; -} - -.category-tab.active { - background: #5b9bd5; - border-color: #5b9bd5; - color: white; -} - -.formulas-container { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-bottom: 20px; -} - -.formula-btn { - padding: 8px 16px; - background: white; - border: 1px solid #ccc; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-size: 0.95rem; - transition: background 0.2s; -} - -.formula-btn:hover { - background: #f9f9f9; -} - .create-cheat-sheet h2 { margin-top: 0; margin-bottom: 1.5rem; - color: #333; + color: var(--text); } .editor-container { display: flex !important; flex-direction: row !important; - gap: 2rem; - margin: 1rem 0; + gap: 1rem; + margin: 0.5rem 0; width: 100%; } .input-section { flex: 1; - min-width: 45%; } .preview-section { flex: 1; - min-width: 45%; background: transparent; padding: 0; } .textarea-field { width: 100%; - min-height: 500px; /* Use min-height so it can also grow if needed */ + min-height: 1000px; padding: 1rem; font-family: 'Consolas', monospace; font-size: 14px; - border: 1px solid #ddd; + border: 1px solid var(--border); border-radius: 4px; resize: vertical; box-sizing: border-box; @@ -124,7 +74,7 @@ width: 100%; padding: 0.8rem; margin-bottom: 1.5rem; - border: 1px solid #ddd; + border: 1px solid var(--border); border-radius: 4px; font-size: 1.1rem; } @@ -153,41 +103,225 @@ } .btn.primary { - background-color: #3498db; + background-color: var(--btn-primary); color: white; } .btn.primary:hover { - background-color: #2980b9; + background-color: var(--btn-primary-hover); } .btn.download { - background-color: #27ae60; + background-color: var(--btn-download); color: white; } .btn.download:hover { - background-color: #2ecc71; + background-color: var(--btn-download-hover); } .btn.clear { - background-color: #e74c3c; + background-color: var(--btn-clear); color: white; } .btn.clear:hover { - background-color: #c0392b; + background-color: var(--btn-clear-hover); } .preview-box { - background: white; - padding: 2rem; - border: 1px solid #ddd; + background: var(--box-bg); + padding: 1rem; + border: 1px solid var(--border); border-radius: 4px; - min-height: 500px; - height: 100%; /* Fill height when container is flexed or taller */ - overflow-y: visible; /* Let it grow with content */ + min-height: 1000px; + height: 100%; + overflow-y: visible; text-align: left; width: 100%; box-sizing: border-box; -} \ No newline at end of file +} + +/* ---- Class selection checkboxes ---- */ +.class-checkboxes { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1rem; + margin-top: 0.5rem; +} + +.class-checkbox-label { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.5rem 1rem; + background: var(--panel-bg); + border: 2px solid var(--border); + border-radius: 6px; + cursor: pointer; + font-weight: 500; + font-size: 0.95rem; + transition: all 0.15s ease; + user-select: none; +} + +.class-checkbox-label:hover { + border-color: #3498db; + background: var(--card-bg); +} + +.class-checkbox-label.checked { + border-color: #3498db; + background: #222a34; + color: #2980b9; + font-weight: 600; +} + +.category-checkbox-label { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.4rem 0.8rem; + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.15s ease; + user-select: none; +} + +.category-checkbox-label:hover { + border-color: #3498db; + background: var(--card-bg); +} + +.category-checkbox-label.checked { + border-color: #3498db; + background: #222a34; + color: #2980b9; +} + +.category-checkbox-label input[type="checkbox"] { + accent-color: #3498db; + width: 14px; + height: 14px; +} + +.generate-btn { + margin-top: 0.5rem; + font-size: 1rem; + padding: 0.7rem 1.5rem; +} + +.generate-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn.preview { + background-color: var(--btn-preview); + color: white; +} + +.btn.preview:hover { + background-color: var(--btn-preview-hover); +} + +.btn.preview:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* ---- Circular compile button ---- */ +.compile-circle { + width: 50px; + height: 50px; + border-radius: 50%; + background-color: var(--btn-preview); + color: white; + font-size: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + align-self: center; + padding: 0; + border: none; + cursor: pointer; + transition: background-color 0.2s, transform 0.2s; +} + +.compile-circle:hover:not(:disabled) { + background-color: var(--btn-preview-hover); + transform: rotate(90deg); +} + +.compile-circle:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* ---- Formula selection ---- */ +.formula-selection { + margin-bottom: 1.5rem; + padding: 1rem; + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: 8px; +} + +/* ---- Class + Dropdown Selection ---- */ +.category-dropdowns { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid #e2e8f0; +} + +.class-category-section { + margin-bottom: 1rem; +} + +.class-category-label { + display: block; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text); +} + +.category-checkboxes { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.input-section label, +.preview-section label { + display: block; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text); +} + +/* ---- Footer ---- */ +.app-footer { + text-align: center; + padding: 1rem; + margin-top: 1rem; +} + +.app-footer a { + color: #666; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + border-radius: 50%; + transition: background-color 0.2s, color 0.2s; +} + +.app-footer a:hover { + color: var(--text); + background-color: #f0f0f0; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0edaea1..a2e6eb4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -27,7 +27,7 @@ function App() { return (
-

Cheat Sheet Manager

+

Cheat Sheet Generator

Write cheat sheets with LaTeX support

@@ -37,6 +37,13 @@ function App() { onCancel={() => {}} />
+
); } diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index 9589f71..76009d1 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -1,49 +1,29 @@ -import React, { useState, useEffect } from 'react'; - -const mathFormulas = { - Algebra: { - "Linear Eq.": [ - { name: "Slope-Intercept", latex: "y = mx + b" }, - { name: "Point-Slope", latex: "y - y_1 = m(x - x_1)" }, - { name: "Standard Form", latex: "Ax + By = C" } - ], - "Quadratic Eq.": [ - { name: "Quadratic Formula", latex: "x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}" }, - { name: "Vertex Form", latex: "y = a(x-h)^2 + k" }, - { name: "Standard Form", latex: "y = ax^2 + bx + c" } - ], - "Exponents": [ - { name: "Product Rule", latex: "x^a \\cdot x^b = x^{a+b}" }, - { name: "Quotient Rule", latex: "\\frac{x^a}{x^b} = x^{a-b}" }, - { name: "Power Rule", latex: "(x^a)^b = x^{ab}" }, - { name: "Negative Exponent", latex: "x^{-a} = \\frac{1}{x^a}" } - ], - "Logarithms": [ - { name: "Product Rule", latex: "\\log_b(xy) = \\log_b(x) + \\log_b(y)" }, - { name: "Quotient Rule", latex: "\\log_b(\\frac{x}{y}) = \\log_b(x) - \\log_b(y)" }, - { name: "Power Rule", latex: "\\log_b(x^k) = k \\log_b(x)" }, - { name: "Change of Base", latex: "\\log_b(x) = \\frac{\\log_c(x)}{\\log_c(b)}" } - ] - }, - Geometry: { - "Area": [ - { name: "Circle", latex: "A = \\pi r^2" }, - { name: "Triangle", latex: "A = \\frac{1}{2}bh" }, - { name: "Rectangle", latex: "A = lw" } - ] - } -}; +import React, { useState, useEffect, useRef } from 'react'; const CreateCheatSheet = ({ onSave, initialData }) => { const [title, setTitle] = useState(initialData ? initialData.title : ''); const [content, setContent] = useState(initialData ? initialData.content : ''); const [pdfBlob, setPdfBlob] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [isCompiling, setIsCompiling] = useState(false); + const isCompilingRef = useRef(false); - // States for formula selector - const [activeSubject, setActiveSubject] = useState('Algebra'); - const [activeCategory, setActiveCategory] = useState('Quadratic Eq.'); + // Formula selection state + const [classesData, setClassesData] = useState([]); + const [selectedClasses, setSelectedClasses] = useState({}); // { "ClassName": true } + const [selectedCategories, setSelectedCategories] = useState({}); // { "ClassName:CategoryName": true } + const [isGenerating, setIsGenerating] = useState(false); + const isGeneratingRef = useRef(false); + // Fetch the full class/category/formula structure from backend + useEffect(() => { + fetch('/api/classes/') + .then((res) => res.json()) + .then((data) => { + setClassesData(data.classes || []); + }) + .catch((err) => console.error('Failed to fetch classes', err)); + }, []); useEffect(() => { if (initialData) { @@ -52,42 +32,132 @@ const CreateCheatSheet = ({ onSave, initialData }) => { } }, [initialData]); - const handleSubmit = (e) => { - e.preventDefault(); - onSave({ title, content }); + // Toggle class selection + const toggleClass = (className) => { + setSelectedClasses((prev) => { + const newSelected = { ...prev }; + if (newSelected[className]) { + delete newSelected[className]; + // Clear categories for this class + Object.keys(selectedCategories).forEach((key) => { + if (key.startsWith(className + ':')) { + delete newSelected[key]; + } + }); + } else { + newSelected[className] = true; + } + return newSelected; + }); }; - const handlePreview = async () => { - setIsLoading(true); + // Toggle category selection + const toggleCategory = (className, categoryName) => { + const key = `${className}:${categoryName}`; + setSelectedCategories((prev) => { + const newSelected = { ...prev }; + if (newSelected[key]) { + delete newSelected[key]; + } else { + newSelected[key] = true; + } + return newSelected; + }); + }; + + // Get selected formulas for API + const getSelectedFormulasList = () => { + const formulas = []; + + // For each selected class and category, get all formulas + classesData.forEach((cls) => { + if (!selectedClasses[cls.name]) return; + + cls.categories.forEach((cat) => { + const key = `${cls.name}:${cat.name}`; + if (selectedCategories[key]) { + cat.formulas.forEach((f) => { + formulas.push({ + class: cls.name, + category: cat.name, + name: f.name + }); + }); + } + }); + }); + + return formulas; + }; + + // Generate LaTeX from selected formulas + const handleGenerateSheet = async () => { + if (isGeneratingRef.current) return; + + const selectedList = getSelectedFormulasList(); + if (selectedList.length === 0) { + alert('Please select at least one category first.'); + return; + } + + isGeneratingRef.current = true; + setIsGenerating(true); try { - const response = await fetch('http://localhost:8000/api/compile/', { + const response = await fetch('/api/generate-sheet/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content }) + body: JSON.stringify({ formulas: selectedList }), }); - if (!response.ok) { - throw new Error('Failed to compile LaTeX'); - } + if (!response.ok) throw new Error('Failed to generate sheet'); + const data = await response.json(); + setContent(data.tex_code); + setPdfBlob(null); + handlePreview(data.tex_code); + } catch (error) { + console.error('Error generating sheet:', error); + alert('Failed to generate LaTeX. Is the backend running?'); + } finally { + setIsGenerating(false); + isGeneratingRef.current = false; + } + }; + + // Send the current LaTeX code to Tectonic for PDF preview + const handlePreview = async (latexContent = null) => { + if (isCompilingRef.current) return; + + const contentToCompile = latexContent || content; + if (!contentToCompile) return; + + isCompilingRef.current = true; + setIsCompiling(true); + try { + const response = await fetch('/api/compile/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: contentToCompile }), + }); + if (!response.ok) throw new Error('Failed to compile LaTeX'); const blob = await response.blob(); setPdfBlob(URL.createObjectURL(blob)); } catch (error) { console.error('Error generating PDF:', error); alert('Failed to generate PDF. Please check the backend service.'); } finally { - setIsLoading(false); + setIsCompiling(false); + isCompilingRef.current = false; } }; const handleDownloadPDF = async () => { + setIsLoading(true); try { - const response = await fetch('http://localhost:8000/api/compile/', { + const response = await fetch('/api/compile/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content }) + body: JSON.stringify({ content }), }); - if (!response.ok) { - throw new Error('Failed to compile LaTeX'); - } + if (!response.ok) throw new Error('Failed to compile LaTeX'); const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -96,29 +166,55 @@ const CreateCheatSheet = ({ onSave, initialData }) => { document.body.appendChild(a); a.click(); document.body.removeChild(a); + URL.revokeObjectURL(url); } catch (error) { console.error('Error generating PDF:', error); alert('Failed to generate PDF. Check console for details.'); + } finally { + setIsLoading(false); + } + }; + + const handleDownloadTex = () => { + if (!content) { + alert('No LaTeX code to download. Generate a sheet first.'); + return; } + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${title || 'cheat-sheet'}.tex`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const handleSave = (e) => { + e.preventDefault(); + onSave({ title, content }); }; const handleClear = () => { - if (window.confirm('Are you sure you want to clear the editor? This cannot be undone.')) { + if (window.confirm('Are you sure you want to clear everything? This cannot be undone.')) { setTitle(''); setContent(''); setPdfBlob(null); + setSelectedClasses({}); + setSelectedCategories({}); onSave({ title: '', content: '' }, false); } }; - const insertFormula = (formulaLatex) => { - setContent(prevContent => prevContent + (prevContent.endsWith('\n') ? '' : '\n') + `\\[ ${formulaLatex} \\]\n`); - }; + const selectedCount = getSelectedFormulasList().length; + const hasSelectedClasses = Object.keys(selectedClasses).length > 0; return (
-

Cheat Sheet Editor

-
+ + + {/* Title */}
{ id="title" value={title} onChange={(e) => setTitle(e.target.value)} + placeholder="My Math Cheat Sheet" required className="input-field" />
-
-
- {Object.keys(mathFormulas).map(subject => ( - - ))} + {/* Class Selection with Dropdowns */} +
+ + + {/* Class Checkboxes */} +
+ {classesData.map((cls) => { + const isChecked = !!selectedClasses[cls.name]; + return ( + + ); + })}
- {activeSubject && mathFormulas[activeSubject] && ( -
- {Object.keys(mathFormulas[activeSubject]).map(category => ( - - ))} + {/* Category Dropdowns for selected classes */} + {hasSelectedClasses && ( +
+ + + {classesData.map((cls) => { + if (!selectedClasses[cls.name]) return null; + + return ( +
+ +
+ {cls.categories.map((cat) => { + const key = `${cls.name}:${cat.name}`; + const isChecked = !!selectedCategories[key]; + return ( + + ); + })} +
+
+ ); + })}
)} - {activeSubject && activeCategory && mathFormulas[activeSubject][activeCategory] && ( -
- {mathFormulas[activeSubject][activeCategory].map(formula => ( - - ))} -
+ {/* Generate button */} + + + {selectedCount > 0 && ( +

+ {selectedCount} formula(s) will be included +

)}
- + + {/* Editor + Preview */}
- +