diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1be33d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# General +.DS_Store +Thumbs.db +*.log + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo + +# Python (Backend) +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +instance/ +.pytest_cache/ +.coverage +htmlcov/ + +# Node.js (Frontend) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.npm/ +.cache/ + +# Frontend Build Output +mengyadriftbottle-frontend/dist/ +mengyadriftbottle-frontend/build/ + +# Local Environment Variables +.env.local +.env.development.local +.env.test.local +.env.production.local diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8baae94 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +FROM python:3.11-slim AS base + +# Install Node.js 20 + nginx for building frontend and serving +RUN apt-get update \ + && apt-get install -y curl gnupg nginx \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs build-essential \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy source code +COPY mengyadriftbottle-backend ./mengyadriftbottle-backend +COPY mengyadriftbottle-frontend ./mengyadriftbottle-frontend + +# -------- Build frontend -------- +WORKDIR /app/mengyadriftbottle-frontend +RUN npm install \ + && npm run build + +# -------- Install backend deps -------- +WORKDIR /app/mengyadriftbottle-backend +RUN pip install --no-cache-dir -r requirements.txt \ + && pip install --no-cache-dir gunicorn + +# Prepare runtime artifacts +WORKDIR /app +RUN mkdir -p frontend-dist \ + && cp -r /app/mengyadriftbottle-frontend/dist/* /app/frontend-dist/ + +# Seed data directory (can be overridden via volume) +RUN mkdir -p /app/data \ + && cp /app/mengyadriftbottle-backend/*.json /app/data/ + +# Copy nginx config and startup script +COPY nginx.conf /etc/nginx/nginx.conf +COPY start.sh /app/start.sh +RUN chmod +x /app/start.sh + +EXPOSE 6767 + +ENV PORT=6767 \ + BACKEND_PORT=5002 \ + DRIFT_BOTTLE_FRONTEND_DIST=/app/frontend-dist \ + DRIFT_BOTTLE_DATA_DIR=/app/data + +CMD ["/app/start.sh"] diff --git a/README.md b/README.md index d9b3e6e..58e893c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,66 @@ -# mengyadriftbottle -萌芽匿名漂流瓶 +# 萌芽漂流瓶(前后端分离版) + +- `mengyadriftbottle-backend/`: Flask API(`/api`)+ 管理后台视图(`/admin`)。 +- `mengyadriftbottle-frontend/`: React + Vite 单页应用,调用上述 API 并提供用户交互界面。 + +## 快速开始 + +### 后端 +```cmd +cd mengyadriftbottle-backend +python -m venv .venv +.venv\Scripts\activate +pip install -r requirements.txt +python app.py +``` +服务器默认监听 `http://localhost:5002`,所有公开接口挂载在 `http://localhost:5002/api`。 + +### 前端 +确保后端已运行后启动 Vite: +```cmd +cd mengyadriftbottle-frontend +npm install +npm run dev +``` +此命令会在 `http://localhost:5173` 打开 React 界面,所有 `/api/*` 请求会被代理到本地 Flask 服务。 + +### 自定义配置 +在 `mengyadriftbottle-frontend` 下创建 `.env` 覆盖默认地址: +``` +VITE_API_BASE_URL=http://your-domain/api +VITE_ADMIN_URL=https://your-domain/admin/login +``` + +后端使用根目录的 `templates/`(管理后台视图)和 `static/`(样式文件),数据文件(`bottles.json`、`config.json` 等)现已迁移至 `mengyadriftbottle-backend/` 目录,所有前端页面由 React 单页应用提供。 + +### 轻量后台 + +- 访问 `http://localhost:5002/admin?token=shumengya520`(或自定义 `DRIFT_BOTTLE_ADMIN_TOKEN`)即可查看/删除漂流瓶列表。 +- 旧版登录后台依然可通过 `http://localhost:5002/admin/login` 使用。 + +## Docker 部署 + +若希望以单一容器运行前后端,可使用根目录提供的 `Dockerfile`: + +1. 构建镜像: + ```cmd + cd e:\Python\前后端分离项目\萌芽漂流瓶 + docker build -t mengyadriftbottle:latest . + ``` +2. 创建持久化目录(宿主机): + ```bash + mkdir -p /shumengya/docker/storage/mengyadriftbottle + ``` +3. 运行容器(对外暴露 6767 端口,并挂载数据目录): + ```bash + docker run -d \ + --name mengyadriftbottle \ + -p 6767:6767 \ + -v /shumengya/docker/storage/mengyadriftbottle:/app/mengyadriftbottle-backend \ + mengyadriftbottle:latest + ``` + +容器内会: +- 使用 `serve` 在 6767 端口提供构建后的前端静态文件。 +- 使用 `gunicorn` 在 5002 端口运行 Flask API(前端通过 `VITE_API_BASE_URL` 访问 `http://localhost:5002/api`)。 +- 将 `/app/mengyadriftbottle-backend` 作为数据目录,因此挂载即可让 `bottles.json` 等文件持久化在宿主机 `/shumengya/docker/storage/mengyadriftbottle`。 diff --git a/docker-build-run.sh b/docker-build-run.sh new file mode 100644 index 0000000..2ac2699 --- /dev/null +++ b/docker-build-run.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_NAME=${IMAGE_NAME:-mengyadriftbottle} +CONTAINER_NAME=${CONTAINER_NAME:-mengyadriftbottle} +FRONTEND_PORT=${FRONTEND_PORT:-6767} +BACKEND_PORT=${BACKEND_PORT:-5002} +PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" +DATA_DIR="${DATA_DIR:-}" + +echo "[1/3] Building image $IMAGE_NAME ..." +docker build -t "$IMAGE_NAME" "$PROJECT_DIR" + +echo "[2/3] Removing old container if it exists ..." +if docker ps -a --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then + docker rm -f "$CONTAINER_NAME" >/dev/null +fi + +echo "[3/3] Starting container $CONTAINER_NAME ..." +RUN_ARGS=( + -d + --name "$CONTAINER_NAME" + -p ${FRONTEND_PORT}:6767 + -e BACKEND_PORT=5002 +) + +if [ -n "$DATA_DIR" ]; then + mkdir -p "$DATA_DIR" + RUN_ARGS+=( -v "$DATA_DIR:/app/data" ) +fi + +docker run "${RUN_ARGS[@]}" "$IMAGE_NAME" + +echo "Container is up. Frontend: http://localhost:${FRONTEND_PORT}" +echo "Logs: docker logs -f ${CONTAINER_NAME}" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a9a5712 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3.9" + +services: + mengyadriftbottle: + build: . + container_name: mengyadriftbottle + restart: unless-stopped + ports: + - "6767:6767" + environment: + BACKEND_PORT: "5002" + DRIFT_BOTTLE_DATA_DIR: "/app/data" + volumes: + - /shumengya/docker/storage/mengyadriftbottle:/app/data diff --git a/mengyadriftbottle-backend/README.md b/mengyadriftbottle-backend/README.md new file mode 100644 index 0000000..7495ee9 --- /dev/null +++ b/mengyadriftbottle-backend/README.md @@ -0,0 +1,35 @@ +# Mengya Drift Bottle – Backend + +Flask API that powers the React frontend located in `../mengyadriftbottle-frontend`. + +## Features + +- JSON-first `/api` endpoints for throwing, picking up, and reacting to bottles +- Rate limiting per IP (5 seconds) for throw/pickup actions +- File-backed persistence with automatic schema upgrades +- Lightweight token portal via `/admin?token=...` for quick moderation +- Full legacy dashboard (`/admin/login`) preserved for session-based moderation +- CORS enabled for local development via Vite + +## Quick Start + +```bash +cd mengyadriftbottle-backend +python -m venv .venv +.venv\Scripts\activate +pip install -r requirements.txt +python app.py +``` + +The server listens on `http://localhost:5002` by default and exposes the API under `http://localhost:5002/api`. + +## Environment Variables + +| Name | Description | Default | +| ---- | ----------- | ------- | +| `DRIFT_BOTTLE_SECRET` | Secret key for Flask sessions | Random value generated at runtime | +| `DRIFT_BOTTLE_ADMIN_TOKEN` | Token required for `/admin?token=...` | `shumengya520` | + +## File Storage + +Bottle data, config, mottos, and filter words continue to use the JSON files located at the repository root (`../bottles.json`, etc.). The backend automatically creates them if missing. diff --git a/mengyadriftbottle-backend/app.py b/mengyadriftbottle-backend/app.py new file mode 100644 index 0000000..ccec9c6 --- /dev/null +++ b/mengyadriftbottle-backend/app.py @@ -0,0 +1,572 @@ +"""Flask backend for the Mengya Drift Bottle application. + +This module exposes a JSON-first API that is consumed by the React +frontend located in ``mengyadriftbottle-frontend``. The historical +server-rendered templates are preserved for the admin console, while the +public-facing experience now lives entirely in the frontend project. +""" +from __future__ import annotations + +import hashlib +import json +import os +import random +import re +import secrets +import time +from collections import defaultdict +from datetime import datetime +from functools import wraps +from pathlib import Path +from typing import Any, Dict, Iterable + +from flask import ( + Flask, + abort, + flash, + jsonify, + redirect, + render_template, + request, + send_from_directory, + session, + url_for, +) +from flask_cors import CORS + +# --------------------------------------------------------------------------- +# Paths & global configuration +# --------------------------------------------------------------------------- +BACKEND_ROOT = Path(__file__).resolve().parent # Backend directory +PROJECT_ROOT = BACKEND_ROOT.parent # Root project directory +FRONTEND_ROOT = PROJECT_ROOT / "mengyadriftbottle-frontend" # Frontend directory +TEMPLATES_DIR = FRONTEND_ROOT / "templates" # Templates in frontend +STATIC_DIR = FRONTEND_ROOT / "static" # Static files in frontend +FRONTEND_DIST = Path( + os.environ.get("DRIFT_BOTTLE_FRONTEND_DIST") or PROJECT_ROOT / "frontend-dist" +) +DATA_DIR = Path(os.environ.get("DRIFT_BOTTLE_DATA_DIR", BACKEND_ROOT)) +DATA_DIR.mkdir(parents=True, exist_ok=True) + +BOTTLES_FILE = DATA_DIR / "bottles.json" +FILTER_WORDS_FILE = DATA_DIR / "filter_words.json" +MOTTOS_FILE = DATA_DIR / "mottos.json" +CONFIG_FILE = DATA_DIR / "config.json" + +API_PREFIX = "/api" +OPERATION_INTERVAL = 5 # seconds + +app = Flask( + __name__, + static_folder=str(STATIC_DIR), + template_folder=str(TEMPLATES_DIR), +) +app.config["JSON_AS_ASCII"] = False +app.config["JSON_SORT_KEYS"] = False +app.secret_key = os.environ.get("DRIFT_BOTTLE_SECRET") or secrets.token_hex(24) + +# Only allow cross-origin requests for the API endpoints so the React app +# running on Vite's dev server can communicate with Flask during local dev. +CORS(app, resources={f"{API_PREFIX}/*": {"origins": "*"}}) + +last_operation_time: Dict[str, Dict[str, float]] = defaultdict( + lambda: {"throw": 0.0, "pickup": 0.0} +) +last_picked_bottle: Dict[str, str | None] = defaultdict(lambda: None) + +DEFAULT_CONFIG = { + "name_limit": 7, + "message_limit": 100, + "admin_username": "shumengya", + "admin_password": hashlib.sha256("tyh@19900420".encode()).hexdigest(), +} + +SIMPLE_ADMIN_TOKEN = os.environ.get("DRIFT_BOTTLE_ADMIN_TOKEN", "shumengya520") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def ensure_file(path: Path, default_content: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists(): + return + with path.open("w", encoding="utf-8") as fh: + json.dump(default_content, fh, indent=4, ensure_ascii=False) + + +def ensure_config_file() -> Dict[str, Any]: + ensure_file(CONFIG_FILE, DEFAULT_CONFIG) + return read_config() + + +def read_config() -> Dict[str, Any]: + if CONFIG_FILE.exists(): + try: + with CONFIG_FILE.open("r", encoding="utf-8") as fh: + config = json.load(fh) + except json.JSONDecodeError: + config = DEFAULT_CONFIG.copy() + else: + config = DEFAULT_CONFIG.copy() + + for key, value in DEFAULT_CONFIG.items(): + config.setdefault(key, value) + return config + + +def save_config(config: Dict[str, Any]) -> bool: + try: + with CONFIG_FILE.open("w", encoding="utf-8") as fh: + json.dump(config, fh, indent=4, ensure_ascii=False) + return True + except OSError: + return False + + +def ensure_mottos_file() -> None: + default_mottos = [ + "良言一句三冬暖,恶语伤人六月寒", + "己所不欲,勿施于人", + "赠人玫瑰,手有余香", + "海内存知己,天涯若比邻", + "一期一会,珍惜每一次相遇", + "星河滚烫,你是人间理想", + ] + ensure_file(MOTTOS_FILE, default_mottos) + + +def read_mottos() -> Iterable[str]: + ensure_mottos_file() + try: + with MOTTOS_FILE.open("r", encoding="utf-8") as fh: + return json.load(fh) + except json.JSONDecodeError: + return ["良言一句三冬暖,恶语伤人六月寒"] + + +def get_random_motto() -> str: + mottos = list(read_mottos()) + if not mottos: + return "良言一句三冬暖,恶语伤人六月寒" + return random.choice(mottos) + + +def ensure_filter_words_file() -> None: + ensure_file(FILTER_WORDS_FILE, ["敏感词1", "敏感词2"]) + + +def read_filter_words() -> Iterable[str]: + ensure_filter_words_file() + try: + with FILTER_WORDS_FILE.open("r", encoding="utf-8") as fh: + return json.load(fh) + except json.JSONDecodeError: + return [] + + +def filter_sensitive_words(text: str) -> str: + filtered = text + for word in read_filter_words(): + filtered = re.sub(re.escape(word), "*" * len(word), filtered) + return filtered + + +def read_bottles() -> list[Dict[str, Any]]: + if not BOTTLES_FILE.exists(): + with BOTTLES_FILE.open("w", encoding="utf-8") as fh: + json.dump([], fh) + return [] + + try: + with BOTTLES_FILE.open("r", encoding="utf-8") as fh: + bottles = json.load(fh) + except json.JSONDecodeError: + return [] + + for bottle in bottles: + bottle.setdefault("likes", 0) + bottle.setdefault("dislikes", 0) + write_bottles(bottles) + return bottles + + +def write_bottles(bottles: list[Dict[str, Any]]) -> None: + BOTTLES_FILE.parent.mkdir(parents=True, exist_ok=True) + with BOTTLES_FILE.open("w", encoding="utf-8") as fh: + json.dump(bottles, fh, indent=4, ensure_ascii=False) + + +def get_client_ip() -> str: + forwarded = request.headers.get("X-Forwarded-For") + if forwarded: + return forwarded.split(",")[0].strip() + real_ip = request.headers.get("X-Real-IP") + if real_ip: + return real_ip + return request.remote_addr or "unknown" + + +def check_operation_interval(operation_type: str) -> float: + ip = get_client_ip() + current_time = time.time() + last_time = last_operation_time[ip][operation_type] + remaining = OPERATION_INTERVAL - (current_time - last_time) + if remaining > 0: + return max(0.0, remaining) + last_operation_time[ip][operation_type] = current_time + return 0.0 + + +def admin_required(func): + @wraps(func) + def decorated(*args, **kwargs): + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + return func(*args, **kwargs) + + return decorated + + +def get_request_payload() -> Dict[str, Any]: + if request.is_json: + payload = request.get_json(silent=True) + if isinstance(payload, dict): + return payload + if request.form: + return request.form.to_dict() + return {} + + +def normalize_name(value: str | None) -> str | None: + if value is None: + return None + stripped = value.strip() + return stripped or None + + +def is_valid_simple_admin_token(token: str | None) -> bool: + if not token: + return False + # compare_digest防止时间侧信道 + return secrets.compare_digest(str(token), SIMPLE_ADMIN_TOKEN) + + +def bootstrap_storage() -> None: + ensure_filter_words_file() + ensure_mottos_file() + ensure_config_file() + if not BOTTLES_FILE.exists(): + write_bottles([]) + + +bootstrap_storage() + + +# ---------------------------------------------------------------------- # +# ==============================后端公开API============================== # +# ---------------------------------------------------------------------- # + + +@app.get("/") +def root() -> Any: + return jsonify({ + "message": "Mengya Drift Bottle后端API正在运行中...", + "api_base": API_PREFIX, + }) + + +@app.get(f"{API_PREFIX}/health") +def health_check() -> Any: + return jsonify({"status": "ok", "timestamp": datetime.utcnow().isoformat()}) + + +@app.get(f"{API_PREFIX}/motto") +def random_motto() -> Any: + return jsonify({"success": True, "motto": get_random_motto()}) + + +@app.get(f"{API_PREFIX}/stats") +def get_stats() -> Any: + bottles = read_bottles() + return jsonify({"success": True, "stats": {"total_bottles": len(bottles)}}) + + +@app.get(f"{API_PREFIX}/config") +def get_config() -> Any: + config = read_config() + return jsonify({ + "success": True, + "config": { + "name_limit": config.get("name_limit", 7), + "message_limit": config.get("message_limit", 100), + }, + }) + + +@app.post(f"{API_PREFIX}/throw") +def throw_bottle() -> Any: + wait_time = check_operation_interval("throw") + if wait_time > 0: + return ( + jsonify( + { + "success": False, + "error": f"操作太频繁,请等待 {round(wait_time, 1)} 秒后再试。", + "wait_time": round(wait_time, 1), + } + ), + 429, + ) + + data = get_request_payload() + name = normalize_name(data.get("name")) + message = normalize_name(data.get("message")) + gender = data.get("gender") or "保密" + qq_number = normalize_name(data.get("qq") or data.get("qq_number")) + + if not name or not message or not gender: + return jsonify({"success": False, "error": "姓名、消息和性别是必需的"}), 400 + + config = read_config() + name_limit = int(config.get("name_limit", 7)) + message_limit = int(config.get("message_limit", 100)) + + if len(name) > name_limit: + return jsonify({"success": False, "error": f"名字最多{name_limit}个字符"}), 400 + if len(message) > message_limit: + return jsonify({"success": False, "error": f"消息内容最多{message_limit}个字符"}), 400 + + filtered_name = filter_sensitive_words(name) + filtered_message = filter_sensitive_words(message) + + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ip_address = get_client_ip() + + qq_avatar_url = None + if qq_number and qq_number.isdigit(): + qq_avatar_url = f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100" + + new_bottle = { + "id": f"{timestamp}_{random.randint(1000, 9999)}", + "name": filtered_name, + "message": filtered_message, + "timestamp": timestamp, + "ip_address": ip_address, + "gender": gender, + "qq_number": qq_number, + "qq_avatar_url": qq_avatar_url, + "likes": 0, + "dislikes": 0, + } + + bottles = read_bottles() + bottles.append(new_bottle) + write_bottles(bottles) + + return jsonify({"success": True, "message": "漂流瓶投放成功!"}) + + +@app.get(f"{API_PREFIX}/pickup") +def pickup_bottle() -> Any: + wait_time = check_operation_interval("pickup") + if wait_time > 0: + return ( + jsonify( + { + "success": False, + "message": f"操作太频繁,请等待 {round(wait_time, 1)} 秒后再试。", + "wait_time": round(wait_time, 1), + } + ), + 429, + ) + + bottles = read_bottles() + if not bottles: + return jsonify({"success": False, "message": "海里没有漂流瓶。"}) + + ip_address = get_client_ip() + last_bottle_id = last_picked_bottle[ip_address] + + if len(bottles) == 1: + selected_bottle = bottles[0] + else: + available = [b for b in bottles if b["id"] != last_bottle_id] + available = available or bottles + weights = [max(1, 10 - min(9, b.get("dislikes", 0))) for b in available] + selected_bottle = random.choices(available, weights=weights, k=1)[0] + + last_picked_bottle[ip_address] = selected_bottle["id"] + return jsonify({"success": True, "bottle": selected_bottle}) + + +@app.post(f"{API_PREFIX}/react") +def react_to_bottle() -> Any: + data = request.get_json(silent=True) or {} + bottle_id = data.get("bottle_id") + reaction = data.get("reaction") + + if not bottle_id or reaction not in {"like", "dislike"}: + return jsonify({"success": False, "error": "参数错误"}), 400 + + bottles = read_bottles() + for bottle in bottles: + if bottle["id"] == bottle_id: + key = "likes" if reaction == "like" else "dislikes" + bottle[key] = bottle.get(key, 0) + 1 + write_bottles(bottles) + return jsonify({"success": True, "message": "反馈已记录"}) + + return jsonify({"success": False, "error": "未找到漂流瓶"}), 404 + + +# --------------------------------------------------------------------------- +# 管理员相关操作API +# --------------------------------------------------------------------------- + + +def summarize_bottles(bottles: list[Dict[str, Any]]) -> Dict[str, Any]: + return { + "total": len(bottles), + "likes": sum(int(b.get("likes", 0) or 0) for b in bottles), + "dislikes": sum(int(b.get("dislikes", 0) or 0) for b in bottles), + } + + +@app.get("/admin") +def admin_simple_portal(): + token = request.args.get("token", "") + status_key = request.args.get("status") + status_messages = { + "deleted": "漂流瓶已删除", + "missing": "未找到指定漂流瓶,可能已经被删除", + "unauthorized": "无权执行该操作,请检查token", + } + feedback = status_messages.get(status_key) + + authorized = is_valid_simple_admin_token(token) + bottles_raw = read_bottles() if authorized else [] + bottles = sorted( + bottles_raw, + key=lambda item: item.get("timestamp", ""), + reverse=True, + ) if authorized else [] + stats = summarize_bottles(bottles) if authorized else None + + return render_template( + "admin_simple.html", + authorized=authorized, + token=token, + stats=stats, + bottles=bottles, + feedback=feedback, + sample_token=SIMPLE_ADMIN_TOKEN if os.environ.get("DRIFT_BOTTLE_ADMIN_TOKEN") is None else "***", + ) + + +@app.post("/admin/simple/delete/") +def admin_simple_delete(bottle_id: str): + token = request.form.get("token") or request.args.get("token") + if not is_valid_simple_admin_token(token): + return redirect(url_for("admin_simple_portal", status="unauthorized")) + + bottles = read_bottles() + new_bottles = [b for b in bottles if b["id"] != bottle_id] + status = "missing" + if len(new_bottles) != len(bottles): + write_bottles(new_bottles) + status = "deleted" + + return redirect(url_for("admin_simple_portal", token=token, status=status)) + + +@app.route("/admin/login", methods=["GET", "POST"]) +def admin_login(): + if request.method == "POST": + username = request.form.get("username", "") + password = request.form.get("password", "") + + config = read_config() + if ( + username == config.get("admin_username") + and hashlib.sha256(password.encode()).hexdigest() + == config.get("admin_password") + ): + session["admin_logged_in"] = True + return redirect(url_for("admin_dashboard")) + + flash("用户名或密码错误", "error") + + return render_template("admin_login.html") + + +@app.get("/admin/logout") +def admin_logout(): + session.pop("admin_logged_in", None) + return redirect(url_for("admin_login")) + + +@app.get("/admin/dashboard") +@admin_required +def admin_dashboard(): + bottles = read_bottles() + return render_template("admin_dashboard.html", bottles=bottles) + + +@app.post("/admin/delete_bottle/") +@admin_required +def delete_bottle(bottle_id: str): + bottles = [b for b in read_bottles() if b["id"] != bottle_id] + write_bottles(bottles) + flash("漂流瓶已成功删除", "success") + return redirect(url_for("admin_dashboard")) + + +@app.route("/admin/settings", methods=["GET", "POST"]) +@admin_required +def admin_settings(): + config = read_config() + if request.method == "POST": + try: + name_limit = int(request.form.get("name_limit", 7)) + message_limit = int(request.form.get("message_limit", 100)) + if name_limit < 1 or message_limit < 1: + raise ValueError + config["name_limit"] = name_limit + config["message_limit"] = message_limit + if save_config(config): + flash("设置已成功保存", "success") + else: + flash("保存设置时出错", "error") + except ValueError: + flash("请输入有效的数值", "error") + + return render_template("admin_settings.html", config=config) + + +# ---------------------------------------------------------------------- # +# ==============================后端公开API============================== # +# ---------------------------------------------------------------------- # + + +@app.route("/", defaults={"path": ""}) +@app.route("/") +def serve_frontend_app(path: str): + """Serve the compiled React frontend (single-page app).""" + + if path.startswith("api/"): + abort(404) + + if not FRONTEND_DIST.exists(): + return ("Frontend build not found. Please run npm run build first.", 404) + + if path and (FRONTEND_DIST / path).is_file(): + return send_from_directory(str(FRONTEND_DIST), path) + + return send_from_directory(str(FRONTEND_DIST), "index.html") + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5002, debug=True) diff --git a/mengyadriftbottle-backend/bottles.json b/mengyadriftbottle-backend/bottles.json new file mode 100644 index 0000000..dd53bbb --- /dev/null +++ b/mengyadriftbottle-backend/bottles.json @@ -0,0 +1,14 @@ +[ + { + "id": "2025-11-16 19:00:54_5309", + "name": "树萌芽", + "message": "Hello World!", + "timestamp": "2025-11-16 19:00:54", + "ip_address": "127.0.0.1", + "gender": "男", + "qq_number": "3205788256", + "qq_avatar_url": "http://q1.qlogo.cn/g?b=qq&nk=3205788256&s=100", + "likes": 1, + "dislikes": 1 + } +] \ No newline at end of file diff --git a/mengyadriftbottle-backend/config.json b/mengyadriftbottle-backend/config.json new file mode 100644 index 0000000..b1245fc --- /dev/null +++ b/mengyadriftbottle-backend/config.json @@ -0,0 +1,6 @@ +{ + "name_limit": 7, + "message_limit": 150, + "admin_username": "shumengya", + "admin_password": "06dc4b37c16a43fa94a3191e3be039ab6b05dbecc1c55e7860cf9911c72e71f8" +} diff --git a/mengyadriftbottle-backend/filter_words.json b/mengyadriftbottle-backend/filter_words.json new file mode 100644 index 0000000..4420c03 --- /dev/null +++ b/mengyadriftbottle-backend/filter_words.json @@ -0,0 +1,19 @@ +[ + "你TM", + "唐伟", + "糖伟", + "糖萎", + "月抛", + "约炮", + "cnm", + "傻逼", + "狗日的", + "妈卖批", + "nmsl", + "杂种", + "操你妈", + "草你妈", + "日你妈", + "你他妈", + "超你妈" +] diff --git a/mengyadriftbottle-backend/mottos.json b/mengyadriftbottle-backend/mottos.json new file mode 100644 index 0000000..e5b376a --- /dev/null +++ b/mengyadriftbottle-backend/mottos.json @@ -0,0 +1,8 @@ +[ + "良言一句三冬暖,恶语伤人六月寒", + "己所不欲,勿施于人", + "赠人玫瑰,手有余香", + "海内存知己,天涯若比邻", + "一期一会,珍惜每一次相遇", + "星河滚烫,你是人间理想" +] diff --git a/mengyadriftbottle-backend/requirements.txt b/mengyadriftbottle-backend/requirements.txt new file mode 100644 index 0000000..47bb3a9 --- /dev/null +++ b/mengyadriftbottle-backend/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.3 +flask-cors==4.0.0 diff --git a/mengyadriftbottle-frontend/.gitignore b/mengyadriftbottle-frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/mengyadriftbottle-frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/mengyadriftbottle-frontend/README.md b/mengyadriftbottle-frontend/README.md new file mode 100644 index 0000000..5eb22a8 --- /dev/null +++ b/mengyadriftbottle-frontend/README.md @@ -0,0 +1,29 @@ +## Mengya Drift Bottle – Frontend + +React + Vite single-page application that consumes the Flask API exposed under `/api`. + +### Available Scripts + +```bash +# start dev server with API proxy to http://localhost:5002 +npm run dev + +# lint with ESLint +npm run lint + +# production build +npm run build +``` + +The dev server proxies every `/api/*` request to the backend, so start the Flask app before opening the React UI. + +### Configuration + +Set the following environment variables in a `.env` file if you need to override defaults: + +``` +VITE_API_BASE_URL=http://localhost:5002/api +VITE_ADMIN_URL=http://localhost:5002/admin/login +``` + +In production you typically serve the static build (`dist/`) behind the same origin as the backend, allowing the default relative `/api` base to keep working. diff --git a/mengyadriftbottle-frontend/eslint.config.js b/mengyadriftbottle-frontend/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/mengyadriftbottle-frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/mengyadriftbottle-frontend/index.html b/mengyadriftbottle-frontend/index.html new file mode 100644 index 0000000..c67eae4 --- /dev/null +++ b/mengyadriftbottle-frontend/index.html @@ -0,0 +1,17 @@ + + + + + + 萌芽漂流瓶(´,,•ω•,,)♡ + + + + +
+ + + diff --git a/mengyadriftbottle-frontend/package-lock.json b/mengyadriftbottle-frontend/package-lock.json new file mode 100644 index 0000000..18f7d5f --- /dev/null +++ b/mengyadriftbottle-frontend/package-lock.json @@ -0,0 +1,2891 @@ +{ + "name": "mengyadriftbottle-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mengyadriftbottle-frontend", + "version": "0.0.0", + "dependencies": { + "@fortawesome/fontawesome-free": "^6.7.2", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.2.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", + "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", + "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.1.tgz", + "integrity": "sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.254", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", + "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "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.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/mengyadriftbottle-frontend/package.json b/mengyadriftbottle-frontend/package.json new file mode 100644 index 0000000..900ba34 --- /dev/null +++ b/mengyadriftbottle-frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "mengyadriftbottle-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.7.2", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.2.2" + } +} diff --git a/mengyadriftbottle-frontend/public/background/image1.png b/mengyadriftbottle-frontend/public/background/image1.png new file mode 100644 index 0000000..95e5fa7 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image1.png differ diff --git a/mengyadriftbottle-frontend/public/background/image10.png b/mengyadriftbottle-frontend/public/background/image10.png new file mode 100644 index 0000000..43258f0 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image10.png differ diff --git a/mengyadriftbottle-frontend/public/background/image11.png b/mengyadriftbottle-frontend/public/background/image11.png new file mode 100644 index 0000000..c6d7936 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image11.png differ diff --git a/mengyadriftbottle-frontend/public/background/image12.png b/mengyadriftbottle-frontend/public/background/image12.png new file mode 100644 index 0000000..75ac329 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image12.png differ diff --git a/mengyadriftbottle-frontend/public/background/image13.png b/mengyadriftbottle-frontend/public/background/image13.png new file mode 100644 index 0000000..4046b23 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image13.png differ diff --git a/mengyadriftbottle-frontend/public/background/image14.png b/mengyadriftbottle-frontend/public/background/image14.png new file mode 100644 index 0000000..aae3831 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image14.png differ diff --git a/mengyadriftbottle-frontend/public/background/image15.png b/mengyadriftbottle-frontend/public/background/image15.png new file mode 100644 index 0000000..bb302ba Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image15.png differ diff --git a/mengyadriftbottle-frontend/public/background/image16.png b/mengyadriftbottle-frontend/public/background/image16.png new file mode 100644 index 0000000..2e1e09c Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image16.png differ diff --git a/mengyadriftbottle-frontend/public/background/image17.png b/mengyadriftbottle-frontend/public/background/image17.png new file mode 100644 index 0000000..3979bc2 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image17.png differ diff --git a/mengyadriftbottle-frontend/public/background/image18.png b/mengyadriftbottle-frontend/public/background/image18.png new file mode 100644 index 0000000..766c952 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image18.png differ diff --git a/mengyadriftbottle-frontend/public/background/image19.png b/mengyadriftbottle-frontend/public/background/image19.png new file mode 100644 index 0000000..28e5d8f Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image19.png differ diff --git a/mengyadriftbottle-frontend/public/background/image2.png b/mengyadriftbottle-frontend/public/background/image2.png new file mode 100644 index 0000000..5fd8cc7 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image2.png differ diff --git a/mengyadriftbottle-frontend/public/background/image20.png b/mengyadriftbottle-frontend/public/background/image20.png new file mode 100644 index 0000000..fc705aa Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image20.png differ diff --git a/mengyadriftbottle-frontend/public/background/image21.png b/mengyadriftbottle-frontend/public/background/image21.png new file mode 100644 index 0000000..7b2106d Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image21.png differ diff --git a/mengyadriftbottle-frontend/public/background/image22.png b/mengyadriftbottle-frontend/public/background/image22.png new file mode 100644 index 0000000..6538ef6 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image22.png differ diff --git a/mengyadriftbottle-frontend/public/background/image23.png b/mengyadriftbottle-frontend/public/background/image23.png new file mode 100644 index 0000000..9ecf491 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image23.png differ diff --git a/mengyadriftbottle-frontend/public/background/image24.png b/mengyadriftbottle-frontend/public/background/image24.png new file mode 100644 index 0000000..126cc80 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image24.png differ diff --git a/mengyadriftbottle-frontend/public/background/image25.png b/mengyadriftbottle-frontend/public/background/image25.png new file mode 100644 index 0000000..f4bbea4 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image25.png differ diff --git a/mengyadriftbottle-frontend/public/background/image26.png b/mengyadriftbottle-frontend/public/background/image26.png new file mode 100644 index 0000000..69a9b66 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image26.png differ diff --git a/mengyadriftbottle-frontend/public/background/image27.png b/mengyadriftbottle-frontend/public/background/image27.png new file mode 100644 index 0000000..51d4576 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image27.png differ diff --git a/mengyadriftbottle-frontend/public/background/image28.png b/mengyadriftbottle-frontend/public/background/image28.png new file mode 100644 index 0000000..9c3de2c Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image28.png differ diff --git a/mengyadriftbottle-frontend/public/background/image29.png b/mengyadriftbottle-frontend/public/background/image29.png new file mode 100644 index 0000000..8f84592 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image29.png differ diff --git a/mengyadriftbottle-frontend/public/background/image3.png b/mengyadriftbottle-frontend/public/background/image3.png new file mode 100644 index 0000000..47c1d39 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image3.png differ diff --git a/mengyadriftbottle-frontend/public/background/image4.png b/mengyadriftbottle-frontend/public/background/image4.png new file mode 100644 index 0000000..7ee1f97 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image4.png differ diff --git a/mengyadriftbottle-frontend/public/background/image5.png b/mengyadriftbottle-frontend/public/background/image5.png new file mode 100644 index 0000000..e0769a3 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image5.png differ diff --git a/mengyadriftbottle-frontend/public/background/image6.png b/mengyadriftbottle-frontend/public/background/image6.png new file mode 100644 index 0000000..d1cae35 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image6.png differ diff --git a/mengyadriftbottle-frontend/public/background/image7.png b/mengyadriftbottle-frontend/public/background/image7.png new file mode 100644 index 0000000..b098235 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image7.png differ diff --git a/mengyadriftbottle-frontend/public/background/image8.png b/mengyadriftbottle-frontend/public/background/image8.png new file mode 100644 index 0000000..17fb526 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image8.png differ diff --git a/mengyadriftbottle-frontend/public/background/image9.png b/mengyadriftbottle-frontend/public/background/image9.png new file mode 100644 index 0000000..35e4143 Binary files /dev/null and b/mengyadriftbottle-frontend/public/background/image9.png differ diff --git a/mengyadriftbottle-frontend/public/legacy-style.css b/mengyadriftbottle-frontend/public/legacy-style.css new file mode 100644 index 0000000..3e6c434 --- /dev/null +++ b/mengyadriftbottle-frontend/public/legacy-style.css @@ -0,0 +1,560 @@ +body { + font-family: 'Segoe UI', Roboto, "Helvetica Neue", Arial, sans-serif; + background: linear-gradient(135deg, #f5e8f0 0%, #f3e5fc 100%); + color: #5e5166; + margin: 0; + padding: 0; + min-height: 100vh; + box-sizing: border-box; + position: relative; + overflow-x: hidden; +} + +.container { + background-color: rgba(255, 255, 255, 0.9); + padding: 25px 30px; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(155, 89, 182, 0.15); + width: 90%; + max-width: 600px; + text-align: center; + margin: 40px auto; + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.5); +} + +h1 { + font-family: '楷体', 'STKaiti', '华文楷体', KaiTi, '宋体', SimSun, sans-serif; + color: #d873a9; + margin-bottom: 10px; + font-size: 2.4em; + text-shadow: 0 2px 4px rgba(0,0,0,0.05); + letter-spacing: 1px; +} + +h2 { + color: #b07cc6; + margin-top: 20px; + margin-bottom: 20px; + font-weight: 600; + font-size: 1.5em; +} + +h2 i { + margin-right: 8px; + color: #c27ba0; +} + +.action-section { + margin-bottom: 30px; + padding: 25px 20px; + background-color: #fff; + border-radius: 12px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); + position: relative; + overflow: hidden; +} + +.throw-section { + border-top: 4px solid #ffb6c1; +} + +.pickup-section { + border-top: 4px solid #c5a3ff; +} + +.action-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f9e0f0' fill-opacity='0.2' fill-rule='evenodd'%3E%3Ccircle cx='3' cy='3' r='3'/%3E%3Ccircle cx='13' cy='13' r='3'/%3E%3C/g%3E%3C/svg%3E"); + opacity: 0.5; + z-index: -1; +} + +label { + display: block; + margin-bottom: 8px; + color: #7d5ba6; + font-weight: 600; + text-align: left; + font-size: 0.95em; +} + +input[type="text"], +textarea, +select { + width: 100%; + padding: 12px 15px; + margin-bottom: 18px; + border: 1px solid #e1d1f0; + border-radius: 8px; + background-color: #fdfaff; + color: #5e4b6b; + font-size: 0.95em; + box-sizing: border-box; + transition: all 0.3s ease; +} + +input[type="text"]:focus, +textarea:focus, +select:focus { + outline: none; + border-color: #d291bc; + box-shadow: 0 0 0 3px rgba(219, 112, 194, 0.1); +} + +textarea { + resize: vertical; + min-height: 80px; +} + +::placeholder { + color: #cbb8db; + opacity: 0.7; +} + +button { + background-color: #aa67e5; + color: white; + padding: 12px 25px; + border: none; + border-radius: 30px; + cursor: pointer; + font-size: 1.05em; + font-weight: 600; + transition: all 0.3s ease; + box-shadow: 0 4px 8px rgba(170, 103, 229, 0.3); + position: relative; + overflow: hidden; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.btn-throw { + background: linear-gradient(135deg, #ff8fbc 0%, #eb6dab 100%); +} + +.btn-pickup { + background: linear-gradient(135deg, #a47aed 0%, #876bd3 100%); +} + +button i { + margin-right: 8px; +} + +button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(170, 103, 229, 0.4); +} + +button:active { + transform: translateY(0); +} + +#throw-status, +#pickup-status { + margin-top: 15px; + font-style: italic; + color: #b56ab0; + min-height: 1.5em; + font-weight: 500; +} + +#bottle-display { + margin-top: 25px; + padding: 0; + background-color: #fff; + border-radius: 12px; + text-align: left; + overflow: hidden; + box-shadow: 0 5px 15px rgba(138, 80, 201, 0.1); + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + border: 1px solid #f1e1fb; +} + +.bottle-header { + background: linear-gradient(135deg, #f9ddff 0%, #e9cdff 100%); + padding: 15px 20px; + position: relative; + border-bottom: 1px solid #f1e1ff; + display: flex; + align-items: center; +} + +#bottle-avatar { + max-width: 60px; + border-radius: 50%; + border: 3px solid white; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1); + margin-right: 15px; +} + +.gender-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.8em; + margin-left: 8px; + background-color: #f0e6ff; + color: #7155a8; +} + +#bottle-display h3 { + color: #8156c5; + margin: 0; + font-size: 1.2em; + flex-grow: 1; +} + +.message-content { + padding: 20px; + background-color: #fff; +} + +#bottle-display p { + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; + color: #51456a; + line-height: 1.6; + font-size: 1.05em; +} + +.bottle-footer { + padding: 12px 20px; + background-color: #fbf8ff; + border-top: 1px solid #f1e8ff; +} + +.bottle-info small { + display: inline-block; + margin-right: 15px; + color: #9d8aaf; + font-size: 0.85em; +} + +.bottle-info i { + margin-right: 5px; + color: #b67fdb; +} + +.bottle-reactions { + margin-top: 15px; + display: flex; + gap: 15px; + justify-content: flex-end; +} + +.reaction-btn { + background: #ffffff; + border: 1px solid #e9d8ff; + border-radius: 20px; + padding: 6px 15px; + font-size: 0.9em; + display: inline-flex; + align-items: center; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); +} + +.reaction-btn i { + margin-right: 6px; + font-size: 1.1em; +} + +.like-btn { + color: #5aaa9d; +} + +.like-btn:hover:not(:disabled) { + background-color: #e5fff8; + border-color: #93e7d7; +} + +.dislike-btn { + color: #d76b8b; +} + +.dislike-btn:hover:not(:disabled) { + background-color: #fff1f5; + border-color: #ffb8c9; +} + +.reaction-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.cooldown-timer { + margin-top: 15px; + background-color: #f8f0ff; + padding: 10px 15px; + border-radius: 25px; + color: #aa67e5; + font-size: 0.95em; + display: inline-flex; + align-items: center; + border: 1px dashed #d7c3f0; + animation: pulse-soft 1.5s infinite; +} + +.cooldown-timer i { + margin-right: 8px; + color: #d873a9; + animation: spin 2s linear infinite; +} + +.char-count { + font-size: 0.8em; + color: #aa67e5; + margin-left: 10px; + background-color: #f8f4ff; + padding: 3px 8px; + border-radius: 10px; + font-weight: normal; + transition: all 0.2s ease; +} + +.char-count-limit { + background-color: #ffebf3; + color: #e65c8f; + animation: pulse 0.5s ease; +} + +@keyframes pulse-soft { + 0% { + box-shadow: 0 0 0 0 rgba(170, 103, 229, 0.2); + } + 70% { + box-shadow: 0 0 0 8px rgba(170, 103, 229, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(170, 103, 229, 0); + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.appear { + animation: appear 0.5s ease-out; +} + +@keyframes appear { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +footer { + margin-top: 40px; + color: #a07bb8; + font-size: 0.9em; +} + +footer i { + color: #ff85a2; +} + +.admin-link { + margin-top: 10px; + font-size: 0.8em; +} + +.admin-link a { + color: #a07bb8; + text-decoration: none; + opacity: 0.7; + transition: all 0.2s ease; +} + +.admin-link a:hover { + opacity: 1; + text-decoration: underline; +} + +@media (max-width: 768px) { + body { + padding: 10px; + } + .container { + padding: 20px 15px; + width: 95%; + } + h1 { + font-size: 2em; + } + .wave-container { + height: 120px; + } +} + +@media (max-width: 480px) { + h1 { + font-size: 1.7em; + } + h2 { + font-size: 1.3em; + } + .bottle-info small { + display: block; + margin-bottom: 5px; + } + button { + padding: 10px 20px; + font-size: 0.95em; + width: 100%; + } +} + +/* 波浪动画 */ +.wave-container { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + height: 200px; + overflow: hidden; + z-index: -1; +} + +.wave { + position: absolute; + bottom: 0; + left: 0; + width: 200%; + height: 100%; + background-repeat: repeat-x; + background-position: 0 bottom; + transform-origin: center bottom; +} + +.wave1 { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 88.7'%3E%3Cpath d='M800 56.9c-155.5 0-204.9-50-405.5-49.9-200 0-250 49.9-394.5 49.9v31.8h800v-.2-31.6z' fill='%23f8d8eb' fill-opacity='0.4'/%3E%3C/svg%3E"); + background-size: 50% 100px; + animation: wave 25s -3s linear infinite; + opacity: 0.6; + z-index: 1; +} + +.wave2 { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 88.7'%3E%3Cpath d='M800 56.9c-155.5 0-204.9-50-405.5-49.9-200 0-250 49.9-394.5 49.9v31.8h800v-.2-31.6z' fill='%23e6c0e9' fill-opacity='0.3'/%3E%3C/svg%3E"); + background-size: 50% 120px; + animation: wave 20s linear reverse infinite; + opacity: 0.4; + z-index: 0; +} + +@keyframes wave { + 0% {transform: translateX(0) translateZ(0) scaleY(1)} + 50% {transform: translateX(-25%) translateZ(0) scaleY(0.8)} + 100% {transform: translateX(-50%) translateZ(0) scaleY(1)} +} + +/* 标题和介绍文字 */ +.heart-icon { + color: #ff6b9c; + margin: 0 8px; + animation: pulse 1.5s infinite; + display: inline-block; +} + +@keyframes pulse { + 0% {transform: scale(1);} + 50% {transform: scale(1.2);} + 100% {transform: scale(1);} +} + +.tagline { + color: #9d7bb0; + margin-bottom: 25px; + font-style: italic; + font-size: 1.1em; +} + +.motto-container { + background-color: #fff; + border-radius: 12px; + padding: 18px 20px; + margin-bottom: 25px; + border-left: 5px solid #aa67e5; + box-shadow: 0 3px 15px rgba(160, 120, 200, 0.12); + text-align: center; + position: relative; + font-family: '楷体', 'STKaiti', '华文楷体', KaiTi, '宋体', SimSun, sans-serif; +} + +.motto-container i { + color: #d291bc; + font-size: 1rem; + opacity: 0.7; + margin: 0 8px; +} + +.motto-container i.fa-quote-left { + position: relative; + top: -5px; + left: -3px; +} + +.motto-container i.fa-quote-right { + position: relative; + bottom: -5px; + right: -3px; +} + +#random-motto { + font-size: 1.3em; + color: #7d5ba6; + font-weight: bold; + line-height: 1.5; +} + +.stats-container { + background-color: #f8e4ff; + border-radius: 12px; + padding: 12px 15px; + margin-bottom: 25px; + border: 1px dashed #d7b5f3; + box-shadow: 0 3px 10px rgba(160, 120, 200, 0.1); +} + +.stats-container p { + margin: 0; + color: #8a5fad; + font-weight: 600; + font-size: 1.05em; +} + +.stats-container i { + margin-right: 8px; + color: #d873a9; +} + +#total-bottles { + display: inline-block; + background-color: #fff; + padding: 3px 10px; + border-radius: 20px; + margin: 0 5px; + color: #aa67e5; + font-weight: bold; + min-width: 30px; +} diff --git a/mengyadriftbottle-frontend/public/logo.png b/mengyadriftbottle-frontend/public/logo.png new file mode 100644 index 0000000..9d3bd8d Binary files /dev/null and b/mengyadriftbottle-frontend/public/logo.png differ diff --git a/mengyadriftbottle-frontend/public/logo2.png b/mengyadriftbottle-frontend/public/logo2.png new file mode 100644 index 0000000..e9afb93 Binary files /dev/null and b/mengyadriftbottle-frontend/public/logo2.png differ diff --git a/mengyadriftbottle-frontend/public/logo3.png b/mengyadriftbottle-frontend/public/logo3.png new file mode 100644 index 0000000..8b909d3 Binary files /dev/null and b/mengyadriftbottle-frontend/public/logo3.png differ diff --git a/mengyadriftbottle-frontend/public/vite.svg b/mengyadriftbottle-frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/mengyadriftbottle-frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mengyadriftbottle-frontend/src/App.css b/mengyadriftbottle-frontend/src/App.css new file mode 100644 index 0000000..01f7b8a --- /dev/null +++ b/mengyadriftbottle-frontend/src/App.css @@ -0,0 +1,507 @@ +* { + box-sizing: border-box; +} + +:root { + --app-background-image: url('/background/image1.png'); +} + +body { + font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: #f7eaf3; + background-image: var(--app-background-image); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + color: #5e5166; + margin: 0; + min-height: 100vh; + position: relative; + overflow-x: hidden; +} + +.background-overlay { + position: fixed; + inset: 0; + background: rgba(255, 214, 233, 0.35); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + pointer-events: none; + z-index: -1; +} + +.page-wrapper { + min-height: 100vh; + padding: 30px 10px 60px; + position: relative; + z-index: 0; +} + +.container { + background-color: rgba(255, 255, 255, 0.7); + padding: 25px 30px; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(155, 89, 182, 0.15); + width: 90%; + max-width: 650px; + text-align: center; + margin: 0 auto; + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.5); +} + +h1 { + font-family: '楷体', 'STKaiti', '华文楷体', KaiTi, '宋体', SimSun, sans-serif; + color: #d873a9; + margin-bottom: 10px; + font-size: 2.4em; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + letter-spacing: 1px; +} + +h2 { + color: #b07cc6; + margin-top: 20px; + margin-bottom: 20px; + font-weight: 600; + font-size: 1.5em; +} + +h2 i { + margin-right: 8px; + color: #c27ba0; +} + +.tagline { + color: #9d7bb0; + margin-bottom: 25px; + font-style: italic; + font-size: 1.1em; +} + +.motto-container { + background-color: #fff; + border-radius: 12px; + padding: 18px 20px; + margin-bottom: 25px; + border-left: 5px solid #aa67e5; + box-shadow: 0 3px 15px rgba(160, 120, 200, 0.12); + text-align: center; + position: relative; + font-family: '楷体', 'STKaiti', '华文楷体', KaiTi, '宋体', SimSun, sans-serif; +} + +.motto-container i { + color: #d291bc; + font-size: 1rem; + opacity: 0.7; + margin: 0 8px; +} + +#random-motto { + font-size: 1.3em; + color: #7d5ba6; + font-weight: bold; + line-height: 1.5; +} + +.refresh-motto { + position: absolute; + top: 12px; + right: 12px; + border: none; + background: transparent; + color: #aa67e5; + cursor: pointer; + font-size: 0.85em; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.refresh-motto:hover { + opacity: 0.8; +} + +.stats-container { + background-color: #f8e4ff; + border-radius: 12px; + padding: 12px 15px; + margin-bottom: 25px; + border: 1px dashed #d7b5f3; + box-shadow: 0 3px 10px rgba(160, 120, 200, 0.1); +} + +.stats-container p { + margin: 0; + color: #8a5fad; + font-weight: 600; + font-size: 1.05em; +} + +.stats-container i { + margin-right: 8px; + color: #d873a9; +} + +#total-bottles { + display: inline-block; + background-color: #fff; + padding: 3px 10px; + border-radius: 20px; + margin: 0 5px; + color: #aa67e5; + font-weight: bold; + min-width: 30px; +} + +.action-section { + margin-bottom: 30px; + padding: 25px 20px; + background-color: #fff; + border-radius: 12px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); + position: relative; + overflow: hidden; +} + +.throw-section { + border-top: 4px solid #ffb6c1; +} + +.pickup-section { + border-top: 4px solid #c5a3ff; +} + +.action-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f9e0f0' fill-opacity='0.2' fill-rule='evenodd'%3E%3Ccircle cx='3' cy='3' r='3'/%3E%3Ccircle cx='13' cy='13' r='3'/%3E%3C/g%3E%3C/svg%3E"); + opacity: 0.5; + z-index: -1; +} + +label { + display: block; + margin-bottom: 8px; + color: #7d5ba6; + font-weight: 600; + text-align: left; + font-size: 0.95em; +} + +input[type='text'], +textarea, +select { + width: 100%; + padding: 12px 15px; + margin-bottom: 18px; + border: 1px solid #e1d1f0; + border-radius: 8px; + background-color: #fdfaff; + color: #5e4b6b; + font-size: 0.95em; + transition: all 0.3s ease; +} + +input[type='text']:focus, +textarea:focus, +select:focus { + outline: none; + border-color: #d291bc; + box-shadow: 0 0 0 3px rgba(219, 112, 194, 0.1); +} + +textarea { + resize: vertical; + min-height: 90px; +} + +::placeholder { + color: #cbb8db; + opacity: 0.7; +} + +button { + background-color: #aa67e5; + color: #fff; + padding: 12px 25px; + border: none; + border-radius: 30px; + cursor: pointer; + font-size: 1.05em; + font-weight: 600; + transition: all 0.3s ease; + box-shadow: 0 4px 8px rgba(170, 103, 229, 0.3); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.btn-throw { + background: linear-gradient(135deg, #ff8fbc 0%, #eb6dab 100%); +} + +.btn-pickup { + background: linear-gradient(135deg, #a47aed 0%, #876bd3 100%); +} + +button:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(170, 103, 229, 0.4); +} + +button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +#throw-status, +#pickup-status { + margin-top: 15px; + font-style: italic; + color: #b56ab0; + min-height: 1.5em; + font-weight: 500; +} + +#bottle-display { + margin-top: 25px; + background-color: #fff; + border-radius: 12px; + text-align: left; + box-shadow: 0 5px 15px rgba(138, 80, 201, 0.1); + border: 1px solid #f1e1fb; +} + +.bottle-header { + background: linear-gradient(135deg, #f9ddff 0%, #e9cdff 100%); + padding: 15px 20px; + display: flex; + align-items: center; + gap: 15px; +} + +#bottle-avatar { + width: 60px; + height: 60px; + border-radius: 50%; + border: 3px solid #fff; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1); +} + +.gender-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.8em; + margin-left: 8px; + background-color: #f0e6ff; + color: #7155a8; +} + +.message-content { + padding: 20px; +} + +#bottle-message { + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; + color: #51456a; + line-height: 1.6; + font-size: 1.05em; +} + +.bottle-footer { + padding: 12px 20px; + background-color: #fbf8ff; + border-top: 1px solid #f1e8ff; +} + +.bottle-info small { + display: inline-block; + margin-right: 15px; + color: #9d8aaf; + font-size: 0.85em; +} + +.bottle-reactions { + margin-top: 15px; + display: flex; + gap: 15px; + justify-content: flex-end; +} + +.reaction-btn { + background: #ffffff; + border: 1px solid #e9d8ff; + border-radius: 20px; + padding: 6px 15px; + font-size: 0.9em; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; +} + +.like-btn { + color: #5aaa9d; +} + +.dislike-btn { + color: #d76b8b; +} + +.reaction-btn:hover:not(:disabled) { + transform: translateY(-1px); +} + +.cooldown-timer { + margin-top: 15px; + background-color: #f8f0ff; + padding: 10px 15px; + border-radius: 25px; + color: #aa67e5; + font-size: 0.95em; + display: inline-flex; + gap: 6px; + align-items: center; + border: 1px dashed #d7c3f0; + animation: pulse-soft 1.5s infinite; +} + +.char-count { + font-size: 0.8em; + color: #aa67e5; + margin-left: 10px; + background-color: #f8f4ff; + padding: 3px 8px; + border-radius: 10px; +} + +.char-count-limit { + background-color: #ffebf3; + color: #e65c8f; + animation: pulse 0.5s ease; +} + +@keyframes pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} + +@keyframes pulse-soft { + 0% { + box-shadow: 0 0 0 0 rgba(170, 103, 229, 0.2); + } + 70% { + box-shadow: 0 0 0 8px rgba(170, 103, 229, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(170, 103, 229, 0); + } +} + +.appear { + animation: appear 0.5s ease-out; +} + +@keyframes appear { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.heart-icon { + color: #ff6b9c; + margin: 0 8px; + animation: heart 1.5s infinite; +} + +@keyframes heart { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + } +} + +footer { + margin-top: 40px; + color: #a07bb8; + font-size: 0.9em; +} + +footer i { + color: #ff85a2; +} + +.admin-link { + margin-top: 10px; + font-size: 0.8em; +} + +.admin-link a { + color: #a07bb8; + text-decoration: none; + opacity: 0.8; +} + +.admin-link a:hover { + opacity: 1; + text-decoration: underline; +} + +@media (max-width: 768px) { + .container { + width: 95%; + padding: 20px 18px; + } + + h1 { + font-size: 2em; + } +} + +@media (max-width: 480px) { + body { + padding: 0; + } + + .container { + width: 98%; + padding: 18px 14px; + } + + button { + width: 100%; + } + + .bottle-info small { + display: block; + margin-bottom: 5px; + } +} diff --git a/mengyadriftbottle-frontend/src/App.jsx b/mengyadriftbottle-frontend/src/App.jsx new file mode 100644 index 0000000..6dd8b87 --- /dev/null +++ b/mengyadriftbottle-frontend/src/App.jsx @@ -0,0 +1,446 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import '@fortawesome/fontawesome-free/css/all.min.css' +import './App.css' + +const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || '/api').replace(/\/$/, '') +const ADMIN_URL = import.meta.env.VITE_ADMIN_URL || 'http://localhost:5002/admin/login' +const DEFAULT_FORM = Object.freeze({ + name: '', + message: '', + gender: '保密', + qq: '', +}) + +const BACKGROUND_IMAGES = Array.from({ length: 29 }, (_, index) => `/background/image${index + 1}.png`) + +const formatCount = (value) => { + const num = typeof value === 'number' ? value : Number(value || 0) + return Number.isNaN(num) ? 0 : num +} + +function App() { + const [formData, setFormData] = useState({ ...DEFAULT_FORM }) + const [limits, setLimits] = useState({ name: 7, message: 100 }) + const [stats, setStats] = useState({ total_bottles: 0 }) + const [motto, setMotto] = useState('载入中...') + const [throwStatus, setThrowStatus] = useState('') + const [pickupStatus, setPickupStatus] = useState('') + const [currentBottle, setCurrentBottle] = useState(null) + const [cooldowns, setCooldowns] = useState({ throw: 0, pickup: 0 }) + const [loadingAction, setLoadingAction] = useState({ throw: false, pickup: false }) + const [reactionDisabled, setReactionDisabled] = useState(false) + const isThrowing = loadingAction.throw + const isPicking = loadingAction.pickup + + const randomBackground = useMemo(() => { + if (!BACKGROUND_IMAGES.length) { + return '' + } + const index = Math.floor(Math.random() * BACKGROUND_IMAGES.length) + return BACKGROUND_IMAGES[index] + }, []) + + useEffect(() => { + if (randomBackground) { + document.documentElement.style.setProperty('--app-background-image', `url(${randomBackground})`) + } + }, [randomBackground]) + + const startCooldown = useCallback((type, seconds = 5) => { + const duration = Math.max(1, Math.ceil(seconds)) + setCooldowns((prev) => ({ ...prev, [type]: duration })) + }, []) + + useEffect(() => { + const timer = setInterval(() => { + setCooldowns((prev) => { + const next = { + throw: prev.throw > 0 ? prev.throw - 1 : 0, + pickup: prev.pickup > 0 ? prev.pickup - 1 : 0, + } + if (next.throw === prev.throw && next.pickup === prev.pickup) { + return prev + } + return next + }) + }, 1000) + return () => clearInterval(timer) + }, []) + + const fetchConfig = useCallback(async () => { + try { + const response = await fetch(`${API_BASE_URL}/config`) + const data = await response.json() + if (data.success && data.config) { + setLimits({ + name: data.config.name_limit ?? 7, + message: data.config.message_limit ?? 100, + }) + } + } catch (error) { + console.error('Failed to fetch config', error) + } + }, []) + + const fetchStats = useCallback(async () => { + try { + const response = await fetch(`${API_BASE_URL}/stats`) + const data = await response.json() + if (data.success && data.stats) { + setStats(data.stats) + } + } catch (error) { + console.error('Failed to fetch stats', error) + } + }, []) + + const fetchMotto = useCallback(async () => { + try { + const response = await fetch(`${API_BASE_URL}/motto`) + const data = await response.json() + if (data.success) { + setMotto(data.motto) + } + } catch (error) { + console.error('Failed to fetch motto', error) + setMotto('良言一句三冬暖,恶语伤人六月寒') + } + }, []) + + useEffect(() => { + fetchConfig() + fetchStats() + fetchMotto() + }, [fetchConfig, fetchStats, fetchMotto]) + + const handleInputChange = useCallback((event) => { + const { name, value } = event.target + if (name === 'qq' && value && /[^0-9]/.test(value)) { + return + } + setFormData((prev) => ({ ...prev, [name]: value })) + }, []) + + const resetForm = useCallback(() => { + setFormData({ ...DEFAULT_FORM }) + }, []) + + const handleThrowSubmit = useCallback( + async (event) => { + event.preventDefault() + if (cooldowns.throw > 0 || isThrowing) { + return + } + + setLoadingAction((prev) => ({ ...prev, throw: true })) + setThrowStatus('正在扔瓶子...') + + try { + const response = await fetch(`${API_BASE_URL}/throw`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: formData.name, + message: formData.message, + gender: formData.gender, + qq: formData.qq, + }), + }) + + const data = await response.json() + if (response.ok && data.success) { + setThrowStatus('瓶子已成功扔出!祝你好运~') + resetForm() + fetchStats() + startCooldown('throw', 5) + } else { + const waitTime = data.wait_time ?? 0 + setThrowStatus(`出错了: ${data.error || data.message || '未知错误'}`) + if (waitTime) { + startCooldown('throw', waitTime) + } + } + } catch (error) { + console.error('Throw request failed', error) + setThrowStatus('请求失败,请检查网络连接。') + } finally { + setLoadingAction((prev) => ({ ...prev, throw: false })) + } + }, + [cooldowns.throw, isThrowing, formData, fetchStats, resetForm, startCooldown], + ) + + const handlePickup = useCallback(async () => { + if (cooldowns.pickup > 0 || isPicking) { + return + } + setLoadingAction((prev) => ({ ...prev, pickup: true })) + setPickupStatus('正在打捞瓶子...') + + try { + const response = await fetch(`${API_BASE_URL}/pickup`) + const data = await response.json() + if (response.ok && data.success && data.bottle) { + setCurrentBottle(data.bottle) + setReactionDisabled(false) + setPickupStatus('捡到了一个瓶子!缘分来了~') + startCooldown('pickup', 5) + } else { + setCurrentBottle(null) + const waitTime = data.wait_time ?? 0 + setPickupStatus(data.message || data.error || '海里没有瓶子了,或者出错了。') + if (waitTime) { + startCooldown('pickup', waitTime) + } + } + } catch (error) { + console.error('Pickup request failed', error) + setPickupStatus('请求失败,请检查网络连接。') + setCurrentBottle(null) + } finally { + setLoadingAction((prev) => ({ ...prev, pickup: false })) + } + }, [cooldowns.pickup, isPicking, startCooldown]) + + const handleReaction = useCallback( + async (reaction) => { + if (!currentBottle) { + return + } + setReactionDisabled(true) + try { + const response = await fetch(`${API_BASE_URL}/react`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ bottle_id: currentBottle.id, reaction }), + }) + const data = await response.json() + if (response.ok && data.success) { + setCurrentBottle((prev) => + prev + ? { + ...prev, + likes: reaction === 'like' ? formatCount(prev.likes) + 1 : prev.likes, + dislikes: reaction === 'dislike' ? formatCount(prev.dislikes) + 1 : prev.dislikes, + } + : prev, + ) + setPickupStatus( + reaction === 'like' ? '感谢您的点赞!' : '已记录您的反馈,该瓶子被捡起的概率将会降低。', + ) + } else { + setPickupStatus(data.error || '记录反馈时出错。') + setReactionDisabled(false) + } + } catch (error) { + console.error('Reaction request failed', error) + setPickupStatus('请求失败,请稍后再试。') + setReactionDisabled(false) + } + }, + [currentBottle], + ) + + const nameCharCount = useMemo(() => formData.name.length, [formData.name]) + const messageCharCount = useMemo(() => formData.message.length, [formData.message]) + + return ( +
+