diff --git a/backend-api/alembic/versions/j1k2l3m4n567_add_contact_us_tables.py b/backend-api/alembic/versions/j1k2l3m4n567_add_contact_us_tables.py new file mode 100644 index 00000000..2049f04d --- /dev/null +++ b/backend-api/alembic/versions/j1k2l3m4n567_add_contact_us_tables.py @@ -0,0 +1,78 @@ +"""add contact us tables + +Revision ID: j1k2l3m4n567 +Revises: h1i2j3k4l567 +Create Date: 2026-01-20 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = "j1k2l3m4n567" +down_revision: Union[str, Sequence[str], None] = "h1i2j3k4l567" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "contact_submissions", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("first_name", sa.String(length=100), nullable=False), + sa.Column("last_name", sa.String(length=100), nullable=False), + sa.Column("email", sa.String(length=255), nullable=False), + sa.Column("phone", sa.String(length=20), nullable=True), + sa.Column("company", sa.String(length=255), nullable=True), + sa.Column("subject", sa.String(length=50), nullable=False), + sa.Column("message", sa.Text(), nullable=False), + sa.Column("status", sa.String(length=20), server_default=sa.text("'new'"), nullable=False), + sa.Column("priority", sa.String(length=20), server_default=sa.text("'medium'"), nullable=False), + sa.Column("assigned_to", sa.Integer(), nullable=True), + sa.Column("source", sa.String(length=50), nullable=True), + sa.Column("ip_address", postgresql.INET(), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), + sa.Column("resolved_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(["assigned_to"], ["user.id"]), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_table( + "submission_notes", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("submission_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("admin_user_id", sa.Integer(), nullable=True), + sa.Column("note", sa.Text(), nullable=False), + sa.Column("is_internal", sa.Boolean(), server_default=sa.text("true"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), + sa.ForeignKeyConstraint(["admin_user_id"], ["user.id"]), + sa.ForeignKeyConstraint(["submission_id"], ["contact_submissions.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_table( + "submission_history", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("submission_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("admin_user_id", sa.Integer(), nullable=True), + sa.Column("action", sa.String(length=50), nullable=False), + sa.Column("field_name", sa.String(length=100), nullable=True), + sa.Column("old_value", sa.Text(), nullable=True), + sa.Column("new_value", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), + sa.ForeignKeyConstraint(["admin_user_id"], ["user.id"]), + sa.ForeignKeyConstraint(["submission_id"], ["contact_submissions.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade() -> None: + op.drop_table("submission_history") + op.drop_table("submission_notes") + op.drop_table("contact_submissions") diff --git a/backend-api/app/api/v1/contact.py b/backend-api/app/api/v1/contact.py new file mode 100644 index 00000000..4af0ae7c --- /dev/null +++ b/backend-api/app/api/v1/contact.py @@ -0,0 +1,271 @@ +"""Contact Us API endpoints.""" + +from datetime import datetime +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.permissions import require_admin +from app.db.session import get_async_session +from app.models.contact import ContactSubmission, SubmissionHistory, SubmissionNote +from app.models.user import User +from app.schemas.contact import ( + ContactSubmissionCreate, + ContactSubmissionRead, + ContactSubmissionUpdate, + SubmissionHistoryRead, + SubmissionNoteCreate, + SubmissionNoteRead, +) + +router = APIRouter(prefix="/contact", tags=["Contact"]) + + +def _build_history_entry( + submission_id: UUID, + admin_user_id: int | None, + action: str, + field_name: str | None, + old_value: str | None, + new_value: str | None, +) -> SubmissionHistory: + return SubmissionHistory( + submission_id=submission_id, + admin_user_id=admin_user_id, + action=action, + field_name=field_name, + old_value=old_value, + new_value=new_value, + ) + + +@router.post("/", response_model=ContactSubmissionRead, status_code=status.HTTP_201_CREATED) +async def create_contact_submission( + payload: ContactSubmissionCreate, + request: Request, + db: AsyncSession = Depends(get_async_session), +) -> ContactSubmission: + """Create a new Contact Us submission (public).""" + submission = ContactSubmission( + first_name=payload.first_name, + last_name=payload.last_name, + email=payload.email, + phone=payload.phone, + company=payload.company, + subject=payload.subject, + message=payload.message, + source=payload.source, + ip_address=request.client.host if request.client else None, + ) + db.add(submission) + await db.flush() + db.add( + _build_history_entry( + submission_id=submission.id, + admin_user_id=None, + action="create", + field_name=None, + old_value=None, + new_value=None, + ) + ) + await db.commit() + await db.refresh(submission) + return submission + + +@router.get("/submissions", response_model=list[ContactSubmissionRead]) +async def list_submissions( + _: User = Depends(require_admin), + db: AsyncSession = Depends(get_async_session), +) -> list[ContactSubmission]: + """List all contact submissions (admin only).""" + result = await db.execute( + select(ContactSubmission).order_by(ContactSubmission.created_at.desc()) + ) + return list(result.scalars().all()) + + +@router.get("/submissions/{submission_id}", response_model=ContactSubmissionRead) +async def get_submission( + submission_id: UUID, + _: User = Depends(require_admin), + db: AsyncSession = Depends(get_async_session), +) -> ContactSubmission: + """Get a single submission (admin only).""" + result = await db.execute( + select(ContactSubmission).where(ContactSubmission.id == submission_id) + ) + submission = result.scalar_one_or_none() + if not submission: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Submission not found") + return submission + + +@router.patch("/submissions/{submission_id}", response_model=ContactSubmissionRead) +async def update_submission( + submission_id: UUID, + payload: ContactSubmissionUpdate, + admin_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_async_session), +) -> ContactSubmission: + """Update a submission (admin only).""" + result = await db.execute( + select(ContactSubmission).where(ContactSubmission.id == submission_id) + ) + submission = result.scalar_one_or_none() + if not submission: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Submission not found") + + history_entries: list[SubmissionHistory] = [] + + def track_change(field: str, old: str | None, new: str | None) -> None: + if old != new: + history_entries.append( + _build_history_entry( + submission_id=submission.id, + admin_user_id=admin_user.id, + action="update", + field_name=field, + old_value=old, + new_value=new, + ) + ) + + if "status" in payload.model_fields_set: + if payload.status is None: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="status cannot be null") + track_change("status", submission.status, payload.status) + new_status = payload.status.lower() + submission.status = payload.status + if new_status == "resolved": + submission.resolved_at = datetime.utcnow() + elif submission.resolved_at is not None: + submission.resolved_at = None + + if "priority" in payload.model_fields_set: + if payload.priority is None: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="priority cannot be null") + track_change("priority", submission.priority, payload.priority) + submission.priority = payload.priority + + if "assigned_to" in payload.model_fields_set: + if payload.assigned_to is not None: + user_result = await db.execute( + select(User).where(User.id == payload.assigned_to) + ) + assigned_user = user_result.scalar_one_or_none() + if not assigned_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="assigned_to user not found", + ) + track_change( + "assigned_to", + str(submission.assigned_to) if submission.assigned_to is not None else None, + str(payload.assigned_to) if payload.assigned_to is not None else None, + ) + submission.assigned_to = payload.assigned_to + + if "resolved_at" in payload.model_fields_set: + track_change( + "resolved_at", + submission.resolved_at.isoformat() if submission.resolved_at else None, + payload.resolved_at.isoformat() if payload.resolved_at else None, + ) + submission.resolved_at = payload.resolved_at + + for entry in history_entries: + db.add(entry) + + await db.commit() + await db.refresh(submission) + return submission + + +@router.delete("/submissions/{submission_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_submission( + submission_id: UUID, + _: User = Depends(require_admin), + db: AsyncSession = Depends(get_async_session), +) -> None: + """Delete a submission (admin only).""" + result = await db.execute( + select(ContactSubmission).where(ContactSubmission.id == submission_id) + ) + submission = result.scalar_one_or_none() + if not submission: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Submission not found") + + await db.delete(submission) + await db.commit() + + +@router.get("/submissions/{submission_id}/notes", response_model=list[SubmissionNoteRead]) +async def list_notes( + submission_id: UUID, + _: User = Depends(require_admin), + db: AsyncSession = Depends(get_async_session), +) -> list[SubmissionNote]: + result = await db.execute( + select(SubmissionNote) + .where(SubmissionNote.submission_id == submission_id) + .order_by(SubmissionNote.created_at.desc()) + ) + return list(result.scalars().all()) + + +@router.post( + "/submissions/{submission_id}/notes", + response_model=SubmissionNoteRead, + status_code=status.HTTP_201_CREATED, +) +async def add_note( + submission_id: UUID, + payload: SubmissionNoteCreate, + admin_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_async_session), +) -> SubmissionNote: + result = await db.execute( + select(ContactSubmission).where(ContactSubmission.id == submission_id) + ) + submission = result.scalar_one_or_none() + if not submission: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Submission not found") + + note = SubmissionNote( + submission_id=submission_id, + admin_user_id=admin_user.id, + note=payload.note, + is_internal=payload.is_internal, + ) + db.add(note) + db.add( + _build_history_entry( + submission_id=submission_id, + admin_user_id=admin_user.id, + action="note", + field_name="note", + old_value=None, + new_value=payload.note, + ) + ) + await db.commit() + await db.refresh(note) + return note + + +@router.get("/submissions/{submission_id}/history", response_model=list[SubmissionHistoryRead]) +async def list_history( + submission_id: UUID, + _: User = Depends(require_admin), + db: AsyncSession = Depends(get_async_session), +) -> list[SubmissionHistory]: + result = await db.execute( + select(SubmissionHistory) + .where(SubmissionHistory.submission_id == submission_id) + .order_by(SubmissionHistory.created_at.desc()) + ) + return list(result.scalars().all()) diff --git a/backend-api/app/api/v1/router.py b/backend-api/app/api/v1/router.py index c59a8758..0a06f296 100644 --- a/backend-api/app/api/v1/router.py +++ b/backend-api/app/api/v1/router.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from app.api.v1 import auth, test, evidence, m365_connections, scans, benchmarks, platforms +from app.api.v1 import auth, test, evidence, m365_connections, scans, benchmarks, platforms, contact api_router = APIRouter() @@ -23,3 +23,6 @@ # Evidence routes api_router.include_router(evidence.router) + +# Contact routes +api_router.include_router(contact.router) diff --git a/backend-api/app/main.py b/backend-api/app/main.py index 1c8710a9..ec9e1add 100644 --- a/backend-api/app/main.py +++ b/backend-api/app/main.py @@ -5,7 +5,6 @@ from app.core.config import get_settings from app.core.middleware import RequestLoggingMiddleware from app.core.errors import not_found_handler, NotFound - settings = get_settings() diff --git a/backend-api/app/models/__init__.py b/backend-api/app/models/__init__.py index 9e6e8e80..004d1684 100644 --- a/backend-api/app/models/__init__.py +++ b/backend-api/app/models/__init__.py @@ -10,6 +10,7 @@ from app.models.scan_result import ScanResult from app.models.compliance import Scan from app.models.evidence_validation import EvidenceValidation +from app.models.contact import ContactSubmission, SubmissionNote, SubmissionHistory __all__ = [ "User", @@ -23,4 +24,7 @@ "ScanResult", "Scan", "EvidenceValidation", + "ContactSubmission", + "SubmissionNote", + "SubmissionHistory", ] diff --git a/backend-api/app/models/contact.py b/backend-api/app/models/contact.py new file mode 100644 index 00000000..42abb963 --- /dev/null +++ b/backend-api/app/models/contact.py @@ -0,0 +1,108 @@ +"""Models for Contact Us submissions and related notes/history.""" + +from __future__ import annotations + +from datetime import datetime +from typing import TYPE_CHECKING +from uuid import UUID, uuid4 + +from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, text +from sqlalchemy.dialects.postgresql import INET, UUID as PG_UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.sql import func + +from app.db.base import Base + +if TYPE_CHECKING: + from app.models.user import User + + +class ContactSubmission(Base): + __tablename__ = "contact_submissions" + + id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid4) + first_name: Mapped[str] = mapped_column(String(100), nullable=False) + last_name: Mapped[str] = mapped_column(String(100), nullable=False) + email: Mapped[str] = mapped_column(String(255), nullable=False) + phone: Mapped[str | None] = mapped_column(String(20)) + company: Mapped[str | None] = mapped_column(String(255)) + subject: Mapped[str] = mapped_column(String(50), nullable=False) + message: Mapped[str] = mapped_column(Text, nullable=False) + status: Mapped[str] = mapped_column( + String(20), + server_default=text("'new'"), + nullable=False, + ) + priority: Mapped[str] = mapped_column( + String(20), + server_default=text("'medium'"), + nullable=False, + ) + assigned_to: Mapped[int | None] = mapped_column(ForeignKey("user.id")) + source: Mapped[str | None] = mapped_column(String(50)) + ip_address: Mapped[str | None] = mapped_column(INET) + + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime, server_default=func.now(), onupdate=func.now() + ) + resolved_at: Mapped[datetime | None] = mapped_column(DateTime) + + assigned_user: Mapped["User | None"] = relationship( + back_populates="assigned_submissions" + ) + notes: Mapped[list["SubmissionNote"]] = relationship( + back_populates="submission", + cascade="all, delete-orphan", + passive_deletes=True, + ) + history: Mapped[list["SubmissionHistory"]] = relationship( + back_populates="submission", + cascade="all, delete-orphan", + passive_deletes=True, + ) + + +class SubmissionNote(Base): + __tablename__ = "submission_notes" + + id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid4) + submission_id: Mapped[UUID] = mapped_column( + PG_UUID(as_uuid=True), + ForeignKey("contact_submissions.id", ondelete="CASCADE"), + nullable=False, + ) + admin_user_id: Mapped[int | None] = mapped_column(ForeignKey("user.id")) + note: Mapped[str] = mapped_column(Text, nullable=False) + is_internal: Mapped[bool] = mapped_column( + Boolean, + server_default=text("true"), + nullable=False, + ) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime, server_default=func.now(), onupdate=func.now() + ) + + submission: Mapped["ContactSubmission"] = relationship(back_populates="notes") + admin_user: Mapped["User | None"] = relationship(back_populates="submission_notes") + + +class SubmissionHistory(Base): + __tablename__ = "submission_history" + + id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid4) + submission_id: Mapped[UUID] = mapped_column( + PG_UUID(as_uuid=True), + ForeignKey("contact_submissions.id", ondelete="CASCADE"), + nullable=False, + ) + admin_user_id: Mapped[int | None] = mapped_column(ForeignKey("user.id")) + action: Mapped[str] = mapped_column(String(50), nullable=False) + field_name: Mapped[str | None] = mapped_column(String(100)) + old_value: Mapped[str | None] = mapped_column(Text) + new_value: Mapped[str | None] = mapped_column(Text) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + + submission: Mapped["ContactSubmission"] = relationship(back_populates="history") + admin_user: Mapped["User | None"] = relationship(back_populates="submission_history") diff --git a/backend-api/app/models/user.py b/backend-api/app/models/user.py index d409ba64..211d3689 100644 --- a/backend-api/app/models/user.py +++ b/backend-api/app/models/user.py @@ -11,6 +11,7 @@ from app.models.compliance import Scan from app.models.evidence_validation import EvidenceValidation from app.models.m365_connection import M365Connection + from app.models.contact import ContactSubmission, SubmissionHistory, SubmissionNote from app.models.oauth_account import OAuthAccount @@ -55,3 +56,12 @@ class User(SQLAlchemyBaseUserTable[int], Base): evidence_validations: Mapped[list["EvidenceValidation"]] = relationship( back_populates="user" ) + assigned_submissions: Mapped[list["ContactSubmission"]] = relationship( + back_populates="assigned_user" + ) + submission_notes: Mapped[list["SubmissionNote"]] = relationship( + back_populates="admin_user" + ) + submission_history: Mapped[list["SubmissionHistory"]] = relationship( + back_populates="admin_user" + ) diff --git a/backend-api/app/schemas/contact.py b/backend-api/app/schemas/contact.py new file mode 100644 index 00000000..02d3e482 --- /dev/null +++ b/backend-api/app/schemas/contact.py @@ -0,0 +1,73 @@ +"""Pydantic schemas for Contact Us submissions.""" + +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel, EmailStr, Field + + +class ContactSubmissionBase(BaseModel): + first_name: str = Field(..., min_length=1, max_length=100) + last_name: str = Field(..., min_length=1, max_length=100) + email: EmailStr + phone: str | None = Field(None, max_length=20) + company: str | None = Field(None, max_length=255) + subject: str = Field(..., min_length=1, max_length=50) + message: str = Field(..., min_length=1) + source: str | None = Field(None, max_length=50) + + +class ContactSubmissionCreate(ContactSubmissionBase): + pass + + +class ContactSubmissionUpdate(BaseModel): + status: str | None = Field(None, min_length=1, max_length=20) + priority: str | None = Field(None, min_length=1, max_length=20) + assigned_to: int | None = None + resolved_at: datetime | None = None + + +class ContactSubmissionRead(ContactSubmissionBase): + id: UUID + status: str + priority: str + assigned_to: int | None + created_at: datetime + updated_at: datetime + resolved_at: datetime | None + + class Config: + from_attributes = True + + +class SubmissionNoteCreate(BaseModel): + note: str = Field(..., min_length=1) + is_internal: bool = True + + +class SubmissionNoteRead(BaseModel): + id: UUID + submission_id: UUID + admin_user_id: int | None + note: str + is_internal: bool + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +class SubmissionHistoryRead(BaseModel): + id: UUID + submission_id: UUID + admin_user_id: int | None + action: str + field_name: str | None + old_value: str | None + new_value: str | None + created_at: datetime + + class Config: + from_attributes = True diff --git a/frontend/contact_submissions_page.html b/frontend/contact_submissions_page.html new file mode 100644 index 00000000..1d14e2d3 --- /dev/null +++ b/frontend/contact_submissions_page.html @@ -0,0 +1,741 @@ + + + + + + Contact Submissions - AutoAudit + + + + +
+ + + + +
+ +
+
+ New +

Hardik Patel

+

General Inquiry

+
+ +
+ New +

Nick Johnson

+

Technical Support

+
+ +
+

Sarah Williams

+

Sales Question

+
+ +
+

Michael Chen

+

Partnership Opportunity

+
+ +
+

Emily Davis

+

Request a Demo

+
+
+ + +
+ +
+

Support

+ General Inquiry +
+ + +
+

Contact Information

+
+
+
👤 Name
+
Hardik Patel
+
+
+
📧 Email
+
hardik@example.com
+
+
+
📞 Phone
+
0468414429
+
+
+
🏢 Company
+
Tech Solutions Inc.
+
+
+
+ + +
+

Message

+
+

Hello, I'm reaching out to learn more about AutoAudit's compliance monitoring capabilities for our Microsoft 365 environment. We're a mid-sized organization with approximately 500 users, and we're looking for an automated solution to help us maintain CIS benchmark compliance.

+

Specifically, I'd like to understand:

+

1. How frequently does AutoAudit perform compliance scans?

+

2. What kind of reporting capabilities do you offer?

+

3. Can we customize the compliance benchmarks to match our specific requirements?

+

4. What's the typical implementation timeline?

+

We have a compliance audit scheduled for next quarter, so we're looking to implement a solution as soon as possible. Would it be possible to schedule a demo to see the platform in action?

+

Looking forward to hearing from you.

+
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ + +
+ + +
+

Notes

+ +
+
+ + +
+ +
+
+ + +
+
+

Follow-up scheduled

+

Demo call scheduled for next Tuesday at 2 PM EST

+

1/23/2026, 6:27:52 AM

+
+ +
+

Initial contact

+

Customer seems very interested in compliance features

+

1/22/2026, 6:48:38 AM

+
+
+ + +
+

History

+ +
+ Note +

Added note: "Follow-up scheduled"

+

1/23/2026, 6:27:52 AM

+
+ +
+ Update +

Status changed to: In Progress

+

1/22/2026, 6:48:41 AM

+
+ +
+ Note +

Added note: "Initial contact"

+

1/22/2026, 6:48:38 AM

+
+ +
+ Create +

Submission created

+

1/22/2026, 6:48:07 AM

+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e86de348..64ee2005 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,19 +16,21 @@ "lucide-react": "^0.542.0", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.1", - "tailwindcss": "^4.1.18" + "react-router-dom": "^7.9.1" }, "devDependencies": { "@vitejs/plugin-react": "^5.1.2", + "tailwindcss": "^4.1.18", "vite": "^7.3.0" } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -37,7 +39,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "dev": true, "license": "MIT", "engines": { @@ -45,20 +49,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -74,13 +79,25 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { - "version": "7.28.5", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -90,11 +107,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -104,8 +123,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", "engines": { @@ -113,25 +144,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -142,6 +177,8 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { @@ -150,6 +187,8 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -158,6 +197,8 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -165,6 +206,8 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -172,23 +215,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -199,6 +246,8 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { @@ -213,6 +262,8 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { @@ -226,36 +277,42 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.27.2", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", "debug": "^4.3.1" }, "engines": { @@ -263,7 +320,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, "license": "MIT", "dependencies": { @@ -701,6 +760,8 @@ }, "node_modules/@esbuild/win32-x64": { "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -715,7 +776,9 @@ } }, "node_modules/@fontsource/league-spartan": { - "version": "5.2.8", + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/league-spartan/-/league-spartan-5.2.7.tgz", + "integrity": "sha512-OwYjMcJOGAGB680+4+Z47c/G5Ky8f8mT9Md6e8DB8/6fi1RGd/Ct5pUe3KT/lhUeNGUT8m5R4PKww47qtU8NHw==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -723,6 +786,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -732,6 +797,8 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -741,19 +808,38 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -763,17 +849,21 @@ }, "node_modules/@kurkle/color": { "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", - "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz", + "integrity": "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==", "cpu": [ "arm" ], @@ -785,9 +875,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", - "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.2.tgz", + "integrity": "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==", "cpu": [ "arm64" ], @@ -799,9 +889,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", - "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.2.tgz", + "integrity": "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==", "cpu": [ "arm64" ], @@ -813,9 +903,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", - "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.2.tgz", + "integrity": "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==", "cpu": [ "x64" ], @@ -827,9 +917,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", - "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.2.tgz", + "integrity": "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==", "cpu": [ "arm64" ], @@ -841,9 +931,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", - "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.2.tgz", + "integrity": "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==", "cpu": [ "x64" ], @@ -855,9 +945,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", - "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.2.tgz", + "integrity": "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==", "cpu": [ "arm" ], @@ -869,9 +959,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", - "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.2.tgz", + "integrity": "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==", "cpu": [ "arm" ], @@ -883,9 +973,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", - "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.2.tgz", + "integrity": "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==", "cpu": [ "arm64" ], @@ -897,9 +987,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", - "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.2.tgz", + "integrity": "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==", "cpu": [ "arm64" ], @@ -911,9 +1001,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", - "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.2.tgz", + "integrity": "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.2.tgz", + "integrity": "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==", "cpu": [ "loong64" ], @@ -925,9 +1029,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", - "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.2.tgz", + "integrity": "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.2.tgz", + "integrity": "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==", "cpu": [ "ppc64" ], @@ -939,9 +1057,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", - "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.2.tgz", + "integrity": "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==", "cpu": [ "riscv64" ], @@ -953,9 +1071,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", - "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.2.tgz", + "integrity": "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==", "cpu": [ "riscv64" ], @@ -967,9 +1085,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", - "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.2.tgz", + "integrity": "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==", "cpu": [ "s390x" ], @@ -981,9 +1099,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", - "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.2.tgz", + "integrity": "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==", "cpu": [ "x64" ], @@ -995,9 +1113,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", - "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.2.tgz", + "integrity": "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==", "cpu": [ "x64" ], @@ -1008,10 +1126,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.2.tgz", + "integrity": "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", - "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.2.tgz", + "integrity": "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==", "cpu": [ "arm64" ], @@ -1023,9 +1155,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", - "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.2.tgz", + "integrity": "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==", "cpu": [ "arm64" ], @@ -1037,9 +1169,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", - "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.2.tgz", + "integrity": "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==", "cpu": [ "ia32" ], @@ -1051,7 +1183,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.5", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.2.tgz", + "integrity": "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==", "cpu": [ "x64" ], @@ -1063,7 +1197,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.5", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.2.tgz", + "integrity": "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==", "cpu": [ "x64" ], @@ -1076,8 +1212,9 @@ }, "node_modules/@testing-library/dom": { "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -1092,8 +1229,19 @@ "node": ">=18" } }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/@testing-library/react": { - "version": "16.3.1", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" @@ -1119,6 +1267,8 @@ }, "node_modules/@testing-library/user-event": { "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", + "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" @@ -1133,10 +1283,14 @@ }, "node_modules/@types/aria-query": { "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", "dependencies": { @@ -1149,6 +1303,8 @@ }, "node_modules/@types/babel__generator": { "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", "dependencies": { @@ -1157,6 +1313,8 @@ }, "node_modules/@types/babel__template": { "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", "dependencies": { @@ -1166,6 +1324,8 @@ }, "node_modules/@types/babel__traverse": { "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1174,11 +1334,27 @@ }, "node_modules/@types/estree": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~7.10.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1196,40 +1372,44 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", + "node_modules/@vitejs/plugin-react/node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/ansi-styles": { - "version": "5.2.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", - "engines": { - "node": ">=10" + "optional": true, + "peer": true, + "bin": { + "acorn": "bin/acorn" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.9", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.28.1", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "dev": true, "funding": [ { @@ -1246,13 +1426,11 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -1261,8 +1439,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/caniuse-lite": { - "version": "1.0.30001760", + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", "dev": true, "funding": [ { @@ -1281,7 +1470,9 @@ "license": "CC-BY-4.0" }, "node_modules/chart.js": { - "version": "4.5.1", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" @@ -1292,22 +1483,15 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "1.1.1", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/debug": { - "version": "4.4.3", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1324,6 +1508,8 @@ }, "node_modules/dequal": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", "engines": { "node": ">=6" @@ -1331,15 +1517,21 @@ }, "node_modules/dom-accessibility-api": { "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.267", + "version": "1.5.213", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz", + "integrity": "sha512-xr9eRzSLNa4neDO0xVFrkXu3vyIzG4Ay08dApecw42Z1NbmCt+keEpXdvlYGVe0wtvY5dhW0Ay0lY0IOfsCg0Q==", "dev": true, "license": "ISC" }, "node_modules/esbuild": { "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1380,28 +1572,14 @@ }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/fdir": { - "version": "6.5.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1419,18 +1597,36 @@ }, "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/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "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": "MIT", "bin": { @@ -1442,6 +1638,8 @@ }, "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": { @@ -1453,6 +1651,8 @@ }, "node_modules/lru-cache": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", "dependencies": { @@ -1461,6 +1661,8 @@ }, "node_modules/lucide-react": { "version": "0.542.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz", + "integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1468,6 +1670,8 @@ }, "node_modules/lz-string": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "license": "MIT", "bin": { "lz-string": "bin/bin.js" @@ -1475,11 +1679,15 @@ }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -1496,28 +1704,22 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, - "node_modules/picomatch": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/postcss": { "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -1545,6 +1747,8 @@ }, "node_modules/pretty-format": { "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1", @@ -1555,39 +1759,49 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/react": { - "version": "19.2.3", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.3", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", - "peer": true, "dependencies": { - "scheduler": "^0.27.0" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.2.3" + "react": "^19.1.1" } }, "node_modules/react-is": { "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, - "node_modules/react-refresh": { - "version": "0.18.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-router": { - "version": "7.11.0", + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.1.tgz", + "integrity": "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -1607,10 +1821,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.11.0", + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.1.tgz", + "integrity": "sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw==", "license": "MIT", "dependencies": { - "react-router": "7.11.0" + "react-router": "7.9.1" }, "engines": { "node": ">=20.0.0" @@ -1620,76 +1836,103 @@ "react-dom": ">=18" } }, - "node_modules/rollup": { - "version": "4.53.5", - "dev": true, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.5", - "@rollup/rollup-android-arm64": "4.53.5", - "@rollup/rollup-darwin-arm64": "4.53.5", - "@rollup/rollup-darwin-x64": "4.53.5", - "@rollup/rollup-freebsd-arm64": "4.53.5", - "@rollup/rollup-freebsd-x64": "4.53.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", - "@rollup/rollup-linux-arm-musleabihf": "4.53.5", - "@rollup/rollup-linux-arm64-gnu": "4.53.5", - "@rollup/rollup-linux-arm64-musl": "4.53.5", - "@rollup/rollup-linux-loong64-gnu": "4.53.5", - "@rollup/rollup-linux-ppc64-gnu": "4.53.5", - "@rollup/rollup-linux-riscv64-gnu": "4.53.5", - "@rollup/rollup-linux-riscv64-musl": "4.53.5", - "@rollup/rollup-linux-s390x-gnu": "4.53.5", - "@rollup/rollup-linux-x64-gnu": "4.53.5", - "@rollup/rollup-linux-x64-musl": "4.53.5", - "@rollup/rollup-openharmony-arm64": "4.53.5", - "@rollup/rollup-win32-arm64-msvc": "4.53.5", - "@rollup/rollup-win32-ia32-msvc": "4.53.5", - "@rollup/rollup-win32-x64-gnu": "4.53.5", - "@rollup/rollup-win32-x64-msvc": "4.53.5", - "fsevents": "~2.3.2" + "node": ">=18" } }, "node_modules/scheduler": { - "version": "0.27.0", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, - "node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/set-cookie-parser": { - "version": "2.7.2", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "license": "MIT" }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/tailwindcss": { "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, "license": "MIT" }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/tinyglobby": { "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1703,8 +1946,50 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/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/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/update-browserslist-db": { - "version": "1.2.3", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -1733,10 +2018,11 @@ } }, "node_modules/vite": { - "version": "7.3.0", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -1806,10 +2092,106 @@ } } }, + "node_modules/vite/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/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz", + "integrity": "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.2", + "@rollup/rollup-android-arm64": "4.55.2", + "@rollup/rollup-darwin-arm64": "4.55.2", + "@rollup/rollup-darwin-x64": "4.55.2", + "@rollup/rollup-freebsd-arm64": "4.55.2", + "@rollup/rollup-freebsd-x64": "4.55.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", + "@rollup/rollup-linux-arm-musleabihf": "4.55.2", + "@rollup/rollup-linux-arm64-gnu": "4.55.2", + "@rollup/rollup-linux-arm64-musl": "4.55.2", + "@rollup/rollup-linux-loong64-gnu": "4.55.2", + "@rollup/rollup-linux-loong64-musl": "4.55.2", + "@rollup/rollup-linux-ppc64-gnu": "4.55.2", + "@rollup/rollup-linux-ppc64-musl": "4.55.2", + "@rollup/rollup-linux-riscv64-gnu": "4.55.2", + "@rollup/rollup-linux-riscv64-musl": "4.55.2", + "@rollup/rollup-linux-s390x-gnu": "4.55.2", + "@rollup/rollup-linux-x64-gnu": "4.55.2", + "@rollup/rollup-linux-x64-musl": "4.55.2", + "@rollup/rollup-openbsd-x64": "4.55.2", + "@rollup/rollup-openharmony-arm64": "4.55.2", + "@rollup/rollup-win32-arm64-msvc": "4.55.2", + "@rollup/rollup-win32-ia32-msvc": "4.55.2", + "@rollup/rollup-win32-x64-gnu": "4.55.2", + "@rollup/rollup-win32-x64-msvc": "4.55.2", + "fsevents": "~2.3.2" + } + }, "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/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 06f9cf55..391d5cb6 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -18,6 +18,7 @@ import AboutUs from './pages/Landing/AboutUs'; import ContactPage from './pages/Contact/ContactPage'; import LoginPage from './pages/Auth/LoginPage'; import SignUpPage from './pages/Auth/SignUpPage'; +import ContactAdminPage from './pages/Admin/ContactAdminPage.jsx'; import GoogleCallbackPage from './pages/Auth/GoogleCallbackPage'; // Auth Context @@ -45,6 +46,29 @@ const ProtectedRoute = ({ children }) => { return isAuthenticated ? children : null; }; +// Admin-only Route Component +const AdminRoute = ({ children }) => { + const navigate = useNavigate(); + const { isAuthenticated, isLoading, user } = useAuth(); + + useEffect(() => { + if (isLoading) return; + if (!isAuthenticated) { + navigate('/login'); + return; + } + if (user?.role !== 'admin') { + navigate('/dashboard'); + } + }, [isAuthenticated, isLoading, navigate, user]); + + if (isLoading) { + return
Loading...
; + } + + return isAuthenticated && user?.role === 'admin' ? children : null; +}; + // Dashboard Layout Component (with sidebar) const DashboardLayout = ({ children, sidebarWidth, isDarkMode, onThemeToggle, onSidebarWidthChange }) => { return ( @@ -180,6 +204,15 @@ function App() { } /> + + + + } + /> + {/* Protected Dashboard Routes */} ({ detail: response.statusText })); + throw new APIError(error.detail || 'Failed to delete submission', response.status, error); + } +} + +export async function getContactNotes(token, id) { + return fetchWithAuth(`/v1/contact/submissions/${id}/notes`, token); +} + +export async function addContactNote(token, id, payload) { + return fetchWithAuth(`/v1/contact/submissions/${id}/notes`, token, { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +export async function getContactHistory(token, id) { + return fetchWithAuth(`/v1/contact/submissions/${id}/history`, token); +} + // Platform endpoints export async function getPlatforms(token) { return fetchWithAuth('/v1/platforms', token); diff --git a/frontend/src/pages/Admin/ContactAdminPage.css b/frontend/src/pages/Admin/ContactAdminPage.css new file mode 100644 index 00000000..359dc9d3 --- /dev/null +++ b/frontend/src/pages/Admin/ContactAdminPage.css @@ -0,0 +1,486 @@ +.contact-admin { + min-height: 100vh; + background: #0a1628; + color: #ffffff; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + overflow-x: hidden; +} + +.contact-admin__container { + max-width: 1600px; + margin: 0 auto; + padding: 2rem 3%; +} + +.contact-admin__page-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 2rem; +} + +.contact-admin__page-header-content h1 { + font-size: 2rem; + margin-bottom: 0.5rem; +} + +.contact-admin__page-header-content p { + color: #b0c4de; + font-size: 1rem; +} + + +.contact-admin__error, +.contact-admin__message { + padding: 0.75rem 1rem; + border-radius: 12px; + margin-bottom: 1rem; +} + +.contact-admin__error { + background: rgba(239, 68, 68, 0.15); + border: 1px solid rgba(239, 68, 68, 0.4); + color: #fecaca; +} + +.contact-admin__message { + background: rgba(34, 197, 94, 0.15); + border: 1px solid rgba(34, 197, 94, 0.4); + color: #bbf7d0; +} + +.contact-admin__layout { + display: grid; + grid-template-columns: 350px 1fr; + gap: 2rem; +} + +.contact-admin__list { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(64, 224, 208, 0.1); + border-radius: 20px; + padding: 1.5rem; + max-height: calc(100vh - 160px); + overflow-y: auto; +} + +.contact-admin__list ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.contact-admin__list li { + background: rgba(255, 255, 255, 0.05); + border: 2px solid rgba(64, 224, 208, 0.1); + border-radius: 12px; + padding: 1.25rem; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.75rem; +} + +.contact-admin__list li:hover { + background: rgba(255, 255, 255, 0.08); + border-color: #40e0d0; + transform: translateX(5px); +} + +.contact-admin__list li.active { + background: rgba(64, 224, 208, 0.1); + border-color: #40e0d0; +} + +.contact-admin__list h3 { + font-size: 1.1rem; + margin-bottom: 0.35rem; +} + +.contact-admin__list p { + color: #b0c4de; + font-size: 0.9rem; +} + +.badge { + background: linear-gradient(135deg, #40e0d0, #1e90ff); + color: #ffffff; + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; + text-transform: capitalize; +} + +.badge--in_progress { + background: linear-gradient(135deg, #f59e0b, #f97316); +} + +.badge--resolved, +.badge--closed { + background: linear-gradient(135deg, #22c55e, #16a34a); +} + +.contact-admin__detail { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(64, 224, 208, 0.1); + border-radius: 20px; + padding: 2rem; + max-height: calc(100vh - 160px); + overflow-y: auto; +} + +.contact-admin__subject-header { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid rgba(64, 224, 208, 0.1); +} + +.contact-admin__subject-header h2 { + font-size: 1.8rem; + margin-bottom: 0.5rem; +} + +.contact-admin__subject-tag { + background: rgba(64, 224, 208, 0.1); + color: #40e0d0; + padding: 0.5rem 1rem; + border-radius: 8px; + font-size: 0.9rem; + display: inline-block; + text-transform: capitalize; +} + +.contact-admin__contact-info, +.contact-admin__message-section { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(64, 224, 208, 0.1); + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 2rem; +} + +.contact-admin__contact-info h3, +.contact-admin__message-section h3, +.contact-admin__notes h3, +.contact-admin__history h3 { + font-size: 1.1rem; + margin-bottom: 1rem; +} + +.contact-admin__info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.contact-admin__info-item { + background: rgba(255, 255, 255, 0.03); + padding: 1rem; + border-radius: 8px; + border: 1px solid rgba(64, 224, 208, 0.1); +} + +.contact-admin__info-label { + color: #b0c4de; + font-size: 0.85rem; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.contact-admin__info-value { + color: #ffffff; + font-size: 1rem; + font-weight: 500; +} + +.contact-admin__message-box { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(64, 224, 208, 0.1); + border-radius: 10px; + padding: 1.5rem; + color: #b0c4de; + line-height: 1.8; + font-size: 0.95rem; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; +} + +.contact-admin__message-box--scroll { + height: 300px; + overflow-y: auto; +} + +.contact-admin__actions { + display: grid; + grid-template-columns: 1fr; + gap: 1.25rem; + margin-bottom: 2rem; +} + +.form-label { + display: block; + margin: 0 0 8px 0; + font-weight: 600; + color: var(--text-primary); + font-size: 14px; +} + +.form-select, +.form-input, +.form-file { + width: 100%; + padding: 12px 14px; + border: 1px solid var(--border-color, #334155); + border-radius: 10px; + font-size: 15px; + background: var(--bg-primary, #0f172a); + color: var(--text-primary); + transition: all 0.3s ease; + border-radius: 10px; +} + +.form-select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + /* Match other app dropdowns in dark mode */ + background: var(--bg-tertiary, #334155); + background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 12px 8px; + padding-right: 40px; + min-height: 46px; + line-height: 1.4; +} + +.form-select option { + background: var(--bg-secondary, #1e293b); + color: var(--text-primary); +} + +.form-select:focus, +.form-input:focus, +.form-file:focus { + outline: none; + border-color: var(--teal); + box-shadow: 0 0 0 2px rgba(100, 223, 223, 0.2); +} + +.form-select:disabled, +.form-input:disabled, +.form-file:disabled { + opacity: 0.6; + cursor: not-allowed; + background: var(--bg-tertiary, #334155); +} + +.contact-admin__action-buttons { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 2rem; +} + +.contact-admin__assign { + padding: 1rem; + background: linear-gradient(135deg, #40e0d0, #1e90ff); + color: #ffffff; + border: none; + border-radius: 10px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(64, 224, 208, 0.3); +} + +.contact-admin__assign:hover { + transform: translateY(-2px); + box-shadow: 0 6px 25px rgba(64, 224, 208, 0.5); +} + +.contact-admin__delete { + padding: 1rem; + background: linear-gradient(135deg, #dc3545, #c82333); + color: #ffffff; + border: none; + border-radius: 10px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(220, 53, 69, 0.3); +} + +.contact-admin__delete:hover { + transform: translateY(-2px); + box-shadow: 0 6px 25px rgba(220, 53, 69, 0.5); +} + +.contact-admin__notes { + margin-bottom: 2rem; +} + +.contact-admin__notes textarea { + width: 100%; + min-height: 120px; + padding: 1rem; + background: rgba(255, 255, 255, 0.05); + border: 2px solid rgba(64, 224, 208, 0.2); + border-radius: 12px; + color: #ffffff; + font-size: 0.95rem; + font-family: inherit; + resize: vertical; + transition: all 0.3s ease; +} + +.contact-admin__notes textarea::placeholder { + color: #6c7a8d; +} + +.contact-admin__notes textarea:focus { + outline: none; + border-color: #40e0d0; + background: rgba(255, 255, 255, 0.08); +} + +.contact-admin__note-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1rem; +} + +.contact-admin__internal-toggle { + display: flex; + align-items: center; + gap: 0.5rem; + color: #b0c4de; + font-size: 0.9rem; +} + +.contact-admin__internal-toggle input { + width: 18px; + height: 18px; + accent-color: #40e0d0; + cursor: pointer; +} + +.contact-admin__note-actions button { + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #40e0d0, #1e90ff); + color: #ffffff; + border: none; + border-radius: 10px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.contact-admin__note-actions button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(64, 224, 208, 0.3); +} + +.contact-admin__notes-list { + margin-bottom: 2rem; +} + +.contact-admin__note-item { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(64, 224, 208, 0.1); + border-radius: 12px; + padding: 1rem; + margin-bottom: 1rem; +} + +.contact-admin__note-item h4 { + font-size: 1rem; + margin-bottom: 0.5rem; +} + +.contact-admin__note-item p { + color: #b0c4de; + font-size: 0.9rem; + margin-bottom: 0.5rem; +} + +.contact-admin__timestamp { + color: #6c7a8d; + font-size: 0.85rem; +} + +.contact-admin__history-item { + background: rgba(255, 255, 255, 0.03); + border-left: 3px solid #40e0d0; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; +} + +.contact-admin__history-badge { + display: inline-block; + background: rgba(64, 224, 208, 0.2); + color: #40e0d0; + padding: 0.25rem 0.75rem; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; + margin-bottom: 0.5rem; + text-transform: uppercase; +} + +.contact-admin__history-item p { + color: #b0c4de; + font-size: 0.9rem; + margin-bottom: 0.5rem; +} + +.contact-admin__empty { + text-align: center; + padding: 4rem 1rem; + color: #b0c4de; +} + +@media (max-width: 1200px) { + .contact-admin__layout { + grid-template-columns: 300px 1fr; + } +} + +@media (max-width: 968px) { + .contact-admin__layout { + grid-template-columns: 1fr; + } + + .contact-admin__list { + max-height: 400px; + } + + .contact-admin__info-grid { + grid-template-columns: 1fr; + } + + .contact-admin__actions, + .contact-admin__action-buttons { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .contact-admin__page-header { + flex-direction: column; + gap: 1rem; + } + +} diff --git a/frontend/src/pages/Admin/ContactAdminPage.jsx b/frontend/src/pages/Admin/ContactAdminPage.jsx new file mode 100644 index 00000000..718b7297 --- /dev/null +++ b/frontend/src/pages/Admin/ContactAdminPage.jsx @@ -0,0 +1,363 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import "./ContactAdminPage.css"; +import { useAuth } from "../../context/AuthContext"; +import { + addContactNote, + deleteContactSubmission, + getContactHistory, + getContactNotes, + getContactSubmissions, + updateContactSubmission, +} from "../../api/client"; + +const statusOptions = ["new", "in_progress", "resolved", "closed"]; +const priorityOptions = ["low", "medium", "high", "urgent"]; + +const ContactAdminPage = () => { + const { token, user } = useAuth(); + const [submissions, setSubmissions] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [notes, setNotes] = useState([]); + const [history, setHistory] = useState([]); + const [noteText, setNoteText] = useState(""); + const [isInternal, setIsInternal] = useState(true); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + const [actionMessage, setActionMessage] = useState(""); + const latestSelectionRef = useRef(null); + + useEffect(() => { + if (!actionMessage) return; + const timeoutId = setTimeout(() => { + setActionMessage(""); + }, 5000); + return () => clearTimeout(timeoutId); + }, [actionMessage]); + + const selectedSubmission = useMemo( + () => submissions.find((item) => item.id === selectedId) || null, + [submissions, selectedId] + ); + + const loadSubmissions = async () => { + setError(""); + setIsLoading(true); + try { + const data = await getContactSubmissions(token); + setSubmissions(data); + if (data.length && !selectedId) { + setSelectedId(data[0].id); + } + } catch (err) { + setError(err?.message || "Unable to load submissions."); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!user || user.role !== "admin") { + return; + } + loadSubmissions(); + }, [user, token]); + + useEffect(() => { + if (!selectedId) return; + + const loadDetail = async () => { + latestSelectionRef.current = selectedId; + try { + const [noteData, historyData] = await Promise.all([ + getContactNotes(token, selectedId), + getContactHistory(token, selectedId), + ]); + if (latestSelectionRef.current !== selectedId) { + return; + } + setNotes(noteData); + setHistory(historyData); + } catch (err) { + setError(err?.message || "Unable to load submission details."); + } + }; + + loadDetail(); + }, [selectedId, token]); + + const handleUpdate = async (updates) => { + if (!selectedSubmission) return; + const currentId = selectedSubmission.id; + setActionMessage(""); + try { + const updated = await updateContactSubmission(token, selectedSubmission.id, updates); + setSubmissions((prev) => + prev.map((item) => (item.id === updated.id ? updated : item)) + ); + const historyData = await getContactHistory(token, selectedSubmission.id); + if (latestSelectionRef.current === currentId) { + setHistory(historyData); + } + setActionMessage("Submission updated."); + } catch (err) { + setError(err?.message || "Unable to update submission."); + } + }; + + const handleAddNote = async () => { + if (!noteText.trim() || !selectedSubmission) return; + const currentId = selectedSubmission.id; + setActionMessage(""); + try { + const newNote = await addContactNote(token, selectedSubmission.id, { + note: noteText.trim(), + is_internal: isInternal, + }); + setNotes((prev) => [newNote, ...prev]); + const historyData = await getContactHistory(token, selectedSubmission.id); + if (latestSelectionRef.current === currentId) { + setHistory(historyData); + } + setNoteText(""); + setActionMessage("Note added."); + } catch (err) { + setError(err?.message || "Unable to add note."); + } + }; + + const handleDelete = async () => { + if (!selectedSubmission) return; + setActionMessage(""); + try { + await deleteContactSubmission(token, selectedSubmission.id); + setSubmissions((prev) => prev.filter((item) => item.id !== selectedSubmission.id)); + setSelectedId(null); + setNotes([]); + setHistory([]); + setActionMessage("Submission deleted."); + } catch (err) { + setError(err?.message || "Unable to delete submission."); + } + }; + + if (user?.role !== "admin") { + return ( +
+
+

Admin access required

+

You do not have permission to view this page.

+
+
+ ); + } + + const messageWordCount = selectedSubmission?.message + ? selectedSubmission.message.trim().split(/\s+/).filter(Boolean).length + : 0; + const isLongMessage = messageWordCount > 500; + + return ( +
+
+
+

Contact Submissions

+

Review and manage incoming Contact Us requests.

+
+
+ + {error &&
{error}
} + {actionMessage &&
{actionMessage}
} + +
+
+ {isLoading ? ( +

Loading submissions...

+ ) : submissions.length ? ( +
    + {submissions.map((submission) => { + const isActive = submission.id === selectedId; + + return ( +
  • setSelectedId(submission.id)} + > +
    +

    + {submission.first_name} {submission.last_name} +

    +

    {submission.subject}

    +
    + + {submission.status} + +
  • + ); + })} +
+ ) : ( +

No submissions yet.

+ )} +
+ +
+ {selectedSubmission ? ( +
+
+
+

{selectedSubmission.subject}

+ + {selectedSubmission.status} + +
+
+ +
+

Contact Information

+
+
+ Name + + {selectedSubmission.first_name} {selectedSubmission.last_name} + +
+
+ Email + + {selectedSubmission.email} + +
+
+ Phone + + {selectedSubmission.phone || "Not provided"} + +
+
+ Company + + {selectedSubmission.company || "Not provided"} + +
+
+
+ +
+

Message

+
+ {selectedSubmission.message} +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+

Notes

+