From f3d9bfd311cd878e40c8f88f8a9f07931305f9c5 Mon Sep 17 00:00:00 2001 From: Kamisato Date: Thu, 5 Mar 2026 20:57:20 -0800 Subject: [PATCH 01/11] adding and updating a bunch of files with new requirements and started adding cheat_sheet code blocks --- backend/api/admin.py | 28 ++++ backend/api/apps.py | 6 + backend/api/models.py | 45 ++++++- backend/api/serializers.py | 72 +++++++++++ backend/api/tests.py | 253 +++++++++++++++++++++++++++++++++++++ backend/api/urls.py | 14 +- backend/api/views.py | 121 +++++++++++++++--- 7 files changed, 518 insertions(+), 21 deletions(-) create mode 100644 backend/api/admin.py create mode 100644 backend/api/apps.py create mode 100644 backend/api/tests.py diff --git a/backend/api/admin.py b/backend/api/admin.py new file mode 100644 index 0000000..9cb95b6 --- /dev/null +++ b/backend/api/admin.py @@ -0,0 +1,28 @@ +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/models.py b/backend/api/models.py index c8c4634..3fa3ac3 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -1 +1,44 @@ -# 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() + columns = models.IntegerField(default=2) + margin = models.CharField(max_length=20, default="0.5in") + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name + + +class CheatSheet(models.Model): + title = models.CharField(max_length=200) + content = models.TextField(blank=True, default="") + template = models.ForeignKey( + Template, on_delete=models.SET_NULL, null=True, blank=True + ) + columns = models.IntegerField(default=2) + margin = 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.title + + +class PracticeProblem(models.Model): + cheat_sheet = models.ForeignKey( + CheatSheet, on_delete=models.CASCADE, related_name="problems" + ) + question = models.TextField() + answer = 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..74d0151 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): + practice_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", + "practice_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..e3c5966 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -1,7 +1,19 @@ -from django.urls import path +from django.urls import path, include +from rest_framework.routers import DefaultRouter from . import views +router = DefaultRouter() +router.register(r"templates", views.TemplateViewSet, basename="template") +router.register(r"cheatsheets", views.CheatSheetViewSet, basename="cheatsheet") +router.register(r"problems", views.PracticeProblemViewSet, basename="problem") + urlpatterns = [ path("health/", views.health_check, name="health-check"), path("compile/", views.compile_latex, name="compile-latex"), + path( + "cheatsheets/from-template/", + views.create_from_template, + name="create-from-template", + ), + path("", include(router.urls)), ] diff --git a/backend/api/views.py b/backend/api/views.py index eb3b2d5..4dcb2c4 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,51 +1,134 @@ from rest_framework.decorators import api_view from rest_framework.response import Response +from rest_framework import viewsets, status from django.http import FileResponse import subprocess import tempfile import os +from .models import Template, CheatSheet, PracticeProblem +from .serializers import ( + TemplateSerializer, + CheatSheetSerializer, + PracticeProblemSerializer, + CompileRequestSerializer, +) + + +# --------------------------------------------------------------------------- +# Standalone function-based views +# --------------------------------------------------------------------------- + @api_view(["GET"]) def health_check(request): return Response({"status": "ok"}) + @api_view(["POST"]) def compile_latex(request): - content = request.data.get("content", "") - - # Simple boilerplate to ensure it's a valid document if user just provides text + """Compile raw LaTeX content or a saved CheatSheet into a PDF.""" + serializer = CompileRequestSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + content = serializer.validated_data.get("content", "") + cheat_sheet_id = serializer.validated_data.get("cheat_sheet_id") + + # Build from a saved cheat sheet if an id was provided + if cheat_sheet_id: + try: + sheet = CheatSheet.objects.get(id=cheat_sheet_id) + content = sheet.build_full_latex() + except CheatSheet.DoesNotExist: + return Response( + {"error": "CheatSheet not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + # Wrap raw content if it isn't already a full document 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 = ( + "\\documentclass{article}\n" + "\\usepackage[utf8]{inputenc}\n" + "\\usepackage{amsmath, amssymb, geometry}\n" + "\\geometry{a4paper, margin=1in}\n" + "\\begin{document}\n" + + content + + "\n\\end{document}" + ) + 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( ["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) + + +@api_view(["POST"]) +def create_from_template(request): + """Create a new CheatSheet pre-populated from a Template.""" + template_id = request.data.get("template_id") + title = request.data.get("title", "Untitled Cheat Sheet") + + try: + template = Template.objects.get(id=template_id) + except Template.DoesNotExist: + return Response( + {"error": "Template not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + cheat_sheet = CheatSheet.objects.create( + title=title, + latex_content=template.latex_content, + template=template, + columns=template.default_columns, + margins=template.default_margins, + ) + + out = CheatSheetSerializer(cheat_sheet) + return Response(out.data, status=status.HTTP_201_CREATED) + + +# --------------------------------------------------------------------------- +# ViewSets (registered via the DefaultRouter in urls.py) +# --------------------------------------------------------------------------- + +class TemplateViewSet(viewsets.ModelViewSet): + """CRUD for pre-made Templates.""" + queryset = Template.objects.all() + serializer_class = TemplateSerializer + + +class CheatSheetViewSet(viewsets.ModelViewSet): + """CRUD for user CheatSheets.""" + queryset = CheatSheet.objects.all() + serializer_class = CheatSheetSerializer + + +class PracticeProblemViewSet(viewsets.ModelViewSet): + """CRUD for practice problems attached to a CheatSheet.""" + queryset = PracticeProblem.objects.all() + serializer_class = PracticeProblemSerializer From 99fe247c31040c65b4393644417741aeeed7ff7c Mon Sep 17 00:00:00 2001 From: Kamisato Date: Thu, 5 Mar 2026 21:22:32 -0800 Subject: [PATCH 02/11] changed requirements.txt and added CreateCheatSheet.jsx --- backend/.env.docker | 6 +- backend/api/formula_loader.py | 66 ++++++++++++++++++++++ backend/api/urls.py | 15 +---- backend/api/views.py | 99 +++++++-------------------------- backend/cheat_sheet/__init__.py | 2 + backend/cheat_sheet/settings.py | 20 +++---- backend/conftest.py | 12 ++++ backend/pytest.ini | 3 + backend/requirements.txt | 5 +- 9 files changed, 124 insertions(+), 104 deletions(-) create mode 100644 backend/api/formula_loader.py create mode 100644 backend/conftest.py create mode 100644 backend/pytest.ini diff --git a/backend/.env.docker b/backend/.env.docker index f91313c..a3d64bf 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 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/api/formula_loader.py b/backend/api/formula_loader.py new file mode 100644 index 0000000..5a386b6 --- /dev/null +++ b/backend/api/formula_loader.py @@ -0,0 +1,66 @@ +""" +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 +""" + +import os +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/urls.py b/backend/api/urls.py index e3c5966..dee8f55 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -1,19 +1,8 @@ -from django.urls import path, include -from rest_framework.routers import DefaultRouter +from django.urls import path from . import views -router = DefaultRouter() -router.register(r"templates", views.TemplateViewSet, basename="template") -router.register(r"cheatsheets", views.CheatSheetViewSet, basename="cheatsheet") -router.register(r"problems", views.PracticeProblemViewSet, basename="problem") - urlpatterns = [ path("health/", views.health_check, name="health-check"), path("compile/", views.compile_latex, name="compile-latex"), - path( - "cheatsheets/from-template/", - views.create_from_template, - name="create-from-template", - ), - path("", include(router.urls)), + path("formulas/", views.get_formulas, name="get-formulas"), ] diff --git a/backend/api/views.py b/backend/api/views.py index 4dcb2c4..49ef44c 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,50 +1,39 @@ from rest_framework.decorators import api_view from rest_framework.response import Response -from rest_framework import viewsets, status from django.http import FileResponse import subprocess import tempfile import os -from .models import Template, CheatSheet, PracticeProblem -from .serializers import ( - TemplateSerializer, - CheatSheetSerializer, - PracticeProblemSerializer, - CompileRequestSerializer, -) +from .formula_loader import load_all_formulas -# --------------------------------------------------------------------------- -# Standalone function-based views -# --------------------------------------------------------------------------- - @api_view(["GET"]) def health_check(request): return Response({"status": "ok"}) -@api_view(["POST"]) -def compile_latex(request): - """Compile raw LaTeX content or a saved CheatSheet into a PDF.""" - serializer = CompileRequestSerializer(data=request.data) - serializer.is_valid(raise_exception=True) +@api_view(["GET"]) +def get_formulas(request): + """GET /api/formulas/ — optionally filter with ?subject=Algebra""" + all_formulas = load_all_formulas() - content = serializer.validated_data.get("content", "") - cheat_sheet_id = serializer.validated_data.get("cheat_sheet_id") + subject_filter = request.query_params.get("subject") + if subject_filter: + key_lookup = {k.lower(): k for k in all_formulas} + match_key = key_lookup.get(subject_filter.lower()) + if match_key: + return Response({match_key: all_formulas[match_key]}) + return Response({}) - # Build from a saved cheat sheet if an id was provided - if cheat_sheet_id: - try: - sheet = CheatSheet.objects.get(id=cheat_sheet_id) - content = sheet.build_full_latex() - except CheatSheet.DoesNotExist: - return Response( - {"error": "CheatSheet not found"}, - status=status.HTTP_404_NOT_FOUND, - ) + return Response(all_formulas) - # Wrap raw content if it isn't already a full document + +@api_view(["POST"]) +def compile_latex(request): + content = request.data.get("content", "") + + # Simple boilerplate to ensure it's a valid document if user just provides text if r"\begin{document}" not in content: content = ( "\\documentclass{article}\n" @@ -56,12 +45,14 @@ def compile_latex(request): + "\n\\end{document}" ) + # Create a temporary directory to run tectonic 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 result = subprocess.run( ["tectonic", tex_file_path], cwd=tempdir, @@ -84,51 +75,3 @@ def compile_latex(request): return response else: return Response({"error": "PDF not generated"}, status=500) - - -@api_view(["POST"]) -def create_from_template(request): - """Create a new CheatSheet pre-populated from a Template.""" - template_id = request.data.get("template_id") - title = request.data.get("title", "Untitled Cheat Sheet") - - try: - template = Template.objects.get(id=template_id) - except Template.DoesNotExist: - return Response( - {"error": "Template not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - cheat_sheet = CheatSheet.objects.create( - title=title, - latex_content=template.latex_content, - template=template, - columns=template.default_columns, - margins=template.default_margins, - ) - - out = CheatSheetSerializer(cheat_sheet) - return Response(out.data, status=status.HTTP_201_CREATED) - - -# --------------------------------------------------------------------------- -# ViewSets (registered via the DefaultRouter in urls.py) -# --------------------------------------------------------------------------- - -class TemplateViewSet(viewsets.ModelViewSet): - """CRUD for pre-made Templates.""" - queryset = Template.objects.all() - serializer_class = TemplateSerializer - - -class CheatSheetViewSet(viewsets.ModelViewSet): - """CRUD for user CheatSheets.""" - queryset = CheatSheet.objects.all() - serializer_class = CheatSheetSerializer - - -class PracticeProblemViewSet(viewsets.ModelViewSet): - """CRUD for practice problems attached to a CheatSheet.""" - queryset = PracticeProblem.objects.all() - serializer_class = PracticeProblemSerializer diff --git a/backend/cheat_sheet/__init__.py b/backend/cheat_sheet/__init__.py index e69de29..063cd2c 100644 --- a/backend/cheat_sheet/__init__.py +++ b/backend/cheat_sheet/__init__.py @@ -0,0 +1,2 @@ +import pymysql +pymysql.install_as_MySQLdb() 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..af95133 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -2,4 +2,7 @@ django>=6.0,<7.0 djangorestframework>=3.15 django-cors-headers>=4.4 python-dotenv>=1.0,<2.0 - +dj-database-url>=2.1 +pymysql>=1.1 +pytest>=8.0 +pytest-django>=4.8 From acc4abc795ef69150763d80173d13b5cf862cb4d Mon Sep 17 00:00:00 2001 From: Kamisato Date: Thu, 5 Mar 2026 22:36:04 -0800 Subject: [PATCH 03/11] updated front end to remove hard coded formulas --- backend/api/urls.py | 3 +- backend/api/views.py | 182 +++++- docker-compose.yml | 20 + frontend/package-lock.json | 629 ++++++++++++++----- frontend/package.json | 4 +- frontend/src/App.css | 83 ++- frontend/src/components/CreateCheatSheet.jsx | 277 ++++---- 7 files changed, 916 insertions(+), 282 deletions(-) diff --git a/backend/api/urls.py b/backend/api/urls.py index dee8f55..696d05b 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -3,6 +3,7 @@ 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"), - path("formulas/", views.get_formulas, name="get-formulas"), ] diff --git a/backend/api/views.py b/backend/api/views.py index 49ef44c..c601b60 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -5,8 +5,146 @@ import tempfile import os -from .formula_loader import load_all_formulas +# ------------------------------------------------------------------ +# All formula blocks live here in the backend. The frontend never +# stores any LaTeX. Each class maps to a dict of categories, and +# each category is a list of { name, latex } entries. +# ------------------------------------------------------------------ +FORMULA_DATA = { + "Algebra": { + "Linear Equations": [ + {"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 Equations": [ + {"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\\left(\\frac{x}{y}\\right) = \\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"}, + ], + "Perimeter": [ + {"name": "Circle (Circumference)", "latex": "C = 2\\pi r"}, + {"name": "Rectangle", "latex": "P = 2l + 2w"}, + ], + "Volume": [ + {"name": "Sphere", "latex": "V = \\frac{4}{3}\\pi r^3"}, + {"name": "Cylinder", "latex": "V = \\pi r^2 h"}, + {"name": "Cone", "latex": "V = \\frac{1}{3}\\pi r^2 h"}, + ], + }, + "Calculus": { + "Derivatives": [ + {"name": "Power Rule", "latex": "\\frac{d}{dx} x^n = nx^{n-1}"}, + {"name": "Product Rule", "latex": "\\frac{d}{dx}[fg] = f'g + fg'"}, + {"name": "Quotient Rule", "latex": "\\frac{d}{dx}\\left[\\frac{f}{g}\\right] = \\frac{f'g - fg'}{g^2}"}, + {"name": "Chain Rule", "latex": "\\frac{d}{dx} f(g(x)) = f'(g(x)) \\cdot g'(x)"}, + ], + "Integrals": [ + {"name": "Power Rule", "latex": "\\int x^n \\, dx = \\frac{x^{n+1}}{n+1} + C"}, + {"name": "Substitution", "latex": "\\int f(g(x)) g'(x) \\, dx = \\int f(u) \\, du"}, + ], + "Limits": [ + {"name": "Definition", "latex": "\\lim_{x \\to a} f(x) = L"}, + {"name": "L'Hopital's Rule", "latex": "\\lim_{x \\to a} \\frac{f(x)}{g(x)} = \\lim_{x \\to a} \\frac{f'(x)}{g'(x)}"}, + ], + }, + "Trigonometry": { + "Basic Identities": [ + {"name": "Pythagorean", "latex": "\\sin^2\\theta + \\cos^2\\theta = 1"}, + {"name": "Tangent", "latex": "\\tan\\theta = \\frac{\\sin\\theta}{\\cos\\theta}"}, + ], + "Common Values": [ + {"name": "sin(0)", "latex": "\\sin(0) = 0"}, + {"name": "sin(pi/6)", "latex": "\\sin\\left(\\frac{\\pi}{6}\\right) = \\frac{1}{2}"}, + {"name": "sin(pi/4)", "latex": "\\sin\\left(\\frac{\\pi}{4}\\right) = \\frac{\\sqrt{2}}{2}"}, + {"name": "sin(pi/3)", "latex": "\\sin\\left(\\frac{\\pi}{3}\\right) = \\frac{\\sqrt{3}}{2}"}, + {"name": "sin(pi/2)", "latex": "\\sin\\left(\\frac{\\pi}{2}\\right) = 1"}, + ], + "Double Angle": [ + {"name": "sin(2x)", "latex": "\\sin(2\\theta) = 2\\sin\\theta\\cos\\theta"}, + {"name": "cos(2x)", "latex": "\\cos(2\\theta) = \\cos^2\\theta - \\sin^2\\theta"}, + ], + }, +} + + +def _build_latex_for_classes(selected_classes): + """ + Given a list of class names (e.g. ["Algebra", "Calculus"]), + build a complete LaTeX document string with all their formula blocks. + """ + body_lines = [] + + idx = 0 + while idx < len(selected_classes): + class_name = selected_classes[idx] + categories = FORMULA_DATA.get(class_name) + if categories is None: + idx += 1 + continue + + body_lines.append("\\section{" + class_name + "}") + body_lines.append("") + + cat_names = list(categories.keys()) + cat_idx = 0 + while cat_idx < len(cat_names): + cat_name = cat_names[cat_idx] + formulas = categories[cat_name] + + body_lines.append("\\subsection{" + cat_name + "}") + body_lines.append("") + + f_idx = 0 + while f_idx < len(formulas): + formula = formulas[f_idx] + body_lines.append("\\textbf{" + formula["name"] + "}") + body_lines.append("\\[ " + formula["latex"] + " \\]") + body_lines.append("") + f_idx += 1 + + cat_idx += 1 + + idx += 1 + + body = "\n".join(body_lines) + + tex = ( + "\\documentclass{article}\n" + "\\usepackage[utf8]{inputenc}\n" + "\\usepackage{amsmath, amssymb}\n" + "\\usepackage[a4paper, margin=1in]{geometry}\n" + "\\begin{document}\n\n" + + body + + "\n\\end{document}" + ) + + return tex + + +# ------------------------------------------------------------------ +# API endpoints +# ------------------------------------------------------------------ @api_view(["GET"]) def health_check(request): @@ -14,26 +152,40 @@ def health_check(request): @api_view(["GET"]) -def get_formulas(request): - """GET /api/formulas/ — optionally filter with ?subject=Algebra""" - all_formulas = load_all_formulas() +def get_classes(request): + """ + GET /api/classes/ + Returns the list of available class names for the dropdown. + """ + class_names = list(FORMULA_DATA.keys()) + return Response({"classes": class_names}) + + +@api_view(["POST"]) +def generate_sheet(request): + """ + POST /api/generate-sheet/ + Accepts { "classes": ["Algebra", "Calculus"] } + Returns { "tex_code": "\\documentclass{article}..." } + """ + selected = request.data.get("classes", []) - subject_filter = request.query_params.get("subject") - if subject_filter: - key_lookup = {k.lower(): k for k in all_formulas} - match_key = key_lookup.get(subject_filter.lower()) - if match_key: - return Response({match_key: all_formulas[match_key]}) - return Response({}) + if not selected: + return Response({"error": "No classes selected"}, status=400) - return Response(all_formulas) + tex_code = _build_latex_for_classes(selected) + return Response({"tex_code": tex_code}) @api_view(["POST"]) def compile_latex(request): + """ + POST /api/compile/ + Accepts { "content": "...full LaTeX code..." } + Compiles with Tectonic on the backend and returns the PDF. + """ content = request.data.get("content", "") - # Simple boilerplate to ensure it's a valid document if user just provides text if r"\begin{document}" not in content: content = ( "\\documentclass{article}\n" @@ -45,15 +197,13 @@ def compile_latex(request): + "\n\\end{document}" ) - # Create a temporary directory to run tectonic 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 - result = subprocess.run( + subprocess.run( ["tectonic", tex_file_path], cwd=tempdir, capture_output=True, diff --git a/docker-compose.yml b/docker-compose.yml index 7d304aa..11bde3c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,29 @@ 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: + - "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 frontend: build: ./frontend @@ -19,3 +36,6 @@ services: - /app/node_modules depends_on: - backend + +volumes: + mariadb_data: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4c2c8ad..46ab9fe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,10 @@ "name": "cheat-sheet-frontend", "version": "0.0.0", "dependencies": { + "axios": "^1.7.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.0" }, "devDependencies": { "@types/react": "^18.3.12", @@ -830,20 +832,20 @@ } }, "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==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "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", + "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" }, "engines": { @@ -867,9 +869,9 @@ } }, "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==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -1013,9 +1015,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 +1029,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 +1043,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 +1057,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 +1071,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 +1085,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 +1099,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 +1113,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 +1127,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 +1141,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 +1155,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 +1169,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 +1183,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 +1197,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 +1211,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 +1225,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 +1239,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 +1253,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 +1267,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 +1281,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 +1295,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 +1309,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 +1323,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 +1337,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 +1351,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" ], @@ -1471,9 +1473,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -1494,9 +1496,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1533,6 +1535,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1541,13 +1560,16 @@ "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" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/brace-expansion": { @@ -1595,6 +1617,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1606,9 +1641,9 @@ } }, "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": [ { @@ -1663,6 +1698,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1677,6 +1724,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1724,13 +1784,81 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "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" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -1797,9 +1925,9 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { @@ -1809,7 +1937,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2057,12 +2185,48 @@ } }, "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==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2078,6 +2242,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2088,6 +2261,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2114,6 +2324,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2124,6 +2346,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2326,10 +2587,40 @@ "yallist": "^3.0.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2373,9 +2664,9 @@ "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" }, @@ -2483,9 +2774,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": [ { @@ -2521,6 +2812,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2566,6 +2863,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2577,9 +2912,9 @@ } }, "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 +2928,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,6 +2975,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1903c5f..bc2cf50 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,8 +9,10 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.7.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.0" }, "devDependencies": { "@types/react": "^18.3.12", diff --git a/frontend/src/App.css b/frontend/src/App.css index b75905f..d24d11f 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -110,7 +110,7 @@ .textarea-field { width: 100%; - min-height: 500px; /* Use min-height so it can also grow if needed */ + min-height: 500px; padding: 1rem; font-family: 'Consolas', monospace; font-size: 14px; @@ -185,9 +185,84 @@ border: 1px solid #ddd; border-radius: 4px; min-height: 500px; - height: 100%; /* Fill height when container is flexed or taller */ - overflow-y: visible; /* Let it grow with content */ + height: 100%; + overflow-y: visible; text-align: left; width: 100%; box-sizing: border-box; -} \ No newline at end of file +} + +/* ---- Class selection checkboxes ---- */ +.class-selection { + margin-bottom: 1.5rem; + padding: 1rem; + background: #f8f9fa; + border: 1px solid #e2e8f0; + border-radius: 8px; +} + +.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: white; + border: 2px solid #ddd; + 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: #f0f7ff; +} + +.class-checkbox-label.checked { + border-color: #3498db; + background: #e8f4fd; + color: #2980b9; + font-weight: 600; +} + +.class-checkbox-label input[type="checkbox"] { + accent-color: #3498db; + width: 16px; + height: 16px; +} + +.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: #8e44ad; + color: white; +} + +.btn.preview:hover { + background-color: #7d3c98; +} + +.btn.preview:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index 9589f71..95ff81e 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -1,49 +1,26 @@ 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" } - ] - } -}; - 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); - // States for formula selector - const [activeSubject, setActiveSubject] = useState('Algebra'); - const [activeCategory, setActiveCategory] = useState('Quadratic Eq.'); + // Class selection state + const [availableClasses, setAvailableClasses] = useState([]); + const [selectedClasses, setSelectedClasses] = useState([]); + const [isGenerating, setIsGenerating] = useState(false); + // Fetch the list of available classes from the backend on mount + useEffect(() => { + fetch('/api/classes/') + .then((res) => res.json()) + .then((data) => { + setAvailableClasses(data.classes || []); + }) + .catch((err) => console.error('Failed to fetch classes', err)); + }, []); useEffect(() => { if (initialData) { @@ -52,42 +29,73 @@ const CreateCheatSheet = ({ onSave, initialData }) => { } }, [initialData]); - const handleSubmit = (e) => { - e.preventDefault(); - onSave({ title, content }); + // Toggle a class in the selected list + const toggleClass = (className) => { + setSelectedClasses((prev) => { + const alreadySelected = prev.indexOf(className) !== -1; + if (alreadySelected) { + return prev.filter((c) => c !== className); + } + return [...prev, className]; + }); + }; + + // Ask the backend to build the LaTeX code for the selected classes + const handleGenerateSheet = async () => { + if (selectedClasses.length === 0) { + alert('Please select at least one class first.'); + return; + } + + setIsGenerating(true); + try { + const response = await fetch('/api/generate-sheet/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ classes: selectedClasses }), + }); + if (!response.ok) throw new Error('Failed to generate sheet'); + const data = await response.json(); + setContent(data.tex_code); + // Clear any old preview since the code changed + setPdfBlob(null); + } catch (error) { + console.error('Error generating sheet:', error); + alert('Failed to generate LaTeX. Is the backend running?'); + } finally { + setIsGenerating(false); + } }; + // Send the current LaTeX code to Tectonic for PDF preview const handlePreview = async () => { - setIsLoading(true); + setIsCompiling(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(); 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); } }; 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 +104,53 @@ 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); + } + }; + + // Download the .tex source code directly + 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([]); onSave({ title: '', content: '' }, false); } }; - const insertFormula = (formulaLatex) => { - setContent(prevContent => prevContent + (prevContent.endsWith('\n') ? '' : '\n') + `\\[ ${formulaLatex} \\]\n`); - }; - return (
-

Cheat Sheet Editor

-
+

Cheat Sheet Generator

+ + + {/* 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 => ( - - ))} + {/* Step 1: Class Selection */} +
+ +
+ {availableClasses.map((cls) => { + const isChecked = selectedClasses.indexOf(cls) !== -1; + return ( + + ); + })}
- {activeSubject && mathFormulas[activeSubject] && ( -
- {Object.keys(mathFormulas[activeSubject]).map(category => ( - - ))} -
- )} + {/* Step 2: Generate button */} + - {activeSubject && activeCategory && mathFormulas[activeSubject][activeCategory] && ( -
- {mathFormulas[activeSubject][activeCategory].map(formula => ( - - ))} -
+ {selectedClasses.length > 0 && ( +

+ Selected: {selectedClasses.join(', ')} +

)}
- + + {/* Editor + Preview */}
+ {/* Left: LaTeX code output (editable so users can tweak) */}
- +