From 8e903d9222820accf226744ea6a087ee7714f86f Mon Sep 17 00:00:00 2001 From: Jarvis Deploy Date: Tue, 10 Feb 2026 23:05:41 +0000 Subject: [PATCH] =?UTF-8?q?CLIO=20v1.0=20=E2=80=94=20Scanner=20Inteligente?= =?UTF-8?q?=20com=20IA=20(MVP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 + backend/app/__init__.py | 0 backend/app/config.py | 17 + backend/app/database.py | 17 + backend/app/main.py | 42 + backend/app/models/__init__.py | 2 + backend/app/models/document.py | 18 + backend/app/models/user.py | 13 + backend/app/routers/__init__.py | 0 backend/app/routers/auth.py | 37 + backend/app/routers/documents.py | 143 ++ backend/app/schemas/__init__.py | 0 backend/app/schemas/auth.py | 21 + backend/app/schemas/document.py | 22 + backend/app/services/__init__.py | 0 backend/app/services/ai_service.py | 65 + backend/app/utils/__init__.py | 0 backend/app/utils/security.py | 42 + backend/requirements.txt | 9 + frontend/next-env.d.ts | 5 + frontend/next.config.js | 14 + frontend/package-lock.json | 1624 ++++++++++++++++++++ frontend/package.json | 25 + frontend/postcss.config.js | 6 + frontend/public/icon-192.svg | 6 + frontend/public/icon-512.svg | 6 + frontend/public/manifest.json | 14 + frontend/public/sw.js | 24 + frontend/src/app/globals.css | 71 + frontend/src/app/layout.tsx | 30 + frontend/src/app/page.tsx | 50 + frontend/src/app/register-sw.tsx | 12 + frontend/src/components/Dashboard.tsx | 69 + frontend/src/components/DocumentDetail.tsx | 150 ++ frontend/src/components/DocumentList.tsx | 137 ++ frontend/src/components/LoginScreen.tsx | 106 ++ frontend/src/components/Scanner.tsx | 273 ++++ frontend/src/lib/api.ts | 48 + frontend/tailwind.config.ts | 21 + frontend/tsconfig.json | 40 + start-backend.sh | 4 + 41 files changed, 3190 insertions(+) create mode 100644 .gitignore create mode 100644 backend/app/__init__.py create mode 100644 backend/app/config.py create mode 100644 backend/app/database.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/document.py create mode 100644 backend/app/models/user.py create mode 100644 backend/app/routers/__init__.py create mode 100644 backend/app/routers/auth.py create mode 100644 backend/app/routers/documents.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/schemas/auth.py create mode 100644 backend/app/schemas/document.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/services/ai_service.py create mode 100644 backend/app/utils/__init__.py create mode 100644 backend/app/utils/security.py create mode 100644 backend/requirements.txt create mode 100644 frontend/next-env.d.ts create mode 100644 frontend/next.config.js create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/icon-192.svg create mode 100644 frontend/public/icon-512.svg create mode 100644 frontend/public/manifest.json create mode 100644 frontend/public/sw.js create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/src/app/register-sw.tsx create mode 100644 frontend/src/components/Dashboard.tsx create mode 100644 frontend/src/components/DocumentDetail.tsx create mode 100644 frontend/src/components/DocumentList.tsx create mode 100644 frontend/src/components/LoginScreen.tsx create mode 100644 frontend/src/components/Scanner.tsx create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json create mode 100755 start-backend.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65396f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.next/ +venv/ +__pycache__/ +*.pyc +.env +uploads/ diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..cf04651 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,17 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + DATABASE_URL: str = "postgresql+asyncpg://clio:Clio2026!@localhost:5432/clio" + SECRET_KEY: str = "clio-secret-key-2026-musa-da-historia" + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 + OPENAI_API_KEY: str = "" + OPENAI_MODEL_TEXT: str = "gpt-4o-mini" + OPENAI_MODEL_VISION: str = "gpt-4o" + FREE_SCAN_LIMIT: int = 5 + UPLOAD_DIR: str = "/opt/clio/uploads" + + class Config: + env_file = ".env" + +settings = Settings() diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..05baa4b --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,17 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import DeclarativeBase +from app.config import settings + +engine = create_async_engine(settings.DATABASE_URL, echo=False) +async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + +class Base(DeclarativeBase): + pass + +async def get_db(): + async with async_session() as session: + yield session + +async def init_db(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..b67424d --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,42 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager +from sqlalchemy import select +from app.database import init_db, async_session +from app.models.user import User +from app.routers import auth, documents +from app.utils.security import hash_password + +@asynccontextmanager +async def lifespan(app: FastAPI): + await init_db() + # Seed admin user + async with async_session() as db: + result = await db.execute(select(User).where(User.email == "admin@clio.com")) + if not result.scalar_one_or_none(): + admin = User( + email="admin@clio.com", + name="Admin CLIO", + password_hash=hash_password("Clio@2026"), + plan="premium" + ) + db.add(admin) + await db.commit() + yield + +app = FastAPI(title="CLIO API", version="1.0.0", lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(auth.router) +app.include_router(documents.router) + +@app.get("/api/health") +async def health(): + return {"status": "ok", "app": "CLIO API v1.0 — Scanner Inteligente com IA"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..c1cd4e1 --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,2 @@ +from app.models.user import User +from app.models.document import Document diff --git a/backend/app/models/document.py b/backend/app/models/document.py new file mode 100644 index 0000000..be3c31d --- /dev/null +++ b/backend/app/models/document.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON +from datetime import datetime, timezone +from app.database import Base + +class Document(Base): + __tablename__ = "documents" + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + title = Column(String(500)) + category = Column(String(50), index=True) + original_image = Column(Text) + extracted_text = Column(Text) + summary = Column(Text) + extracted_data = Column(JSON) + risk_alerts = Column(JSON) + tags = Column(JSON) + file_size = Column(Integer) + created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=True) diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..276570a --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,13 @@ +from sqlalchemy import Column, Integer, String, DateTime +from datetime import datetime, timezone +from app.database import Base + +class User(Base): + __tablename__ = "users" + id = Column(Integer, primary_key=True, index=True) + email = Column(String(255), unique=True, index=True, nullable=False) + name = Column(String(200), nullable=True) + password_hash = Column(String(255), nullable=False) + plan = Column(String(20), default="free") + scan_count_today = Column(Integer, default=0) + created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 0000000..f17a981 --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,37 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from app.database import get_db +from app.models.user import User +from app.schemas.auth import RegisterRequest, LoginRequest, TokenResponse, UserResponse +from app.utils.security import hash_password, verify_password, create_access_token, get_current_user + +router = APIRouter(prefix="/api/auth", tags=["auth"]) + +def user_to_dict(user: User) -> dict: + return {"id": user.id, "email": user.email, "name": user.name, "plan": user.plan} + +@router.post("/register", response_model=TokenResponse) +async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)): + existing = await db.execute(select(User).where(User.email == req.email)) + if existing.scalar_one_or_none(): + raise HTTPException(status_code=400, detail="Email já cadastrado") + user = User(email=req.email, name=req.name or req.email.split("@")[0], password_hash=hash_password(req.password)) + db.add(user) + await db.commit() + await db.refresh(user) + token = create_access_token({"sub": str(user.id)}) + return TokenResponse(access_token=token, user=UserResponse(**user_to_dict(user))) + +@router.post("/login", response_model=TokenResponse) +async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)): + result = await db.execute(select(User).where(User.email == req.email)) + user = result.scalar_one_or_none() + if not user or not verify_password(req.password, user.password_hash): + raise HTTPException(status_code=401, detail="Email ou senha incorretos") + token = create_access_token({"sub": str(user.id)}) + return TokenResponse(access_token=token, user=UserResponse(**user_to_dict(user))) + +@router.get("/me") +async def me(user: User = Depends(get_current_user)): + return user_to_dict(user) diff --git a/backend/app/routers/documents.py b/backend/app/routers/documents.py new file mode 100644 index 0000000..f032792 --- /dev/null +++ b/backend/app/routers/documents.py @@ -0,0 +1,143 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func, desc +from typing import Optional +import base64 +import os +from app.database import get_db +from app.models.user import User +from app.models.document import Document +from app.schemas.document import ScanRequest, DocumentResponse, DocumentListResponse +from app.services.ai_service import analyze_document +from app.utils.security import get_current_user +from app.config import settings + +router = APIRouter(prefix="/api/documents", tags=["documents"]) + +@router.post("/scan", response_model=DocumentResponse) +async def scan_document( + req: ScanRequest, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + # Check scan limit for free users + if user.plan == "free" and user.scan_count_today >= settings.FREE_SCAN_LIMIT: + raise HTTPException(status_code=429, detail="Limite de scans diários atingido. Faça upgrade para Premium.") + + # Calculate file size + image_data = req.image + if "," in image_data: + image_data_clean = image_data.split(",", 1)[1] + else: + image_data_clean = image_data + file_size = len(base64.b64decode(image_data_clean)) + + # AI analysis + try: + result = await analyze_document(req.image) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erro na análise IA: {str(e)}") + + # Save document + doc = Document( + user_id=user.id, + title=result.get("title", "Documento sem título"), + category=result.get("category", "outro"), + original_image=req.image, + extracted_text=result.get("extracted_text", ""), + summary=result.get("summary", ""), + extracted_data=result.get("extracted_data", {}), + risk_alerts=result.get("risk_alerts", []), + tags=result.get("tags", []), + file_size=file_size + ) + db.add(doc) + + # Update scan count + user.scan_count_today += 1 + await db.commit() + await db.refresh(doc) + + return DocumentResponse( + id=doc.id, title=doc.title, category=doc.category, + extracted_text=doc.extracted_text, summary=doc.summary, + extracted_data=doc.extracted_data, risk_alerts=doc.risk_alerts, + tags=doc.tags, file_size=doc.file_size, created_at=doc.created_at + ) + +@router.get("/", response_model=DocumentListResponse) +async def list_documents( + search: Optional[str] = None, + category: Optional[str] = None, + page: int = Query(1, ge=1), + limit: int = Query(20, ge=1, le=100), + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + query = select(Document).where(Document.user_id == user.id) + count_query = select(func.count(Document.id)).where(Document.user_id == user.id) + + if category: + query = query.where(Document.category == category) + count_query = count_query.where(Document.category == category) + if search: + search_filter = Document.extracted_text.ilike(f"%{search}%") + query = query.where(search_filter) + count_query = count_query.where(search_filter) + + total = (await db.execute(count_query)).scalar() + result = await db.execute(query.order_by(desc(Document.created_at)).offset((page-1)*limit).limit(limit)) + docs = result.scalars().all() + + return DocumentListResponse( + documents=[DocumentResponse( + id=d.id, title=d.title, category=d.category, + extracted_text=d.extracted_text, summary=d.summary, + extracted_data=d.extracted_data, risk_alerts=d.risk_alerts, + tags=d.tags, file_size=d.file_size, created_at=d.created_at + ) for d in docs], + total=total + ) + +@router.get("/{doc_id}", response_model=DocumentResponse) +async def get_document( + doc_id: int, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + result = await db.execute(select(Document).where(Document.id == doc_id, Document.user_id == user.id)) + doc = result.scalar_one_or_none() + if not doc: + raise HTTPException(status_code=404, detail="Documento não encontrado") + return DocumentResponse( + id=doc.id, title=doc.title, category=doc.category, + extracted_text=doc.extracted_text, summary=doc.summary, + extracted_data=doc.extracted_data, risk_alerts=doc.risk_alerts, + tags=doc.tags, file_size=doc.file_size, created_at=doc.created_at + ) + +@router.get("/{doc_id}/image") +async def get_document_image( + doc_id: int, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + result = await db.execute(select(Document).where(Document.id == doc_id, Document.user_id == user.id)) + doc = result.scalar_one_or_none() + if not doc: + raise HTTPException(status_code=404, detail="Documento não encontrado") + return {"image": doc.original_image} + +@router.delete("/{doc_id}") +async def delete_document( + doc_id: int, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + result = await db.execute(select(Document).where(Document.id == doc_id, Document.user_id == user.id)) + doc = result.scalar_one_or_none() + if not doc: + raise HTTPException(status_code=404, detail="Documento não encontrado") + await db.delete(doc) + await db.commit() + return {"message": "Documento excluído"} diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py new file mode 100644 index 0000000..23ad39a --- /dev/null +++ b/backend/app/schemas/auth.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel +from typing import Optional + +class RegisterRequest(BaseModel): + email: str + password: str + name: Optional[str] = None + +class LoginRequest(BaseModel): + email: str + password: str + +class UserResponse(BaseModel): + id: int + email: str + name: Optional[str] + plan: str + +class TokenResponse(BaseModel): + access_token: str + user: UserResponse diff --git a/backend/app/schemas/document.py b/backend/app/schemas/document.py new file mode 100644 index 0000000..ca7612c --- /dev/null +++ b/backend/app/schemas/document.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel +from typing import Optional, List, Any +from datetime import datetime + +class ScanRequest(BaseModel): + image: str # base64 + +class DocumentResponse(BaseModel): + id: int + title: Optional[str] + category: Optional[str] + extracted_text: Optional[str] + summary: Optional[str] + extracted_data: Optional[Any] + risk_alerts: Optional[Any] + tags: Optional[Any] + file_size: Optional[int] + created_at: datetime + +class DocumentListResponse(BaseModel): + documents: List[DocumentResponse] + total: int diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/ai_service.py b/backend/app/services/ai_service.py new file mode 100644 index 0000000..9073de4 --- /dev/null +++ b/backend/app/services/ai_service.py @@ -0,0 +1,65 @@ +import openai +import json +from app.config import settings + +client = openai.AsyncOpenAI(api_key=settings.OPENAI_API_KEY) + +SYSTEM_PROMPT = """Você é CLIO, assistente de IA especializada em análise de documentos. +Ao receber a imagem de um documento, você deve: + +1. Extrair TODO o texto visível (OCR) +2. Identificar a categoria: contrato, nf (nota fiscal), receita (médica), rg, cnh, certidao, boleto, outro +3. Extrair dados estruturados relevantes conforme o tipo: + - CNH: nome, cpf, rg, validade, categoria, registro + - RG: nome, rg, cpf, data_nascimento, naturalidade + - NF: cnpj_emitente, razao_social, valor_total, itens, data_emissao + - Contrato: partes, objeto, valor, prazo, data_assinatura + - Receita: paciente, medico, crm, medicamentos, posologia + - Boleto: beneficiario, valor, vencimento, codigo_barras + - Certidão: tipo, nome, cartorio, data +4. Gerar um resumo em bullets (máx 5 pontos) +5. Identificar alertas de risco (cláusulas abusivas, prazos vencendo, valores suspeitos) +6. Sugerir tags relevantes + +Responda SEMPRE em JSON válido com esta estrutura: +{ + "title": "título descritivo curto do documento", + "category": "categoria", + "extracted_text": "texto completo extraído", + "extracted_data": { ... dados estruturados ... }, + "summary": "• ponto 1\\n• ponto 2\\n• ponto 3", + "risk_alerts": ["alerta 1", "alerta 2"], + "tags": ["tag1", "tag2"] +}""" + +async def analyze_document(image_base64: str) -> dict: + """Analyze a document image using GPT-4o vision.""" + # Remove data URL prefix if present + if "," in image_base64: + image_base64 = image_base64.split(",", 1)[1] + + response = await client.chat.completions.create( + model=settings.OPENAI_MODEL_VISION, + messages=[ + {"role": "system", "content": SYSTEM_PROMPT}, + { + "role": "user", + "content": [ + {"type": "text", "text": "Analise este documento. Extraia todas as informações e retorne o JSON estruturado."}, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{image_base64}", + "detail": "high" + } + } + ] + } + ], + max_tokens=4096, + temperature=0.1, + response_format={"type": "json_object"} + ) + + result = json.loads(response.choices[0].message.content) + return result diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/utils/security.py b/backend/app/utils/security.py new file mode 100644 index 0000000..26e4c9d --- /dev/null +++ b/backend/app/utils/security.py @@ -0,0 +1,42 @@ +from passlib.context import CryptContext +from jose import jwt, JWTError +from datetime import datetime, timedelta, timezone +from fastapi import Depends, HTTPException +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from app.config import settings +from app.database import get_db +from app.models.user import User + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +security = HTTPBearer() + +def hash_password(password: str) -> str: + return pwd_context.hash(password) + +def verify_password(plain: str, hashed: str) -> bool: + return pwd_context.verify(plain, hashed) + +def create_access_token(data: dict) -> str: + to_encode = data.copy() + expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: AsyncSession = Depends(get_db) +) -> User: + try: + payload = jwt.decode(credentials.credentials, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + user_id = payload.get("sub") + if user_id is None: + raise HTTPException(status_code=401, detail="Token inválido") + except JWTError: + raise HTTPException(status_code=401, detail="Token inválido") + result = await db.execute(select(User).where(User.id == int(user_id))) + user = result.scalar_one_or_none() + if user is None: + raise HTTPException(status_code=401, detail="Usuário não encontrado") + return user diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..a306a1f --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,9 @@ +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +sqlalchemy[asyncio]==2.0.35 +asyncpg==0.30.0 +pydantic-settings==2.5.2 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +openai==1.51.0 +python-multipart==0.0.12 diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..40c3d68 --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 0000000..9876987 --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,14 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + async rewrites() { + return [ + { + source: '/api/:path*', + destination: 'http://127.0.0.1:8096/api/:path*', + }, + ]; + }, +}; + +module.exports = nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..ce4f137 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1624 @@ +{ + "name": "clio-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clio-frontend", + "version": "1.0.0", + "dependencies": { + "html5-qrcode": "^2.3.8", + "lucide-react": "^0.441.0", + "next": "14.2.15", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.16.10", + "@types/react": "^18.3.11", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "typescript": "^5.6.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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/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", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz", + "integrity": "sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz", + "integrity": "sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz", + "integrity": "sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz", + "integrity": "sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz", + "integrity": "sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz", + "integrity": "sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz", + "integrity": "sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz", + "integrity": "sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", + "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz", + "integrity": "sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "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" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "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/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html5-qrcode": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", + "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==", + "license": "Apache-2.0" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.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", + "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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.441.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.441.0.tgz", + "integrity": "sha512-0vfExYtvSDhkC2lqg0zYVW1Uu9GsI4knuV9GP9by5z0Xhc4Zi5RejTxfz9LsjRmCyWVzHCJvxGKZWcRyvQCWVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.15.tgz", + "integrity": "sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.15", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.15", + "@next/swc-darwin-x64": "14.2.15", + "@next/swc-linux-arm64-gnu": "14.2.15", + "@next/swc-linux-arm64-musl": "14.2.15", + "@next/swc-linux-x64-gnu": "14.2.15", + "@next/swc-linux-x64-musl": "14.2.15", + "@next/swc-win32-arm64-msvc": "14.2.15", + "@next/swc-win32-ia32-msvc": "14.2.15", + "@next/swc-win32-x64-msvc": "14.2.15" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "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": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "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": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..eb6222a --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "clio-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev -p 3086", + "build": "next build", + "start": "next start -p 3086" + }, + "dependencies": { + "next": "14.2.15", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "html5-qrcode": "^2.3.8", + "lucide-react": "^0.441.0" + }, + "devDependencies": { + "@types/node": "^20.16.10", + "@types/react": "^18.3.11", + "typescript": "^5.6.2", + "tailwindcss": "^3.4.13", + "postcss": "^8.4.47", + "autoprefixer": "^10.4.20" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/public/icon-192.svg b/frontend/public/icon-192.svg new file mode 100644 index 0000000..c7ba68e --- /dev/null +++ b/frontend/public/icon-192.svg @@ -0,0 +1,6 @@ + + + + 📜 + CLIO + \ No newline at end of file diff --git a/frontend/public/icon-512.svg b/frontend/public/icon-512.svg new file mode 100644 index 0000000..7b60cc4 --- /dev/null +++ b/frontend/public/icon-512.svg @@ -0,0 +1,6 @@ + + + + 📜 + CLIO + \ No newline at end of file diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 0000000..ccf3ece --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "CLIO — Scanner Inteligente com IA", + "short_name": "CLIO", + "description": "Escaneie documentos com IA. OCR, categorização e extração automática.", + "start_url": "/", + "display": "standalone", + "background_color": "#0A0E17", + "theme_color": "#6C63FF", + "orientation": "portrait", + "icons": [ + { "src": "/icon-192.svg", "sizes": "192x192", "type": "image/svg+xml" }, + { "src": "/icon-512.svg", "sizes": "512x512", "type": "image/svg+xml" } + ] +} diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 0000000..be249f7 --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1,24 @@ +const CACHE_NAME = 'clio-v1'; +const STATIC_ASSETS = ['/', '/manifest.json']; + +self.addEventListener('install', e => { + e.waitUntil(caches.open(CACHE_NAME).then(c => c.addAll(STATIC_ASSETS))); + self.skipWaiting(); +}); + +self.addEventListener('activate', e => { + e.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))))); + self.clients.claim(); +}); + +self.addEventListener('fetch', e => { + if (e.request.method !== 'GET') return; + if (e.request.url.includes('/api/')) return; + e.respondWith( + fetch(e.request).then(res => { + const clone = res.clone(); + caches.open(CACHE_NAME).then(c => c.put(e.request, clone)); + return res; + }).catch(() => caches.match(e.request)) + ); +}); diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..00cc4b8 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,71 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + background: #0A0E17; + color: #e5e7eb; + font-family: 'Inter', system-ui, sans-serif; +} + +.glass { + background: rgba(17, 24, 39, 0.7); + backdrop-filter: blur(20px); + border: 1px solid rgba(108, 99, 255, 0.15); +} + +.glass-card { + background: rgba(17, 24, 39, 0.6); + backdrop-filter: blur(16px); + border: 1px solid rgba(108, 99, 255, 0.1); + border-radius: 16px; +} + +.gradient-border { + border: 1px solid transparent; + background-clip: padding-box; + position: relative; +} +.gradient-border::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background: linear-gradient(135deg, #6C63FF, #00D4AA); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; +} + +.btn-primary { + @apply bg-primary hover:bg-primary/80 text-white font-medium py-3 px-6 rounded-xl transition-all duration-200; +} + +.btn-accent { + @apply bg-accent hover:bg-accent/80 text-dark-900 font-medium py-3 px-6 rounded-xl transition-all duration-200; +} + +.input-field { + @apply w-full bg-dark-800 border border-dark-600 rounded-xl px-4 py-3 text-gray-200 placeholder-gray-500 focus:outline-none focus:border-primary/50 focus:ring-1 focus:ring-primary/30 transition-all; +} + +.category-badge { + @apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium; +} + +.scan-pulse { + animation: pulse-ring 2s ease-out infinite; +} + +@keyframes pulse-ring { + 0% { box-shadow: 0 0 0 0 rgba(108, 99, 255, 0.4); } + 70% { box-shadow: 0 0 0 20px rgba(108, 99, 255, 0); } + 100% { box-shadow: 0 0 0 0 rgba(108, 99, 255, 0); } +} + +/* Scrollbar */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: #0A0E17; } +::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #6C63FF; } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..e899a68 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata, Viewport } from 'next'; +import RegisterSW from './register-sw'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'CLIO — Scanner Inteligente com IA', + description: 'Escaneie documentos com IA. OCR inteligente, categorização automática, extração de dados e alertas de risco.', + manifest: '/manifest.json', + icons: { icon: '/icon-192.svg', apple: '/icon-512.svg' }, +}; + +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, + maximumScale: 1, + themeColor: '#0A0E17', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {children} + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..909dbdd --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { api } from '@/lib/api'; +import LoginScreen from '@/components/LoginScreen'; +import Dashboard from '@/components/Dashboard'; + +export default function Home() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const token = localStorage.getItem('clio_token'); + const savedUser = localStorage.getItem('clio_user'); + if (token && savedUser) { + setUser(JSON.parse(savedUser)); + } + setLoading(false); + }, []); + + const handleLogin = (token: string, userData: any) => { + localStorage.setItem('clio_token', token); + localStorage.setItem('clio_user', JSON.stringify(userData)); + setUser(userData); + }; + + const handleLogout = () => { + localStorage.removeItem('clio_token'); + localStorage.removeItem('clio_user'); + setUser(null); + }; + + if (loading) { + return ( +
+
+
📜
+
CLIO
+
Carregando...
+
+
+ ); + } + + if (!user) { + return ; + } + + return ; +} diff --git a/frontend/src/app/register-sw.tsx b/frontend/src/app/register-sw.tsx new file mode 100644 index 0000000..ed6bfdc --- /dev/null +++ b/frontend/src/app/register-sw.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { useEffect } from 'react'; + +export default function RegisterSW() { + useEffect(() => { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').catch(() => {}); + } + }, []); + return null; +} diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx new file mode 100644 index 0000000..adc8431 --- /dev/null +++ b/frontend/src/components/Dashboard.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { useState } from 'react'; +import Scanner from '@/components/Scanner'; +import DocumentList from '@/components/DocumentList'; +import DocumentDetail from '@/components/DocumentDetail'; + +type View = 'scan' | 'history' | 'detail'; + +export default function Dashboard({ user, onLogout }: { user: any; onLogout: () => void }) { + const [view, setView] = useState('scan'); + const [selectedDoc, setSelectedDoc] = useState(null); + const [refreshKey, setRefreshKey] = useState(0); + + const openDoc = (id: number) => { setSelectedDoc(id); setView('detail'); }; + const onScanComplete = () => { setRefreshKey(k => k + 1); setView('history'); }; + + return ( +
+ {/* Header */} +
+
+ 📜 + + CLIO + +
+
+ {user.name || user.email} + + {user.plan === 'premium' ? '⭐ Premium' : 'Free'} + + +
+
+ + {/* Content */} +
+ {view === 'scan' && } + {view === 'history' && } + {view === 'detail' && selectedDoc && ( + { setView('history'); }} + onDelete={() => { setRefreshKey(k => k + 1); setView('history'); }} + /> + )} +
+ + {/* Bottom Nav */} + +
+ ); +} diff --git a/frontend/src/components/DocumentDetail.tsx b/frontend/src/components/DocumentDetail.tsx new file mode 100644 index 0000000..897cd72 --- /dev/null +++ b/frontend/src/components/DocumentDetail.tsx @@ -0,0 +1,150 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { api } from '@/lib/api'; + +export default function DocumentDetail({ + docId, onBack, onDelete +}: { docId: number; onBack: () => void; onDelete: () => void }) { + const [doc, setDoc] = useState(null); + const [showImage, setShowImage] = useState(false); + const [imageData, setImageData] = useState(''); + const [showText, setShowText] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + api.getDocument(docId).then(setDoc).finally(() => setLoading(false)); + }, [docId]); + + const loadImage = async () => { + if (imageData) { setShowImage(!showImage); return; } + const res = await api.getDocumentImage(docId); + setImageData(res.image); + setShowImage(true); + }; + + const handleDelete = async () => { + if (!confirm('Excluir este documento?')) return; + await api.deleteDocument(docId); + onDelete(); + }; + + const downloadImage = () => { + if (!imageData) return; + const link = document.createElement('a'); + link.href = imageData.startsWith('data:') ? imageData : `data:image/jpeg;base64,${imageData}`; + link.download = `${doc?.title || 'documento'}.jpg`; + link.click(); + }; + + const categoryColors: Record = { + contrato: 'bg-blue-500/20 text-blue-400', nf: 'bg-green-500/20 text-green-400', + receita: 'bg-pink-500/20 text-pink-400', rg: 'bg-yellow-500/20 text-yellow-400', + cnh: 'bg-orange-500/20 text-orange-400', outro: 'bg-gray-500/20 text-gray-400', + }; + + if (loading) return
Carregando...
; + if (!doc) return
Documento não encontrado
; + + return ( +
+ {/* Header */} +
+ +
+

{doc.title}

+
+ + {doc.category?.toUpperCase()} + + {new Date(doc.created_at).toLocaleDateString('pt-BR')} +
+
+
+ + {/* Summary */} + {doc.summary && ( +
+

📌 Resumo

+
{doc.summary}
+
+ )} + + {/* Extracted Data */} + {doc.extracted_data && Object.keys(doc.extracted_data).length > 0 && ( +
+

📊 Dados Extraídos

+
+ {Object.entries(doc.extracted_data).map(([key, val]) => ( +
+ {key.replace(/_/g, ' ')} + + {typeof val === 'object' ? JSON.stringify(val) : String(val)} + +
+ ))} +
+
+ )} + + {/* Risk Alerts */} + {doc.risk_alerts && doc.risk_alerts.length > 0 && ( +
+

⚠️ Alertas de Risco

+
+ {doc.risk_alerts.map((alert: string, i: number) => ( +
+ {alert} +
+ ))} +
+
+ )} + + {/* Tags */} + {doc.tags && doc.tags.length > 0 && ( +
+ {doc.tags.map((tag: string, i: number) => ( + #{tag} + ))} +
+ )} + + {/* Full text toggle */} + + {showText && doc.extracted_text && ( +
+ {doc.extracted_text} +
+ )} + + {/* Actions */} +
+ + {imageData && ( + + )} +
+ + {showImage && imageData && ( +
+ {doc.title} +
+ )} + + +
+ ); +} diff --git a/frontend/src/components/DocumentList.tsx b/frontend/src/components/DocumentList.tsx new file mode 100644 index 0000000..c56eb61 --- /dev/null +++ b/frontend/src/components/DocumentList.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { api } from '@/lib/api'; + +const categoryColors: Record = { + contrato: 'bg-blue-500/20 text-blue-400', + nf: 'bg-green-500/20 text-green-400', + receita: 'bg-pink-500/20 text-pink-400', + rg: 'bg-yellow-500/20 text-yellow-400', + cnh: 'bg-orange-500/20 text-orange-400', + certidao: 'bg-purple-500/20 text-purple-400', + boleto: 'bg-red-500/20 text-red-400', + outro: 'bg-gray-500/20 text-gray-400', +}; + +const categoryEmojis: Record = { + contrato: '📄', nf: '🧾', receita: '💊', rg: '🪪', cnh: '🚗', + certidao: '📋', boleto: '💰', outro: '📎', +}; + +export default function DocumentList({ onSelect }: { onSelect: (id: number) => void }) { + const [docs, setDocs] = useState([]); + const [total, setTotal] = useState(0); + const [search, setSearch] = useState(''); + const [category, setCategory] = useState(''); + const [loading, setLoading] = useState(true); + + const load = async () => { + setLoading(true); + try { + const res = await api.getDocuments({ search: search || undefined, category: category || undefined }); + setDocs(res.documents); + setTotal(res.total); + } catch (err) {} + setLoading(false); + }; + + useEffect(() => { load(); }, [search, category]); + + const formatDate = (d: string) => { + const date = new Date(d); + return date.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }); + }; + + const formatSize = (bytes: number) => { + if (!bytes) return ''; + if (bytes < 1024) return `${bytes}B`; + if (bytes < 1024*1024) return `${(bytes/1024).toFixed(0)}KB`; + return `${(bytes/1024/1024).toFixed(1)}MB`; + }; + + return ( +
+

📋 Histórico

+ + {/* Search */} + setSearch(e.target.value)} + className="input-field" + /> + + {/* Category filter */} +
+ + {['contrato','nf','receita','rg','cnh','boleto','outro'].map(cat => ( + + ))} +
+ + {/* List */} + {loading ? ( +
Carregando...
+ ) : docs.length === 0 ? ( +
+
📭
+

Nenhum documento encontrado

+

Escaneie seu primeiro documento!

+
+ ) : ( +
+ {docs.map(doc => ( + + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/LoginScreen.tsx b/frontend/src/components/LoginScreen.tsx new file mode 100644 index 0000000..5909a37 --- /dev/null +++ b/frontend/src/components/LoginScreen.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { useState } from 'react'; +import { api } from '@/lib/api'; + +export default function LoginScreen({ onLogin }: { onLogin: (token: string, user: any) => void }) { + const [isRegister, setIsRegister] = useState(false); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [name, setName] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + try { + const res = isRegister + ? await api.register(email, password, name) + : await api.login(email, password); + onLogin(res.access_token, res.user); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Logo */} +
+
📜
+

+ CLIO +

+

Scanner Inteligente com IA

+
+ + {/* Form */} +
+

+ {isRegister ? 'Criar conta' : 'Entrar'} +

+ +
+ {isRegister && ( + setName(e.target.value)} + className="input-field" + /> + )} + setEmail(e.target.value)} + className="input-field" + required + /> + setPassword(e.target.value)} + className="input-field" + required + /> + + {error && ( +
+ {error} +
+ )} + + +
+ +
+ +
+
+ +

+ Musa da História • AI Vertice +

+
+
+ ); +} diff --git a/frontend/src/components/Scanner.tsx b/frontend/src/components/Scanner.tsx new file mode 100644 index 0000000..d35e0b4 --- /dev/null +++ b/frontend/src/components/Scanner.tsx @@ -0,0 +1,273 @@ +'use client'; + +import { useState, useRef, useCallback, useEffect } from 'react'; +import { api } from '@/lib/api'; + +export default function Scanner({ onComplete }: { onComplete: () => void }) { + const [mode, setMode] = useState<'idle' | 'camera' | 'processing' | 'result'>('idle'); + const [result, setResult] = useState(null); + const [error, setError] = useState(''); + const videoRef = useRef(null); + const canvasRef = useRef(null); + const streamRef = useRef(null); + const fileInputRef = useRef(null); + + const stopCamera = useCallback(() => { + if (streamRef.current) { + streamRef.current.getTracks().forEach(t => t.stop()); + streamRef.current = null; + } + }, []); + + useEffect(() => { + return () => stopCamera(); + }, [stopCamera]); + + const startCamera = async () => { + setError(''); + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment', width: { ideal: 1920 }, height: { ideal: 1080 } } + }); + streamRef.current = stream; + if (videoRef.current) { + videoRef.current.srcObject = stream; + await videoRef.current.play(); + } + setMode('camera'); + } catch (err) { + setError('Não foi possível acessar a câmera. Verifique as permissões.'); + } + }; + + const capturePhoto = () => { + if (!videoRef.current || !canvasRef.current) return; + const video = videoRef.current; + const canvas = canvasRef.current; + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d')!; + ctx.drawImage(video, 0, 0); + const base64 = canvas.toDataURL('image/jpeg', 0.85); + stopCamera(); + processImage(base64); + }; + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + const base64 = ev.target?.result as string; + processImage(base64); + }; + reader.readAsDataURL(file); + }; + + const processImage = async (base64: string) => { + setMode('processing'); + setError(''); + try { + const res = await api.scanDocument(base64); + setResult(res); + setMode('result'); + } catch (err: any) { + setError(err.message); + setMode('idle'); + } + }; + + const categoryColors: Record = { + contrato: 'bg-blue-500/20 text-blue-400', + nf: 'bg-green-500/20 text-green-400', + receita: 'bg-pink-500/20 text-pink-400', + rg: 'bg-yellow-500/20 text-yellow-400', + cnh: 'bg-orange-500/20 text-orange-400', + certidao: 'bg-purple-500/20 text-purple-400', + boleto: 'bg-red-500/20 text-red-400', + outro: 'bg-gray-500/20 text-gray-400', + }; + + const categoryLabels: Record = { + contrato: '📄 Contrato', + nf: '🧾 Nota Fiscal', + receita: '💊 Receita', + rg: '🪪 RG', + cnh: '🚗 CNH', + certidao: '📋 Certidão', + boleto: '💰 Boleto', + outro: '📎 Outro', + }; + + if (mode === 'camera') { + return ( +
+
+