From 9a1a9d32474616dd99e0b6cb981cfb2f18c76a52 Mon Sep 17 00:00:00 2001 From: handsomezhuzhu <2658601135@qq.com> Date: Fri, 17 Apr 2026 21:15:06 +0800 Subject: [PATCH] refactor: remove legacy frontend code and implement new Next.js structure - Deleted the old Register page and utility functions. - Removed Tailwind CSS configuration and Vite configuration files. - Added a new script for starting a single container with FastAPI and Next.js. - Updated README to reflect the current status of the Next.js frontend. - Implemented new login and registration API routes with improved error handling. - Refactored frontend API calls to use the new proxy structure. - Enhanced error handling in API response processing. - Updated components to align with the new API endpoints and structure. --- .github/workflows/docker-publish.yml | 19 +- .gitignore | 2 - AGENTS.md | 16 +- Dockerfile | 51 +- README.md | 94 +- backend/main.py | 210 +- docker-compose-single.yml | 6 +- docs/MYSQL_SETUP.md | 328 +- docs/PROJECT_STRUCTURE.md | 136 +- docs/audit/architecture.md | 38 +- docs/audit/frontend-migration.md | 72 +- frontend/.dockerignore | 9 - frontend/.env.example | 4 - frontend/Dockerfile | 18 - frontend/index.html | 14 - frontend/package-lock.json | 2928 ----------------- frontend/package.json | 45 - frontend/postcss.config.js | 6 - frontend/src/App.jsx | 101 - frontend/src/api/client.js | 240 -- frontend/src/components/Layout.jsx | 142 - frontend/src/components/Pagination.jsx | 87 - frontend/src/components/ParsingProgress.jsx | 121 - frontend/src/components/ProtectedRoute.jsx | 28 - frontend/src/context/AuthContext.jsx | 93 - frontend/src/index.css | 22 - frontend/src/index.jsx | 10 - frontend/src/pages/AdminPanel.jsx | 356 -- frontend/src/pages/AdminSettings.jsx | 562 ---- frontend/src/pages/Dashboard.jsx | 187 -- frontend/src/pages/ExamDetail.jsx | 387 --- frontend/src/pages/ExamList.jsx | 342 -- frontend/src/pages/Login.jsx | 117 - frontend/src/pages/MistakeList.jsx | 242 -- frontend/src/pages/MistakePlayer.jsx | 412 --- frontend/src/pages/QuestionBank.jsx | 170 - frontend/src/pages/QuizPlayer.jsx | 369 --- frontend/src/pages/Register.jsx | 159 - frontend/src/utils/helpers.js | 117 - frontend/tailwind.config.js | 26 - frontend/vite.config.js | 27 - scripts/start_single_container.py | 98 + web/README.md | 11 +- web/src/app/(auth)/login/page.tsx | 15 +- web/src/app/(auth)/register/page.tsx | 15 +- web/src/app/api/auth/login/route.ts | 37 - web/src/app/api/auth/me/route.ts | 30 - web/src/app/frontend-api/auth/login/route.ts | 60 + .../auth/logout/route.ts | 0 web/src/app/frontend-api/auth/me/route.ts | 57 + .../exams/[examId]/progress/route.ts | 24 +- .../proxy/[...path]/route.ts | 11 +- .../components/app-shell/logout-button.tsx | 2 +- .../components/exams/exam-detail-client.tsx | 2 +- .../practice/mistake-practice-client.tsx | 8 +- .../practice/quiz-player-client.tsx | 8 +- web/src/lib/api/browser.ts | 27 +- web/src/lib/api/config.ts | 4 +- web/src/lib/api/response.ts | 61 + web/src/lib/api/server.ts | 24 +- 60 files changed, 819 insertions(+), 7988 deletions(-) delete mode 100644 frontend/.dockerignore delete mode 100644 frontend/.env.example delete mode 100644 frontend/Dockerfile delete mode 100644 frontend/index.html delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/postcss.config.js delete mode 100644 frontend/src/App.jsx delete mode 100644 frontend/src/api/client.js delete mode 100644 frontend/src/components/Layout.jsx delete mode 100644 frontend/src/components/Pagination.jsx delete mode 100644 frontend/src/components/ParsingProgress.jsx delete mode 100644 frontend/src/components/ProtectedRoute.jsx delete mode 100644 frontend/src/context/AuthContext.jsx delete mode 100644 frontend/src/index.css delete mode 100644 frontend/src/index.jsx delete mode 100644 frontend/src/pages/AdminPanel.jsx delete mode 100644 frontend/src/pages/AdminSettings.jsx delete mode 100644 frontend/src/pages/Dashboard.jsx delete mode 100644 frontend/src/pages/ExamDetail.jsx delete mode 100644 frontend/src/pages/ExamList.jsx delete mode 100644 frontend/src/pages/Login.jsx delete mode 100644 frontend/src/pages/MistakeList.jsx delete mode 100644 frontend/src/pages/MistakePlayer.jsx delete mode 100644 frontend/src/pages/QuestionBank.jsx delete mode 100644 frontend/src/pages/QuizPlayer.jsx delete mode 100644 frontend/src/pages/Register.jsx delete mode 100644 frontend/src/utils/helpers.js delete mode 100644 frontend/tailwind.config.js delete mode 100644 frontend/vite.config.js create mode 100644 scripts/start_single_container.py delete mode 100644 web/src/app/api/auth/login/route.ts delete mode 100644 web/src/app/api/auth/me/route.ts create mode 100644 web/src/app/frontend-api/auth/login/route.ts rename web/src/app/{api => frontend-api}/auth/logout/route.ts (100%) create mode 100644 web/src/app/frontend-api/auth/me/route.ts rename web/src/app/{api => frontend-api}/exams/[examId]/progress/route.ts (71%) rename web/src/app/{api => frontend-api}/proxy/[...path]/route.ts (90%) create mode 100644 web/src/lib/api/response.ts diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index e2687f0..1118554 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,4 +1,4 @@ -name: Build and Publish Docker Images +name: Build and Publish Single-Container Image on: push: @@ -18,15 +18,6 @@ jobs: permissions: contents: read packages: write - strategy: - matrix: - include: - - image_suffix: backend - context: ./backend - file: ./backend/Dockerfile - - image_suffix: frontend - context: ./web - file: ./web/Dockerfile steps: - name: Checkout repository @@ -46,7 +37,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.image_suffix }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=semver,pattern={{version}} @@ -57,13 +48,13 @@ jobs: id: build uses: docker/build-push-action@v5 with: - context: ${{ matrix.context }} - file: ${{ matrix.file }} + context: . + file: ./Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha - cache-to: type=gha,mode=max,scope=${{ matrix.image_suffix }} + cache-to: type=gha,mode=max,scope=single-container platforms: linux/amd64,linux/arm64 - name: Image digest diff --git a/.gitignore b/.gitignore index 3d49bab..baebcd4 100644 --- a/.gitignore +++ b/.gitignore @@ -44,8 +44,6 @@ yarn-error.log .pnp.js # Build -frontend/build/ -frontend/dist/ .next/ web/.next/ web/out/ diff --git a/AGENTS.md b/AGENTS.md index 6c99ff3..e15623e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,19 +2,19 @@ ## Project Structure & Module Organization - `backend/`: FastAPI API. Keep HTTP entrypoints in `routers/`, reusable business logic in `services/`, database definitions in `models.py`, request/response schemas in `schemas.py`, and migrations in `alembic/`. -- `frontend/`: React 18 + Vite client. Put route screens in `src/pages/`, shared UI in `src/components/`, auth state in `src/context/`, API wrappers in `src/api/`, and helpers in `src/utils/`. -- `docs/` holds deployment and architecture notes, `scripts/run_local.sh` bootstraps local Linux/macOS development, `test_data/` contains sample question files, and `.github/workflows/docker-publish.yml` publishes container images. +- `web/`: Next.js frontend for active development. Keep route screens under `src/app/` or related route segments, shared UI in `src/components/`, API wrappers in `src/lib/`, and helpers close to their consumers. +- `docs/` holds deployment and architecture notes, `scripts/run_local.sh` bootstraps local Linux/macOS development, `test_data/` contains sample question files, and `.github/workflows/docker-publish.yml` publishes the single-container image. ## Build, Test, and Development Commands -- `docker compose up -d --build`: start MySQL, backend on `:8000`, and frontend on `:3000`. -- `docker compose -f docker-compose-single.yml up -d --build`: start the single-container SQLite deployment. +- `docker compose -f docker-compose-single.yml up -d --build`: start the default single-container deployment with FastAPI proxying the embedded Next.js frontend. +- `docker compose up -d --build`: start the split development stack with backend on `:8000` and frontend on `:3000`. - `cd backend && pip install -r requirements.txt && alembic upgrade head && uvicorn main:app --reload --host 0.0.0.0 --port 8000`: run the API locally. -- `cd frontend && npm install && npm run dev`: start the Vite dev server. -- `cd frontend && npm run build`: create a production frontend bundle. +- `cd web && npm install && npm run dev`: start the Next.js dev server. +- `cd web && npm run build`: create a production frontend bundle. ## Coding Style & Naming Conventions - Python uses 4-space indentation, `snake_case` for modules/functions, and `PascalCase` for ORM or Pydantic classes. -- React files use `PascalCase.jsx` for pages/components and `camelCase` for state, helpers, and API wrappers. +- React and Next.js files use the naming conventions already established in `web/`; preserve route segment and component naming patterns in place. - Keep route handlers thin: validation in schemas, orchestration in routers, reusable logic in `backend/services/`. - No formatter or lint script is enforced today, so match surrounding style before making broad formatting changes. @@ -22,7 +22,7 @@ - The repository currently has no committed automated test suite or coverage gate. - Before opening a PR, smoke-test auth, exam creation/upload, parsing progress, quiz playback, mistake review, and admin settings. - Use `test_data/sample_questions*.txt` for parser and import checks. -- If you add tests, place backend tests under `backend/tests/test_*.py` and frontend tests under `frontend/src/__tests__/`. +- If you add tests, place backend tests under `backend/tests/test_*.py` and frontend tests under `web/src/__tests__/`. ## Commit & Pull Request Guidelines - Recent history favors short, focused subjects, often imperative and sometimes Chinese, such as `安全修复和管理员账号密码自定义`. diff --git a/Dockerfile b/Dockerfile index 29f321e..a337df3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,51 +1,50 @@ -# ==================== 多阶段构建:前后端整合单容器 ==================== -# Stage 1: 构建前端 -FROM node:18-slim AS frontend-builder +# ==================== 多阶段构建:单容器运行 FastAPI + Next.js ==================== +# Stage 1: 构建 Next.js 前端 +FROM node:20-slim AS web-builder -WORKDIR /frontend +WORKDIR /web -# 复制前端依赖文件 -COPY frontend/package*.json ./ - -# 安装依赖 +COPY web/package*.json ./ RUN npm ci -# 复制前端源代码 -COPY frontend/ ./ +COPY web/ ./ + +ENV NEXT_TELEMETRY_DISABLED=1 -# 构建前端(生成静态文件到 dist 目录) RUN npm run build -# Stage 2: 构建后端并整合前端 -FROM python:3.11-slim +# Stage 2: 运行 FastAPI + Next.js +FROM node:20-slim WORKDIR /app -# 安装操作系统依赖(python-magic 需要 libmagic) +# 安装 Python 运行时和操作系统依赖 RUN apt-get update \ - && apt-get install -y --no-install-recommends libmagic1 \ + && apt-get install -y --no-install-recommends python3 python3-pip python3-venv libmagic1 \ && rm -rf /var/lib/apt/lists/* -# 复制后端依赖文件 -COPY backend/requirements.txt ./ +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" -# 安装 Python 依赖(使用预编译wheel包,无需gcc) -RUN pip install -r requirements.txt +# 安装后端依赖 +COPY backend/requirements.txt ./backend-requirements.txt +RUN python -m pip install --no-cache-dir -r backend-requirements.txt -# 复制后端代码 +# 复制后端代码和启动脚本 COPY backend/ ./ +COPY scripts/start_single_container.py ./scripts/start_single_container.py -# 从前端构建阶段复制静态文件到后端 static 目录 -COPY --from=frontend-builder /frontend/build ./static +# 复制 Next.js standalone 产物 +COPY --from=web-builder /web/.next/standalone ./web +COPY --from=web-builder /web/.next/static ./web/.next/static # 创建上传目录 RUN mkdir -p ./uploads -# 暴露端口 EXPOSE 8000 -# 设置环境变量 ENV PYTHONUNBUFFERED=1 +ENV NEXT_SERVER_URL=http://127.0.0.1:3000 +ENV NEXT_TELEMETRY_DISABLED=1 -# 启动命令 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["python", "scripts/start_single_container.py"] diff --git a/README.md b/README.md index 13d1493..f37f83c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ QQuiz 是一个用于题库导入、刷题训练和错题管理的全栈应用 ## 快速开始 -### 方式一:直接运行 GitHub Actions 构建好的镜像 +QQuiz 默认以单容器形式发布和部署。GitHub Actions 只构建根目录 `Dockerfile` 生成的单容器镜像,README 也以这个路径为主。 + +### 方式一:直接运行 GitHub Actions 构建好的单容器镜像 适合只想快速启动,不想先克隆仓库。 @@ -58,58 +60,61 @@ GEMINI_API_KEY=your-real-gemini-api-key #### 3. 拉取镜像 ```bash -docker pull ghcr.io/handsomezhuzhu/qquiz-backend:latest -docker pull ghcr.io/handsomezhuzhu/qquiz-frontend:latest +docker pull ghcr.io/handsomezhuzhu/qquiz:latest ``` -#### 4. 创建网络和数据卷 +#### 4. 创建数据卷 ```bash -docker network create qquiz_net -docker volume create qquiz_sqlite_data -docker volume create qquiz_upload_files +docker volume create qquiz_data +docker volume create qquiz_uploads ``` -#### 5. 启动后端 +#### 5. 启动容器 ```bash docker run -d \ - --name qquiz_backend \ - --network qquiz_net \ + --name qquiz \ --env-file .env \ -e DATABASE_URL=sqlite+aiosqlite:////app/data/qquiz.db \ -e UPLOAD_DIR=/app/uploads \ - -v qquiz_sqlite_data:/app/data \ - -v qquiz_upload_files:/app/uploads \ + -v qquiz_data:/app/data \ + -v qquiz_uploads:/app/uploads \ -p 8000:8000 \ - ghcr.io/handsomezhuzhu/qquiz-backend:latest -``` - -#### 6. 启动前端 - -```bash -docker run -d \ - --name qquiz_frontend \ - --network qquiz_net \ - -e API_BASE_URL=http://qquiz_backend:8000 \ - -p 3000:3000 \ - ghcr.io/handsomezhuzhu/qquiz-frontend:latest + --restart unless-stopped \ + ghcr.io/handsomezhuzhu/qquiz:latest ``` 访问: -- 前端:`http://localhost:3000` -- 后端:`http://localhost:8000` +- 应用:`http://localhost:8000` +- API 文档:`http://localhost:8000/docs` 停止: ```bash -docker rm -f qquiz_frontend qquiz_backend +docker rm -f qquiz ``` -### 方式二:从源码用 Docker Compose 启动 +### 方式二:从源码启动单容器 -#### 前后端分离,推荐 +适合需要自行构建镜像或修改代码后再部署。 + +```bash +cp .env.example .env +docker compose -f docker-compose-single.yml up -d --build +``` + +访问: + +- 应用:`http://localhost:8000` +- API 文档:`http://localhost:8000/docs` + +### 可选:开发或兼容性部署 + +以下方式保留用于开发调试或兼容场景,不再作为默认部署方案: + +#### 前后端分离开发栈 ```bash cp .env.example .env @@ -121,24 +126,14 @@ docker compose up -d --build - 前端:`http://localhost:3000` - 后端:`http://localhost:8000` -#### 使用 MySQL +#### 分离栈叠加 MySQL ```bash cp .env.example .env docker compose -f docker-compose.yml -f docker-compose.mysql.yml up -d --build ``` -#### 单容器模式 - -```bash -cp .env.example .env -docker compose -f docker-compose-single.yml up -d --build -``` - -访问: - -- 应用:`http://localhost:8000` -- API 文档:`http://localhost:8000/docs` +MySQL 相关说明见 [docs/MYSQL_SETUP.md](docs/MYSQL_SETUP.md)。 ## 本地开发 @@ -163,8 +158,8 @@ npm run dev 说明: -- `web/` 是当前主前端,基于 Next.js -- `frontend/` 是保留中的旧 Vite 前端,主要用于单容器兼容路径 +- `web/` 是唯一前端工程,基于 Next.js +- 单容器镜像会在同一个容器里运行 FastAPI 和 Next.js,并由 FastAPI 代理前端请求 ## 关键环境变量 @@ -191,14 +186,13 @@ npm run dev ```text QQuiz/ ├─ backend/ FastAPI 后端 -├─ web/ Next.js 前端 -├─ frontend/ Legacy Vite 前端 +├─ web/ Next.js 前端工程 ├─ docs/ 文档与截图 ├─ test_data/ 示例题库文件 -├─ docker-compose.yml 前后端分离部署 -├─ docker-compose.mysql.yml MySQL overlay -├─ docker-compose-single.yml 单容器部署 -└─ Dockerfile 单容器镜像构建 +├─ docker-compose-single.yml 单容器部署(默认) +├─ Dockerfile 单容器镜像构建(默认) +├─ docker-compose.yml 前后端分离开发/兼容部署 +└─ docker-compose.mysql.yml MySQL overlay(可选) ``` ## 技术栈 @@ -210,7 +204,7 @@ QQuiz/ ```bash cd web && npm run build -docker compose build backend frontend +docker compose -f docker-compose-single.yml build ``` 建议至少手动验证: diff --git a/backend/main.py b/backend/main.py index f9e22b3..ac03a53 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,16 +1,16 @@ """ -QQuiz FastAPI Application - 单容器模式(前后端整合) +QQuiz FastAPI Application - single-container API and frontend proxy. """ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.staticfiles import StaticFiles -from fastapi.responses import HTMLResponse, FileResponse, JSONResponse +from fastapi.responses import JSONResponse, StreamingResponse from contextlib import asynccontextmanager import os -from pathlib import Path from dotenv import load_dotenv +import httpx from slowapi.errors import RateLimitExceeded from slowapi.middleware import SlowAPIMiddleware +from starlette.background import BackgroundTask from database import init_db, init_default_config, get_db_context from rate_limit import limiter @@ -18,6 +18,22 @@ from rate_limit import limiter # Load environment variables load_dotenv() +NEXT_SERVER_URL = os.getenv("NEXT_SERVER_URL", "http://127.0.0.1:3000").rstrip("/") +INTERNAL_API_URL = os.getenv("INTERNAL_API_URL", "http://127.0.0.1:8000").rstrip("/") +SESSION_COOKIE_NAME = "access_token" +FRONTEND_PROXY_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] +HOP_BY_HOP_HEADERS = { + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", + "content-length", +} + async def rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded): return JSONResponse( @@ -32,6 +48,11 @@ async def lifespan(app: FastAPI): # Startup print("🚀 Starting QQuiz Application...") + app.state.frontend_client = httpx.AsyncClient( + follow_redirects=False, + timeout=httpx.Timeout(30.0, connect=5.0), + ) + # Initialize database await init_db() @@ -49,6 +70,7 @@ async def lifespan(app: FastAPI): yield # Shutdown + await app.state.frontend_client.aclose() print("👋 Shutting down QQuiz Application...") @@ -89,44 +111,152 @@ app.include_router(admin.router, prefix="/api/admin", tags=["Admin"]) @app.get("/health") async def health_check(): """Health check endpoint""" - return {"status": "healthy"} + + try: + response = await app.state.frontend_client.get( + f"{NEXT_SERVER_URL}/login", + headers={"Accept-Encoding": "identity"}, + ) + except httpx.HTTPError: + return JSONResponse( + status_code=503, + content={ + "status": "degraded", + "api": "healthy", + "frontend": "unavailable", + }, + ) + + frontend_status = "healthy" if response.status_code < 500 else "unavailable" + if frontend_status != "healthy": + return JSONResponse( + status_code=503, + content={ + "status": "degraded", + "api": "healthy", + "frontend": frontend_status, + }, + ) + + return {"status": "healthy", "api": "healthy", "frontend": "healthy"} -# ============ 静态文件服务(前后端整合) ============ +def build_frontend_target(request: Request, full_path: str) -> str: + normalized_path = f"/{full_path}" if full_path else "/" + query = request.url.query + return f"{NEXT_SERVER_URL}{normalized_path}{f'?{query}' if query else ''}" -# 检查静态文件目录是否存在 -STATIC_DIR = Path(__file__).parent / "static" -if STATIC_DIR.exists(): - # 挂载静态资源(JS、CSS、图片等) - app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="static_assets") - # 前端应用的所有路由(SPA路由) - @app.get("/{full_path:path}") - async def serve_frontend(full_path: str): - """ - 服务前端应用 - - API 路由已在上面定义,优先匹配 - - 其他所有路由返回 index.html(SPA 单页应用) - """ - index_file = STATIC_DIR / "index.html" - if index_file.exists(): - return FileResponse(index_file) - else: - return { - "message": "Frontend not built yet", - "hint": "Run 'cd frontend && npm run build' to build the frontend" - } -else: - print("⚠️ 静态文件目录不存在,前端功能不可用") - print("提示:请先构建前端应用或使用开发模式") +def build_internal_api_target(request: Request, full_path: str, trailing_slash: bool = False) -> str: + normalized_path = full_path.strip("/") + if trailing_slash and normalized_path: + normalized_path = f"{normalized_path}/" + query = request.url.query + return f"{INTERNAL_API_URL}/api/{normalized_path}{f'?{query}' if query else ''}" - # 如果没有静态文件,显示 API 信息 - @app.get("/") - async def root(): - """Root endpoint""" - return { - "message": "Welcome to QQuiz API", - "version": "1.0.0", - "docs": "/docs", - "note": "Frontend not built. Please build frontend or use docker-compose." - } + +def filter_proxy_headers(request: Request) -> dict[str, str]: + headers = { + key: value + for key, value in request.headers.items() + if key.lower() not in HOP_BY_HOP_HEADERS and key.lower() != "host" + } + # Avoid sending compressed payloads through the proxy so response headers stay accurate. + headers["Accept-Encoding"] = "identity" + return headers + + +def apply_proxy_headers(proxy_response: StreamingResponse, upstream_headers: httpx.Headers) -> None: + proxy_response.raw_headers = [ + (key.encode("latin-1"), value.encode("latin-1")) + for key, value in upstream_headers.multi_items() + if key.lower() not in HOP_BY_HOP_HEADERS + ] + + +@app.api_route("/frontend-api/proxy/{full_path:path}", methods=FRONTEND_PROXY_METHODS, include_in_schema=False) +async def proxy_browser_api(request: Request, full_path: str): + """ + Serve browser-originated API calls directly from FastAPI in single-container mode. + This avoids relying on Next.js route handlers for the /frontend-api/proxy/* namespace. + """ + target = build_internal_api_target(request, full_path) + body = await request.body() + client: httpx.AsyncClient = app.state.frontend_client + headers = filter_proxy_headers(request) + token = request.cookies.get(SESSION_COOKIE_NAME) + if token: + headers["Authorization"] = f"Bearer {token}" + + try: + async def send_request(target_url: str) -> httpx.Response: + upstream_request = client.build_request( + method=request.method, + url=target_url, + headers=headers, + content=body if body else None, + ) + return await client.send( + upstream_request, + stream=True, + follow_redirects=True, + ) + + upstream_response = await send_request(target) + if ( + request.method in {"GET", "HEAD"} + and upstream_response.status_code == 404 + and full_path + and not full_path.endswith("/") + ): + await upstream_response.aclose() + upstream_response = await send_request( + build_internal_api_target(request, full_path, trailing_slash=True) + ) + except httpx.HTTPError: + return JSONResponse( + status_code=502, + content={"detail": "Backend API is unavailable."}, + ) + + proxy_response = StreamingResponse( + upstream_response.aiter_raw(), + status_code=upstream_response.status_code, + background=BackgroundTask(upstream_response.aclose), + ) + apply_proxy_headers(proxy_response, upstream_response.headers) + return proxy_response + + +@app.api_route("/", methods=FRONTEND_PROXY_METHODS, include_in_schema=False) +@app.api_route("/{full_path:path}", methods=FRONTEND_PROXY_METHODS, include_in_schema=False) +async def proxy_frontend(request: Request, full_path: str = ""): + """ + Forward all non-API traffic to the embedded Next.js server. + FastAPI keeps ownership of /api/*, /docs, /openapi.json, /redoc and /health. + """ + target = build_frontend_target(request, full_path) + body = await request.body() + client: httpx.AsyncClient = app.state.frontend_client + + try: + upstream_request = client.build_request( + method=request.method, + url=target, + headers=filter_proxy_headers(request), + content=body if body else None, + ) + upstream_response = await client.send(upstream_request, stream=True) + except httpx.HTTPError: + return JSONResponse( + status_code=502, + content={"detail": "Frontend server is unavailable."}, + ) + + proxy_response = StreamingResponse( + upstream_response.aiter_raw(), + status_code=upstream_response.status_code, + background=BackgroundTask(upstream_response.aclose), + ) + apply_proxy_headers(proxy_response, upstream_response.headers) + return proxy_response diff --git a/docker-compose-single.yml b/docker-compose-single.yml index 799ee1a..7e660e2 100644 --- a/docker-compose-single.yml +++ b/docker-compose-single.yml @@ -1,8 +1,6 @@ # ==================== 单容器部署配置 ==================== # 使用方法:docker-compose -f docker-compose-single.yml up -d -version: '3.8' - services: qquiz: build: @@ -34,4 +32,8 @@ services: volumes: qquiz_data: + # Reuse the previous split-stack SQLite volume during migration. + name: qquiz_sqlite_data qquiz_uploads: + # Reuse the previous split-stack uploads volume during migration. + name: qquiz_upload_files diff --git a/docs/MYSQL_SETUP.md b/docs/MYSQL_SETUP.md index 7997a04..056bb19 100644 --- a/docs/MYSQL_SETUP.md +++ b/docs/MYSQL_SETUP.md @@ -1,262 +1,140 @@ -# MySQL 安装与配置指南 +# MySQL 可选配置指南 -QQuiz 使用 MySQL 8.0 作为数据库,你可以选择 Docker 部署或本地安装。 +QQuiz 默认部署路径是单容器 + SQLite。README、根目录 `Dockerfile`、`docker-compose-single.yml` 和 GitHub Actions 发布镜像都围绕这个模式设计。 -## 方式一:使用 Docker (推荐) +只有在你明确需要把数据库独立出去时,才需要 MySQL。常见原因: -### 优点 -- 无需手动安装 MySQL -- 自动配置和初始化 -- 隔离环境,不影响系统 +- 需要多个应用实例共享同一数据库 +- 已有 MySQL 运维体系 +- 希望把应用容器和数据库生命周期分开 -### 使用步骤 +## 场景一:源码部署时附加 MySQL 容器 -1. **安装 Docker Desktop** - - 下载地址:https://www.docker.com/products/docker-desktop/ - - 安装后启动 Docker Desktop +这是当前最直接的 MySQL 用法,适合你已经克隆仓库并接受“应用容器 + MySQL 容器”的可选部署方式。 -2. **运行启动脚本** - ```bash - scripts\fix_and_start.bat - ``` - 选择 **[1] Use Docker** - -3. **完成!** - - Docker 会自动下载 MySQL 镜像 - - 自动创建数据库和用户 - - 自动启动服务 - ---- - -## 方式二:本地安装 MySQL - -### 下载 MySQL - -1. 访问 MySQL 官网下载页面: - https://dev.mysql.com/downloads/installer/ - -2. 选择 **MySQL Installer for Windows** - -3. 下载 `mysql-installer-community-8.0.x.x.msi` - -### 安装步骤 - -1. **运行安装程序** - - 双击下载的 .msi 文件 - -2. **选择安装类型** - - 选择 "Developer Default" 或 "Server only" - - 点击 Next - -3. **配置 MySQL Server** - - **Config Type**: Development Computer - - **Port**: 3306 (默认) - - **Authentication Method**: 选择 "Use Strong Password Encryption" - -4. **设置 Root 密码** - - 输入并记住 root 用户的密码 - - 建议密码:`root` (开发环境) - -5. **Windows Service 配置** - - ✅ Configure MySQL Server as a Windows Service - - Service Name: MySQL80 - - ✅ Start the MySQL Server at System Startup - -6. **完成安装** - - 点击 Execute 开始安装 - - 等待安装完成 - - 点击 Finish - -### 验证安装 - -打开命令提示符,运行: +1. 复制环境变量模板: ```bash -mysql --version +cp .env.example .env ``` -应该显示:`mysql Ver 8.0.x for Win64 on x86_64` +Windows PowerShell: -### 配置 QQuiz 数据库 - -**方式 A:使用脚本自动创建 (推荐)** - -运行: -```bash -scripts\fix_and_start.bat -``` -选择 **[2] Use Local MySQL** - -**方式 B:手动创建** - -1. 打开 MySQL 命令行客户端: - ```bash - mysql -u root -p - ``` - -2. 输入 root 密码 - -3. 创建数据库和用户: - ```sql - CREATE DATABASE qquiz_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - CREATE USER 'qquiz'@'localhost' IDENTIFIED BY 'qquiz_password'; - GRANT ALL PRIVILEGES ON qquiz_db.* TO 'qquiz'@'localhost'; - FLUSH PRIVILEGES; - EXIT; - ``` - ---- - -## 数据库配置说明 - -### .env 文件配置 - -确保 `.env` 文件中的数据库连接字符串正确: - -**本地 MySQL:** -```env -DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@localhost:3306/qquiz_db +```powershell +Copy-Item .env.example .env ``` -**Docker MySQL:** +2. 把 `.env` 中的数据库连接改成 MySQL 容器地址: + ```env DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@mysql:3306/qquiz_db ``` -### 连接参数说明 +3. 启动应用和 MySQL: -- `mysql+aiomysql://` - 使用 aiomysql 异步驱动 -- `qquiz` - 数据库用户名 -- `qquiz_password` - 数据库密码 -- `localhost` 或 `mysql` - 数据库主机地址 -- `3306` - MySQL 默认端口 -- `qquiz_db` - 数据库名称 - ---- - -## 常见问题 - -### 1. 端口 3306 被占用 - -**错误信息:** -``` -Error: Port 3306 is already in use +```bash +docker compose -f docker-compose.yml -f docker-compose.mysql.yml up -d --build ``` -**解决方案:** -- 检查是否已经有 MySQL 运行:`netstat -ano | findstr :3306` -- 停止现有的 MySQL 服务 -- 或修改 `.env` 中的端口号 +4. 访问: -### 2. 无法连接到 MySQL +- 前端:`http://localhost:3000` +- 后端:`http://localhost:8000` -**错误信息:** -``` -Can't connect to MySQL server on 'localhost' -``` +说明: -**解决方案:** +- 这条路径是 MySQL 兼容部署,不是默认发布路径 +- 默认发布镜像仍然是根目录单容器镜像 -1. **检查 MySQL 服务是否运行** - - 按 Win+R,输入 `services.msc` - - 查找 "MySQL80" 服务 - - 确认状态为 "正在运行" +## 场景二:单容器应用连接外部 MySQL -2. **启动 MySQL 服务** - ```bash - net start MySQL80 - ``` +如果你想继续使用单容器应用镜像,但数据库由外部 MySQL 托管,可以直接让应用容器连接现有数据库。 -3. **检查防火墙设置** - - 确保防火墙允许 MySQL 端口 3306 +### 1. 准备 MySQL 8.0 数据库 -### 3. 密码验证失败 +执行以下 SQL 创建数据库和账号: -**错误信息:** -``` -Access denied for user 'qquiz'@'localhost' -``` - -**解决方案:** - -重新创建用户并设置密码: ```sql -mysql -u root -p -DROP USER IF EXISTS 'qquiz'@'localhost'; -CREATE USER 'qquiz'@'localhost' IDENTIFIED BY 'qquiz_password'; -GRANT ALL PRIVILEGES ON qquiz_db.* TO 'qquiz'@'localhost'; +CREATE DATABASE qquiz_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'qquiz'@'%' IDENTIFIED BY 'qquiz_password'; +GRANT ALL PRIVILEGES ON qquiz_db.* TO 'qquiz'@'%'; FLUSH PRIVILEGES; ``` -### 4. 字符集问题 +### 2. 修改 `.env` -**解决方案:** +把 `DATABASE_URL` 改成你的 MySQL 地址,例如: + +```env +DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@mysql.example.com:3306/qquiz_db +UPLOAD_DIR=/app/uploads +``` + +### 3. 启动单容器镜像 + +```bash +docker pull ghcr.io/handsomezhuzhu/qquiz:latest + +docker volume create qquiz_uploads + +docker run -d \ + --name qquiz \ + --env-file .env \ + -v qquiz_uploads:/app/uploads \ + -p 8000:8000 \ + --restart unless-stopped \ + ghcr.io/handsomezhuzhu/qquiz:latest +``` + +说明: + +- 这里不需要本地 SQLite 数据卷,因为数据库已经外置到 MySQL +- 仍然建议保留上传目录卷,避免容器重建后丢失上传文件 + +## 本地开发连接 MySQL + +如果你是在本机直接跑后端,`.env` 中可使用本地 MySQL 地址: + +```env +DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@localhost:3306/qquiz_db +``` + +然后分别启动后端和前端: + +```bash +cd backend +pip install -r requirements.txt +alembic upgrade head +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +```bash +cd web +npm install +npm run dev +``` + +## 常见问题 + +### 1. 连接不上 MySQL + +检查以下几项: + +- `DATABASE_URL` 中的主机名、端口、用户名和密码是否正确 +- MySQL 是否允许对应来源地址连接 +- 3306 端口是否开放 + +### 2. 容器里能连,宿主机里不能连 + +这是因为容器内部和宿主机访问地址不同: + +- 容器之间互联时通常使用服务名,例如 `mysql` +- 宿主机连接本机 MySQL 时通常使用 `localhost` + +### 3. 字符集异常 + +建议数据库和表统一使用 `utf8mb4`: -确保数据库使用 UTF-8 字符集: ```sql ALTER DATABASE qquiz_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ``` - ---- - -## 管理工具推荐 - -### 1. MySQL Workbench (官方) -- 下载:https://dev.mysql.com/downloads/workbench/ -- 功能:可视化数据库管理、SQL 编辑器、备份还原 - -### 2. DBeaver (免费开源) -- 下载:https://dbeaver.io/download/ -- 功能:多数据库支持、数据导入导出、ER 图 - -### 3. phpMyAdmin (Web 界面) -- 适合习惯 Web 界面的用户 - ---- - -## 数据库备份与恢复 - -### 备份数据库 - -```bash -mysqldump -u qquiz -p qquiz_db > backup.sql -``` - -### 恢复数据库 - -```bash -mysql -u qquiz -p qquiz_db < backup.sql -``` - ---- - -## 切换回 PostgreSQL - -如果需要切换回 PostgreSQL: - -1. 修改 `requirements.txt`: - ``` - asyncpg==0.29.0 # 替换 aiomysql - ``` - -2. 修改 `.env`: - ``` - DATABASE_URL=postgresql+asyncpg://qquiz:qquiz_password@localhost:5432/qquiz_db - ``` - -3. 修改 `docker-compose.yml`: - - 将 `mysql` 服务改回 `postgres` - -4. 重新安装依赖: - ```bash - pip install -r requirements.txt - ``` - ---- - -## 技术支持 - -如遇到其他问题,请: -1. 检查 MySQL 错误日志 -2. 确认防火墙和网络配置 -3. 查看项目 issues: https://github.com/handsomezhuzhu/QQuiz/issues diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 1ba77f1..c11c89c 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -30,36 +30,17 @@ QQuiz/ │ ├── alembic.ini # Alembic 配置 │ └── Dockerfile # 后端 Docker 镜像 │ -├── frontend/ # React 前端 +├── web/ # Next.js 前端 │ ├── src/ -│ │ ├── api/ -│ │ │ └── client.js # API 客户端(Axios)⭐ -│ │ ├── components/ -│ │ │ ├── Layout.jsx # 主布局(导航栏) -│ │ │ └── ProtectedRoute.jsx # 路由保护 -│ │ ├── context/ -│ │ │ └── AuthContext.jsx # 认证上下文 -│ │ ├── pages/ -│ │ │ ├── Login.jsx # 登录页 -│ │ │ ├── Register.jsx # 注册页 -│ │ │ ├── Dashboard.jsx # 仪表盘 -│ │ │ ├── ExamList.jsx # 题库列表 ⭐ -│ │ │ ├── ExamDetail.jsx # 题库详情(追加上传)⭐ -│ │ │ ├── QuizPlayer.jsx # 刷题核心页面 ⭐ -│ │ │ ├── MistakeList.jsx # 错题本 -│ │ │ └── AdminSettings.jsx # 系统设置 -│ │ ├── utils/ -│ │ │ └── helpers.js # 工具函数 -│ │ ├── App.jsx # 应用根组件 -│ │ ├── index.jsx # 应用入口 -│ │ └── index.css # 全局样式 -│ ├── public/ -│ │ └── index.html # HTML 模板 +│ │ ├── app/ # App Router 页面、布局、Route Handlers +│ │ ├── components/ # 共享 UI 组件 +│ │ ├── lib/ # API、认证、格式化等公共逻辑 +│ │ └── middleware.ts # 登录态守卫 │ ├── package.json # Node 依赖 -│ ├── vite.config.js # Vite 配置 -│ ├── tailwind.config.js # Tailwind CSS 配置 -│ ├── postcss.config.js # PostCSS 配置 -│ └── Dockerfile # 前端 Docker 镜像 +│ ├── next.config.mjs # Next.js 配置 +│ ├── tailwind.config.ts # Tailwind CSS 配置 +│ ├── postcss.config.mjs # PostCSS 配置 +│ └── Dockerfile # 分离部署前端镜像 │ ├── docker-compose.yml # Docker 编排配置 ⭐ ├── .env.example # 环境变量模板 @@ -133,74 +114,31 @@ for q in questions_data: ### 前端核心 -#### `client.js` - API 客户端 -封装了所有后端 API: -- `authAPI`: 登录、注册、用户信息 -- `examAPI`: 题库 CRUD、追加文档 -- `questionAPI`: 获取题目、答题 -- `mistakeAPI`: 错题本管理 -- `adminAPI`: 系统配置 +#### `src/lib/api/server.ts` - 服务端 API 访问 +用于 Next Server Components 访问后端: +- 从 `HttpOnly` Cookie 读取会话令牌 +- 直接请求 FastAPI `/api/*` +- 401 时自动重定向回登录页 -**特性:** -- 自动添加 JWT Token -- 统一错误处理和 Toast 提示 -- 401 自动跳转登录 +#### `src/lib/api/browser.ts` - 浏览器端 API 访问 +用于客户端交互: +- 请求同源 `/frontend-api/proxy/*` +- 统一处理错误信息 +- 默认禁用缓存,保持刷题和后台状态最新 -#### `ExamDetail.jsx` - 题库详情 -最复杂的前端页面,包含: -- **追加上传**: 上传新文档并去重 -- **状态轮询**: 每 3 秒轮询一次状态 -- **智能按钮**: - - 处理中时禁用「添加文档」 - - 就绪后显示「开始/继续刷题」 -- **进度展示**: 题目数、完成度、进度条 +#### `src/components/exams/exam-detail-client.tsx` - 题库详情 +负责: +- 追加上传文档 +- 展示解析进度 +- 通过 `/frontend-api/exams/{examId}/progress` 订阅同源 SSE +- 处理解析完成/失败后的页面刷新 -**状态轮询实现:** -```javascript -useEffect(() => { - const interval = setInterval(() => { - pollExamStatus() // 轮询状态 - }, 3000) - - return () => clearInterval(interval) -}, [examId]) - -const pollExamStatus = async () => { - const newExam = await examAPI.getDetail(examId) - - // 检测状态变化 - if (exam?.status === 'processing' && newExam.status === 'ready') { - toast.success('文档解析完成!') - await loadExamDetail() // 重新加载数据 - } - - setExam(newExam) -} -``` - -#### `QuizPlayer.jsx` - 刷题核心 -实现完整的刷题流程: -1. 基于 `current_index` 加载当前题目 -2. 根据题型显示不同的答题界面 -3. 提交答案并检查(简答题调用 AI 评分) -4. 答错自动加入错题本 -5. 点击下一题自动更新进度 - -**断点续做实现:** -```javascript -// 始终基于 exam.current_index 加载题目 -const loadCurrentQuestion = async () => { - const question = await questionAPI.getCurrentQuestion(examId) - // 后端会根据 current_index 返回对应题目 -} - -// 下一题时更新进度 -const handleNext = async () => { - const newIndex = exam.current_index + 1 - await examAPI.updateProgress(examId, newIndex) - await loadCurrentQuestion() -} -``` +#### `src/components/practice/quiz-player-client.tsx` - 刷题核心 +负责: +- 加载当前题目 +- 提交答案并展示结果 +- 推进刷题进度 +- 管理简答题与错题练习等交互 --- @@ -323,17 +261,17 @@ CREATE UNIQUE INDEX ix_user_mistakes_unique ON user_mistakes(user_id, question_i - **OpenAI/Anthropic/Qwen**: AI 解析和评分 ### 前端 +- **Next.js 14 App Router**: 前端运行时 - **React 18**: UI 框架 -- **Vite**: 构建工具(比 CRA 更快) +- **TypeScript**: 类型系统 - **Tailwind CSS**: 原子化 CSS -- **Axios**: HTTP 客户端 -- **React Router**: 路由管理 -- **React Hot Toast**: 消息提示 +- **TanStack Query**: 客户端缓存和数据同步 +- **Route Handlers**: 同源认证与代理层 ### 部署 - **Docker + Docker Compose**: 容器化部署 -- **PostgreSQL 15**: 关系型数据库 -- **Nginx** (可选): 反向代理 +- **SQLite / MySQL**: 关系型数据库 +- **FastAPI reverse proxy**: 单容器模式下代理 Next.js --- diff --git a/docs/audit/architecture.md b/docs/audit/architecture.md index 5abbf18..a0aa08e 100644 --- a/docs/audit/architecture.md +++ b/docs/audit/architecture.md @@ -19,17 +19,17 @@ Audit date: 2026-04-17 ### Frontend -- Runtime: React 18 + Vite SPA -- Routing: `react-router-dom` -- Auth state: client-only `localStorage` token + context -- API transport: axios interceptor with browser redirects -- Styling: Tailwind CSS with page-local utility classes +- Runtime: Next.js App Router + TypeScript +- Routing: file-system routing + middleware guards +- Auth state: `HttpOnly` cookie managed by Next route handlers +- API transport: server/client fetch helpers with same-origin proxy routes +- Styling: Tailwind CSS + shadcn/ui patterns ### Deployment -- `docker-compose.yml`: development-oriented split stack -- `docker-compose-single.yml`: monolith container with SQLite -- `Dockerfile`: FastAPI serves the built SPA as static assets +- `docker-compose.yml`: split development stack +- `docker-compose-single.yml`: default single-container deployment +- `Dockerfile`: single image running FastAPI + embedded Next.js ## Target Architecture @@ -51,20 +51,20 @@ Audit date: 2026-04-17 ### Deployment -- Split deployment becomes the primary production shape -- Monolith mode remains secondary compatibility mode -- Development and production Compose files must be separated +- Single-container deployment is the primary release path +- Split deployment remains available for development and compatibility testing +- Development and production Compose files must stay explicitly separated ## Core Constraints -1. Do not overwrite existing uncommitted user changes in the legacy frontend. -2. Keep the legacy `frontend/` app available until the new `web/` app reaches functional parity. -3. Preserve backend API contracts where possible during the frontend migration. -4. Fix deployment/documentation drift before treating new frontend work as production-ready. +1. Preserve backend API contracts where possible across frontend changes. +2. Keep single-container and split-stack behavior aligned on the same `web/` frontend. +3. Fix deployment/documentation drift before treating changes as production-ready. +4. Avoid reintroducing duplicate frontend implementations. ## Immediate Workstreams -1. Remove abandoned ESA captcha wiring from the legacy frontend. -2. Write audit documents and freeze the migration backlog. -3. Scaffold the new `web/` frontend without disturbing the legacy app. -4. Fix first-order deployment issues such as health checks and documented mount paths. +1. Keep single-container delivery using the same `web/` frontend as split deployment. +2. Continue moving backend orchestration into typed services. +3. Tighten health checks and deployment docs around the embedded Next runtime. +4. Cover remaining functional gaps with smoke tests. diff --git a/docs/audit/frontend-migration.md b/docs/audit/frontend-migration.md index 1632258..703c290 100644 --- a/docs/audit/frontend-migration.md +++ b/docs/audit/frontend-migration.md @@ -1,70 +1,50 @@ -# Frontend Migration Plan +# Frontend Cutover Notes ## Decision -The legacy Vite SPA remains in `frontend/` as a fallback. +`web/` is now the only frontend in the repository. -The new frontend is being built in `web/` with: +The previous Vite SPA has been removed so that: -- Next.js App Router -- TypeScript -- Tailwind CSS -- shadcn/ui component model +- split deployment and single-container deployment use the same UI +- documentation no longer has to describe two competing frontend stacks +- future frontend changes only need to be implemented once -The abandoned ESA captcha integration has been removed from the legacy login page. - -## Why a Rewrite Instead of an In-Place Port - -The legacy frontend mixes too many browser-only assumptions into core runtime -boundaries: - -- token storage in `localStorage` -- `window.location` redirects inside transport code -- client-only route protection -- SSE token passing in query strings - -Those patterns do not map cleanly onto Next App Router and server-first auth. - -## New Runtime Model +## Runtime Model ### Auth -- Login goes through Next route handlers +- Login goes through Next route handlers under `/frontend-api/auth/*` - Backend JWT is stored in an `HttpOnly` cookie - Browser code never reads the raw token ### Data -- Server pages use server-side fetch helpers -- Client mutations use browser-side fetch helpers against Next proxy routes -- URL state is used for pagination and filters +- Server pages use server-side fetch helpers against FastAPI +- Client mutations use browser-side fetch helpers against `/frontend-api/proxy/*` +- FastAPI continues to own the public `/api/*` surface ### Streaming -- Browser connects to a same-origin Next progress route +- Browser connects to `/frontend-api/exams/{examId}/progress` - The route reads the session cookie and proxies backend SSE -- Backend URL tokens are hidden from the browser +- Backend token query parameters stay hidden from the browser -## Directory Map +## Deployment Outcome -```text -web/ - src/app/ - src/components/ - src/lib/ - src/middleware.ts -``` +### Split Stack -## Migration Order +- `backend` serves API traffic on `:8000` +- `web` serves Next.js on `:3000` -1. Auth shell, layouts, middleware, and proxy routes -2. Dashboard, exams list, questions list, and admin overview -3. Exam detail upload and progress streaming -4. Quiz and mistake-practice flows -5. Cutover, smoke testing, and legacy frontend retirement +### Single Container -## Non-Goals for This First Slice +- the container runs both FastAPI and Next.js +- FastAPI stays on `:8000` +- non-API requests are proxied from FastAPI to the embedded Next server -- No immediate removal of the legacy `frontend/` -- No backend contract rewrite yet -- No server actions as the primary data mutation layer +## Follow-up Expectations + +1. New frontend work lands only in `web/` +2. Single-container smoke tests must validate both UI and API paths +3. Deployment docs must continue to describe `web/` as the sole frontend diff --git a/frontend/.dockerignore b/frontend/.dockerignore deleted file mode 100644 index c0bf172..0000000 --- a/frontend/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -node_modules -npm-debug.log -build -.git -.gitignore -.dockerignore -Dockerfile -.env -.env.local diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index ae54091..0000000 --- a/frontend/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# Frontend Environment Variables - -# API URL -VITE_API_URL=http://localhost:8000 diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index a53da8e..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM node:18-alpine - -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Install dependencies -RUN npm install - -# Copy application code -COPY . . - -# Expose port -EXPOSE 3000 - -# Start development server -CMD ["npm", "start"] diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index 4182b3c..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - QQuiz - 智能刷题平台 - - -
- - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 7fc0811..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,2928 +0,0 @@ -{ - "name": "qquiz-frontend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "qquiz-frontend", - "version": "1.0.0", - "dependencies": { - "axios": "^1.6.5", - "lucide-react": "^0.309.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-hot-toast": "^2.4.1", - "react-router-dom": "^6.21.1" - }, - "devDependencies": { - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.16", - "postcss": "^8.4.33", - "tailwindcss": "^3.4.1", - "vite": "^5.0.11" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmmirror.com/@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/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "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", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@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": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmmirror.com/@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/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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/@remix-run/router": { - "version": "1.23.1", - "resolved": "https://registry.npmmirror.com/@remix-run/router/-/router-1.23.1.tgz", - "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.22", - "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.22.tgz", - "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", - "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.27.0", - "caniuse-lite": "^1.0.30001754", - "fraction.js": "^5.3.4", - "normalize-range": "^0.1.2", - "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/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.32", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", - "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.0", - "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", - "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", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/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.30001757", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", - "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", - "dev": true, - "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.npmmirror.com/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.npmmirror.com/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/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.262", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", - "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.19.1", - "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/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/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmmirror.com/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/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/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/goober": { - "version": "2.1.18", - "resolved": "https://registry.npmmirror.com/goober/-/goober-2.1.18.tgz", - "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", - "license": "MIT", - "peerDependencies": { - "csstype": "^3.0.10" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.309.0", - "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.309.0.tgz", - "integrity": "sha512-zNVPczuwFrCfksZH3zbd1UDE6/WYhYAdbe2k7CImVyPAkXLgIwbs6eXQ4loigqDnUFjyFYCI5jZ1y10Kqal0dg==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "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/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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", - "peer": true, - "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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-hot-toast": { - "version": "2.6.0", - "resolved": "https://registry.npmmirror.com/react-hot-toast/-/react-hot-toast-2.6.0.tgz", - "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", - "license": "MIT", - "dependencies": { - "csstype": "^3.1.3", - "goober": "^2.1.16" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.30.2", - "resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.30.2.tgz", - "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.2", - "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.30.2.tgz", - "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.1", - "react-router": "6.30.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", - "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.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/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/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.18", - "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.18.tgz", - "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", - "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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", - "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.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index e7a10a6..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "qquiz-frontend", - "version": "1.0.0", - "type": "module", - "description": "QQuiz Frontend - React Application", - "private": true, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.21.1", - "axios": "^1.6.5", - "react-hot-toast": "^2.4.1", - "lucide-react": "^0.309.0" - }, - "devDependencies": { - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.16", - "postcss": "^8.4.33", - "tailwindcss": "^3.4.1", - "vite": "^5.0.11" - }, - "scripts": { - "dev": "vite", - "start": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "eslintConfig": { - "extends": [ - "react-app" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 2e7af2b..0000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx deleted file mode 100644 index b743100..0000000 --- a/frontend/src/App.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react' -import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' -import { Toaster } from 'react-hot-toast' -import { AuthProvider } from './context/AuthContext' -import { ProtectedRoute } from './components/ProtectedRoute' -import Layout from './components/Layout' - -// Auth Pages -import Login from './pages/Login' -import Register from './pages/Register' - -// Main Pages -import Dashboard from './pages/Dashboard' -import ExamList from './pages/ExamList' -import ExamDetail from './pages/ExamDetail' -import QuizPlayer from './pages/QuizPlayer' -import MistakeList from './pages/MistakeList' -import MistakePlayer from './pages/MistakePlayer' -import QuestionBank from './pages/QuestionBank' - -// Admin Pages -import AdminPanel from './pages/AdminPanel' -import AdminSettings from './pages/AdminSettings' - -function App() { - return ( - - -
- - - - {/* Public Routes */} - } /> - } /> - - {/* Protected Routes with Layout */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - {/* Admin Only Routes */} - - - - } - /> - - - - } - /> - - - {/* Default Route */} - } /> - - {/* 404 */} - } /> - -
-
-
- ) -} - -export default App diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js deleted file mode 100644 index 70f2415..0000000 --- a/frontend/src/api/client.js +++ /dev/null @@ -1,240 +0,0 @@ -/** - * API Client for QQuiz Backend - */ -import axios from 'axios' -import toast from 'react-hot-toast' - -export const API_BASE_URL = import.meta.env.VITE_API_URL || '/api' -export const AUTH_TOKEN_STORAGE_KEY = 'access_token' - -const AUTH_USER_STORAGE_KEY = 'user' -const PUBLIC_REQUEST_PATHS = ['/auth/login', '/auth/register'] - -const getRequestPath = (config) => { - const url = config?.url || '' - - if (!url) return '' - - if (url.startsWith('http://') || url.startsWith('https://')) { - try { - return new URL(url).pathname - } catch (error) { - return url - } - } - - return url.startsWith('/') ? url : `/${url}` -} - -const isPublicRequest = (config) => { - if (config?.skipAuthHandling === true) { - return true - } - - const path = getRequestPath(config) - return PUBLIC_REQUEST_PATHS.some((publicPath) => path.endsWith(publicPath)) -} - -export const buildApiUrl = (path) => { - const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL - const normalizedPath = path.startsWith('/') ? path : `/${path}` - return `${base}${normalizedPath}` -} - -export const getAccessToken = () => localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) - -export const clearAuthStorage = () => { - localStorage.removeItem(AUTH_TOKEN_STORAGE_KEY) - localStorage.removeItem(AUTH_USER_STORAGE_KEY) -} - -// Create axios instance -const api = axios.create({ - baseURL: API_BASE_URL, - timeout: 30000, - headers: { - 'Content-Type': 'application/json' - } -}) - -// Request interceptor - Add auth token -api.interceptors.request.use( - (config) => { - const token = getAccessToken() - if (token && !isPublicRequest(config)) { - config.headers = config.headers || {} - config.headers.Authorization = `Bearer ${token}` - } - return config - }, - (error) => { - return Promise.reject(error) - } -) - -// Response interceptor - Handle errors -api.interceptors.response.use( - (response) => response, - (error) => { - const status = error.response?.status - const message = error.response?.data?.detail || 'An error occurred' - const requestConfig = error.config || {} - const hasAuthHeader = Boolean( - requestConfig.headers?.Authorization || requestConfig.headers?.authorization - ) - - if (status === 401 && !isPublicRequest(requestConfig) && hasAuthHeader) { - clearAuthStorage() - if (window.location.pathname !== '/login') { - window.location.href = '/login' - } - toast.error('Session expired. Please login again.') - } else if (status === 401) { - toast.error(message) - } else if (status === 403) { - toast.error('Permission denied') - } else if (status === 429) { - toast.error(message) - } else if (status >= 500) { - toast.error('Server error. Please try again later.') - } else { - toast.error(message) - } - - return Promise.reject(error) - } -) - -// ============ Auth APIs ============ -export const authAPI = { - register: (username, password) => - api.post('/auth/register', { username, password }, { skipAuthHandling: true }), - - login: (username, password) => - api.post('/auth/login', { username, password }, { skipAuthHandling: true }), - - getCurrentUser: () => - api.get('/auth/me'), - - changePassword: (oldPassword, newPassword) => - api.post('/auth/change-password', null, { - params: { old_password: oldPassword, new_password: newPassword } - }) -} - -// ============ Exam APIs ============ -export const examAPI = { - // Create exam with first document - create: (title, file, isRandom = false) => { - const formData = new FormData() - formData.append('title', title) - formData.append('file', file) - formData.append('is_random', isRandom) - return api.post('/exams/create', formData, { - headers: { 'Content-Type': 'multipart/form-data' } - }) - }, - - // Append document to existing exam - appendDocument: (examId, file) => { - const formData = new FormData() - formData.append('file', file) - return api.post(`/exams/${examId}/append`, formData, { - headers: { 'Content-Type': 'multipart/form-data' } - }) - }, - - // Get user's exam list - getList: (skip = 0, limit = 20) => - api.get('/exams/', { params: { skip, limit } }), - - // Get exam detail - getDetail: (examId) => - api.get(`/exams/${examId}`), - - // Delete exam - delete: (examId) => - api.delete(`/exams/${examId}`), - - // Update quiz progress - updateProgress: (examId, currentIndex) => - api.put(`/exams/${examId}/progress`, { current_index: currentIndex }) -} - -// ============ Question APIs ============ -export const questionAPI = { - // Get all questions (Question Bank) - getAll: (skip = 0, limit = 50, examId = null) => { - const params = { skip, limit } - if (examId) params.exam_id = examId - return api.get('/questions/', { params }) - }, - - // Get all questions for an exam - getExamQuestions: (examId, skip = 0, limit = 50) => - api.get(`/questions/exam/${examId}/questions`, { params: { skip, limit } }), - - // Get current question (based on exam's current_index) - getCurrentQuestion: (examId) => - api.get(`/questions/exam/${examId}/current`), - - // Get question by ID - getById: (questionId) => - api.get(`/questions/${questionId}`), - - // Check answer - checkAnswer: (questionId, userAnswer) => - api.post('/questions/check', { - question_id: questionId, - user_answer: userAnswer - }) -} - -// ============ Mistake APIs ============ -export const mistakeAPI = { - // Get user's mistake book - getList: (skip = 0, limit = 50, examId = null) => { - const params = { skip, limit } - if (examId) params.exam_id = examId - return api.get('/mistakes/', { params }) - }, - - // Add to mistake book - add: (questionId) => - api.post('/mistakes/add', { question_id: questionId }), - - // Remove from mistake book by mistake ID - remove: (mistakeId) => - api.delete(`/mistakes/${mistakeId}`), - - // Remove from mistake book by question ID - removeByQuestionId: (questionId) => - api.delete(`/mistakes/question/${questionId}`) -} - -// ============ Admin APIs ============ -export const adminAPI = { - // Config - getConfig: () => api.get('/admin/config'), - updateConfig: (config) => api.put('/admin/config', config), - - // Users - getUsers: (skip = 0, limit = 50, search = null) => - api.get('/admin/users', { params: { skip, limit, search } }), - createUser: (username, password, is_admin = false) => - api.post('/admin/users', { username, password, is_admin }), - updateUser: (userId, data) => - api.put(`/admin/users/${userId}`, data), - deleteUser: (userId) => - api.delete(`/admin/users/${userId}`), - - // Statistics - getStatistics: () => api.get('/admin/statistics'), - getHealth: () => api.get('/admin/health'), - - // Export - exportUsers: () => api.get('/admin/export/users', { responseType: 'blob' }), - exportStatistics: () => api.get('/admin/export/statistics', { responseType: 'blob' }) -} - -export default api diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx deleted file mode 100644 index a1e2f76..0000000 --- a/frontend/src/components/Layout.jsx +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Main Layout Component with Navigation - */ -import React, { useState } from 'react' -import { Link, useNavigate, useLocation, Outlet } from 'react-router-dom' -import { useAuth } from '../context/AuthContext' -import { - BookOpen, - LayoutDashboard, - FolderOpen, - XCircle, - Settings, - LogOut, - Menu, - X, - Shield -} from 'lucide-react' - -export const Layout = () => { - const { user, logout, isAdmin } = useAuth() - const navigate = useNavigate() - const location = useLocation() - const [mobileMenuOpen, setMobileMenuOpen] = useState(false) - - const handleLogout = () => { - logout() - navigate('/login') - } - - const navigation = [ - { name: '首页', href: '/dashboard', icon: LayoutDashboard }, - { name: '题库管理', href: '/exams', icon: FolderOpen }, - { name: '错题本', href: '/mistakes', icon: XCircle }, - ] - - if (isAdmin) { - navigation.push({ name: '管理面板', href: '/admin', icon: Shield }) - navigation.push({ name: '系统设置', href: '/admin/settings', icon: Settings }) - } - - const isActive = (href) => location.pathname === href - - return ( -
- {/* Mobile Header */} -
-
-
- - QQuiz -
- -
- - {/* Mobile Menu */} - {mobileMenuOpen && ( -
- {navigation.map((item) => ( - setMobileMenuOpen(false)} - className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(item.href) - ? 'bg-primary-50 text-primary-600' - : 'text-gray-700 hover:bg-gray-100' - }`} - > - - {item.name} - - ))} - -
- )} -
- -
- {/* Desktop Sidebar */} -
-
- {/* Logo */} -
-
- -
-
-

QQuiz

-

{user?.username}

-
-
- - {/* Navigation */} - - - {/* Logout */} -
- -
-
-
- - {/* Main Content */} -
- -
-
-
- ) -} - -export default Layout diff --git a/frontend/src/components/Pagination.jsx b/frontend/src/components/Pagination.jsx deleted file mode 100644 index 7e51e3b..0000000 --- a/frontend/src/components/Pagination.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react' - -const Pagination = ({ - currentPage, - totalItems, - pageSize, - onPageChange, - onPageSizeChange, - pageSizeOptions = [10, 20, 50, 100] -}) => { - const totalPages = Math.ceil(totalItems / pageSize) - const [inputPage, setInputPage] = useState(currentPage) - - useEffect(() => { - setInputPage(currentPage) - }, [currentPage]) - - const handlePageSubmit = (e) => { - e.preventDefault() - let page = parseInt(inputPage) - if (isNaN(page)) page = 1 - if (page < 1) page = 1 - if (page > totalPages) page = totalPages - onPageChange(page) - setInputPage(page) - } - - if (totalItems === 0) return null - - return ( -
- {/* Info */} -
- 显示 {Math.min((currentPage - 1) * pageSize + 1, totalItems)} - {Math.min(currentPage * pageSize, totalItems)} 共 {totalItems} 条 -
- -
- {/* Page Size Selector */} -
- - -
- - {/* Navigation */} -
- - - {/* Manual Input */} -
- setInputPage(e.target.value)} - className="w-12 text-center py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent mx-1" - /> - / {totalPages} -
- - -
-
-
- ) -} - -export default Pagination diff --git a/frontend/src/components/ParsingProgress.jsx b/frontend/src/components/ParsingProgress.jsx deleted file mode 100644 index 8c30020..0000000 --- a/frontend/src/components/ParsingProgress.jsx +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Parsing Progress Component - * Displays real-time progress for document parsing - */ -import React from 'react' -import { Loader, CheckCircle, XCircle, FileText, Layers } from 'lucide-react' - -export const ParsingProgress = ({ progress }) => { - if (!progress) return null - - const { status, message, progress: percentage, total_chunks, current_chunk, questions_extracted, questions_added, duplicates_removed } = progress - - const getStatusIcon = () => { - switch (status) { - case 'completed': - return - case 'failed': - return - default: - return - } - } - - const getStatusColor = () => { - switch (status) { - case 'completed': - return 'bg-green-500' - case 'failed': - return 'bg-red-500' - case 'processing_chunk': - return 'bg-blue-500' - default: - return 'bg-primary-500' - } - } - - return ( -
-
-
- {getStatusIcon()} -
- -
- {/* Status Message */} -

- {status === 'completed' ? '解析完成' : status === 'failed' ? '解析失败' : '正在解析文档'} -

-

{message}

- - {/* Progress Bar */} - {status !== 'completed' && status !== 'failed' && ( -
-
- 进度 - {percentage.toFixed(0)}% -
-
-
-
-
- )} - - {/* Details Grid */} -
- {total_chunks > 0 && ( -
-
- - 文档拆分 -
-

- {current_chunk}/{total_chunks} -

-

部分

-
- )} - - {questions_extracted > 0 && ( -
-
- - 已提取 -
-

{questions_extracted}

-

题目

-
- )} - - {questions_added > 0 && ( -
-
- - 已添加 -
-

{questions_added}

-

题目

-
- )} - - {duplicates_removed > 0 && ( -
-
- - 已去重 -
-

{duplicates_removed}

-

题目

-
- )} -
-
-
-
- ) -} - -export default ParsingProgress diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx deleted file mode 100644 index 512520f..0000000 --- a/frontend/src/components/ProtectedRoute.jsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Protected Route Component - */ -import React from 'react' -import { Navigate } from 'react-router-dom' -import { useAuth } from '../context/AuthContext' - -export const ProtectedRoute = ({ children, adminOnly = false }) => { - const { user, loading } = useAuth() - - if (loading) { - return ( -
-
-
- ) - } - - if (!user) { - return - } - - if (adminOnly && !user.is_admin) { - return - } - - return children -} diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx deleted file mode 100644 index ae38602..0000000 --- a/frontend/src/context/AuthContext.jsx +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Authentication Context - */ -import React, { createContext, useContext, useState, useEffect } from 'react' -import { authAPI, AUTH_TOKEN_STORAGE_KEY, clearAuthStorage } from '../api/client' -import toast from 'react-hot-toast' - -const AuthContext = createContext(null) - -export const useAuth = () => { - const context = useContext(AuthContext) - if (!context) { - throw new Error('useAuth must be used within AuthProvider') - } - return context -} - -export const AuthProvider = ({ children }) => { - const [user, setUser] = useState(null) - const [loading, setLoading] = useState(true) - - // Load user from localStorage on mount - useEffect(() => { - const loadUser = async () => { - const token = localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) - if (token) { - try { - const response = await authAPI.getCurrentUser() - setUser(response.data) - } catch (error) { - console.error('Failed to load user:', error) - clearAuthStorage() - } - } - setLoading(false) - } - - loadUser() - }, []) - - const login = async (username, password) => { - try { - const response = await authAPI.login(username, password) - const { access_token } = response.data - - // Save token - localStorage.setItem(AUTH_TOKEN_STORAGE_KEY, access_token) - - // Get user info - const userResponse = await authAPI.getCurrentUser() - setUser(userResponse.data) - - toast.success('Login successful!') - return true - } catch (error) { - console.error('Login failed:', error) - return false - } - } - - const register = async (username, password) => { - try { - await authAPI.register(username, password) - toast.success('Registration successful! Please login.') - return true - } catch (error) { - console.error('Registration failed:', error) - return false - } - } - - const logout = () => { - clearAuthStorage() - setUser(null) - toast.success('Logged out successfully') - } - - const value = { - user, - loading, - login, - register, - logout, - isAuthenticated: !!user, - isAdmin: user?.is_admin || false - } - - return ( - - {children} - - ) -} diff --git a/frontend/src/index.css b/frontend/src/index.css deleted file mode 100644 index 8970c1b..0000000 --- a/frontend/src/index.css +++ /dev/null @@ -1,22 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx deleted file mode 100644 index 995053a..0000000 --- a/frontend/src/index.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import './index.css' -import App from './App' - -ReactDOM.createRoot(document.getElementById('root')).render( - - - -) diff --git a/frontend/src/pages/AdminPanel.jsx b/frontend/src/pages/AdminPanel.jsx deleted file mode 100644 index 4e7436a..0000000 --- a/frontend/src/pages/AdminPanel.jsx +++ /dev/null @@ -1,356 +0,0 @@ -/** - * Admin Panel - 完整的管理员面板 - */ -import React, { useState, useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { adminAPI } from '../api/client' -import { useAuth } from '../context/AuthContext' -import { - Users, BarChart3, Settings, Trash2, Plus, Search, - ArrowLeft, Shield, Activity, Database, Download -} from 'lucide-react' -import toast from 'react-hot-toast' - -export const AdminPanel = () => { - const { user } = useAuth() - const navigate = useNavigate() - const [activeTab, setActiveTab] = useState('stats') - - // 统计数据 - const [stats, setStats] = useState(null) - const [health, setHealth] = useState(null) - - // 用户数据 - const [users, setUsers] = useState([]) - const [usersTotal, setUsersTotal] = useState(0) - const [searchQuery, setSearchQuery] = useState('') - const [showCreateModal, setShowCreateModal] = useState(false) - const [newUser, setNewUser] = useState({ username: '', password: '', is_admin: false }) - - useEffect(() => { - loadStats() - loadHealth() - loadUsers() - }, []) - - const loadStats = async () => { - try { - const res = await adminAPI.getStatistics() - setStats(res.data) - } catch (error) { - console.error('Failed to load statistics:', error) - } - } - - const loadHealth = async () => { - try { - const res = await adminAPI.getHealth() - setHealth(res.data) - } catch (error) { - console.error('Failed to load health:', error) - } - } - - const loadUsers = async () => { - try { - const res = await adminAPI.getUsers(0, 100, searchQuery || null) - setUsers(res.data.users) - setUsersTotal(res.data.total) - } catch (error) { - console.error('Failed to load users:', error) - toast.error('加载用户列表失败') - } - } - - const handleCreateUser = async () => { - if (!newUser.username || !newUser.password) { - toast.error('请填写用户名和密码') - return - } - try { - await adminAPI.createUser(newUser.username, newUser.password, newUser.is_admin) - toast.success('用户创建成功') - setShowCreateModal(false) - setNewUser({ username: '', password: '', is_admin: false }) - loadUsers() - } catch (error) { - toast.error(error.response?.data?.detail || '创建用户失败') - } - } - - const handleDeleteUser = async (userId, username) => { - if (!confirm(`确定删除用户 ${username}?`)) return - try { - await adminAPI.deleteUser(userId) - toast.success('用户已删除') - loadUsers() - } catch (error) { - toast.error(error.response?.data?.detail || '删除失败') - } - } - - const handleExportUsers = async () => { - try { - const response = await adminAPI.exportUsers() - const url = window.URL.createObjectURL(new Blob([response.data])) - const link = document.createElement('a') - link.href = url - link.setAttribute('download', 'users.csv') - document.body.appendChild(link) - link.click() - link.remove() - toast.success('导出成功') - } catch (error) { - toast.error('导出失败') - } - } - - return ( -
-
-

管理员面板

-

系统统计与用户管理

-
- - {/* Tabs */} -
-
- - -
- - {/* Stats Tab */} - {activeTab === 'stats' && stats && ( -
- {/* Overview Cards */} -
-
-
-
-

用户总数

-

{stats.users?.total || 0}

-
- -
-
- -
-
-
-

题库总数

-

{stats.exams?.total || 0}

-
- -
-
- -
-
-
-

题目总数

-

{stats.questions?.total || 0}

-
- -
-
- -
-
-
-

今日活跃

-

{stats.activity?.today_active_users || 0}

-
- -
-
-
- - {/* System Health */} - {health && ( -
-

系统状态

-
-
- 状态 - - {health.status} - -
-
- 数据库 - {health.system?.database_url || 'SQLite'} -
- {health.database?.size_mb && ( -
- 数据库大小 - {health.database.size_mb} MB -
- )} -
-
- )} -
- )} - - {/* Users Tab */} - {activeTab === 'users' && ( -
- {/* Actions */} -
-
- - setSearchQuery(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && loadUsers()} - className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500" - /> -
-
- - -
-
- - {/* Users Table */} -
- - - - - - - - - - - - - - {users.map((u) => ( - - - - - - - - - - ))} - -
ID用户名角色题库数错题数注册时间操作
{u.id}{u.username} - {u.is_admin ? ( - 管理员 - ) : ( - 普通用户 - )} - {u.exam_count || 0}{u.mistake_count || 0} - {new Date(u.created_at).toLocaleDateString()} - - -
-
-
- )} -
- - {/* Create User Modal */} - {showCreateModal && ( -
-
-

创建新用户

-
-
- - setNewUser({ ...newUser, username: e.target.value })} - className="w-full px-4 py-2 border border-gray-300 rounded-lg" - /> -
-
- - setNewUser({ ...newUser, password: e.target.value })} - className="w-full px-4 py-2 border border-gray-300 rounded-lg" - /> -
-
- setNewUser({ ...newUser, is_admin: e.target.checked })} - className="rounded" - /> - -
-
-
- - -
-
-
- )} -
- ) -} - -export default AdminPanel diff --git a/frontend/src/pages/AdminSettings.jsx b/frontend/src/pages/AdminSettings.jsx deleted file mode 100644 index b25943a..0000000 --- a/frontend/src/pages/AdminSettings.jsx +++ /dev/null @@ -1,562 +0,0 @@ -/** - * Admin Settings Page - Enhanced with API Configuration - */ -import React, { useState, useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { adminAPI } from '../api/client' -import { useAuth } from '../context/AuthContext' -import { Settings, Save, Loader, Key, Link as LinkIcon, Eye, EyeOff, ArrowLeft } from 'lucide-react' -import toast from 'react-hot-toast' - -export const AdminSettings = () => { - const { user } = useAuth() - const navigate = useNavigate() - const [loading, setLoading] = useState(true) - const [saving, setSaving] = useState(false) - const [showApiKeys, setShowApiKeys] = useState({ - openai: false, - anthropic: false, - qwen: false, - gemini: false - }) - - const [config, setConfig] = useState({ - allow_registration: true, - max_upload_size_mb: 10, - max_daily_uploads: 20, - ai_provider: 'gemini', - // OpenAI - openai_api_key: '', - openai_base_url: 'https://api.openai.com/v1', - openai_model: 'gpt-4o-mini', - // Anthropic - anthropic_api_key: '', - anthropic_model: 'claude-3-haiku-20240307', - // Qwen - qwen_api_key: '', - qwen_base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', - qwen_model: 'qwen-plus', - // Gemini - gemini_api_key: '', - gemini_base_url: '', - gemini_model: 'gemini-2.0-flash-exp' - }) - - useEffect(() => { - loadConfig() - }, []) - - const loadConfig = async () => { - try { - const response = await adminAPI.getConfig() - setConfig(response.data) - } catch (error) { - console.error('Failed to load config:', error) - toast.error('加载配置失败') - } finally { - setLoading(false) - } - } - - const handleSave = async () => { - setSaving(true) - try { - await adminAPI.updateConfig(config) - toast.success('配置保存成功!') - } catch (error) { - console.error('Failed to save config:', error) - toast.error('保存配置失败') - } finally { - setSaving(false) - } - } - - const handleChange = (key, value) => { - setConfig({ - ...config, - [key]: value - }) - } - - const toggleApiKeyVisibility = (provider) => { - setShowApiKeys({ - ...showApiKeys, - [provider]: !showApiKeys[provider] - }) - } - - // Get complete API endpoint URL - const getCompleteEndpoint = (provider) => { - const endpoints = { - openai: '/chat/completions', - anthropic: '/messages', - qwen: '/chat/completions' - } - - let baseUrl = '' - if (provider === 'openai') { - baseUrl = config.openai_base_url || 'https://api.openai.com/v1' - } else if (provider === 'anthropic') { - baseUrl = 'https://api.anthropic.com/v1' - } else if (provider === 'qwen') { - baseUrl = config.qwen_base_url || 'https://dashscope.aliyuncs.com/compatible-mode/v1' - } - - // Remove trailing slash - baseUrl = baseUrl.replace(/\/$/, '') - - return `${baseUrl}${endpoints[provider]}` - } - - if (loading) { - return ( -
- -
- ) - } - - return ( -
-
-

系统设置

-

配置系统参数与 AI 接口

-
- - {/* Content */} -
- {/* Basic Settings */} -
-

基础设置

- - {/* Allow Registration */} -
-
-

允许用户注册

-

关闭后新用户无法注册

-
- -
- - {/* Max Upload Size */} -
- - handleChange('max_upload_size_mb', parseInt(e.target.value))} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" - /> -

建议:5-20 MB

-
- - {/* Max Daily Uploads */} -
- - handleChange('max_daily_uploads', parseInt(e.target.value))} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" - /> -

建议:10-50 次

-
- - {/* AI Provider */} -
- - -

- 选择后在下方配置对应的 API 密钥。Gemini 支持原生 PDF 解析 -

-
-
- - {/* OpenAI Configuration */} -
-
- -

OpenAI 配置

- {config.ai_provider === 'openai' && ( - 当前使用 - )} -
- - {/* Text-only warning */} -
-

- ⚠️ OpenAI 仅支持文本解析,不支持 PDF 原生理解。PDF 文件将通过文本提取处理,可能丢失格式和图片信息。 -

-
- - {/* API Key */} -
- -
- handleChange('openai_api_key', e.target.value)} - placeholder="sk-proj-..." - className="w-full px-4 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> - -
-

从 https://platform.openai.com/api-keys 获取

-
- - {/* Base URL */} -
- -
- - handleChange('openai_base_url', e.target.value)} - placeholder="https://api.openai.com/v1" - className="w-full pl-10 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> -
-

- 完整 endpoint: {getCompleteEndpoint('openai')} -

-
- - {/* Model */} -
- - handleChange('openai_model', e.target.value)} - placeholder="gpt-4o-mini" - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> - - - - - - -

可输入自定义模型名称,或从建议中选择

-
-
- - {/* Anthropic Configuration */} -
-
- -

Anthropic 配置

- {config.ai_provider === 'anthropic' && ( - 当前使用 - )} -
- - {/* Text-only warning */} -
-

- ⚠️ Anthropic 仅支持文本解析,不支持 PDF 原生理解。PDF 文件将通过文本提取处理,可能丢失格式和图片信息。 -

-
- - {/* API Key */} -
- -
- handleChange('anthropic_api_key', e.target.value)} - placeholder="sk-ant-..." - className="w-full px-4 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> - -
-

从 https://console.anthropic.com/settings/keys 获取

-
- - {/* Base URL (fixed for Anthropic) */} -
- -
- - -
-

- 完整 endpoint: {getCompleteEndpoint('anthropic')} -

-
- - {/* Model */} -
- - handleChange('anthropic_model', e.target.value)} - placeholder="claude-3-haiku-20240307" - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> - - - - - - -

可输入自定义模型名称,或从建议中选择

-
-
- - {/* Qwen Configuration */} -
-
- -

通义千问 配置

- {config.ai_provider === 'qwen' && ( - 当前使用 - )} -
- - {/* Text-only warning */} -
-

- ⚠️ 通义千问 仅支持文本解析,不支持 PDF 原生理解。PDF 文件将通过文本提取处理,可能丢失格式和图片信息。 -

-
- - {/* API Key */} -
- -
- handleChange('qwen_api_key', e.target.value)} - placeholder="sk-..." - className="w-full px-4 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> - -
-

从 https://dashscope.console.aliyun.com/apiKey 获取

-
- - {/* Base URL */} -
- -
- - handleChange('qwen_base_url', e.target.value)} - placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1" - className="w-full pl-10 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> -
-

- 完整 endpoint: {getCompleteEndpoint('qwen')} -

-
- - {/* Model */} -
- - handleChange('qwen_model', e.target.value)} - placeholder="qwen-plus" - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> - - - - - - -

可输入自定义模型名称,或从建议中选择

-
-
- - {/* Gemini Configuration */} -
-
- -

Google Gemini 配置

- {config.ai_provider === 'gemini' && ( - 当前使用 - )} -
- - {/* PDF support highlight */} -
-

- ✅ Gemini 支持原生 PDF 理解,可直接处理 PDF 文件(最多 1000 页),完整保留图片、表格、公式等内容。 -

-
- - {/* API Key */} -
- -
- handleChange('gemini_api_key', e.target.value)} - placeholder="AIza..." - className="w-full px-4 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> - -
-

从 https://aistudio.google.com/apikey 获取

-
- - {/* Base URL (optional) */} -
- -
- - handleChange('gemini_base_url', e.target.value)} - placeholder="https://generativelanguage.googleapis.com(留空使用默认)" - className="w-full pl-10 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> -
-

- 可配置自定义代理或中转服务(支持 Key 轮训等)。留空则使用 Google 官方 API -

-
- - {/* Model */} -
- - handleChange('gemini_model', e.target.value)} - placeholder="gemini-2.0-flash-exp" - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm" - /> - - - - - - -

可输入自定义模型名称,或从建议中选择

-
-
- - {/* Save Button */} -
- -
-
-
- ) -} - -export default AdminSettings diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx deleted file mode 100644 index b773cc1..0000000 --- a/frontend/src/pages/Dashboard.jsx +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Dashboard Page - */ -import React, { useState, useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { examAPI, mistakeAPI } from '../api/client' -import { useAuth } from '../context/AuthContext' - -import { - FolderOpen, XCircle, TrendingUp, BookOpen, ArrowRight, Settings, Shield -} from 'lucide-react' -import { getStatusColor, getStatusText, formatRelativeTime, calculateProgress } from '../utils/helpers' - -export const Dashboard = () => { - const { user, isAdmin } = useAuth() - const navigate = useNavigate() - - const [stats, setStats] = useState({ - totalExams: 0, - totalQuestions: 0, - completedQuestions: 0, - mistakeCount: 0 - }) - - const [recentExams, setRecentExams] = useState([]) - const [loading, setLoading] = useState(true) - - useEffect(() => { - loadDashboardData() - }, []) - - const loadDashboardData = async () => { - try { - const [examsRes, mistakesRes] = await Promise.all([ - examAPI.getList(0, 5), - mistakeAPI.getList(0, 1) - ]) - - const exams = examsRes.data.exams - - // Calculate stats - const totalQuestions = exams.reduce((sum, e) => sum + e.total_questions, 0) - const completedQuestions = exams.reduce((sum, e) => sum + e.current_index, 0) - - setStats({ - totalExams: exams.length, - totalQuestions, - completedQuestions, - mistakeCount: mistakesRes.data.total - }) - - setRecentExams(exams) - } catch (error) { - console.error('Failed to load dashboard:', error) - } finally { - setLoading(false) - } - } - - return ( - <> -
- {/* Welcome */} -
-

- 欢迎回来,{user?.username}! -

-

继续你的学习之旅

-
- - {/* Stats Cards */} -
-
navigate('/exams')} - > -
-
- -
- {stats.totalExams} -
-

题库总数

-
- -
navigate('/questions')} - > -
-
- -
- {stats.totalQuestions} -
-

题目总数 (点击查看)

-
- -
-
-
- -
- {stats.completedQuestions} -
-

已完成

-
- -
navigate('/mistakes')} - > -
-
- -
- {stats.mistakeCount} -
-

错题数量

-
-
- - {/* Recent Exams */} -
-
-

最近的题库

- -
- - {recentExams.length === 0 ? ( -
- -

还没有题库,快去创建一个吧!

-
- ) : ( -
- {recentExams.map((exam) => ( -
navigate(`/exams/${exam.id}`)} - className="border border-gray-200 rounded-lg p-4 hover:border-primary-300 hover:bg-primary-50 transition-all cursor-pointer" - > -
-

{exam.title}

- - {getStatusText(exam.status)} - -
- -
- - {exam.current_index} / {exam.total_questions} 题 - - {formatRelativeTime(exam.updated_at)} -
- - {exam.total_questions > 0 && ( - <> -
-
-
- - {calculateProgress(exam.current_index, exam.total_questions)}% - - - )} -
- ))} -
- )} -
- -
- - ) -} - -export default Dashboard diff --git a/frontend/src/pages/ExamDetail.jsx b/frontend/src/pages/ExamDetail.jsx deleted file mode 100644 index 730a1ed..0000000 --- a/frontend/src/pages/ExamDetail.jsx +++ /dev/null @@ -1,387 +0,0 @@ -/** - * Exam Detail Page - with real-time parsing progress via SSE - */ -import React, { useState, useEffect, useRef } from 'react' -import { useParams, useNavigate } from 'react-router-dom' -import { examAPI, buildApiUrl, getAccessToken } from '../api/client' -import ParsingProgress from '../components/ParsingProgress' -import { - ArrowLeft, Upload, Play, Loader, FileText, AlertCircle, RefreshCw, ArrowRight -} from 'lucide-react' -import toast from 'react-hot-toast' -import { - getStatusColor, - getStatusText, - formatDate, - calculateProgress, - isValidFileType, - getQuestionTypeText -} from '../utils/helpers' - -export const ExamDetail = () => { - const { examId } = useParams() - const navigate = useNavigate() - - const [exam, setExam] = useState(null) - const [loading, setLoading] = useState(true) - const [uploading, setUploading] = useState(false) - const [showUploadModal, setShowUploadModal] = useState(false) - const [uploadFile, setUploadFile] = useState(null) - const [progress, setProgress] = useState(null) - - const eventSourceRef = useRef(null) - - useEffect(() => { - loadExamDetail() - - // Cleanup on unmount - return () => { - if (eventSourceRef.current) { - eventSourceRef.current.close() - eventSourceRef.current = null - } - } - }, [examId]) - - const loadExamDetail = async () => { - try { - const examRes = await examAPI.getDetail(examId) - setExam(examRes.data) - - // Connect to SSE if exam is processing - if (examRes.data.status === 'processing') { - connectSSE() - } else { - setProgress(null) - } - } catch (error) { - console.error('Failed to load exam:', error) - toast.error('加载题库失败') - } finally { - setLoading(false) - } - } - - const connectSSE = () => { - // Close existing connection if any - if (eventSourceRef.current) { - eventSourceRef.current.close() - } - - console.log('[SSE] Connecting to progress stream for exam', examId) - - const token = getAccessToken() - - if (!token) { - console.error('[SSE] Missing access token') - return - } - - const url = `${buildApiUrl(`/exams/${examId}/progress`)}?token=${encodeURIComponent(token)}` - - const eventSource = new EventSource(url) - eventSourceRef.current = eventSource - - eventSource.onmessage = (event) => { - try { - const progressData = JSON.parse(event.data) - console.log('[SSE] Progress update:', progressData) - - setProgress(progressData) - - // Update exam status if completed or failed - if (progressData.status === 'completed') { - toast.success(progressData.message) - setExam(prev => ({ ...prev, status: 'ready' })) - loadExamDetail() // Reload to get updated questions - eventSource.close() - eventSourceRef.current = null - } else if (progressData.status === 'failed') { - toast.error(progressData.message) - setExam(prev => ({ ...prev, status: 'failed' })) - eventSource.close() - eventSourceRef.current = null - } - } catch (error) { - console.error('[SSE] Failed to parse progress data:', error) - } - } - - eventSource.onerror = (error) => { - console.error('[SSE] Connection error:', error) - eventSource.close() - eventSourceRef.current = null - } - - eventSource.onopen = () => { - console.log('[SSE] Connection established') - } - } - - const handleAppendDocument = async (e) => { - e.preventDefault() - - if (!uploadFile) { - toast.error('请选择文件') - return - } - - if (!isValidFileType(uploadFile.name)) { - toast.error('不支持的文件类型') - return - } - - setUploading(true) - - try { - await examAPI.appendDocument(examId, uploadFile) - toast.success('文档上传成功,正在解析并去重...') - setShowUploadModal(false) - setUploadFile(null) - setExam(prev => ({ ...prev, status: 'processing' })) - - // Connect to SSE for real-time progress - connectSSE() - } catch (error) { - console.error('Failed to append document:', error) - toast.error('文档上传失败') - } finally { - setUploading(false) - } - } - - const handleStartQuiz = () => { - if (exam.current_index >= exam.total_questions) { - if (window.confirm('已经完成所有题目,是否从头开始?')) { - navigate(`/quiz/${examId}?reset=true`) - } - } else { - navigate(`/quiz/${examId}`) - } - } - - if (loading) { - return ( -
- -
- ) - } - - if (!exam) { - return ( -
- -

题库不存在

-
- ) - } - - const isProcessing = exam.status === 'processing' - const isReady = exam.status === 'ready' - const isFailed = exam.status === 'failed' - const quizProgress = calculateProgress(exam.current_index, exam.total_questions) - const completionProgress = isProcessing - ? Math.round(Number(progress?.progress ?? 0)) - : quizProgress - - return ( - <> -
- {/* Back Button */} - - - {/* Parsing Progress (only shown when processing) */} - {isProcessing && progress && ( - - )} - - {/* Header */} -
-
-
-

- {exam.title} -

-
- - {getStatusText(exam.status)} - - {isProcessing && ( - - - 正在处理中... - - )} -
-
- - {/* Actions */} -
- - - {isReady && exam.total_questions > 0 && ( - - )} -
-
- - {/* Stats */} -
-
-

题目总数

-

{exam.total_questions}

-
-
-

已完成

-

{exam.current_index}

-
-
-

剩余

-

- {Math.max(0, exam.total_questions - exam.current_index)} -

-
-
-

完成度

-

{completionProgress}%

-
-
- - {/* Progress Bar */} - {(isProcessing || exam.total_questions > 0) && ( -
-
-
-
-
- )} - - {/* Info */} -
-

创建时间:{formatDate(exam.created_at)}

-

最后更新:{formatDate(exam.updated_at)}

-
-
- - {/* Failed Status Warning */} - {isFailed && ( -
-
- -
-

文档解析失败

-

- 请检查文档格式是否正确,或尝试重新上传。 -

-
-
-
- )} - - {/* View All Questions Link */} -
navigate(`/questions?examId=${examId}`)} - > -
-
- -
-
-

查看题库所有题目

-

浏览、搜索和查看该题库中的所有题目详情

-
-
-
- -
-
-
- - {/* Upload Modal */} - {showUploadModal && ( -
-
-

添加题目文档

-

- 上传新文档后,系统会自动解析题目并去除重复题目。 -

- -
-
- - setUploadFile(e.target.files[0])} - required - accept=".txt,.pdf,.doc,.docx,.xlsx,.xls" - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" - /> -

- 支持:TXT, PDF, DOC, DOCX, XLSX, XLS -

-
- -
- - -
-
-
-
- )} - - ) -} - -export default ExamDetail diff --git a/frontend/src/pages/ExamList.jsx b/frontend/src/pages/ExamList.jsx deleted file mode 100644 index d25ec40..0000000 --- a/frontend/src/pages/ExamList.jsx +++ /dev/null @@ -1,342 +0,0 @@ -/** - * Exam List Page - */ -import React, { useState, useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { examAPI } from '../api/client' -import { - Plus, FolderOpen, Loader, AlertCircle, Trash2, Upload -} from 'lucide-react' -import toast from 'react-hot-toast' -import { - getStatusColor, - getStatusText, - formatRelativeTime, - calculateProgress, - isValidFileType -} from '../utils/helpers' - -export const ExamList = () => { - const navigate = useNavigate() - const [exams, setExams] = useState([]) - const [loading, setLoading] = useState(true) - const [showCreateModal, setShowCreateModal] = useState(false) - const [creating, setCreating] = useState(false) - const [pollInterval, setPollInterval] = useState(null) - - const [formData, setFormData] = useState({ - title: '', - file: null, - isRandom: false - }) - - useEffect(() => { - loadExams() - - // Start polling for processing exams - const interval = setInterval(() => { - checkProcessingExams() - }, 3000) // Poll every 3 seconds - - setPollInterval(interval) - - return () => { - if (interval) clearInterval(interval) - } - }, []) - - const loadExams = async () => { - try { - const response = await examAPI.getList() - setExams(response.data.exams) - } catch (error) { - console.error('Failed to load exams:', error) - toast.error('加载题库失败') - } finally { - setLoading(false) - } - } - - const checkProcessingExams = async () => { - try { - const response = await examAPI.getList() - const newExams = response.data.exams - - // Check if any processing exam is now ready - const oldProcessing = exams.filter(e => e.status === 'processing') - const newReady = newExams.filter(e => - oldProcessing.some(old => old.id === e.id && e.status === 'ready') - ) - - if (newReady.length > 0) { - toast.success(`${newReady.length} 个题库解析完成!`) - } - - setExams(newExams) - } catch (error) { - console.error('Failed to poll exams:', error) - } - } - - const handleCreate = async (e) => { - e.preventDefault() - - if (!formData.file) { - toast.error('请选择文件') - return - } - - if (!isValidFileType(formData.file.name)) { - toast.error('不支持的文件类型') - return - } - - setCreating(true) - - try { - const response = await examAPI.create(formData.title, formData.file, formData.isRandom) - toast.success('题库创建成功,正在解析文档...') - setShowCreateModal(false) - setFormData({ title: '', file: null, isRandom: false }) - - // 跳转到新创建的试卷详情页 - if (response.data && response.data.exam_id) { - navigate(`/exams/${response.data.exam_id}`) - } else { - // 如果没有返回 exam_id,刷新列表 - await loadExams() - } - } catch (error) { - console.error('Failed to create exam:', error) - toast.error('创建失败:' + (error.response?.data?.detail || error.message)) - } finally { - setCreating(false) - } - } - - const handleDelete = async (examId) => { - if (!window.confirm('确定要删除这个题库吗?删除后无法恢复。')) { - return - } - - try { - await examAPI.delete(examId) - toast.success('题库已删除') - await loadExams() - } catch (error) { - console.error('Failed to delete exam:', error) - } - } - - if (loading) { - return ( -
- -
- ) - } - - return ( - <> -
- {/* Header */} -
-
-

题库管理

-

共 {exams.length} 个题库

-
- -
- - {/* Exam Grid */} - {exams.length === 0 ? ( -
- -

还没有题库

-

创建第一个题库开始刷题吧!

- -
- ) : ( -
- {exams.map((exam) => ( -
- {/* Header */} -
-

- {exam.title} -

- - {getStatusText(exam.status)} - -
- - {/* Stats */} -
-
- 题目数量 - {exam.total_questions} -
-
- 已完成 - - {exam.current_index} / {exam.total_questions} - -
- {exam.total_questions > 0 && ( -
-
-
- )} -
- - {/* Time */} -

- 创建于 {formatRelativeTime(exam.created_at)} -

- - {/* Actions */} -
- - -
-
- ))} -
- )} -
- - {/* Create Modal */} - {showCreateModal && ( -
-
-

创建新题库

- -
- {/* Title */} -
- - setFormData({ ...formData, title: e.target.value })} - required - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" - placeholder="例如:数据结构期末复习" - /> -
- - {/* File */} -
- - setFormData({ ...formData, file: e.target.files[0] })} - required - accept=".txt,.pdf,.doc,.docx,.xlsx,.xls" - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" - /> -

- 支持:TXT, PDF, DOC, DOCX, XLSX, XLS -

-
- - {/* Order Selection */} -
- -
- - -
-

- 注意:创建后题目顺序将固定,无法再次更改。 -

-
- - {/* Buttons */} -
- - -
-
-
-
- )} - - ) -} - -export default ExamList diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx deleted file mode 100644 index 73fdd71..0000000 --- a/frontend/src/pages/Login.jsx +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Login Page - */ -import React, { useState } from 'react' -import { Link, useNavigate } from 'react-router-dom' -import { useAuth } from '../context/AuthContext' -import { BookOpen } from 'lucide-react' - -export const Login = () => { - const navigate = useNavigate() - const { login } = useAuth() - - const [formData, setFormData] = useState({ - username: '', - password: '' - }) - const [loading, setLoading] = useState(false) - - const handleSubmit = async (e) => { - e.preventDefault() - - setLoading(true) - try { - const success = await login(formData.username, formData.password) - if (success) { - navigate('/dashboard') - } - } finally { - setLoading(false) - } - } - - const handleChange = (e) => { - setFormData({ - ...formData, - [e.target.name]: e.target.value - }) - } - - return ( -
-
- {/* Logo and Title */} -
-
-
- -
-
-

QQuiz

-

智能刷题与题库管理平台

-
- - {/* Login Form */} -
-

登录

- -
- {/* Username */} -
- - -
- - {/* Password */} -
- - -
- - -
- - {/* Register Link */} -
-

- 还没有账号?{' '} - - 立即注册 - -

-
-
- -
-
- ) -} - -export default Login diff --git a/frontend/src/pages/MistakeList.jsx b/frontend/src/pages/MistakeList.jsx deleted file mode 100644 index 3f2b90f..0000000 --- a/frontend/src/pages/MistakeList.jsx +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Mistake List Page (错题本) - */ -import React, { useState, useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { mistakeAPI } from '../api/client' -import Pagination from '../components/Pagination' -import { XCircle, Loader, Trash2, BookOpen, Play, ChevronRight } from 'lucide-react' -import toast from 'react-hot-toast' -import { getQuestionTypeText, formatRelativeTime } from '../utils/helpers' - -export const MistakeList = () => { - const [mistakes, setMistakes] = useState([]) - const [loading, setLoading] = useState(true) - const [expandedId, setExpandedId] = useState(null) - const [showModeModal, setShowModeModal] = useState(false) - - // Pagination - const [page, setPage] = useState(1) - const [limit, setLimit] = useState(10) - const [total, setTotal] = useState(0) - const navigate = useNavigate() - - useEffect(() => { - loadMistakes() - }, [page, limit]) - - const loadMistakes = async () => { - try { - setLoading(true) - const skip = (page - 1) * limit - const response = await mistakeAPI.getList(skip, limit) - setMistakes(response.data.mistakes) - setTotal(response.data.total) - } catch (error) { - console.error('Failed to load mistakes:', error) - toast.error('加载错题本失败') - } finally { - setLoading(false) - } - } - - const handleRemove = async (mistakeId) => { - if (!window.confirm('确定要从错题本中移除这道题吗?')) { - return - } - - try { - await mistakeAPI.remove(mistakeId) - toast.success('已移除') - await loadMistakes() - } catch (error) { - console.error('Failed to remove mistake:', error) - toast.error('移除失败') - } - } - - const toggleExpand = (id) => { - setExpandedId(expandedId === id ? null : id) - } - - if (loading) { - return ( -
- -
- ) - } - - return ( - <> -
- {/* Header */} -
-
-

错题本

-

共 {total} 道错题

-
- - {mistakes.length > 0 && ( - - )} -
- - {/* Empty State */} - {mistakes.length === 0 ? ( -
- -

错题本是空的

-

继续刷题,错题会自动添加到这里

-
- ) : ( -
- {mistakes.map((mistake) => { - const q = mistake.question - const isExpanded = expandedId === mistake.id - - return ( -
- {/* Question Preview */} -
toggleExpand(mistake.id)} - > -
- - - - -
-
- - {getQuestionTypeText(q.type)} - - - {formatRelativeTime(mistake.created_at)} - -
- -

- {q.content} -

- - {isExpanded && ( -
- {/* Options */} - {q.options && q.options.length > 0 && ( -
- {q.options.map((opt, i) => ( -
- {opt} -
- ))} -
- )} - - {/* Answer */} -
-

- 正确答案 -

-

{q.answer}

-
- - {/* Analysis */} - {q.analysis && ( -
-

- 解析 -

-

{q.analysis}

-
- )} -
- )} -
- - -
-
-
- ) - })} - - {/* Pagination */} - { - setLimit(newLimit) - setPage(1) - }} - /> -
- )} -
- - {/* Mode Selection Modal */} - {showModeModal && ( -
-
-

选择刷题模式

-
- - - -
- -
-
- )} - - ) -} - -export default MistakeList diff --git a/frontend/src/pages/MistakePlayer.jsx b/frontend/src/pages/MistakePlayer.jsx deleted file mode 100644 index 0ab8c19..0000000 --- a/frontend/src/pages/MistakePlayer.jsx +++ /dev/null @@ -1,412 +0,0 @@ -/** - * Mistake Player Page - Re-do wrong questions - */ -import React, { useState, useEffect } from 'react' -import { useNavigate, useLocation } from 'react-router-dom' -import { mistakeAPI, questionAPI } from '../api/client' -import { - ArrowLeft, ArrowRight, Check, X, Loader, Trash2, AlertCircle -} from 'lucide-react' -import toast from 'react-hot-toast' -import { getQuestionTypeText } from '../utils/helpers' - -export const MistakePlayer = () => { - const navigate = useNavigate() - const location = useLocation() - const searchParams = new URLSearchParams(location.search) - const mode = searchParams.get('mode') || 'sequential' - - console.log('MistakePlayer mounted, mode:', mode) - - const [loading, setLoading] = useState(true) - const [mistake, setMistake] = useState(null) - const [currentIndex, setCurrentIndex] = useState(0) - const [total, setTotal] = useState(0) - const [randomMistakes, setRandomMistakes] = useState([]) // Store full mistake objects - - const [submitting, setSubmitting] = useState(false) - const [userAnswer, setUserAnswer] = useState('') - const [multipleAnswers, setMultipleAnswers] = useState([]) - const [result, setResult] = useState(null) - - useEffect(() => { - loadMistake() - }, [currentIndex, mode]) - - const loadMistake = async () => { - try { - setLoading(true) - - let currentMistake = null - - if (mode === 'random') { - // Random Mode Logic - if (randomMistakes.length === 0) { - // First load: fetch all mistakes - const response = await mistakeAPI.getList(0, 1000) - const allMistakes = response.data.mistakes - setTotal(response.data.total) - - if (allMistakes.length > 0) { - // Shuffle mistakes - const shuffled = [...allMistakes] - for (let i = shuffled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; - } - setRandomMistakes(shuffled) - currentMistake = shuffled[0] - } - } else { - // Subsequent loads: use stored mistakes - if (currentIndex < randomMistakes.length) { - currentMistake = randomMistakes[currentIndex] - } - } - } else { - // Sequential Mode Logic - const response = await mistakeAPI.getList(currentIndex, 1) - setTotal(response.data.total) - if (response.data.mistakes.length > 0) { - currentMistake = response.data.mistakes[0] - } - } - - if (currentMistake) { - // Ensure options exist for judge type - if (currentMistake.question.type === 'judge' && (!currentMistake.question.options || currentMistake.question.options.length === 0)) { - currentMistake.question.options = ['A. 正确', 'B. 错误'] - } - setMistake(currentMistake) - console.log('Mistake loaded:', currentMistake) - setResult(null) - setUserAnswer('') - setMultipleAnswers([]) - } else { - setMistake(null) - } - } catch (error) { - console.error('Failed to load mistake:', error) - toast.error('加载错题失败') - } finally { - setLoading(false) - console.log('Loading finished') - } - } - - const handleSubmitAnswer = async () => { - let answer = userAnswer - - if (mistake.question.type === 'multiple') { - if (multipleAnswers.length === 0) { - toast.error('请至少选择一个选项') - return - } - answer = multipleAnswers.sort().join('') - } - - if (!answer.trim()) { - toast.error('请输入答案') - return - } - - setSubmitting(true) - - try { - const response = await questionAPI.checkAnswer(mistake.question.id, answer) - setResult(response.data) - - if (response.data.correct) { - toast.success('回答正确!') - } else { - toast.error('回答错误') - } - } catch (error) { - console.error('Failed to check answer:', error) - toast.error('提交答案失败') - } finally { - setSubmitting(false) - } - } - - const handleNext = () => { - if (currentIndex < total - 1) { - setCurrentIndex(prev => prev + 1) - } else { - toast.success('已完成所有错题!') - navigate('/mistakes') - } - } - - const handleRemove = async () => { - if (!window.confirm('确定要从错题本中移除这道题吗?')) { - return - } - try { - await mistakeAPI.remove(mistake.id) - toast.success('已移除') - // Reload current index (which will now be the next item or empty) - // If we remove the last item, we need to go back one step or show empty - if (mode === 'random') { - // Remove from random list - const newRandomList = randomMistakes.filter(m => m.id !== mistake.id) - setRandomMistakes(newRandomList) - setTotal(newRandomList.length) - - if (currentIndex >= newRandomList.length && newRandomList.length > 0) { - setCurrentIndex(prev => prev - 1) - } else if (newRandomList.length === 0) { - setMistake(null) - } else { - // Force reload with new list - const nextMistake = newRandomList[currentIndex] - if (nextMistake.question.type === 'judge' && (!nextMistake.question.options || nextMistake.question.options.length === 0)) { - nextMistake.question.options = ['A. 正确', 'B. 错误'] - } - setMistake(nextMistake) - setResult(null) - setUserAnswer('') - setMultipleAnswers([]) - } - } else { - if (currentIndex >= total - 1 && total > 1) { - setCurrentIndex(prev => prev - 1) - } else { - loadMistake() - } - } - } catch (error) { - console.error('Failed to remove mistake:', error) - toast.error('移除失败') - } - } - - const handleMultipleChoice = (option) => { - const letter = option.charAt(0) - if (multipleAnswers.includes(letter)) { - setMultipleAnswers(multipleAnswers.filter(a => a !== letter)) - } else { - setMultipleAnswers([...multipleAnswers, letter]) - } - } - - if (loading && !mistake) { - return ( -
- -
- ) - } - - if (!mistake) { - return ( -
- -

错题本为空

- -
- ) - } - - const question = mistake.question - - if (!question) { - return ( -
- -

题目数据缺失

- -
- ) - } - - return ( - <> -
- {/* Header */} -
- - -
- 进度: {currentIndex + 1} / {total} -
-
- - {/* Question Card */} -
- {/* Question Header */} -
-
- - {currentIndex + 1} - - - {getQuestionTypeText(question.type)} - -
- - -
- - {/* Question Content */} -
-

- {question.content} -

-
- - {/* Options */} - {question.options && question.options.length > 0 && ( -
- {question.options.map((option, index) => { - const letter = option.charAt(0) - const isSelected = question.type === 'multiple' - ? multipleAnswers.includes(letter) - : userAnswer === letter - - return ( - - ) - })} -
- )} - - {/* Short Answer Input */} - {question.type === 'short' && !result && ( -
-