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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.
+
+
+
+
+
+
+ Status
+
+ New
+ In Progress
+ Resolved
+ Closed
+
+
+
+
+ Priority
+
+ Low
+ Medium
+ High
+ Urgent
+
+
+
+
+
+
+ ✅ Assign to me
+ 🗑️ Delete
+
+
+
+
+
Notes
+
+
+
+
+ Internal only
+
+
Add note
+
+
+
+
+
+
+
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 (
+
+
+
+ {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}
+
+
+
+
+
+ Status
+ handleUpdate({ status: event.target.value })}
+ >
+ {statusOptions.map((option) => (
+
+ {option}
+
+ ))}
+
+
+
+ Priority
+ handleUpdate({ priority: event.target.value })}
+ >
+ {priorityOptions.map((option) => (
+
+ {option}
+
+ ))}
+
+
+
+
+
+ handleUpdate({ assigned_to: user.id })}
+ >
+ Assign to me
+
+
+ Delete
+
+
+
+
+
+
+ {notes.map((note) => (
+
+
Note
+
{note.note}
+
+ {new Date(note.created_at).toLocaleString()}
+
+
+ ))}
+
+
+
+
History
+ {history.map((entry) => (
+
+
{entry.action}
+
+ {entry.action === "note" && entry.new_value
+ ? `Added note: \"${entry.new_value}\"`
+ : entry.field_name
+ ? `${entry.field_name} changed to: ${entry.new_value || "—"}`
+ : "Submission updated"}
+
+
+ {new Date(entry.created_at).toLocaleString()}
+
+
+ ))}
+
+
+ ) : (
+
+
Select a submission
+
Choose a submission to see details, notes, and history.
+
+ )}
+
+
+
+ );
+};
+
+export default ContactAdminPage;
diff --git a/frontend/src/pages/Contact/ContactPage.css b/frontend/src/pages/Contact/ContactPage.css
index 54d4c8d5..24d7a983 100644
--- a/frontend/src/pages/Contact/ContactPage.css
+++ b/frontend/src/pages/Contact/ContactPage.css
@@ -241,6 +241,16 @@
font-size: 1.1rem;
}
+.error-message {
+ margin-top: 1rem;
+ padding: 0.9rem 1rem;
+ border-radius: 12px;
+ border: 1px solid rgba(239, 68, 68, 0.4);
+ background: rgba(239, 68, 68, 0.12);
+ color: #fecaca;
+ text-align: center;
+}
+
.success-message {
display: none;
margin-top: 1rem;
diff --git a/frontend/src/pages/Contact/ContactPage.jsx b/frontend/src/pages/Contact/ContactPage.jsx
index 66fc7bf6..da66ae7a 100644
--- a/frontend/src/pages/Contact/ContactPage.jsx
+++ b/frontend/src/pages/Contact/ContactPage.jsx
@@ -6,6 +6,7 @@ import LandingFooter from "../Landing/components/LandingFooter";
import ContactInfoGrid from "./components/ContactInfoGrid";
import ContactForm from "./components/ContactForm";
import FAQSection from "./components/FAQSection";
+import { createContactSubmission } from "../../api/client";
const ContactHero = () => (
@@ -28,6 +29,20 @@ const ContactPage = ({ onSignIn }) => {
setTimeout(() => setSubmitted(false), 5000);
};
+ const handleSubmit = async (payload) => {
+ await createContactSubmission({
+ first_name: payload.firstName,
+ last_name: payload.lastName,
+ email: payload.email,
+ phone: payload.phone || null,
+ company: payload.company || null,
+ subject: payload.subject,
+ message: payload.message,
+ source: "website",
+ });
+ handleFormSuccess();
+ };
+
return (
@@ -37,7 +52,7 @@ const ContactPage = ({ onSignIn }) => {
diff --git a/frontend/src/pages/Contact/components/ContactForm.jsx b/frontend/src/pages/Contact/components/ContactForm.jsx
index f79d8e3e..f2701de0 100644
--- a/frontend/src/pages/Contact/components/ContactForm.jsx
+++ b/frontend/src/pages/Contact/components/ContactForm.jsx
@@ -10,31 +10,76 @@ const initialState = {
message: "",
};
-const ContactForm = ({ submitted, onSuccess }) => {
+const ContactForm = ({ submitted, onSubmit }) => {
const [formData, setFormData] = useState(initialState);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState("");
const handleChange = (event) => {
const { name, value } = event.target;
setFormData((prev) => ({ ...prev, [name]: value }));
+ if (error) {
+ setError("");
+ }
};
- const handleSubmit = (event) => {
+ const handleSubmit = async (event) => {
event.preventDefault();
- onSuccess();
- setFormData(initialState);
+ setError("");
+ const trimmed = {
+ firstName: formData.firstName.trim(),
+ lastName: formData.lastName.trim(),
+ email: formData.email.trim(),
+ subject: formData.subject.trim(),
+ message: formData.message.trim(),
+ };
+ if (!trimmed.firstName) {
+ setError("Please enter your first name.");
+ return;
+ }
+ if (!trimmed.lastName) {
+ setError("Please enter your last name.");
+ return;
+ }
+ if (!trimmed.email) {
+ setError("Please enter your email address.");
+ return;
+ }
+ const emailLooksValid =
+ /.+@.+\..+/.test(trimmed.email) && !/\s/.test(trimmed.email);
+ if (!emailLooksValid) {
+ setError("Please enter a valid email address.");
+ return;
+ }
+ if (!trimmed.subject) {
+ setError("Please select a subject.");
+ return;
+ }
+ if (!trimmed.message) {
+ setError("Please enter a message.");
+ return;
+ }
+ setIsSubmitting(true);
+ try {
+ await onSubmit(formData);
+ setFormData(initialState);
+ } catch (err) {
+ setError(err?.message || "Unable to send message right now.");
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (