commit 569c10342f35bde0732b7e153db55949b7d32454 Author: Tien Tran Date: Mon Sep 28 00:28:09 2026 -0700 feat: initial commit - EZBio Next.js 16 fullstack URL shortener & bio link diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..97cd6d2 --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +# ========================================== +# EZBio Environment Configuration +# ========================================== + +# Database Connection (PostgreSQL - Neon, Supabase, Vercel Postgres, or Local) +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ezbio?schema=public" + +# App & Domain Settings +NEXT_PUBLIC_APP_URL="http://localhost:3000" +APP_DOMAIN="localhost:3000" + +# Authentication Secret (Generate with: openssl rand -base64 32) +AUTH_SECRET="your-super-secret-random-key-at-least-32-chars-long" + +# ========================================== +# Cloudflare R2 Storage (S3 Compatible) +# ========================================== +# Free Tier: 10GB storage, 1M Class A, 10M Class B operations, 0$ egress +CLOUDFLARE_R2_ACCOUNT_ID="" +CLOUDFLARE_R2_ACCESS_KEY_ID="" +CLOUDFLARE_R2_SECRET_ACCESS_KEY="" +CLOUDFLARE_R2_BUCKET_NAME="" +CLOUDFLARE_R2_PUBLIC_URL="" + +# Warning threshold in MB (10GB = 10240 MB) +CLOUDFLARE_R2_FREE_LIMIT_MB="10240" +CLOUDFLARE_R2_WARNING_PERCENT="80" + +# Fallback: Set to "local" to store files in public/uploads when R2 is not configured +STORAGE_DRIVER="local" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c12789 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +**/.DS_Store +._* +**/._* +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# uploads +/public/uploads +/oldversion_php + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +/src/generated/prisma diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a27938a --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# EZBio — Modern URL Shortener & Link in Bio Platform + +Hệ thống rút gọn liên kết thông minh kết hợp nền tảng Bio Link (Link in Bio) được viết lại toàn diện từ phiên bản PHP sang **Next.js 16 (App Router)**, **TypeScript**, **Tailwind CSS v4**, **Prisma ORM**, hỗ trợ lưu trữ **Cloudflare R2** và tối ưu hóa triển khai trên **Vercel**. + +--- + +## 🌟 Các Tính Năng Nổi Bật + +### 1. Phân hệ Rút gọn Liên kết (URL Shortener Core) +- **Tự động sinh mã ngắn linh hoạt:** Cho phép Admin cấu hình độ dài chuỗi ngẫu nhiên (`default_url_length` từ 3 - 16 ký tự, mặc định 5 hoặc 6). +- **Tùy chọn nâng cao:** Đặt bí danh tùy chỉnh (Custom Short Code), bảo vệ link bằng mật khẩu băm bcrypt, hẹn giờ hết hạn link (`expires_at`), gắn thẻ phân loại (Tags). +- **QR Code tự động:** Tạo mã QR chất lượng cao (PNG & SVG) cho mọi liên kết với tùy chọn màu sắc. +- **Tự động lấy tiêu đề trang:** Cào tự động thẻ `` và OpenGraph metadata của URL đích. +- **Cơ chế chuyển hướng & Tracking:** Thống kê lượt click, thiết bị (Desktop/Mobile/Tablet), hệ điều hành (OS), trình duyệt, vị trí địa lý (GeoIP) và nguồn giới thiệu (Referrer). + +### 2. Phân hệ Bio Link & Landing Page Cá Nhân +- **Đường dẫn cá nhân hóa:** Truy cập qua `/portal/[username]` hoặc Subdomain. +- **Trình soạn thảo trực quan (Live Phone Preview):** Giao diện chỉnh sửa chia 2 cột với khung điện thoại thông minh mô phỏng cập nhật theo thời gian thực. +- **Danh thiếp điện tử (vCard):** Tự động sinh file `.vcf` danh bạ để khách lưu vào điện thoại với 1 chạm. +- **Liên kết Mạng xã hội:** Hỗ trợ Facebook, Twitter/X, Instagram, Threads, TikTok, YouTube, Telegram (mỗi nút đều có tính năng xem mã QR riêng). +- **Bộ hiệu ứng màu sắc:** 6 bộ Gradient cho tiêu đề, 6 bộ Gradient cho hình nền hoặc tải ảnh nền tùy chỉnh. + +### 3. Phân hệ Quản trị & Giám sát Lưu trữ Cloudflare R2 +- **Admin Dashboard:** Biểu đồ tăng trưởng, thống kê thiết bị, top 10 quốc gia. +- **Cloudflare R2 Storage Monitor (Cảnh báo hạn mức Free):** + - Giám sát trực quan dung lượng đã dùng trên tổng 10 GB miễn phí. + - Tự động hiển thị cảnh báo vàng khi đạt 80% (8 GB) và cảnh báo đỏ khẩn cấp khi đạt 90% (9 GB). + - Hỗ trợ cơ chế **Local Storage Fallback** khi chưa cấu hình R2 keys. +- **Quản lý Người dùng & URLs:** Phân quyền Admin/User, khóa/mở khóa tài khoản, thao tác hàng loạt (Bulk Actions). +- **Cấu hình Hệ thống:** Cài đặt SEO Meta, SMTP Email, Logo, Favicon và độ dài mã ngắn mặc định. + +--- + +## 🚀 Hướng Dẫn Cài Đặt & Chạy Cục Bộ (Local Dev) + +### 1. Cấu hình Biến Môi Trường (`.env`) +Mở file `.env` và điền chuỗi kết nối PostgreSQL của bạn: + +```env +DATABASE_URL="postgresql://username:password@your-db-host:5432/ezbio?sslmode=require" +AUTH_SECRET="tao-mot-chuoi-bi-mat-ngau-nhien-it-nhat-32-ky-tu" +NEXT_PUBLIC_APP_URL="http://localhost:3000" +``` + +*(Nếu chưa cấu hình Cloudflare R2, hệ thống sẽ tự động dùng Local Storage lưu tại `public/uploads`).* + +### 2. Đồng bộ CSDL & Tạo Dữ Liệu Khởi Tạo (Seed) +Chạy lệnh sau để tạo các bảng trong PostgreSQL: + +```bash +# Đẩy schema lên database +npx prisma db push + +# Tạo tài khoản Admin, Demo và dữ liệu cấu hình mẫu +npm run seed +``` + +> **Tài khoản mặc định sau khi seed:** +> - **Admin:** `admin@example.com` / `password123` +> - **Demo User:** `demo@example.com` / `password123` + +### 3. Khởi chạy máy chủ phát triển +```bash +npm run dev +``` +Truy cập: `http://localhost:3000` + +--- + +## ☁️ Cấu hình Cloudflare R2 Storage (Khi triển khai) + +Thêm các khóa sau vào `.env` hoặc phần **Environment Variables** trên Vercel: + +```env +STORAGE_DRIVER="cloudflare_r2" +CLOUDFLARE_R2_ACCOUNT_ID="your-account-id" +CLOUDFLARE_R2_ACCESS_KEY_ID="your-access-key-id" +CLOUDFLARE_R2_SECRET_ACCESS_KEY="your-secret-access-key" +CLOUDFLARE_R2_BUCKET_NAME="ezbio-bucket" +CLOUDFLARE_R2_PUBLIC_URL="https://pub-your-bucket.r2.dev" +CLOUDFLARE_R2_FREE_LIMIT_MB="10240" +CLOUDFLARE_R2_WARNING_PERCENT="80" +``` + +--- + +## 🚢 Triển khai lên Vercel & GitHub + +1. Khởi tạo Git và đẩy mã nguồn lên GitHub: + ```bash + git init + git add . + git commit -m "feat: complete EZBio Next.js rewrite" + git branch -M main + git remote add origin https://github.com/your-username/ezbio.git + git push -u origin main + ``` +2. Đăng nhập vào [Vercel](https://vercel.com), chọn **Add New Project** và liên kết với kho lưu trữ GitHub của bạn. +3. Trong mục **Environment Variables** trên Vercel, dán các biến từ file `.env`. +4. Nhấn **Deploy**. Vercel sẽ tự động build và cấp phát tên miền trực tuyến cho bạn! diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e1a852e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8859 @@ +{ + "name": "ezbio", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ezbio", + "version": "0.1.0", + "dependencies": { + "@aws-sdk/client-s3": "^3.1141.0", + "@prisma/client": "^6.19.3", + "bcryptjs": "^3.0.3", + "cheerio": "^1.2.0", + "clsx": "^2.1.1", + "dotenv": "^18.0.4", + "jose": "^6.2.12", + "lucide-react": "^1.48.0", + "next": "16.3.6", + "qrcode": "^1.5.4", + "react": "19.2.8", + "react-dom": "19.2.8", + "tailwind-merge": "^3.7.0", + "ua-parser-js": "^2.0.10" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/node": "^20", + "@types/qrcode": "^1.5.6", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/ua-parser-js": "^0.7.39", + "eslint": "^9", + "eslint-config-next": "16.3.6", + "prisma": "^6.19.3", + "tailwindcss": "^4", + "tsx": "^4.23.15", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@aws-sdk/checksums": { + "version": "3.1001.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.1.tgz", + "integrity": "sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1141.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1141.0.tgz", + "integrity": "sha512-uOVH37xGLenAdJkCPCin/JJG2PgWrFcSsDnQ9+C9Zq8N9Oalo5ol4xmn5fG28iWAlA/b/9boQZgHbMh+UsIhcg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/checksums": "^3.1001.1", + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/credential-provider-node": "^3.972.84", + "@aws-sdk/middleware-sdk-s3": "^3.972.77", + "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.978.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz", + "integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@aws-sdk/xml-builder": "^3.972.41", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.35.0", + "@smithy/signature-v4": "^5.7.3", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz", + "integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz", + "integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz", + "integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/credential-provider-env": "^3.972.72", + "@aws-sdk/credential-provider-http": "^3.972.74", + "@aws-sdk/credential-provider-login": "^3.972.79", + "@aws-sdk/credential-provider-process": "^3.972.72", + "@aws-sdk/credential-provider-sso": "^3.973.16", + "@aws-sdk/credential-provider-web-identity": "^3.972.78", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.79", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz", + "integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.84", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz", + "integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.72", + "@aws-sdk/credential-provider-http": "^3.972.74", + "@aws-sdk/credential-provider-ini": "^3.973.17", + "@aws-sdk/credential-provider-process": "^3.972.72", + "@aws-sdk/credential-provider-sso": "^3.973.16", + "@aws-sdk/credential-provider-web-identity": "^3.972.78", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz", + "integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz", + "integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/token-providers": "3.1138.0", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.78", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz", + "integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.77", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.77.tgz", + "integrity": "sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz", + "integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.47", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz", + "integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@smithy/signature-v4": "^5.7.3", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1138.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz", + "integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz", + "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@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.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "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.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "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.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "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/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "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/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.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/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@next/env": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.6.tgz", + "integrity": "sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.6.tgz", + "integrity": "sha512-jowwDX+7DOlDIjJLgTMxudw+k37QnWu1JkZLkSi9MaJBfDYcfhAPMKBhXL0idYzFN/AGg//axnOR4cLkHX/Rng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "4.9.1", + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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/@next/eslint-plugin-next/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/@next/swc-darwin-arm64": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.6.tgz", + "integrity": "sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.6.tgz", + "integrity": "sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.6.tgz", + "integrity": "sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.6.tgz", + "integrity": "sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.6.tgz", + "integrity": "sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.6.tgz", + "integrity": "sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.6.tgz", + "integrity": "sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.6.tgz", + "integrity": "sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@smithy/core": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.0.tgz", + "integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/ua-parser-js": { + "version": "0.7.39", + "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz", + "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz", + "integrity": "sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/type-utils": "8.70.1", + "@typescript-eslint/utils": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", + "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.1.tgz", + "integrity": "sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.1.tgz", + "integrity": "sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.1", + "@typescript-eslint/types": "^8.70.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.1.tgz", + "integrity": "sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.1.tgz", + "integrity": "sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.1.tgz", + "integrity": "sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/utils": "8.70.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.1.tgz", + "integrity": "sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.1.tgz", + "integrity": "sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.1", + "@typescript-eslint/tsconfig-utils": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.1.tgz", + "integrity": "sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.1.tgz", + "integrity": "sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "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.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "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/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.2.0.tgz", + "integrity": "sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "es-shim-unscopables": "^1.1.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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.11.26", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", + "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.1.tgz", + "integrity": "sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.25", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.438", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001812", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001812.tgz", + "integrity": "sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng==", + "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/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "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==", + "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/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-europe-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", + "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.4.tgz", + "integrity": "sha512-jGt9uI5BIxpVO15kvy+iEFqNAwxYClKWnkptLjSNPnFLC003Imd0ofr3aEVsbpHdSBdSsbcApNIlz4nbyGMwdA==", + "license": "BSD-2-Clause", + "bin": { + "dotenv": "dist/index.cjs" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.439", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.439.tgz", + "integrity": "sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@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.14.0", + "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.5", + "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-config-next": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.6.tgz", + "integrity": "sha512-1Upt3U7BDwU+ilpe2byZjAfts9oNq4d4fv/zXEvs8/4yS+cwOQW/WCxUNy8gCDquX67SzeehDvKblVC6ZBMocQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.3.6", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "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 || ^10.0.0" + } + }, + "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.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "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/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.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-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/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/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "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": "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/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-standalone-pwa": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", + "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "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/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "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/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.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/lucide-react": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.48.0.tgz", + "integrity": "sha512-R0CIKY/fXiC6y9xRBADgsK+VW2p/pcTJOMhLZf1T+uG+vJUvyUR02nGOgmIIC7MPkiNK4Ox8QDnbqF8rJYWPZQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "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/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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/next": { + "version": "16.3.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.6.tgz", + "integrity": "sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.3.6", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.6", + "@next/swc-darwin-x64": "16.3.6", + "@next/swc-linux-arm64-gnu": "16.3.6", + "@next/swc-linux-arm64-musl": "16.3.6", + "@next/swc-linux-x64-gnu": "16.3.6", + "@next/swc-linux-x64-musl": "16.3.6", + "@next/swc-win32-arm64-msvc": "16.3.6", + "@next/swc-win32-x64-msvc": "16.3.6", + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nypm": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.10.tgz", + "integrity": "sha512-W72Hrj1petq+b3Hk2aAC+9zswetlIFTnDW4s5djseZh2nYqBbyQLOtj472HwcbcWykPBUW1WpWpjOd4nK6gMRw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.3.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "devOptional": 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/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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==", + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz", + "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.3.1", + "exsolve": "^1.1.1", + "pathe": "^2.0.3" + } + }, + "node_modules/pkg-types/node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "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.18", + "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/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "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/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", + "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "get-intrinsic": "^1.3.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.4", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "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/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.5.tgz", + "integrity": "sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.1.tgz", + "integrity": "sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.1", + "@typescript-eslint/parser": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/utils": "8.70.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ua-is-frozen": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", + "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/ua-parser-js": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.10.tgz", + "integrity": "sha512-t+3Ktbq0Ies2vaSezfOaWiolH4OigQIO1dk+1xDpOydB1COVPocVYOrEV5rqZ0kFY9XYG1v9LutCyMgYBpABcw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "AGPL-3.0-or-later", + "dependencies": { + "detect-europe-js": "^0.1.2", + "is-standalone-pwa": "^0.1.1", + "ua-is-frozen": "^0.1.2" + }, + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "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/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "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/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "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.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "dev": true, + "license": "MIT", + "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/package.json b/package.json new file mode 100644 index 0000000..7397ecc --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "ezbio", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "prisma generate && next build", + "start": "next start", + "lint": "eslint", + "postinstall": "prisma generate", + "seed": "tsx prisma/seed.ts" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.1141.0", + "@prisma/client": "^6.19.3", + "bcryptjs": "^3.0.3", + "cheerio": "^1.2.0", + "clsx": "^2.1.1", + "dotenv": "^18.0.4", + "jose": "^6.2.12", + "lucide-react": "^1.48.0", + "next": "16.3.6", + "qrcode": "^1.5.4", + "react": "19.2.8", + "react-dom": "19.2.8", + "tailwind-merge": "^3.7.0", + "ua-parser-js": "^2.0.10" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/node": "^20", + "@types/qrcode": "^1.5.6", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/ua-parser-js": "^0.7.39", + "eslint": "^9", + "eslint-config-next": "16.3.6", + "prisma": "^6.19.3", + "tailwindcss": "^4", + "tsx": "^4.23.15", + "typescript": "^5" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..8ded1a5 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,16 @@ + +// This file was generated by Prisma and assumes you have installed the following: +// npm install --save-dev prisma dotenv +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + engine: "classic", + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..0c5ae24 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,127 @@ +// Prisma Schema for EZBio - Modern URL Shortener & Link in Bio + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(cuid()) + name String + email String @unique + password String + role String @default("user") // "admin" | "user" + username String @unique + isActive Boolean @default(true) + emailVerified DateTime? + avatar String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + shortUrls ShortUrl[] + portal Portal? + uploadedFiles UploadedFile[] + + @@map("users") +} + +model ShortUrl { + id String @id @default(cuid()) + originalUrl String + shortCode String @unique + title String? + password String? // bcrypt hash if password protected + expiresAt DateTime? + displayOnPortal Boolean @default(false) + clicksCount Int @default(0) + userId String? + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tags Tag[] @relation("ShortUrlTags") + clicks UrlClick[] + + @@index([userId]) + @@index([shortCode]) + @@map("short_urls") +} + +model Tag { + id String @id @default(cuid()) + name String @unique + shortUrls ShortUrl[] @relation("ShortUrlTags") + createdAt DateTime @default(now()) + + @@map("tags") +} + +model UrlClick { + id String @id @default(cuid()) + shortUrlId String + shortUrl ShortUrl @relation(fields: [shortUrlId], references: [id], onDelete: Cascade) + ipAddress String? + userAgent String? + deviceType String? // "desktop" | "mobile" | "tablet" + operatingSystem String? + browser String? + countryName String? + countryCode String? + city String? + referrer String? + createdAt DateTime @default(now()) + + @@index([shortUrlId]) + @@index([createdAt]) + @@map("url_clicks") +} + +model Portal { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + title String? + description String? + avatar String? + backgroundImage String? + headerColor String? @default("#000000") + titleColorStyle String? @default("gradient-1") + backgroundStyle String? @default("gradient-1") + vcard Json? // { fullName, phone, email, company, website } + socialLinks Json? // { facebook, twitter, instagram, threads, tiktok, youtube, telegram } + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("portals") +} + +model Setting { + id String @id @default(cuid()) + key String @unique + value String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("settings") +} + +model UploadedFile { + id String @id @default(cuid()) + key String @unique // Storage key or local filename + url String // Public URL or relative path + originalName String + mimeType String + sizeBytes BigInt // File size in bytes + category String // "avatar", "background", "logo", "favicon", "og_image" + storageType String @default("cloudflare_r2") // "cloudflare_r2" | "local" + userId String? + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([category]) + @@map("uploaded_files") +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..f7ce2dd --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,166 @@ +import { PrismaClient } from "@prisma/client"; +import bcrypt from "bcryptjs"; + +const prisma = new PrismaClient(); + +async function main() { + console.log("Seeding database..."); + + // 1. Initial System Settings + const initialSettings = [ + { key: "default_url_length", value: "5" }, + { key: "site_name", value: "EZBio - URL Shortener & Link in Bio" }, + { key: "allow_registration", value: "1" }, + { key: "header_color", value: "#4F46E5" }, + { key: "background_color", value: "#F9FAFB" }, + { key: "seo_title", value: "EZBio - Fast URL Shortener & Dynamic Bio Link" }, + { key: "seo_description", value: "Rút gọn link thông minh, tạo mã QR và trang Bio Link đỉnh cao." }, + { key: "seo_keywords", value: "url shortener, link in bio, bio link, qr code generator" }, + { key: "r2_storage_limit_mb", value: "10240" }, + { key: "r2_warning_threshold", value: "80" }, + ]; + + for (const s of initialSettings) { + await prisma.setting.upsert({ + where: { key: s.key }, + update: { value: s.value }, + create: { key: s.key, value: s.value }, + }); + } + + // 2. Admin User + const adminPassword = await bcrypt.hash("password123", 10); + const admin = await prisma.user.upsert({ + where: { email: "admin@example.com" }, + update: {}, + create: { + name: "Admin User", + email: "admin@example.com", + username: "admin", + password: adminPassword, + role: "admin", + isActive: true, + portal: { + create: { + title: "Admin Official Bio", + description: "Quản trị viên hệ thống EZBio Platform", + headerColor: "#000000", + titleColorStyle: "gradient-1", + backgroundStyle: "gradient-2", + vcard: { + fullName: "EZBio Administrator", + email: "admin@example.com", + company: "EZBio Corp", + website: "https://ezbio.io", + }, + socialLinks: { + facebook: "https://facebook.com", + twitter: "https://x.com", + youtube: "https://youtube.com", + telegram: "https://t.me", + }, + }, + }, + }, + }); + + // 3. Demo User + const demoPassword = await bcrypt.hash("password123", 10); + const demo = await prisma.user.upsert({ + where: { email: "demo@example.com" }, + update: {}, + create: { + name: "Demo Creator", + email: "demo@example.com", + username: "demo", + password: demoPassword, + role: "user", + isActive: true, + portal: { + create: { + title: "Trần Tiến • Content Creator", + description: "Chia sẻ kiến thức công nghệ, lập trình Fullstack & AI Agent", + headerColor: "#000000", + titleColorStyle: "gradient-5", + backgroundStyle: "gradient-5", + vcard: { + fullName: "Trần Tiến", + phone: "+84 901 234 567", + email: "demo@example.com", + company: "Tech Creator", + website: "https://github.com", + }, + socialLinks: { + facebook: "https://facebook.com", + twitter: "https://x.com", + instagram: "https://instagram.com", + tiktok: "https://tiktok.com", + youtube: "https://youtube.com", + telegram: "https://t.me", + }, + }, + }, + }, + }); + + // 4. Sample Links + const sampleLinks = [ + { + originalUrl: "https://github.com", + shortCode: "github", + title: "GitHub - Build and ship software", + displayOnPortal: true, + userId: demo.id, + tags: ["tech", "coding"], + }, + { + originalUrl: "https://nextjs.org", + shortCode: "nextjs", + title: "Next.js by Vercel - The React Framework for the Web", + displayOnPortal: true, + userId: demo.id, + tags: ["framework", "react"], + }, + { + originalUrl: "https://vercel.com", + shortCode: "vercel", + title: "Vercel - Build and deploy web applications", + displayOnPortal: true, + userId: demo.id, + tags: ["cloud", "deploy"], + }, + ]; + + for (const link of sampleLinks) { + await prisma.shortUrl.upsert({ + where: { shortCode: link.shortCode }, + update: {}, + create: { + originalUrl: link.originalUrl, + shortCode: link.shortCode, + title: link.title, + displayOnPortal: link.displayOnPortal, + userId: link.userId, + tags: { + connectOrCreate: link.tags.map((name) => ({ + where: { name }, + create: { name }, + })), + }, + }, + }); + } + + console.log("Database seeded successfully!"); + console.log("Admin account: admin@example.com / password123"); + console.log("Demo account: demo@example.com / password123"); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/public/file.svg b/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/public/file.svg @@ -0,0 +1 @@ +<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg> \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/public/globe.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg> \ No newline at end of file diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg> \ No newline at end of file diff --git a/public/window.svg b/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/public/window.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg> \ No newline at end of file diff --git a/src/app/[shortCode]/page.tsx b/src/app/[shortCode]/page.tsx new file mode 100644 index 0000000..a9636e0 --- /dev/null +++ b/src/app/[shortCode]/page.tsx @@ -0,0 +1,78 @@ +import { notFound, redirect } from "next/navigation"; +import { headers, cookies } from "next/headers"; +import { prisma } from "@/lib/prisma"; +import { recordUrlClick } from "@/lib/analytics"; +import PasswordPrompt from "@/components/PasswordPrompt"; + +interface PageProps { + params: Promise<{ shortCode: string }>; +} + +export default async function ShortUrlRedirectPage({ params }: PageProps) { + const { shortCode } = await params; + + // 1. Find short url + const url = await prisma.shortUrl.findUnique({ + where: { shortCode }, + }); + + // If not found, check if it's a bio link portal username + if (!url) { + const user = await prisma.user.findUnique({ + where: { username: shortCode }, + include: { portal: true }, + }); + if (user && user.portal) { + redirect(`/portal/${user.username}`); + } + return notFound(); + } + + // 2. Check if expired + if (url.expiresAt && new Date() > new Date(url.expiresAt)) { + return ( + <div className="min-h-screen flex items-center justify-center bg-slate-900 text-white p-4"> + <div className="max-w-md w-full bg-white/10 backdrop-blur-md border border-white/20 p-8 rounded-3xl text-center shadow-xl"> + <div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-amber-500/20 border border-amber-400/30 flex items-center justify-center text-amber-400"> + <span className="text-2xl font-bold">410</span> + </div> + <h1 className="text-2xl font-bold mb-2">Liên kết đã hết hạn</h1> + <p className="text-slate-300 text-sm"> + Đường dẫn ngắn này đã quá thời gian hiệu lực và không còn khả dụng. + </p> + </div> + </div> + ); + } + + // 3. Check password protection + if (url.password) { + const cookieStore = await cookies(); + const isVerified = cookieStore.get(`verified_${shortCode}`)?.value === "true"; + + if (!isVerified) { + return <PasswordPrompt shortCode={shortCode} />; + } + } + + // 4. Record analytics + const headersList = await headers(); + const userAgent = headersList.get("user-agent") || ""; + const referrer = headersList.get("referer") || ""; + const ipAddress = + headersList.get("x-forwarded-for")?.split(",")[0]?.trim() || + headersList.get("x-real-ip") || + ""; + + // Async record click without blocking + await recordUrlClick({ + shortUrlId: url.id, + ipAddress, + userAgent, + referrer, + headers: headersList, + }); + + // 5. Redirect to destination + redirect(url.originalUrl); +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..359c1c3 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,285 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { + Users, + Link2, + MousePointerClick, + HardDrive, + AlertTriangle, + CheckCircle2, + Smartphone, + Monitor, + Tablet, + Globe, + Settings, +} from "lucide-react"; + +export default function AdminDashboardPage() { + const [stats, setStats] = useState<any>(null); + const [storageStatus, setStorageStatus] = useState<any>(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function loadAdminData() { + try { + const [statsRes, storageRes] = await Promise.all([ + fetch("/api/admin/stats"), + fetch("/api/admin/storage-status"), + ]); + const statsData = await statsRes.json(); + const storageData = await storageRes.json(); + + if (statsData.stats) setStats(statsData.stats); + if (storageData.status) setStorageStatus(storageData.status); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + } + loadAdminData(); + }, []); + + if (loading) { + return ( + <div className="flex items-center justify-center min-h-[60vh]"> + <div className="text-slate-400 animate-pulse text-sm">Đang tải dữ liệu quản trị...</div> + </div> + ); + } + + const isStorageWarning = storageStatus?.isWarning; + const isStorageCritical = storageStatus?.isCritical; + + return ( + <div className="max-w-7xl mx-auto px-4 py-8 space-y-8"> + {/* Header */} + <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> + <div> + <h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white flex items-center gap-2"> + <Settings className="w-6 h-6 text-indigo-500" /> + Tổng quan Quản trị Hệ thống + </h1> + <p className="text-sm text-slate-500 dark:text-slate-400 mt-1"> + Theo dõi người dùng, lưu lượng truy cập toàn sàn và giám sát dung lượng Cloudflare R2 + </p> + </div> + + <div className="flex gap-2"> + <Link + href="/admin/users" + className="px-4 py-2 rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 text-xs font-semibold text-slate-700 dark:text-slate-200 hover:bg-slate-50 transition-all shadow-sm" + > + Quản lý Users + </Link> + <Link + href="/admin/settings" + className="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-semibold shadow-md transition-all" + > + Cài đặt Hệ thống + </Link> + </div> + </div> + + {/* CLOUDFLARE R2 STORAGE MONITOR WIDGET (CRITICAL USER REQUIREMENT) */} + <div + className={`p-6 rounded-3xl border transition-all ${ + isStorageCritical + ? "bg-red-500/10 border-red-500/40 text-red-900 dark:text-red-200" + : isStorageWarning + ? "bg-amber-500/10 border-amber-500/40 text-amber-900 dark:text-amber-200" + : "bg-white dark:bg-slate-900 border-slate-200/80 dark:border-slate-800 shadow-sm" + }`} + > + <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4"> + <div className="flex items-center gap-3"> + <div + className={`w-12 h-12 rounded-2xl flex items-center justify-center ${ + isStorageCritical + ? "bg-red-500/20 text-red-600" + : isStorageWarning + ? "bg-amber-500/20 text-amber-600" + : "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400" + }`} + > + <HardDrive className="w-6 h-6" /> + </div> + <div> + <div className="flex items-center gap-2"> + <h3 className="font-bold text-slate-900 dark:text-white text-base"> + Giám sát Lưu trữ Cloudflare R2 + </h3> + <span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300"> + Driver: {storageStatus?.storageDriver} + </span> + </div> + <p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5"> + Hạn mức miễn phí: 10 GB lưu trữ / tháng. Tự động cảnh báo khi sắp đầy. + </p> + </div> + </div> + + <div className="text-right"> + <div className="text-2xl font-black text-slate-900 dark:text-white"> + {storageStatus?.usedMB || 0} MB{" "} + <span className="text-xs font-normal text-slate-400"> + / {storageStatus?.limitGB || 10} GB + </span> + </div> + <div className="text-xs font-semibold text-slate-500 dark:text-slate-400"> + {storageStatus?.percentUsed || 0}% hạn mức + </div> + </div> + </div> + + {/* Progress Bar */} + <div className="w-full h-3 rounded-full bg-slate-100 dark:bg-slate-800 overflow-hidden mb-3"> + <div + className={`h-full transition-all duration-500 rounded-full ${ + isStorageCritical + ? "bg-red-500" + : isStorageWarning + ? "bg-amber-500" + : "bg-gradient-to-r from-indigo-500 to-emerald-500" + }`} + style={{ width: `${Math.min(storageStatus?.percentUsed || 0, 100)}%` }} + /> + </div> + + {/* Warning Alert if >= 80% */} + {isStorageCritical ? ( + <div className="flex items-center gap-2 text-xs font-semibold text-red-600 dark:text-red-400"> + <AlertTriangle className="w-4 h-4 shrink-0" /> + <span> + CẢNH BÁO KHẨN CẤP: Dung lượng lưu trữ Cloudflare R2 đã vượt 90%! Hãy dọn dẹp các tệp cũ hoặc nâng cấp gói để tránh gián đoạn dịch vụ tải ảnh. + </span> + </div> + ) : isStorageWarning ? ( + <div className="flex items-center gap-2 text-xs font-semibold text-amber-600 dark:text-amber-400"> + <AlertTriangle className="w-4 h-4 shrink-0" /> + <span> + Lưu ý: Dung lượng lưu trữ đã đạt hơn 80% hạn mức 10GB miễn phí của Cloudflare R2. + </span> + </div> + ) : ( + <div className="flex items-center gap-1.5 text-xs text-emerald-600 dark:text-emerald-400"> + <CheckCircle2 className="w-3.5 h-3.5" /> + <span>Trạng thái lưu trữ an toàn ({storageStatus?.totalFiles || 0} tệp đang lưu trữ).</span> + </div> + )} + </div> + + {/* Core KPI Metrics */} + <div className="grid grid-cols-1 sm:grid-cols-3 gap-6"> + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm flex items-center gap-4"> + <div className="w-14 h-14 rounded-2xl bg-blue-500/10 text-blue-600 flex items-center justify-center shrink-0"> + <Users className="w-7 h-7" /> + </div> + <div> + <div className="text-xs font-semibold uppercase tracking-wider text-slate-400"> + Tổng số Người dùng + </div> + <div className="text-3xl font-extrabold text-slate-900 dark:text-white mt-1"> + {stats?.totalUsers || 0} + </div> + </div> + </div> + + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm flex items-center gap-4"> + <div className="w-14 h-14 rounded-2xl bg-indigo-500/10 text-indigo-600 flex items-center justify-center shrink-0"> + <Link2 className="w-7 h-7" /> + </div> + <div> + <div className="text-xs font-semibold uppercase tracking-wider text-slate-400"> + Tổng số URLs Toàn sàn + </div> + <div className="text-3xl font-extrabold text-slate-900 dark:text-white mt-1"> + {stats?.totalUrls || 0} + </div> + </div> + </div> + + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm flex items-center gap-4"> + <div className="w-14 h-14 rounded-2xl bg-purple-500/10 text-purple-600 flex items-center justify-center shrink-0"> + <MousePointerClick className="w-7 h-7" /> + </div> + <div> + <div className="text-xs font-semibold uppercase tracking-wider text-slate-400"> + Tổng Lượt Click + </div> + <div className="text-3xl font-extrabold text-slate-900 dark:text-white mt-1"> + {stats?.totalClicks || 0} + </div> + </div> + </div> + </div> + + {/* Analytics Breakdown Row: Devices & Top Countries */} + <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> + {/* Device Breakdown */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm"> + <h2 className="text-base font-bold text-slate-900 dark:text-white mb-4 flex items-center gap-2"> + <Monitor className="w-4 h-4 text-indigo-500" /> + Thống kê Thiết bị Truy cập + </h2> + + <div className="grid grid-cols-3 gap-4"> + <div className="p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 text-center"> + <Monitor className="w-6 h-6 mx-auto mb-2 text-indigo-500" /> + <div className="text-xs text-slate-400 font-medium">Desktop</div> + <div className="text-xl font-bold text-slate-900 dark:text-white mt-1"> + {stats?.deviceStats?.desktop || 0} + </div> + </div> + + <div className="p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 text-center"> + <Smartphone className="w-6 h-6 mx-auto mb-2 text-purple-500" /> + <div className="text-xs text-slate-400 font-medium">Mobile</div> + <div className="text-xl font-bold text-slate-900 dark:text-white mt-1"> + {stats?.deviceStats?.mobile || 0} + </div> + </div> + + <div className="p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 text-center"> + <Tablet className="w-6 h-6 mx-auto mb-2 text-emerald-500" /> + <div className="text-xs text-slate-400 font-medium">Tablet</div> + <div className="text-xl font-bold text-slate-900 dark:text-white mt-1"> + {stats?.deviceStats?.tablet || 0} + </div> + </div> + </div> + </div> + + {/* Top Countries */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm"> + <h2 className="text-base font-bold text-slate-900 dark:text-white mb-4 flex items-center gap-2"> + <Globe className="w-4 h-4 text-indigo-500" /> + Top 10 Quốc gia có lượt click nhiều nhất + </h2> + + {stats?.countryStats && stats.countryStats.length > 0 ? ( + <div className="divide-y divide-slate-100 dark:divide-slate-800 text-xs"> + {stats.countryStats.map((item: any, idx: number) => ( + <div key={item.country_name} className="py-2.5 flex items-center justify-between"> + <div className="flex items-center gap-2"> + <span className="w-5 text-slate-400 font-bold">#{idx + 1}</span> + <span className="font-semibold text-slate-800 dark:text-slate-200"> + {item.country_name} + </span> + </div> + <span className="font-bold text-indigo-600 dark:text-indigo-400"> + {item.total} clicks + </span> + </div> + ))} + </div> + ) : ( + <div className="py-6 text-center text-slate-400 text-xs">Chưa có dữ liệu địa lý.</div> + )} + </div> + </div> + </div> + ); +} diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx new file mode 100644 index 0000000..c48d5e6 --- /dev/null +++ b/src/app/admin/settings/page.tsx @@ -0,0 +1,431 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Save, + CheckCircle, + AlertCircle, + Sliders, + Hash, + Shield, + Search, + Mail, + HardDrive, +} from "lucide-react"; + +export default function AdminSettingsPage() { + const [settings, setSettings] = useState<Record<string, string>>({ + default_url_length: "5", + site_name: "EZBio - URL Shortener & Link in Bio", + allow_registration: "1", + header_color: "#4F46E5", + background_color: "#F9FAFB", + seo_title: "EZBio - Fast URL Shortener & Dynamic Bio Link", + seo_description: "Shorten links, generate QR codes, and create high-converting bio link landing pages with EZBio.", + seo_keywords: "url shortener, link in bio, bio link, qr code generator, link tracker", + r2_storage_limit_mb: "10240", + r2_warning_threshold: "80", + smtp_host: "", + smtp_port: "587", + smtp_username: "", + smtp_password: "", + smtp_encryption: "tls", + mail_from_address: "", + mail_from_name: "EZBio", + }); + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [successMsg, setSuccessMsg] = useState(""); + const [errorMsg, setErrorMsg] = useState(""); + + useEffect(() => { + async function fetchSettings() { + try { + const res = await fetch("/api/admin/settings"); + const data = await res.json(); + if (data.settings) { + setSettings((prev) => ({ ...prev, ...data.settings })); + } + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + } + fetchSettings(); + }, []); + + async function handleSave(e: React.FormEvent) { + e.preventDefault(); + setSaving(true); + setSuccessMsg(""); + setErrorMsg(""); + + try { + const res = await fetch("/api/admin/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(settings), + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Lỗi lưu cài đặt"); + + setSuccessMsg("Đã lưu tất cả cài đặt hệ thống thành công!"); + setTimeout(() => setSuccessMsg(""), 4000); + } catch (err: any) { + setErrorMsg(err.message); + } finally { + setSaving(false); + } + } + + if (loading) { + return ( + <div className="flex items-center justify-center min-h-[60vh]"> + <div className="text-slate-400 animate-pulse text-sm">Đang tải cấu hình hệ thống...</div> + </div> + ); + } + + return ( + <div className="max-w-5xl mx-auto px-4 py-8 space-y-8"> + {/* Header */} + <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> + <div> + <h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white flex items-center gap-2"> + <Sliders className="w-6 h-6 text-indigo-500" /> + Cấu hình Hệ thống (System Settings) + </h1> + <p className="text-sm text-slate-500 dark:text-slate-400 mt-1"> + Điều chỉnh độ dài mã rút gọn, hạn mức lưu trữ Cloudflare R2, thông tin trang và SMTP + </p> + </div> + + <button + onClick={handleSave} + disabled={saving} + className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold text-sm shadow-md transition-all flex items-center gap-2 disabled:opacity-50" + > + <Save className="w-4 h-4" /> + {saving ? "Đang lưu..." : "Lưu cài đặt"} + </button> + </div> + + {successMsg && ( + <div className="p-4 rounded-2xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 dark:text-emerald-400 flex items-center gap-2 text-sm"> + <CheckCircle className="w-5 h-5 shrink-0" /> + <span>{successMsg}</span> + </div> + )} + + {errorMsg && ( + <div className="p-4 rounded-2xl bg-red-500/10 border border-red-500/30 text-red-600 dark:text-red-400 flex items-center gap-2 text-sm"> + <AlertCircle className="w-5 h-5 shrink-0" /> + <span>{errorMsg}</span> + </div> + )} + + <form onSubmit={handleSave} className="space-y-8"> + {/* CRITICAL SETTING: DEFAULT SHORT CODE LENGTH (USER REQUEST) */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-indigo-200 dark:border-indigo-900 shadow-sm space-y-4"> + <div className="flex items-center gap-3"> + <div className="w-10 h-10 rounded-xl bg-indigo-500/10 text-indigo-600 flex items-center justify-center shrink-0"> + <Hash className="w-5 h-5" /> + </div> + <div> + <h2 className="text-base font-bold text-slate-900 dark:text-white"> + Cấu hình Độ dài Mã Rút gọn Mặc định + </h2> + <p className="text-xs text-slate-500 dark:text-slate-400"> + Cho phép Admin tùy biến độ dài chuỗi ký tự ngẫu nhiên khi người dùng tạo link mới + </p> + </div> + </div> + + <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-2"> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1.5"> + Độ dài ký tự ngẫu nhiên (3 - 16 ký tự) * + </label> + <input + type="number" + min={3} + max={16} + required + value={settings.default_url_length || "5"} + onChange={(e) => setSettings({ ...settings, default_url_length: e.target.value })} + className="w-full px-4 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono" + /> + <p className="text-[11px] text-slate-400 mt-1"> + Ví dụ: Đặt là <strong>6</strong> sẽ tự sinh mã dạng <code>aB3x9k</code> (khoảng 56 tỷ tổ hợp duy nhất). + </p> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1.5"> + Mở đăng ký thành viên mới + </label> + <select + value={settings.allow_registration || "1"} + onChange={(e) => setSettings({ ...settings, allow_registration: e.target.value })} + className="w-full px-4 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + > + <option value="1">Bật - Cho phép người dùng tự do đăng ký tài khoản</option> + <option value="0">Tắt - Chỉ Admin mới có quyền tạo thêm tài khoản</option> + </select> + </div> + </div> + </div> + + {/* CLOUDFLARE R2 STORAGE LIMIT & ALERT SETTING (USER REQUEST) */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm space-y-4"> + <div className="flex items-center gap-3"> + <div className="w-10 h-10 rounded-xl bg-purple-500/10 text-purple-600 flex items-center justify-center shrink-0"> + <HardDrive className="w-5 h-5" /> + </div> + <div> + <h2 className="text-base font-bold text-slate-900 dark:text-white"> + Hạn mức Lưu trữ Cloudflare R2 & Ngưỡng Cảnh báo + </h2> + <p className="text-xs text-slate-500 dark:text-slate-400"> + Theo dõi và hiển thị cảnh báo cho Admin khi sắp dùng hết gói 10GB miễn phí + </p> + </div> + </div> + + <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-2"> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1.5"> + Hạn mức Free Tier (MB) + </label> + <input + type="number" + value={settings.r2_storage_limit_mb || "10240"} + onChange={(e) => setSettings({ ...settings, r2_storage_limit_mb: e.target.value })} + className="w-full px-4 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono" + /> + <p className="text-[11px] text-slate-400 mt-1">10 GB = 10,240 MB.</p> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1.5"> + Ngưỡng cảnh báo vàng (%) + </label> + <input + type="number" + min={50} + max={95} + value={settings.r2_warning_threshold || "80"} + onChange={(e) => setSettings({ ...settings, r2_warning_threshold: e.target.value })} + className="w-full px-4 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono" + /> + <p className="text-[11px] text-slate-400 mt-1"> + Hệ thống sẽ đổi màu cảnh báo khi dung lượng sử dụng vượt quá tỷ lệ này. + </p> + </div> + </div> + </div> + + {/* General Site Branding */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm space-y-4"> + <div className="flex items-center gap-3"> + <div className="w-10 h-10 rounded-xl bg-blue-500/10 text-blue-600 flex items-center justify-center shrink-0"> + <Shield className="w-5 h-5" /> + </div> + <div> + <h2 className="text-base font-bold text-slate-900 dark:text-white"> + Thương hiệu Trang web (General Settings) + </h2> + <p className="text-xs text-slate-500 dark:text-slate-400"> + Tên trang web, màu sắc giao diện chính + </p> + </div> + </div> + + <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-2"> + <div className="sm:col-span-2"> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1.5"> + Tên Trang Web (Site Name) + </label> + <input + type="text" + value={settings.site_name || ""} + onChange={(e) => setSettings({ ...settings, site_name: e.target.value })} + className="w-full px-4 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1.5"> + Màu chủ đạo Header (Hex) + </label> + <input + type="color" + value={settings.header_color || "#4F46E5"} + onChange={(e) => setSettings({ ...settings, header_color: e.target.value })} + className="w-full h-10 rounded-xl border border-slate-200 dark:border-slate-700 cursor-pointer" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1.5"> + Màu nền mặc định (Hex) + </label> + <input + type="color" + value={settings.background_color || "#F9FAFB"} + onChange={(e) => setSettings({ ...settings, background_color: e.target.value })} + className="w-full h-10 rounded-xl border border-slate-200 dark:border-slate-700 cursor-pointer" + /> + </div> + </div> + </div> + + {/* SEO Meta Configurations */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm space-y-4"> + <div className="flex items-center gap-3"> + <div className="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-600 flex items-center justify-center shrink-0"> + <Search className="w-5 h-5" /> + </div> + <div> + <h2 className="text-base font-bold text-slate-900 dark:text-white"> + Cấu hình SEO Meta Toàn trang + </h2> + <p className="text-xs text-slate-500 dark:text-slate-400"> + Tối ưu hóa công cụ tìm kiếm Google và thẻ chia sẻ mạng xã hội (OpenGraph) + </p> + </div> + </div> + + <div className="space-y-3 pt-2"> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1.5"> + SEO Title + </label> + <input + type="text" + value={settings.seo_title || ""} + onChange={(e) => setSettings({ ...settings, seo_title: e.target.value })} + className="w-full px-4 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1.5"> + Meta Description + </label> + <textarea + rows={2} + value={settings.seo_description || ""} + onChange={(e) => setSettings({ ...settings, seo_description: e.target.value })} + className="w-full px-4 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1.5"> + Meta Keywords + </label> + <input + type="text" + value={settings.seo_keywords || ""} + onChange={(e) => setSettings({ ...settings, seo_keywords: e.target.value })} + className="w-full px-4 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + </div> + </div> + + {/* SMTP Email Configurations */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm space-y-4"> + <div className="flex items-center gap-3"> + <div className="w-10 h-10 rounded-xl bg-amber-500/10 text-amber-600 flex items-center justify-center shrink-0"> + <Mail className="w-5 h-5" /> + </div> + <div> + <h2 className="text-base font-bold text-slate-900 dark:text-white"> + Cấu hình Máy chủ Gửi Email (SMTP) + </h2> + <p className="text-xs text-slate-500 dark:text-slate-400"> + Dùng cho xác thực email thành viên và đặt lại mật khẩu + </p> + </div> + </div> + + <div className="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-2"> + <div className="sm:col-span-2"> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + SMTP Host + </label> + <input + type="text" + placeholder="smtp.resend.com hoặc smtp.gmail.com" + value={settings.smtp_host || ""} + onChange={(e) => setSettings({ ...settings, smtp_host: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + SMTP Port + </label> + <input + type="text" + value={settings.smtp_port || "587"} + onChange={(e) => setSettings({ ...settings, smtp_port: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Username + </label> + <input + type="text" + value={settings.smtp_username || ""} + onChange={(e) => setSettings({ ...settings, smtp_username: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Password + </label> + <input + type="password" + value={settings.smtp_password || ""} + onChange={(e) => setSettings({ ...settings, smtp_password: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + From Email Address + </label> + <input + type="email" + placeholder="noreply@domain.com" + value={settings.mail_from_address || ""} + onChange={(e) => setSettings({ ...settings, mail_from_address: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + </div> + </div> + + <div className="flex justify-end pt-4"> + <button + type="submit" + disabled={saving} + className="px-6 py-3 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold text-sm shadow-md transition-all flex items-center gap-2 disabled:opacity-50" + > + <Save className="w-4 h-4" /> + {saving ? "Đang lưu cấu hình..." : "Lưu tất cả thay đổi"} + </button> + </div> + </form> + </div> + ); +} diff --git a/src/app/admin/urls/page.tsx b/src/app/admin/urls/page.tsx new file mode 100644 index 0000000..efa185c --- /dev/null +++ b/src/app/admin/urls/page.tsx @@ -0,0 +1,277 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Link2, + Search, + Trash2, + QrCode, + BarChart2, + ExternalLink, + X, + Copy, + Check, +} from "lucide-react"; + +export default function AdminUrlsPage() { + const [urls, setUrls] = useState<any[]>([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(""); + const [userSearch, setUserSearch] = useState(""); + const [selectedIds, setSelectedIds] = useState<string[]>([]); + const [activeQr, setActiveQr] = useState<any | null>(null); + const [copiedId, setCopiedId] = useState<string | null>(null); + + async function fetchUrls() { + setLoading(true); + try { + const res = await fetch( + `/api/admin/urls?search=${encodeURIComponent(search)}&user_search=${encodeURIComponent(userSearch)}` + ); + const data = await res.json(); + if (data.urls) setUrls(data.urls); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + } + + useEffect(() => { + fetchUrls(); + }, [search, userSearch]); + + async function handleBulkDelete() { + if (selectedIds.length === 0) return; + if (!confirm(`Bạn có chắc muốn xóa ${selectedIds.length} liên kết toàn hệ thống?`)) return; + + try { + await fetch("/api/admin/urls", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ids: selectedIds }), + }); + setSelectedIds([]); + fetchUrls(); + } catch (err) { + console.error(err); + } + } + + function copy(id: string, text: string) { + navigator.clipboard.writeText(text); + setCopiedId(id); + setTimeout(() => setCopiedId(null), 2000); + } + + async function showQr(shortUrl: string) { + setActiveQr({ short_url: shortUrl }); + try { + const res = await fetch(`/api/qr?content=${encodeURIComponent(shortUrl)}&size=300`); + const data = await res.json(); + setActiveQr({ short_url: shortUrl, qr_code: data.qr_code }); + } catch { + // + } + } + + return ( + <div className="max-w-7xl mx-auto px-4 py-8 space-y-6"> + <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> + <div> + <h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white flex items-center gap-2"> + <Link2 className="w-6 h-6 text-indigo-500" /> + Quản lý Liên kết Toàn sàn (Admin) + </h1> + <p className="text-sm text-slate-500 dark:text-slate-400 mt-1"> + Xem và kiểm duyệt tất cả các liên kết rút gọn do người dùng tạo + </p> + </div> + </div> + + {/* Filter toolbar */} + <div className="flex flex-col sm:flex-row items-center justify-between gap-4"> + <div className="flex flex-wrap items-center gap-3 w-full sm:w-auto"> + <div className="relative w-full sm:w-64"> + <Search className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" /> + <input + type="text" + value={search} + onChange={(e) => setSearch(e.target.value)} + placeholder="Tìm theo URL, short code..." + className="w-full pl-10 pr-4 py-2 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div className="relative w-full sm:w-64"> + <Search className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" /> + <input + type="text" + value={userSearch} + onChange={(e) => setUserSearch(e.target.value)} + placeholder="Lọc theo người tạo (tên, email)..." + className="w-full pl-10 pr-4 py-2 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + </div> + + {selectedIds.length > 0 && ( + <button + onClick={handleBulkDelete} + className="px-4 py-2 rounded-xl bg-red-500/10 hover:bg-red-500/20 text-red-600 border border-red-500/30 text-xs font-semibold flex items-center gap-2" + > + <Trash2 className="w-4 h-4" /> + Xóa {selectedIds.length} liên kết + </button> + )} + </div> + + {/* Table */} + <div className="bg-white dark:bg-slate-900 rounded-3xl border border-slate-200/80 dark:border-slate-800 shadow-sm overflow-hidden"> + <div className="overflow-x-auto"> + <table className="w-full text-left text-sm"> + <thead className="bg-slate-50 dark:bg-slate-800/50 text-xs font-semibold uppercase text-slate-500 dark:text-slate-400 border-b border-slate-200 dark:border-slate-800"> + <tr> + <th className="p-4 w-10"> + <input + type="checkbox" + checked={selectedIds.length > 0 && selectedIds.length === urls.length} + onChange={(e) => { + if (e.target.checked) setSelectedIds(urls.map((u) => u.id)); + else setSelectedIds([]); + }} + className="rounded text-indigo-600 focus:ring-indigo-500" + /> + </th> + <th className="p-4">Tiêu đề & URL gốc</th> + <th className="p-4">Người tạo</th> + <th className="p-4">Mã ngắn</th> + <th className="p-4 text-center">Clicks</th> + <th className="p-4 text-right">Mã QR</th> + </tr> + </thead> + <tbody className="divide-y divide-slate-100 dark:divide-slate-800 text-xs"> + {loading ? ( + <tr> + <td colSpan={6} className="p-8 text-center text-slate-400"> + Đang tải dữ liệu liên kết... + </td> + </tr> + ) : urls.length === 0 ? ( + <tr> + <td colSpan={6} className="p-8 text-center text-slate-400"> + Không tìm thấy liên kết nào. + </td> + </tr> + ) : ( + urls.map((u) => ( + <tr key={u.id} className="hover:bg-slate-50/60 dark:hover:bg-slate-800/30 transition-colors"> + <td className="p-4"> + <input + type="checkbox" + checked={selectedIds.includes(u.id)} + onChange={(e) => { + if (e.target.checked) setSelectedIds([...selectedIds, u.id]); + else setSelectedIds(selectedIds.filter((id) => id !== u.id)); + }} + className="rounded text-indigo-600 focus:ring-indigo-500" + /> + </td> + <td className="p-4 max-w-sm"> + <div className="font-semibold text-slate-900 dark:text-white truncate"> + {u.title || "Không có tiêu đề"} + </div> + <div className="text-slate-400 truncate mt-0.5">{u.original_url}</div> + </td> + <td className="p-4"> + {u.user ? ( + <div> + <div className="font-medium text-slate-800 dark:text-slate-200"> + {u.user.name} + </div> + <div className="text-slate-400">{u.user.email}</div> + </div> + ) : ( + <span className="text-slate-400 italic">Khách (Ẩn danh)</span> + )} + </td> + <td className="p-4 font-mono"> + <div className="flex items-center gap-1.5"> + <a + href={u.short_url} + target="_blank" + rel="noopener noreferrer" + className="text-indigo-600 dark:text-indigo-400 hover:underline flex items-center gap-1" + > + /{u.short_code} + <ExternalLink className="w-3 h-3 opacity-60" /> + </a> + <button + onClick={() => copy(u.id, u.short_url)} + className="p-1 text-slate-400 hover:text-slate-700 dark:hover:text-white" + > + {copiedId === u.id ? ( + <Check className="w-3.5 h-3.5 text-emerald-500" /> + ) : ( + <Copy className="w-3.5 h-3.5" /> + )} + </button> + </div> + </td> + <td className="p-4 text-center font-bold text-slate-800 dark:text-slate-200"> + <span className="inline-flex items-center gap-1"> + <BarChart2 className="w-3.5 h-3.5 text-indigo-500" /> + {u.clicks_count} + </span> + </td> + <td className="p-4 text-right"> + <button + onClick={() => showQr(u.short_url)} + className="p-2 text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 rounded-lg" + title="Xem mã QR" + > + <QrCode className="w-4 h-4" /> + </button> + </td> + </tr> + )) + )} + </tbody> + </table> + </div> + </div> + + {/* QR Code Modal */} + {activeQr && ( + <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"> + <div className="relative w-full max-w-xs bg-white dark:bg-slate-900 rounded-3xl p-6 shadow-2xl border border-white/20 text-center"> + <button + onClick={() => setActiveQr(null)} + className="absolute top-4 right-4 p-2 text-slate-400 hover:text-slate-700 dark:hover:text-white" + > + <X className="w-5 h-5" /> + </button> + <h3 className="text-base font-bold text-slate-900 dark:text-white mb-2">Mã QR Code</h3> + <p className="text-xs text-slate-500 truncate mb-4">{activeQr.short_url}</p> + + <div className="w-52 h-52 mx-auto mb-4 p-3 bg-white rounded-2xl border border-slate-200 flex items-center justify-center"> + {activeQr.qr_code ? ( + // eslint-disable-next-line @next/next/no-img-element + <img src={activeQr.qr_code} alt="QR Code" className="w-full h-full object-contain" /> + ) : ( + <div className="text-xs text-slate-400">Đang tạo QR...</div> + )} + </div> + + <button + onClick={() => setActiveQr(null)} + className="w-full py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 text-xs font-semibold" + > + Đóng + </button> + </div> + </div> + )} + </div> + ); +} diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx new file mode 100644 index 0000000..de0743c --- /dev/null +++ b/src/app/admin/users/page.tsx @@ -0,0 +1,467 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Users, + Search, + Plus, + Trash2, + Edit, + CheckCircle, + XCircle, + X, + ShieldCheck, + User, +} from "lucide-react"; + +export default function AdminUsersPage() { + const [users, setUsers] = useState<any[]>([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(""); + const [selectedIds, setSelectedIds] = useState<string[]>([]); + + // Modals + const [showCreateModal, setShowCreateModal] = useState(false); + const [editUser, setEditUser] = useState<any | null>(null); + + // Form states + const [formName, setFormName] = useState(""); + const [formEmail, setFormEmail] = useState(""); + const [formPassword, setFormPassword] = useState(""); + const [formRole, setFormRole] = useState("user"); + const [submitting, setSubmitting] = useState(false); + + async function fetchUsers() { + setLoading(true); + try { + const res = await fetch(`/api/admin/users?search=${encodeURIComponent(search)}`); + const data = await res.json(); + if (data.users) setUsers(data.users); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + } + + useEffect(() => { + fetchUsers(); + }, [search]); + + async function handleBulkAction(action: "delete" | "active" | "inactive") { + if (selectedIds.length === 0) return; + if (action === "delete" && !confirm(`Bạn có chắc muốn xóa ${selectedIds.length} người dùng đã chọn?`)) { + return; + } + + try { + await fetch("/api/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action, userIds: selectedIds }), + }); + setSelectedIds([]); + fetchUsers(); + } catch (err) { + console.error(err); + } + } + + async function handleCreateUser(e: React.FormEvent) { + e.preventDefault(); + setSubmitting(true); + try { + const res = await fetch("/api/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: formName, + email: formEmail, + password: formPassword, + role: formRole, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + + setShowCreateModal(false); + setFormName(""); + setFormEmail(""); + setFormPassword(""); + fetchUsers(); + } catch (err: any) { + alert(err.message || "Lỗi tạo người dùng"); + } finally { + setSubmitting(false); + } + } + + async function handleUpdateUser(e: React.FormEvent) { + e.preventDefault(); + if (!editUser) return; + setSubmitting(true); + try { + const res = await fetch("/api/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: editUser.id, + name: editUser.name, + email: editUser.email, + role: editUser.role, + is_active: editUser.isActive, + password: editUser.new_password || undefined, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + + setEditUser(null); + fetchUsers(); + } catch (err: any) { + alert(err.message || "Lỗi cập nhật người dùng"); + } finally { + setSubmitting(false); + } + } + + return ( + <div className="max-w-7xl mx-auto px-4 py-8 space-y-6"> + <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> + <div> + <h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white flex items-center gap-2"> + <Users className="w-6 h-6 text-indigo-500" /> + Quản lý Người dùng + </h1> + <p className="text-sm text-slate-500 dark:text-slate-400 mt-1"> + Danh sách thành viên, phân quyền Admin/User, khóa hoặc kích hoạt tài khoản + </p> + </div> + + <button + onClick={() => setShowCreateModal(true)} + className="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm shadow-md transition-all flex items-center justify-center gap-2 shrink-0" + > + <Plus className="w-4 h-4" /> + Thêm người dùng mới + </button> + </div> + + {/* Toolbar */} + <div className="flex flex-col sm:flex-row items-center justify-between gap-4"> + <div className="relative w-full sm:w-80"> + <Search className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" /> + <input + type="text" + value={search} + onChange={(e) => setSearch(e.target.value)} + placeholder="Tìm theo tên, email, username..." + className="w-full pl-10 pr-4 py-2 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm" + /> + </div> + + {selectedIds.length > 0 && ( + <div className="flex items-center gap-2"> + <span className="text-xs text-slate-500">Đã chọn {selectedIds.length} mục:</span> + <button + onClick={() => handleBulkAction("active")} + className="px-3 py-1.5 rounded-lg bg-emerald-500/10 text-emerald-600 border border-emerald-500/20 text-xs font-semibold" + > + Kích hoạt + </button> + <button + onClick={() => handleBulkAction("inactive")} + className="px-3 py-1.5 rounded-lg bg-amber-500/10 text-amber-600 border border-amber-500/20 text-xs font-semibold" + > + Khóa + </button> + <button + onClick={() => handleBulkAction("delete")} + className="px-3 py-1.5 rounded-lg bg-red-500/10 text-red-600 border border-red-500/20 text-xs font-semibold" + > + Xóa + </button> + </div> + )} + </div> + + {/* Users Table */} + <div className="bg-white dark:bg-slate-900 rounded-3xl border border-slate-200/80 dark:border-slate-800 shadow-sm overflow-hidden"> + <div className="overflow-x-auto"> + <table className="w-full text-left text-sm"> + <thead className="bg-slate-50 dark:bg-slate-800/50 text-xs font-semibold uppercase text-slate-500 dark:text-slate-400 border-b border-slate-200 dark:border-slate-800"> + <tr> + <th className="p-4 w-10"> + <input + type="checkbox" + checked={selectedIds.length > 0 && selectedIds.length === users.length} + onChange={(e) => { + if (e.target.checked) setSelectedIds(users.map((u) => u.id)); + else setSelectedIds([]); + }} + className="rounded text-indigo-600 focus:ring-indigo-500" + /> + </th> + <th className="p-4">Người dùng</th> + <th className="p-4">Username</th> + <th className="p-4 text-center">Vai trò</th> + <th className="p-4 text-center">Trạng thái</th> + <th className="p-4 text-center">Số Links</th> + <th className="p-4 text-right">Thao tác</th> + </tr> + </thead> + <tbody className="divide-y divide-slate-100 dark:divide-slate-800"> + {loading ? ( + <tr> + <td colSpan={7} className="p-8 text-center text-slate-400"> + Đang tải danh sách người dùng... + </td> + </tr> + ) : users.length === 0 ? ( + <tr> + <td colSpan={7} className="p-8 text-center text-slate-400"> + Không tìm thấy người dùng nào. + </td> + </tr> + ) : ( + users.map((u) => ( + <tr key={u.id} className="hover:bg-slate-50/60 dark:hover:bg-slate-800/30 transition-colors"> + <td className="p-4"> + <input + type="checkbox" + checked={selectedIds.includes(u.id)} + onChange={(e) => { + if (e.target.checked) setSelectedIds([...selectedIds, u.id]); + else setSelectedIds(selectedIds.filter((id) => id !== u.id)); + }} + className="rounded text-indigo-600 focus:ring-indigo-500" + /> + </td> + <td className="p-4"> + <div className="font-semibold text-slate-900 dark:text-white">{u.name}</div> + <div className="text-xs text-slate-400">{u.email}</div> + </td> + <td className="p-4 font-mono text-xs text-slate-600 dark:text-slate-300"> + @{u.username} + </td> + <td className="p-4 text-center"> + <span + className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-bold ${ + u.role === "admin" + ? "bg-purple-100 dark:bg-purple-950/60 text-purple-700 dark:text-purple-300" + : "bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400" + }`} + > + {u.role === "admin" ? <ShieldCheck className="w-3 h-3" /> : <User className="w-3 h-3" />} + {u.role} + </span> + </td> + <td className="p-4 text-center"> + {u.isActive ? ( + <span className="inline-flex items-center gap-1 text-xs text-emerald-600 font-semibold"> + <CheckCircle className="w-3.5 h-3.5" /> Hoạt động + </span> + ) : ( + <span className="inline-flex items-center gap-1 text-xs text-red-500 font-semibold"> + <XCircle className="w-3.5 h-3.5" /> Bị khóa + </span> + )} + </td> + <td className="p-4 text-center font-bold text-slate-700 dark:text-slate-300"> + {u.urls_count} + </td> + <td className="p-4 text-right"> + <button + onClick={() => setEditUser(u)} + className="p-2 text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 rounded-lg transition-colors" + title="Chỉnh sửa" + > + <Edit className="w-4 h-4" /> + </button> + </td> + </tr> + )) + )} + </tbody> + </table> + </div> + </div> + + {/* Create Modal */} + {showCreateModal && ( + <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"> + <div className="relative w-full max-w-md bg-white dark:bg-slate-900 rounded-3xl p-6 shadow-2xl border border-white/20"> + <button + onClick={() => setShowCreateModal(false)} + className="absolute top-4 right-4 p-2 text-slate-400 hover:text-slate-700 dark:hover:text-white" + > + <X className="w-5 h-5" /> + </button> + <h2 className="text-lg font-bold text-slate-900 dark:text-white mb-4"> + Thêm người dùng mới + </h2> + + <form onSubmit={handleCreateUser} className="space-y-3.5"> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Họ và tên * + </label> + <input + type="text" + required + value={formName} + onChange={(e) => setFormName(e.target.value)} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Email * + </label> + <input + type="email" + required + value={formEmail} + onChange={(e) => setFormEmail(e.target.value)} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Mật khẩu khởi tạo * + </label> + <input + type="password" + required + value={formPassword} + onChange={(e) => setFormPassword(e.target.value)} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Vai trò (Role) + </label> + <select + value={formRole} + onChange={(e) => setFormRole(e.target.value)} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + > + <option value="user">Người dùng (User)</option> + <option value="admin">Quản trị viên (Admin)</option> + </select> + </div> + + <div className="flex justify-end gap-2 pt-3"> + <button + type="button" + onClick={() => setShowCreateModal(false)} + className="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 text-xs font-semibold" + > + Hủy + </button> + <button + type="submit" + disabled={submitting} + className="px-5 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-semibold disabled:opacity-50" + > + {submitting ? "Đang xử lý..." : "Tạo người dùng"} + </button> + </div> + </form> + </div> + </div> + )} + + {/* Edit Modal */} + {editUser && ( + <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"> + <div className="relative w-full max-w-md bg-white dark:bg-slate-900 rounded-3xl p-6 shadow-2xl border border-white/20"> + <button + onClick={() => setEditUser(null)} + className="absolute top-4 right-4 p-2 text-slate-400 hover:text-slate-700 dark:hover:text-white" + > + <X className="w-5 h-5" /> + </button> + <h2 className="text-lg font-bold text-slate-900 dark:text-white mb-4"> + Chỉnh sửa Người dùng + </h2> + + <form onSubmit={handleUpdateUser} className="space-y-3.5"> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Họ và tên + </label> + <input + type="text" + value={editUser.name} + onChange={(e) => setEditUser({ ...editUser, name: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Email + </label> + <input + type="email" + value={editUser.email} + onChange={(e) => setEditUser({ ...editUser, email: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Vai trò (Role) + </label> + <select + value={editUser.role} + onChange={(e) => setEditUser({ ...editUser, role: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + > + <option value="user">User</option> + <option value="admin">Admin</option> + </select> + </div> + + <div className="flex items-center gap-2 pt-1"> + <input + type="checkbox" + id="isActiveToggle" + checked={editUser.isActive} + onChange={(e) => setEditUser({ ...editUser, isActive: e.target.checked })} + className="rounded text-indigo-600 focus:ring-indigo-500" + /> + <label htmlFor="isActiveToggle" className="text-xs font-semibold text-slate-700 dark:text-slate-300 cursor-pointer"> + Tài khoản đang hoạt động (Active) + </label> + </div> + + <div className="flex justify-end gap-2 pt-3"> + <button + type="button" + onClick={() => setEditUser(null)} + className="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 text-xs font-semibold" + > + Hủy + </button> + <button + type="submit" + disabled={submitting} + className="px-5 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-semibold disabled:opacity-50" + > + {submitting ? "Đang lưu..." : "Cập nhật"} + </button> + </div> + </form> + </div> + </div> + )} + </div> + ); +} diff --git a/src/app/api/admin/settings/route.ts b/src/app/api/admin/settings/route.ts new file mode 100644 index 0000000..b09da29 --- /dev/null +++ b/src/app/api/admin/settings/route.ts @@ -0,0 +1,66 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + const user = await getCurrentUser(); + if (!user || user.role !== "admin") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const allSettings = await prisma.setting.findMany(); + const settingsMap: Record<string, string> = { + default_url_length: "5", + site_name: "EZBio - URL Shortener & Link in Bio", + allow_registration: "1", + header_color: "#4F46E5", + background_color: "#F9FAFB", + seo_title: "EZBio - Fast URL Shortener & Dynamic Bio Link", + seo_description: "Shorten links, generate QR codes, and create high-converting bio link landing pages with EZBio.", + seo_keywords: "url shortener, link in bio, bio link, qr code generator, link tracker", + }; + + allSettings.forEach((s) => { + settingsMap[s.key] = s.value; + }); + + return NextResponse.json({ success: true, settings: settingsMap }); +} + +export async function POST(req: Request) { + const user = await getCurrentUser(); + if (!user || user.role !== "admin") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + try { + const body = await req.json(); + + // Validate default_url_length if provided + if (body.default_url_length !== undefined) { + const len = parseInt(body.default_url_length, 10); + if (isNaN(len) || len < 3 || len > 16) { + return NextResponse.json( + { error: "Độ dài mã rút gọn mặc định phải nằm trong khoảng từ 3 đến 16 ký tự" }, + { status: 422 } + ); + } + } + + // Upsert all settings in a transaction + const updatePromises = Object.entries(body).map(([key, value]) => + prisma.setting.upsert({ + where: { key }, + update: { value: String(value) }, + create: { key, value: String(value) }, + }) + ); + + await prisma.$transaction(updatePromises); + + return NextResponse.json({ success: true, message: "Đã lưu cài đặt hệ thống thành công" }); + } catch (error) { + console.error("Save settings error:", error); + return NextResponse.json({ error: "Lỗi lưu cấu hình hệ thống" }, { status: 500 }); + } +} diff --git a/src/app/api/admin/stats/route.ts b/src/app/api/admin/stats/route.ts new file mode 100644 index 0000000..c54a7e5 --- /dev/null +++ b/src/app/api/admin/stats/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + const user = await getCurrentUser(); + if (!user || user.role !== "admin") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const [totalUsers, totalUrls, totalClicks] = await Promise.all([ + prisma.user.count(), + prisma.shortUrl.count(), + prisma.urlClick.count(), + ]); + + // Monthly stats (last 12 months) + const monthlyUrls: { date: string; total: number }[] = []; + const monthlyUsers: { date: string; total: number }[] = []; + + const now = new Date(); + for (let i = 11; i >= 0; i--) { + const startOfMonth = new Date(now.getFullYear(), now.getMonth() - i, 1); + const endOfMonth = new Date(now.getFullYear(), now.getMonth() - i + 1, 0, 23, 59, 59, 999); + const dateLabel = `${startOfMonth.getFullYear()}-${String(startOfMonth.getMonth() + 1).padStart(2, "0")}`; + + const [urlsCount, usersCount] = await Promise.all([ + prisma.shortUrl.count({ + where: { + createdAt: { gte: startOfMonth, lte: endOfMonth }, + }, + }), + prisma.user.count({ + where: { + createdAt: { gte: startOfMonth, lte: endOfMonth }, + }, + }), + ]); + + monthlyUrls.push({ date: dateLabel, total: urlsCount }); + monthlyUsers.push({ date: dateLabel, total: usersCount }); + } + + // Device stats + const [desktop, mobile, tablet] = await Promise.all([ + prisma.urlClick.count({ where: { deviceType: "desktop" } }), + prisma.urlClick.count({ where: { deviceType: "mobile" } }), + prisma.urlClick.count({ where: { deviceType: "tablet" } }), + ]); + + // Top countries + const clicks = await prisma.urlClick.findMany({ + select: { countryName: true }, + where: { countryName: { not: null } }, + }); + + const countryMap: Record<string, number> = {}; + clicks.forEach((c) => { + const name = c.countryName || "Unknown"; + countryMap[name] = (countryMap[name] || 0) + 1; + }); + + const countryStats = Object.entries(countryMap) + .map(([country_name, total]) => ({ country_name, total })) + .sort((a, b) => b.total - a.total) + .slice(0, 10); + + return NextResponse.json({ + success: true, + stats: { + totalUsers, + totalUrls, + totalClicks, + monthlyUrls, + monthlyUsers, + deviceStats: { desktop, mobile, tablet }, + countryStats, + }, + }); +} diff --git a/src/app/api/admin/storage-status/route.ts b/src/app/api/admin/storage-status/route.ts new file mode 100644 index 0000000..95baff9 --- /dev/null +++ b/src/app/api/admin/storage-status/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; +import { getStorageStatus } from "@/lib/storage"; + +export async function GET() { + const user = await getCurrentUser(); + if (!user || user.role !== "admin") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + try { + const status = await getStorageStatus(); + return NextResponse.json({ success: true, status }); + } catch (error) { + console.error("Storage status error:", error); + return NextResponse.json({ error: "Lỗi kiểm tra dung lượng lưu trữ" }, { status: 500 }); + } +} diff --git a/src/app/api/admin/urls/route.ts b/src/app/api/admin/urls/route.ts new file mode 100644 index 0000000..6d1ae99 --- /dev/null +++ b/src/app/api/admin/urls/route.ts @@ -0,0 +1,104 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET(req: Request) { + const user = await getCurrentUser(); + if (!user || user.role !== "admin") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { searchParams } = new URL(req.url); + const search = searchParams.get("search") || ""; + const tag = searchParams.get("tag") || ""; + const userSearch = searchParams.get("user_search") || ""; + const page = parseInt(searchParams.get("page") || "1", 10); + const limit = parseInt(searchParams.get("limit") || "15", 10); + + const where: any = {}; + + if (search) { + where.OR = [ + { originalUrl: { contains: search, mode: "insensitive" } }, + { shortCode: { contains: search, mode: "insensitive" } }, + { title: { contains: search, mode: "insensitive" } }, + ]; + } + + if (tag) { + where.tags = { + some: { name: { contains: tag, mode: "insensitive" } }, + }; + } + + if (userSearch) { + where.user = { + OR: [ + { name: { contains: userSearch, mode: "insensitive" } }, + { email: { contains: userSearch, mode: "insensitive" } }, + ], + }; + } + + const [total, urls] = await Promise.all([ + prisma.shortUrl.count({ where }), + prisma.shortUrl.findMany({ + where, + include: { + user: { + select: { id: true, name: true, email: true }, + }, + tags: true, + }, + orderBy: { createdAt: "desc" }, + skip: (page - 1) * limit, + take: limit, + }), + ]); + + const appUrl = (process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000").replace(/\/$/, ""); + + return NextResponse.json({ + success: true, + total, + page, + limit, + totalPages: Math.ceil(total / limit), + urls: urls.map((u) => ({ + id: u.id, + original_url: u.originalUrl, + short_code: u.shortCode, + short_url: `${appUrl}/${u.shortCode}`, + title: u.title || "Chưa có tiêu đề", + clicks_count: u.clicksCount, + display_on_portal: u.displayOnPortal, + is_password_protected: Boolean(u.password), + expires_at: u.expiresAt, + tags: u.tags.map((t) => t.name), + user: u.user, + created_at: u.createdAt, + })), + }); +} + +export async function DELETE(req: Request) { + const user = await getCurrentUser(); + if (!user || user.role !== "admin") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { ids } = await req.json(); + if (!ids || !Array.isArray(ids) || ids.length === 0) { + return NextResponse.json({ error: "Vui lòng chọn ít nhất một liên kết" }, { status: 400 }); + } + + const result = await prisma.shortUrl.deleteMany({ + where: { id: { in: ids } }, + }); + + return NextResponse.json({ + success: true, + deletedCount: result.count, + message: `Đã xóa ${result.count} liên kết thành công`, + }); +} diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..63302bd --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,160 @@ +import { NextResponse } from "next/server"; +import bcrypt from "bcryptjs"; +import { getCurrentUser } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET(req: Request) { + const user = await getCurrentUser(); + if (!user || user.role !== "admin") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { searchParams } = new URL(req.url); + const search = searchParams.get("search") || ""; + const page = parseInt(searchParams.get("page") || "1", 10); + const limit = parseInt(searchParams.get("limit") || "15", 10); + + const where: any = {}; + if (search) { + where.OR = [ + { name: { contains: search, mode: "insensitive" } }, + { email: { contains: search, mode: "insensitive" } }, + { username: { contains: search, mode: "insensitive" } }, + ]; + } + + const [total, users] = await Promise.all([ + prisma.user.count({ where }), + prisma.user.findMany({ + where, + select: { + id: true, + name: true, + email: true, + username: true, + role: true, + isActive: true, + createdAt: true, + _count: { + select: { shortUrls: true }, + }, + }, + orderBy: { createdAt: "desc" }, + skip: (page - 1) * limit, + take: limit, + }), + ]); + + return NextResponse.json({ + success: true, + total, + page, + limit, + totalPages: Math.ceil(total / limit), + users: users.map((u) => ({ + ...u, + urls_count: u._count.shortUrls, + })), + }); +} + +export async function POST(req: Request) { + const currentUser = await getCurrentUser(); + if (!currentUser || currentUser.role !== "admin") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + try { + const body = await req.json(); + const { action, userIds, name, email, password, role, is_active, id } = body; + + // Bulk actions + if (action && Array.isArray(userIds)) { + // Don't modify own account in bulk actions + const targetIds = userIds.filter((uid) => uid !== currentUser.id); + + if (action === "delete") { + await prisma.user.deleteMany({ + where: { id: { in: targetIds } }, + }); + return NextResponse.json({ success: true, message: `Đã xóa ${targetIds.length} người dùng` }); + } + + if (action === "active") { + await prisma.user.updateMany({ + where: { id: { in: targetIds } }, + data: { isActive: true }, + }); + return NextResponse.json({ success: true, message: `Đã kích hoạt ${targetIds.length} tài khoản` }); + } + + if (action === "inactive") { + await prisma.user.updateMany({ + where: { id: { in: targetIds } }, + data: { isActive: false }, + }); + return NextResponse.json({ success: true, message: `Đã vô hiệu hóa ${targetIds.length} tài khoản` }); + } + } + + // Edit single user + if (id) { + const updateData: any = {}; + if (name) updateData.name = name; + if (email) updateData.email = email.toLowerCase().trim(); + if (role) updateData.role = role; + if (is_active !== undefined) updateData.isActive = Boolean(is_active); + if (password && password.trim().length >= 6) { + updateData.password = await bcrypt.hash(password.trim(), 10); + } + + const updated = await prisma.user.update({ + where: { id }, + data: updateData, + }); + + return NextResponse.json({ success: true, user: updated }); + } + + // Create user + if (!name || !email || !password) { + return NextResponse.json({ error: "Thiếu thông tin bắt buộc" }, { status: 400 }); + } + + const baseUsername = email.split("@")[0].toLowerCase().replace(/[^a-z0-9_-]/g, ""); + let username = baseUsername; + let counter = 1; + while (await prisma.user.findUnique({ where: { username } })) { + username = `${baseUsername}${counter}`; + counter++; + } + + const hashedPassword = await bcrypt.hash(password, 10); + const newUser = await prisma.user.create({ + data: { + name, + email: email.toLowerCase().trim(), + username, + password: hashedPassword, + role: role || "user", + isActive: is_active !== undefined ? Boolean(is_active) : true, + portal: { + create: { + title: `${name}'s Bio`, + description: "", + headerColor: "#000000", + titleColorStyle: "gradient-1", + backgroundStyle: "gradient-1", + socialLinks: {}, + vcard: {}, + }, + }, + }, + }); + + return NextResponse.json({ success: true, user: newUser }); + } catch (error) { + console.error("Admin user action error:", error); + return NextResponse.json({ error: "Lỗi xử lý yêu cầu quản lý người dùng" }, { status: 500 }); + } +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..de4fb01 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import bcrypt from "bcryptjs"; +import { prisma } from "@/lib/prisma"; +import { signSession, setAuthCookie } from "@/lib/auth"; + +export async function POST(req: Request) { + try { + const { email, password } = await req.json(); + + if (!email || !password) { + return NextResponse.json({ error: "Vui lòng nhập email và mật khẩu" }, { status: 400 }); + } + + const user = await prisma.user.findUnique({ + where: { email: email.toLowerCase().trim() }, + }); + + if (!user) { + return NextResponse.json({ error: "Email hoặc mật khẩu không chính xác" }, { status: 401 }); + } + + if (!user.isActive) { + return NextResponse.json({ error: "Tài khoản của bạn đã bị khóa. Vui lòng liên hệ quản trị viên." }, { status: 403 }); + } + + const passwordMatch = await bcrypt.compare(password, user.password); + if (!passwordMatch) { + return NextResponse.json({ error: "Email hoặc mật khẩu không chính xác" }, { status: 401 }); + } + + const sessionPayload = { + id: user.id, + email: user.email, + name: user.name, + username: user.username, + role: user.role, + }; + + const token = await signSession(sessionPayload); + await setAuthCookie(token); + + return NextResponse.json({ + success: true, + user: sessionPayload, + }); + } catch (error) { + console.error("Login error:", error); + return NextResponse.json({ error: "Đã có lỗi xảy ra khi đăng nhập" }, { status: 500 }); + } +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..4d285a8 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from "next/server"; +import { clearAuthCookie } from "@/lib/auth"; + +export async function POST() { + await clearAuthCookie(); + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 0000000..8bd1565 --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; + +export async function GET() { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ user: null }); + } + return NextResponse.json({ user }); +} diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..b3b6c88 --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,90 @@ +import { NextResponse } from "next/server"; +import bcrypt from "bcryptjs"; +import { prisma } from "@/lib/prisma"; +import { signSession, setAuthCookie } from "@/lib/auth"; + +export async function POST(req: Request) { + try { + const { name, email, password } = await req.json(); + + if (!name || !email || !password) { + return NextResponse.json({ error: "Vui lòng nhập đầy đủ thông tin" }, { status: 400 }); + } + + if (password.length < 6) { + return NextResponse.json({ error: "Mật khẩu phải từ 6 ký tự trở lên" }, { status: 400 }); + } + + // Check if registration is allowed in settings + const allowRegSetting = await prisma.setting.findUnique({ + where: { key: "allow_registration" }, + }); + if (allowRegSetting && allowRegSetting.value === "0") { + return NextResponse.json({ error: "Hệ thống hiện đang tạm đóng đăng ký mới" }, { status: 403 }); + } + + // Check existing email + const existingUser = await prisma.user.findUnique({ + where: { email: email.toLowerCase().trim() }, + }); + if (existingUser) { + return NextResponse.json({ error: "Email này đã được sử dụng" }, { status: 409 }); + } + + // Generate unique username from email + const baseUsername = email.split("@")[0].toLowerCase().replace(/[^a-z0-9_-]/g, ""); + let username = baseUsername; + let counter = 1; + while (await prisma.user.findUnique({ where: { username } })) { + username = `${baseUsername}${counter}`; + counter++; + } + + const hashedPassword = await bcrypt.hash(password, 10); + + // If first user, make admin + const userCount = await prisma.user.count(); + const role = userCount === 0 ? "admin" : "user"; + + // Create user and initial bio portal + const user = await prisma.user.create({ + data: { + name, + email: email.toLowerCase().trim(), + password: hashedPassword, + username, + role, + portal: { + create: { + title: `${name}'s Bio`, + description: `Welcome to ${name}'s links portal`, + headerColor: "#000000", + titleColorStyle: "gradient-1", + backgroundStyle: "gradient-1", + socialLinks: {}, + vcard: {}, + }, + }, + }, + }); + + const sessionPayload = { + id: user.id, + email: user.email, + name: user.name, + username: user.username, + role: user.role, + }; + + const token = await signSession(sessionPayload); + await setAuthCookie(token); + + return NextResponse.json({ + success: true, + user: sessionPayload, + }); + } catch (error) { + console.error("Register error:", error); + return NextResponse.json({ error: "Đã có lỗi xảy ra khi đăng ký" }, { status: 500 }); + } +} diff --git a/src/app/api/portal/route.ts b/src/app/api/portal/route.ts new file mode 100644 index 0000000..13848e6 --- /dev/null +++ b/src/app/api/portal/route.ts @@ -0,0 +1,85 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + let portal = await prisma.portal.findUnique({ + where: { userId: user.id }, + }); + + if (!portal) { + portal = await prisma.portal.create({ + data: { + userId: user.id, + title: `${user.name}'s Bio`, + description: "", + headerColor: "#000000", + titleColorStyle: "gradient-1", + backgroundStyle: "gradient-1", + socialLinks: {}, + vcard: {}, + }, + }); + } + + return NextResponse.json({ portal }); +} + +export async function PUT(req: Request) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await req.json(); + const { + title, + description, + avatar, + backgroundImage, + headerColor, + titleColorStyle, + backgroundStyle, + vcard, + socialLinks, + } = body; + + const updatedPortal = await prisma.portal.upsert({ + where: { userId: user.id }, + update: { + title: title || "", + description: description || "", + avatar: avatar !== undefined ? avatar : undefined, + backgroundImage: backgroundImage !== undefined ? backgroundImage : undefined, + headerColor: headerColor || "#000000", + titleColorStyle: titleColorStyle || "gradient-1", + backgroundStyle: backgroundStyle || "gradient-1", + vcard: vcard || {}, + socialLinks: socialLinks || {}, + }, + create: { + userId: user.id, + title: title || `${user.name}'s Bio`, + description: description || "", + avatar: avatar || null, + backgroundImage: backgroundImage || null, + headerColor: headerColor || "#000000", + titleColorStyle: titleColorStyle || "gradient-1", + backgroundStyle: backgroundStyle || "gradient-1", + vcard: vcard || {}, + socialLinks: socialLinks || {}, + }, + }); + + return NextResponse.json({ success: true, portal: updatedPortal }); + } catch (error) { + console.error("Portal update error:", error); + return NextResponse.json({ error: "Lỗi cập nhật cấu hình Bio Link" }, { status: 500 }); + } +} diff --git a/src/app/api/qr/route.ts b/src/app/api/qr/route.ts new file mode 100644 index 0000000..7d4b4c9 --- /dev/null +++ b/src/app/api/qr/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from "next/server"; +import { generateQRCodeDataUrl, generateQRCodeSVG } from "@/lib/qr"; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const content = searchParams.get("content"); + const color = searchParams.get("color") || "#000000"; + const background = searchParams.get("background") || "transparent"; + const format = searchParams.get("format") || "dataUrl"; + const size = parseInt(searchParams.get("size") || "300", 10); + + if (!content) { + return NextResponse.json({ error: "Missing content parameter" }, { status: 400 }); + } + + try { + if (format === "svg") { + const svg = await generateQRCodeSVG(content, { color, background, size }); + return new Response(svg, { + headers: { "Content-Type": "image/svg+xml" }, + }); + } + + const dataUrl = await generateQRCodeDataUrl(content, { color, background, size }); + return NextResponse.json({ qr_code: dataUrl }); + } catch (error) { + console.error("QR Code error:", error); + return NextResponse.json({ error: "Failed to generate QR code" }, { status: 500 }); + } +} + +export async function POST(req: Request) { + try { + const { content, color, background, size, format } = await req.json(); + + if (!content) { + return NextResponse.json({ error: "Missing content" }, { status: 400 }); + } + + if (format === "svg") { + const svg = await generateQRCodeSVG(content, { color, background, size }); + return NextResponse.json({ svg }); + } + + const qrCode = await generateQRCodeDataUrl(content, { color, background, size }); + return NextResponse.json({ qr_code: qrCode }); + } catch (error) { + console.error("QR Code error:", error); + return NextResponse.json({ error: "Failed to generate QR code" }, { status: 500 }); + } +} diff --git a/src/app/api/shorten/route.ts b/src/app/api/shorten/route.ts new file mode 100644 index 0000000..bd858d1 --- /dev/null +++ b/src/app/api/shorten/route.ts @@ -0,0 +1,150 @@ +import { NextResponse } from "next/server"; +import bcrypt from "bcryptjs"; +import { prisma } from "@/lib/prisma"; +import { getCurrentUser } from "@/lib/auth"; +import { generateUniqueShortCode } from "@/lib/short-code"; +import { generateQRCodeDataUrl } from "@/lib/qr"; +import { fetchUrlTitle } from "@/lib/url-metadata"; + +export async function POST(req: Request) { + try { + const user = await getCurrentUser(); + const body = await req.json(); + const { url, custom_code, password, tags, expires_at, display_on_portal } = body; + + // Validate URL + if (!url || typeof url !== "string") { + return NextResponse.json({ success: false, message: "Vui lòng nhập đường link hợp lệ" }, { status: 400 }); + } + + let parsedUrl: URL; + try { + parsedUrl = new URL(url.trim()); + if (!["http:", "https:"].includes(parsedUrl.protocol)) { + throw new Error(); + } + } catch { + return NextResponse.json({ success: false, message: "URL không hợp lệ. Vui lòng bắt đầu bằng http:// hoặc https://" }, { status: 422 }); + } + + if (url.length > 2048) { + return NextResponse.json({ success: false, message: "URL vượt quá độ dài cho phép (tối đa 2048 ký tự)" }, { status: 422 }); + } + + // Require auth for advanced options + const hasAdvancedOptions = Boolean(custom_code || password || tags || expires_at); + if (!user && hasAdvancedOptions) { + return NextResponse.json( + { success: false, message: "Vui lòng đăng nhập để sử dụng tính năng tùy biến nâng cao (mã riêng, mật khẩu, tag)" }, + { status: 403 } + ); + } + + // Determine short code + let shortCode = ""; + if (custom_code && typeof custom_code === "string") { + const trimmedCode = custom_code.trim(); + if (!/^[a-zA-Z0-9_-]{3,20}$/.test(trimmedCode)) { + return NextResponse.json( + { success: false, message: "Mã rút gọn chỉ chứa chữ cái, chữ số, dấu gạch dưới hoặc gạch ngang (từ 3-20 ký tự)" }, + { status: 422 } + ); + } + + // Check collision + const existing = await prisma.shortUrl.findUnique({ + where: { shortCode: trimmedCode }, + }); + if (existing) { + return NextResponse.json({ success: false, message: "Mã rút gọn này đã được sử dụng. Vui lòng chọn mã khác." }, { status: 409 }); + } + shortCode = trimmedCode; + } else { + // Auto-generate using configured default_url_length + shortCode = await generateUniqueShortCode(); + } + + // Hash password if set + let hashedPassword: string | null = null; + if (password && typeof password === "string" && password.trim().length >= 4) { + hashedPassword = await bcrypt.hash(password.trim(), 10); + } + + // Process Tags + let tagConnectList: { name: string }[] = []; + if (tags && typeof tags === "string") { + const tagNames = tags + .split(",") + .map((t) => t.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "")) + .filter((t) => t.length > 0 && t.length <= 30); + + const uniqueTags = Array.from(new Set(tagNames)); + tagConnectList = uniqueTags.map((name) => ({ name })); + } + + // Parse expiration + let expirationDate: Date | null = null; + if (expires_at) { + const d = new Date(expires_at); + if (!isNaN(d.getTime())) { + expirationDate = d; + } + } + + // Attempt fast title fetch + let pageTitle: string | null = null; + try { + pageTitle = await fetchUrlTitle(url); + } catch { + // Title fetch is non-blocking + } + + // Create ShortUrl in DB + const createdUrl = await prisma.shortUrl.create({ + data: { + originalUrl: url.trim(), + shortCode, + title: pageTitle, + password: hashedPassword, + expiresAt: expirationDate, + displayOnPortal: Boolean(display_on_portal), + userId: user ? user.id : null, + tags: { + connectOrCreate: tagConnectList.map((tag) => ({ + where: { name: tag.name }, + create: { name: tag.name }, + })), + }, + }, + include: { + tags: true, + }, + }); + + // Build absolute short URL + const appUrl = (process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000").replace(/\/$/, ""); + const fullShortUrl = `${appUrl}/${shortCode}`; + + // Generate QR Code + const qrCodeDataUrl = await generateQRCodeDataUrl(fullShortUrl, { size: 280 }); + + return NextResponse.json({ + success: true, + data: { + id: createdUrl.id, + short_code: shortCode, + short_url: fullShortUrl, + original_url: createdUrl.originalUrl, + title: createdUrl.title, + qr_code: qrCodeDataUrl, + expires_at: createdUrl.expiresAt, + is_password_protected: Boolean(createdUrl.password), + display_on_portal: createdUrl.displayOnPortal, + tags: createdUrl.tags.map((t) => t.name), + }, + }); + } catch (error) { + console.error("Shorten API error:", error); + return NextResponse.json({ success: false, message: "Lỗi hệ thống khi tạo link rút gọn" }, { status: 500 }); + } +} diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts new file mode 100644 index 0000000..ca8880d --- /dev/null +++ b/src/app/api/upload/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; +import { uploadFile } from "@/lib/storage"; + +export async function POST(req: Request) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const formData = await req.formData(); + const file = formData.get("file") as File | null; + const category = (formData.get("category") as string) || "avatar"; + + if (!file) { + return NextResponse.json({ error: "Không tìm thấy tệp tải lên" }, { status: 400 }); + } + + // Validate size (max 5MB) + const MAX_SIZE = 5 * 1024 * 1024; + if (file.size > MAX_SIZE) { + return NextResponse.json({ error: "Kích thước tệp tối đa là 5MB" }, { status: 422 }); + } + + // Validate mime type + const validMimes = ["image/jpeg", "image/png", "image/webp", "image/gif", "image/x-icon", "image/vnd.microsoft.icon"]; + if (!validMimes.includes(file.type)) { + return NextResponse.json({ error: "Định dạng tệp không được hỗ trợ (chỉ nhận JPG, PNG, WEBP, ICO)" }, { status: 422 }); + } + + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + + const validCategory = (["avatar", "background", "logo", "favicon", "og_image"].includes(category) + ? category + : "avatar") as "avatar" | "background" | "logo" | "favicon" | "og_image"; + + const { url, key } = await uploadFile({ + buffer, + fileName: file.name, + mimeType: file.type, + category: validCategory, + userId: user.id, + }); + + return NextResponse.json({ + success: true, + url, + key, + }); + } catch (error) { + console.error("Upload API error:", error); + return NextResponse.json({ error: "Lỗi tải tệp lên hệ thống" }, { status: 500 }); + } +} diff --git a/src/app/api/user/urls/[id]/route.ts b/src/app/api/user/urls/[id]/route.ts new file mode 100644 index 0000000..20f7eb3 --- /dev/null +++ b/src/app/api/user/urls/[id]/route.ts @@ -0,0 +1,212 @@ +import { NextResponse } from "next/server"; +import bcrypt from "bcryptjs"; +import { getCurrentUser } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = await params; + + const url = await prisma.shortUrl.findFirst({ + where: { + id, + userId: user.role === "admin" ? undefined : user.id, + }, + include: { + tags: true, + clicks: { + orderBy: { createdAt: "desc" }, + take: 100, + }, + }, + }); + + if (!url) { + return NextResponse.json({ error: "Không tìm thấy URL" }, { status: 404 }); + } + + // Calculate 7-day clicks + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + const recentClicks = await prisma.urlClick.findMany({ + where: { + shortUrlId: id, + createdAt: { gte: sevenDaysAgo }, + }, + select: { createdAt: true }, + }); + + // Group by date + const clicksByDateMap: Record<string, number> = {}; + for (let i = 6; i >= 0; i--) { + const d = new Date(); + d.setDate(d.getDate() - i); + const dateStr = d.toISOString().split("T")[0]; + clicksByDateMap[dateStr] = 0; + } + recentClicks.forEach((c) => { + const dateStr = c.createdAt.toISOString().split("T")[0]; + if (clicksByDateMap[dateStr] !== undefined) { + clicksByDateMap[dateStr]++; + } + }); + const sevenDaysStats = Object.entries(clicksByDateMap).map(([date, count]) => ({ date, count })); + + // Breakdown by Device + const deviceCounts: Record<string, number> = {}; + // Breakdown by OS + const osCounts: Record<string, number> = {}; + // Breakdown by Country + const countryCounts: Record<string, number> = {}; + + url.clicks.forEach((c) => { + const dev = c.deviceType || "desktop"; + deviceCounts[dev] = (deviceCounts[dev] || 0) + 1; + + const os = c.operatingSystem || "Unknown"; + osCounts[os] = (osCounts[os] || 0) + 1; + + const country = c.countryName || "Unknown"; + countryCounts[country] = (countryCounts[country] || 0) + 1; + }); + + const appUrl = (process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000").replace(/\/$/, ""); + + return NextResponse.json({ + url: { + id: url.id, + original_url: url.originalUrl, + short_code: url.shortCode, + short_url: `${appUrl}/${url.shortCode}`, + title: url.title, + clicks_count: url.clicksCount, + display_on_portal: url.displayOnPortal, + is_password_protected: Boolean(url.password), + expires_at: url.expiresAt, + tags: url.tags.map((t) => t.name), + created_at: url.createdAt, + }, + analytics: { + seven_days_stats: sevenDaysStats, + devices: Object.entries(deviceCounts).map(([name, count]) => ({ name, count })), + os: Object.entries(osCounts).map(([name, count]) => ({ name, count })), + countries: Object.entries(countryCounts) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10), + total_clicks: url.clicksCount, + }, + }); +} + +export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = await params; + const body = await req.json(); + const { short_code, password, remove_password, expires_at, display_on_portal, tags, title } = body; + + const existingUrl = await prisma.shortUrl.findFirst({ + where: { + id, + userId: user.role === "admin" ? undefined : user.id, + }, + }); + + if (!existingUrl) { + return NextResponse.json({ error: "Không tìm thấy URL hoặc bạn không có quyền sửa" }, { status: 404 }); + } + + const dataToUpdate: any = {}; + + if (title !== undefined) { + dataToUpdate.title = title; + } + + if (short_code && short_code !== existingUrl.shortCode) { + const trimmed = short_code.trim(); + if (!/^[a-zA-Z0-9_-]{3,20}$/.test(trimmed)) { + return NextResponse.json({ error: "Mã rút gọn không đúng định dạng (3-20 ký tự)" }, { status: 422 }); + } + const collision = await prisma.shortUrl.findUnique({ + where: { shortCode: trimmed }, + }); + if (collision) { + return NextResponse.json({ error: "Mã rút gọn này đã tồn tại" }, { status: 409 }); + } + dataToUpdate.shortCode = trimmed; + } + + if (remove_password) { + dataToUpdate.password = null; + } else if (password && password.trim().length >= 4) { + dataToUpdate.password = await bcrypt.hash(password.trim(), 10); + } + + if (expires_at !== undefined) { + dataToUpdate.expiresAt = expires_at ? new Date(expires_at) : null; + } + + if (display_on_portal !== undefined) { + dataToUpdate.displayOnPortal = Boolean(display_on_portal); + } + + // Handle Tags + if (tags !== undefined) { + const tagNames = (typeof tags === "string" ? tags.split(",") : tags) + .map((t: string) => t.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "")) + .filter((t: string) => t.length > 0 && t.length <= 30); + + const uniqueTags = Array.from(new Set(tagNames)); + + dataToUpdate.tags = { + set: [], + connectOrCreate: uniqueTags.map((name) => ({ + where: { name: name as string }, + create: { name: name as string }, + })), + }; + } + + const updated = await prisma.shortUrl.update({ + where: { id }, + data: dataToUpdate, + include: { tags: true }, + }); + + return NextResponse.json({ success: true, url: updated }); +} + +export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = await params; + + const existingUrl = await prisma.shortUrl.findFirst({ + where: { + id, + userId: user.role === "admin" ? undefined : user.id, + }, + }); + + if (!existingUrl) { + return NextResponse.json({ error: "Không tìm thấy URL hoặc bạn không có quyền xóa" }, { status: 404 }); + } + + await prisma.shortUrl.delete({ + where: { id }, + }); + + return NextResponse.json({ success: true, message: "Đã xóa URL thành công" }); +} diff --git a/src/app/api/user/urls/[id]/toggle-display/route.ts b/src/app/api/user/urls/[id]/toggle-display/route.ts new file mode 100644 index 0000000..4b8f8e6 --- /dev/null +++ b/src/app/api/user/urls/[id]/toggle-display/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = await params; + const { display_on_portal } = await req.json(); + + const url = await prisma.shortUrl.findFirst({ + where: { + id, + userId: user.role === "admin" ? undefined : user.id, + }, + }); + + if (!url) { + return NextResponse.json({ error: "Không tìm thấy URL" }, { status: 404 }); + } + + const updated = await prisma.shortUrl.update({ + where: { id }, + data: { displayOnPortal: Boolean(display_on_portal) }, + }); + + return NextResponse.json({ success: true, display_on_portal: updated.displayOnPortal }); +} diff --git a/src/app/api/user/urls/bulk-delete/route.ts b/src/app/api/user/urls/bulk-delete/route.ts new file mode 100644 index 0000000..6ad2d9c --- /dev/null +++ b/src/app/api/user/urls/bulk-delete/route.ts @@ -0,0 +1,34 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function POST(req: Request) { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { ids } = await req.json(); + + if (!ids || !Array.isArray(ids) || ids.length === 0) { + return NextResponse.json({ error: "Vui lòng chọn ít nhất một URL để xóa" }, { status: 400 }); + } + + const where: any = { + id: { in: ids }, + }; + + if (user.role !== "admin") { + where.userId = user.id; + } + + const result = await prisma.shortUrl.deleteMany({ + where, + }); + + return NextResponse.json({ + success: true, + deletedCount: result.count, + message: `Đã xóa ${result.count} liên kết thành công`, + }); +} diff --git a/src/app/api/user/urls/route.ts b/src/app/api/user/urls/route.ts new file mode 100644 index 0000000..1597383 --- /dev/null +++ b/src/app/api/user/urls/route.ts @@ -0,0 +1,84 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET(req: Request) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(req.url); + const search = searchParams.get("search") || ""; + const tag = searchParams.get("tag") || ""; + const sort = searchParams.get("sort") || "createdAt"; + const direction = searchParams.get("direction") === "asc" ? "asc" : "desc"; + const page = parseInt(searchParams.get("page") || "1", 10); + const limit = parseInt(searchParams.get("limit") || "10", 10); + + const where: any = { + userId: user.id, + }; + + if (search) { + where.OR = [ + { originalUrl: { contains: search, mode: "insensitive" } }, + { shortCode: { contains: search, mode: "insensitive" } }, + { title: { contains: search, mode: "insensitive" } }, + ]; + } + + if (tag) { + where.tags = { + some: { + name: { contains: tag, mode: "insensitive" }, + }, + }; + } + + const [total, urls] = await Promise.all([ + prisma.shortUrl.count({ where }), + prisma.shortUrl.findMany({ + where, + include: { + tags: true, + _count: { + select: { clicks: true }, + }, + }, + orderBy: sort === "clicks" ? { clicksCount: direction } : { createdAt: direction }, + skip: (page - 1) * limit, + take: limit, + }), + ]); + + const appUrl = (process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000").replace(/\/$/, ""); + + const formattedUrls = urls.map((u) => ({ + id: u.id, + original_url: u.originalUrl, + short_code: u.shortCode, + short_url: `${appUrl}/${u.shortCode}`, + title: u.title || "Chưa có tiêu đề", + clicks_count: u.clicksCount, + display_on_portal: u.displayOnPortal, + is_password_protected: Boolean(u.password), + expires_at: u.expiresAt, + tags: u.tags.map((t) => t.name), + created_at: u.createdAt, + })); + + return NextResponse.json({ + success: true, + total, + page, + limit, + totalPages: Math.ceil(total / limit), + urls: formattedUrls, + }); + } catch (error) { + console.error("User URLs error:", error); + return NextResponse.json({ error: "Lỗi tải danh sách link" }, { status: 500 }); + } +} diff --git a/src/app/api/verify-password/route.ts b/src/app/api/verify-password/route.ts new file mode 100644 index 0000000..47de612 --- /dev/null +++ b/src/app/api/verify-password/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from "next/server"; +import bcrypt from "bcryptjs"; +import { cookies } from "next/headers"; +import { prisma } from "@/lib/prisma"; + +export async function POST(req: Request) { + try { + const { shortCode, password } = await req.json(); + + if (!shortCode || !password) { + return NextResponse.json({ error: "Vui lòng nhập mật khẩu" }, { status: 400 }); + } + + const url = await prisma.shortUrl.findUnique({ + where: { shortCode }, + }); + + if (!url || !url.password) { + return NextResponse.json({ error: "Không tìm thấy liên kết hoặc liên kết không có mật khẩu" }, { status: 404 }); + } + + const match = await bcrypt.compare(password, url.password); + if (!match) { + return NextResponse.json({ error: "Mật khẩu không chính xác" }, { status: 401 }); + } + + // Set verified cookie for this specific shortCode + const cookieStore = await cookies(); + cookieStore.set(`verified_${shortCode}`, "true", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + maxAge: 3600, // 1 hour + path: "/", + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Verify password error:", error); + return NextResponse.json({ error: "Lỗi xác thực mật khẩu" }, { status: 500 }); + } +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..7498a73 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { + Link2, + MousePointerClick, + Sparkles, + ArrowUpRight, + TrendingUp, + Plus, + Copy, + Check, +} from "lucide-react"; + +export default function UserDashboardOverviewPage() { + const [user, setUser] = useState<any>(null); + const [urls, setUrls] = useState<any[]>([]); + const [loading, setLoading] = useState(true); + + // Quick shorten + const [quickUrl, setQuickUrl] = useState(""); + const [shortening, setShortening] = useState(false); + const [result, setResult] = useState<any>(null); + const [copied, setCopied] = useState(false); + + useEffect(() => { + async function loadData() { + try { + const [meRes, urlsRes] = await Promise.all([ + fetch("/api/auth/me"), + fetch("/api/user/urls?limit=7"), + ]); + const meData = await meRes.json(); + const urlsData = await urlsRes.json(); + + setUser(meData.user); + if (urlsData.urls) setUrls(urlsData.urls); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + } + loadData(); + }, []); + + async function handleQuickShorten(e: React.FormEvent) { + e.preventDefault(); + if (!quickUrl) return; + setShortening(true); + + try { + const res = await fetch("/api/shorten", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: quickUrl }), + }); + const data = await res.json(); + if (data.success) { + setResult(data.data); + setQuickUrl(""); + // Refresh URLs + const updated = await fetch("/api/user/urls?limit=7"); + const json = await updated.json(); + if (json.urls) setUrls(json.urls); + } else { + alert(data.message || "Lỗi tạo link"); + } + } catch { + alert("Lỗi kết nối"); + } finally { + setShortening(false); + } + } + + const totalClicks = urls.reduce((acc, u) => acc + (u.clicks_count || 0), 0); + + return ( + <div className="max-w-7xl mx-auto px-4 py-8 space-y-8"> + {/* Welcome Banner */} + <div className="relative overflow-hidden p-8 rounded-3xl bg-gradient-to-r from-indigo-600 via-indigo-700 to-purple-800 text-white shadow-xl"> + <div className="relative z-10 max-w-2xl"> + <span className="inline-block px-3 py-1 rounded-full bg-white/10 backdrop-blur-md text-xs font-semibold uppercase tracking-wider mb-3"> + Bảng điều khiển cá nhân + </span> + <h1 className="text-3xl font-extrabold tracking-tight mb-2"> + Xin chào, {user?.name || "Bạn"}! 👋 + </h1> + <p className="text-indigo-100 text-sm leading-relaxed mb-6"> + Quản lý tất cả liên kết rút gọn, tùy biến trang Bio Link thương hiệu và theo dõi lưu lượng truy cập tại một nơi duy nhất. + </p> + <div className="flex flex-wrap gap-3"> + <Link + href="/dashboard/portal" + className="px-5 py-2.5 rounded-xl bg-white text-indigo-700 font-semibold text-sm shadow-md hover:bg-indigo-50 transition-all flex items-center gap-2" + > + <Sparkles className="w-4 h-4 text-purple-600" /> + Chỉnh sửa Bio Link + </Link> + <Link + href="/dashboard/urls" + className="px-5 py-2.5 rounded-xl bg-white/10 hover:bg-white/20 backdrop-blur-md text-white font-semibold text-sm border border-white/20 transition-all flex items-center gap-2" + > + <Link2 className="w-4 h-4" /> + Danh sách liên kết + </Link> + </div> + </div> + </div> + + {/* Metrics Row */} + <div className="grid grid-cols-1 sm:grid-cols-3 gap-6"> + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm flex items-center gap-4"> + <div className="w-14 h-14 rounded-2xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center shrink-0"> + <Link2 className="w-7 h-7" /> + </div> + <div> + <div className="text-xs font-semibold uppercase tracking-wider text-slate-400"> + Tổng số Link + </div> + <div className="text-2xl font-bold text-slate-900 dark:text-white mt-0.5"> + {urls.length} + </div> + </div> + </div> + + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm flex items-center gap-4"> + <div className="w-14 h-14 rounded-2xl bg-purple-500/10 text-purple-600 dark:text-purple-400 flex items-center justify-center shrink-0"> + <MousePointerClick className="w-7 h-7" /> + </div> + <div> + <div className="text-xs font-semibold uppercase tracking-wider text-slate-400"> + Lượt nhấp chuột + </div> + <div className="text-2xl font-bold text-slate-900 dark:text-white mt-0.5"> + {totalClicks} + </div> + </div> + </div> + + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm flex items-center gap-4"> + <div className="w-14 h-14 rounded-2xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center shrink-0"> + <TrendingUp className="w-7 h-7" /> + </div> + <div> + <div className="text-xs font-semibold uppercase tracking-wider text-slate-400"> + Trang Bio Link + </div> + <div className="text-sm font-bold text-slate-900 dark:text-white mt-0.5 truncate"> + {user?.username ? `@${user.username}` : "Sẵn sàng"} + </div> + </div> + </div> + </div> + + {/* Quick Shorten Bar */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm"> + <h2 className="text-base font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2"> + <Plus className="w-4 h-4 text-indigo-500" /> + Rút gọn link nhanh + </h2> + <form onSubmit={handleQuickShorten} className="flex flex-col sm:flex-row gap-3"> + <input + type="url" + required + value={quickUrl} + onChange={(e) => setQuickUrl(e.target.value)} + placeholder="Dán đường link dài của bạn vào đây..." + className="flex-1 px-4 py-3 rounded-2xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + <button + type="submit" + disabled={shortening} + className="px-6 py-3 rounded-2xl bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm shadow-md transition-all shrink-0 disabled:opacity-50" + > + {shortening ? "Đang rút gọn..." : "Rút gọn ngay"} + </button> + </form> + + {result && ( + <div className="mt-4 p-4 rounded-2xl bg-emerald-50 dark:bg-emerald-950/40 border border-emerald-200 dark:border-emerald-800 flex flex-col sm:flex-row items-center justify-between gap-4"> + <div className="flex items-center gap-3 overflow-hidden"> + <span className="text-xs font-bold uppercase px-2 py-0.5 rounded bg-emerald-500 text-white"> + Thành công + </span> + <a + href={result.short_url} + target="_blank" + rel="noopener noreferrer" + className="font-mono text-sm font-semibold text-emerald-800 dark:text-emerald-300 hover:underline truncate" + > + {result.short_url} + </a> + </div> + <button + onClick={() => { + navigator.clipboard.writeText(result.short_url); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }} + className="px-4 py-2 rounded-xl bg-white dark:bg-slate-800 text-xs font-semibold text-slate-800 dark:text-slate-200 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 flex items-center gap-1.5 shrink-0 shadow-sm" + > + {copied ? <Check className="w-3.5 h-3.5 text-emerald-600" /> : <Copy className="w-3.5 h-3.5" />} + {copied ? "Đã sao chép!" : "Sao chép link"} + </button> + </div> + )} + </div> + + {/* Recent URLs Section */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm"> + <div className="flex items-center justify-between mb-4"> + <h2 className="text-base font-bold text-slate-900 dark:text-white"> + Các liên kết gần đây + </h2> + <Link + href="/dashboard/urls" + className="text-xs font-semibold text-indigo-600 dark:text-indigo-400 hover:underline flex items-center gap-1" + > + Xem tất cả + <ArrowUpRight className="w-3.5 h-3.5" /> + </Link> + </div> + + {loading ? ( + <div className="py-8 text-center text-slate-400 text-sm">Đang tải dữ liệu...</div> + ) : urls.length === 0 ? ( + <div className="py-8 text-center text-slate-400 text-sm">Chưa có liên kết nào được tạo.</div> + ) : ( + <div className="divide-y divide-slate-100 dark:divide-slate-800"> + {urls.map((u) => ( + <div key={u.id} className="py-3.5 flex items-center justify-between gap-4"> + <div className="flex-1 overflow-hidden"> + <div className="font-medium text-slate-900 dark:text-white text-sm truncate"> + {u.title || u.short_code} + </div> + <div className="text-xs text-slate-400 font-mono truncate">{u.short_url}</div> + </div> + <div className="text-xs font-bold text-slate-600 dark:text-slate-300 shrink-0"> + {u.clicks_count} clicks + </div> + </div> + ))} + </div> + )} + </div> + </div> + ); +} diff --git a/src/app/dashboard/portal/page.tsx b/src/app/dashboard/portal/page.tsx new file mode 100644 index 0000000..785d06c --- /dev/null +++ b/src/app/dashboard/portal/page.tsx @@ -0,0 +1,517 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Upload, + Save, + Trash2, + Sparkles, + Smartphone, + CheckCircle, + AlertCircle, + ExternalLink, +} from "lucide-react"; +import PortalView from "@/components/PortalView"; +import { GRADIENT_PRESETS } from "@/lib/theme-presets"; + +export default function PortalEditorPage() { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [successMsg, setSuccessMsg] = useState(""); + const [errorMsg, setErrorMsg] = useState(""); + + const [user, setUser] = useState<{ name: string; username: string }>({ + name: "User", + username: "user", + }); + + const [portal, setPortal] = useState({ + title: "", + description: "", + avatar: "", + backgroundImage: "", + headerColor: "#000000", + titleColorStyle: "gradient-1", + backgroundStyle: "gradient-1", + vcard: { + fullName: "", + phone: "", + email: "", + company: "", + website: "", + }, + socialLinks: { + facebook: "", + twitter: "", + instagram: "", + threads: "", + tiktok: "", + youtube: "", + telegram: "", + }, + }); + + const [sharedUrls, setSharedUrls] = useState<any[]>([]); + + useEffect(() => { + async function loadPortalData() { + try { + const [meRes, portalRes, urlsRes] = await Promise.all([ + fetch("/api/auth/me"), + fetch("/api/portal"), + fetch("/api/user/urls?limit=10"), + ]); + + const meData = await meRes.json(); + if (meData.user) { + setUser({ name: meData.user.name, username: meData.user.username }); + } + + const portalData = await portalRes.json(); + if (portalData.portal) { + const p = portalData.portal; + setPortal({ + title: p.title || "", + description: p.description || "", + avatar: p.avatar || "", + backgroundImage: p.backgroundImage || "", + headerColor: p.headerColor || "#000000", + titleColorStyle: p.titleColorStyle || "gradient-1", + backgroundStyle: p.backgroundStyle || "gradient-1", + vcard: { + fullName: p.vcard?.fullName || "", + phone: p.vcard?.phone || "", + email: p.vcard?.email || "", + company: p.vcard?.company || "", + website: p.vcard?.website || "", + }, + socialLinks: { + facebook: p.socialLinks?.facebook || "", + twitter: p.socialLinks?.twitter || "", + instagram: p.socialLinks?.instagram || "", + threads: p.socialLinks?.threads || "", + tiktok: p.socialLinks?.tiktok || "", + youtube: p.socialLinks?.youtube || "", + telegram: p.socialLinks?.telegram || "", + }, + }); + } + + const urlsData = await urlsRes.json(); + if (urlsData.urls) { + setSharedUrls(urlsData.urls.filter((u: any) => u.display_on_portal)); + } + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + } + + loadPortalData(); + }, []); + + async function handleFileUpload(file: File, category: "avatar" | "background") { + const formData = new FormData(); + formData.append("file", file); + formData.append("category", category); + + try { + const res = await fetch("/api/upload", { + method: "POST", + body: formData, + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + + if (category === "avatar") { + setPortal((prev) => ({ ...prev, avatar: data.url })); + } else { + setPortal((prev) => ({ ...prev, backgroundImage: data.url })); + } + } catch (err: any) { + alert(err.message || "Lỗi tải ảnh lên"); + } + } + + async function handleSave() { + setSaving(true); + setSuccessMsg(""); + setErrorMsg(""); + + try { + const res = await fetch("/api/portal", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(portal), + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + + setSuccessMsg("Cập nhật trang Bio thành công!"); + setTimeout(() => setSuccessMsg(""), 4000); + } catch (err: any) { + setErrorMsg(err.message || "Lỗi lưu cấu hình"); + } finally { + setSaving(false); + } + } + + if (loading) { + return ( + <div className="flex items-center justify-center min-h-[60vh]"> + <div className="text-slate-400 animate-pulse text-sm">Đang tải cấu hình Bio Link...</div> + </div> + ); + } + + const appUrl = (typeof window !== "undefined" ? window.location.origin : ""); + const publicPortalUrl = `${appUrl}/portal/${user.username}`; + + return ( + <div className="max-w-7xl mx-auto px-4 py-8"> + {/* Top Banner */} + <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 bg-white/70 dark:bg-slate-900/70 backdrop-blur-xl border border-white/40 dark:border-slate-800 p-6 rounded-3xl shadow-sm"> + <div> + <h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white flex items-center gap-2"> + <Sparkles className="w-6 h-6 text-indigo-500" /> + Tùy biến Trang Bio cá nhân + </h1> + <p className="text-sm text-slate-500 dark:text-slate-400 mt-1"> + Thiết kế trang liên kết đại diện cá nhân hóa với bản xem trước thời gian thực + </p> + </div> + + <div className="flex items-center gap-3"> + <a + href={publicPortalUrl} + target="_blank" + rel="noopener noreferrer" + className="px-4 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 text-sm font-semibold hover:bg-slate-50 dark:hover:bg-slate-700 flex items-center gap-2 transition-all shadow-sm" + > + <ExternalLink className="w-4 h-4" /> + Xem trang công khai + </a> + <button + onClick={handleSave} + disabled={saving} + className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold text-sm shadow-md hover:shadow-indigo-500/25 transition-all flex items-center gap-2 disabled:opacity-50" + > + <Save className="w-4 h-4" /> + {saving ? "Đang lưu..." : "Lưu thay đổi"} + </button> + </div> + </div> + + {successMsg && ( + <div className="mb-6 p-4 rounded-2xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 dark:text-emerald-400 flex items-center gap-2 text-sm"> + <CheckCircle className="w-5 h-5 shrink-0" /> + <span>{successMsg}</span> + </div> + )} + + {errorMsg && ( + <div className="mb-6 p-4 rounded-2xl bg-red-500/10 border border-red-500/30 text-red-600 dark:text-red-400 flex items-center gap-2 text-sm"> + <AlertCircle className="w-5 h-5 shrink-0" /> + <span>{errorMsg}</span> + </div> + )} + + {/* Main 2-column Grid */} + <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start"> + {/* Left Column: Form Controls (7 cols) */} + <div className="lg:col-span-7 space-y-6"> + {/* Avatar Section */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-sm"> + <h2 className="text-base font-bold text-slate-900 dark:text-white mb-4"> + Ảnh đại diện (Avatar) + </h2> + <div className="flex items-center gap-5"> + <div className="w-20 h-20 rounded-full overflow-hidden border-2 border-indigo-200 dark:border-indigo-900 bg-slate-100 dark:bg-slate-800 shrink-0"> + {portal.avatar ? ( + // eslint-disable-next-line @next/next/no-img-element + <img src={portal.avatar} alt="Avatar" className="w-full h-full object-cover" /> + ) : ( + <div className="w-full h-full flex items-center justify-center text-slate-400 text-xs"> + Chưa có + </div> + )} + </div> + <div className="flex-1"> + <label className="cursor-pointer inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 text-sm font-semibold transition-all"> + <Upload className="w-4 h-4" /> + Tải ảnh lên + <input + type="file" + accept="image/*" + className="hidden" + onChange={(e) => { + if (e.target.files?.[0]) handleFileUpload(e.target.files[0], "avatar"); + }} + /> + </label> + <p className="text-xs text-slate-400 mt-1.5"> + Định dạng JPG, PNG hoặc WEBP (Tối đa 2MB). Tự động lưu lên Cloudflare R2. + </p> + </div> + </div> + </div> + + {/* Basic Information */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-sm space-y-4"> + <h2 className="text-base font-bold text-slate-900 dark:text-white"> + Thông tin cơ bản + </h2> + <div> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1.5"> + Tiêu đề trang (Title) + </label> + <input + type="text" + value={portal.title} + onChange={(e) => setPortal({ ...portal, title: e.target.value })} + placeholder="VD: Trần Tiến - Digital Creator" + className="w-full px-4 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + <div> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1.5"> + Mô tả ngắn (Bio Description) + </label> + <textarea + rows={3} + value={portal.description} + onChange={(e) => setPortal({ ...portal, description: e.target.value })} + placeholder="Viết một vài dòng giới thiệu về bạn hoặc dự án của bạn..." + className="w-full px-4 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + </div> + + {/* Appearance & Themes */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-sm space-y-6"> + <h2 className="text-base font-bold text-slate-900 dark:text-white"> + Giao diện & Hiệu ứng màu sắc + </h2> + + {/* Title Gradient Style */} + <div> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2"> + Kiểu Gradient cho Tiêu đề + </label> + <div className="grid grid-cols-3 gap-2.5"> + {Object.values(GRADIENT_PRESETS).map((preset) => ( + <button + key={preset.id} + type="button" + onClick={() => setPortal({ ...portal, titleColorStyle: preset.id })} + className={`h-11 rounded-xl font-bold text-xs flex items-center justify-center border-2 transition-all ${ + portal.titleColorStyle === preset.id + ? "border-indigo-500 scale-105 shadow-md bg-indigo-50/20" + : "border-slate-200 dark:border-slate-700 hover:border-slate-300" + }`} + > + <span className={preset.titleStyle}>Aa {preset.name}</span> + </button> + ))} + </div> + </div> + + {/* Background Style */} + <div> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2"> + Kiểu màu nền Gradient + </label> + <div className="grid grid-cols-3 gap-2.5"> + {Object.values(GRADIENT_PRESETS).map((preset) => ( + <button + key={preset.id} + type="button" + onClick={() => setPortal({ ...portal, backgroundStyle: preset.id, backgroundImage: "" })} + className={`h-16 rounded-xl border-2 transition-all relative overflow-hidden ${ + portal.backgroundStyle === preset.id && !portal.backgroundImage + ? "border-indigo-500 scale-105 shadow-md ring-2 ring-indigo-500/20" + : "border-slate-200 dark:border-slate-700 hover:border-slate-300" + }`} + style={{ background: preset.background }} + > + <span className="absolute bottom-1 right-2 text-[10px] font-bold text-slate-900 bg-white/70 px-1.5 py-0.5 rounded backdrop-blur-xs"> + {preset.name} + </span> + </button> + ))} + </div> + </div> + + {/* Custom Background Image */} + <div className="pt-2"> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2"> + Hoặc tải lên Ảnh nền tùy chỉnh + </label> + <div className="flex items-center gap-4"> + {portal.backgroundImage ? ( + <div className="relative w-28 h-16 rounded-xl overflow-hidden border border-slate-300 shadow-sm shrink-0"> + {/* eslint-disable-next-line @next/next/no-img-element */} + <img src={portal.backgroundImage} alt="Background" className="w-full h-full object-cover" /> + <button + onClick={() => setPortal({ ...portal, backgroundImage: "" })} + className="absolute top-1 right-1 p-1 bg-red-600 text-white rounded-full hover:bg-red-700 text-xs shadow-md" + title="Xóa ảnh nền" + > + <Trash2 className="w-3.5 h-3.5" /> + </button> + </div> + ) : ( + <label className="cursor-pointer inline-flex items-center gap-2 px-4 py-2.5 rounded-xl border border-dashed border-slate-300 dark:border-slate-700 hover:border-indigo-500 text-slate-600 dark:text-slate-300 text-xs font-semibold transition-all"> + <Upload className="w-4 h-4" /> + Chọn ảnh nền JPG / PNG + <input + type="file" + accept="image/*" + className="hidden" + onChange={(e) => { + if (e.target.files?.[0]) handleFileUpload(e.target.files[0], "background"); + }} + /> + </label> + )} + </div> + </div> + </div> + + {/* Social Links */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-sm space-y-3"> + <h2 className="text-base font-bold text-slate-900 dark:text-white mb-2"> + Liên kết Mạng xã hội + </h2> + {Object.keys(portal.socialLinks).map((network) => ( + <div key={network} className="flex items-center gap-3"> + <span className="w-24 text-xs font-semibold uppercase text-slate-500 capitalize"> + {network} + </span> + <input + type="url" + value={(portal.socialLinks as any)[network] || ""} + onChange={(e) => + setPortal({ + ...portal, + socialLinks: { ...portal.socialLinks, [network]: e.target.value }, + }) + } + placeholder={`https://${network}.com/...`} + className="flex-1 px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + ))} + </div> + + {/* Contact info (vCard) */} + <div className="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-sm space-y-3"> + <h2 className="text-base font-bold text-slate-900 dark:text-white mb-2"> + Danh thiếp điện tử (vCard Contact) + </h2> + <div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> + <div> + <label className="block text-[11px] font-semibold text-slate-500 uppercase mb-1"> + Họ và tên + </label> + <input + type="text" + value={portal.vcard.fullName} + onChange={(e) => + setPortal({ + ...portal, + vcard: { ...portal.vcard, fullName: e.target.value }, + }) + } + placeholder="Nguyễn Văn A" + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + <div> + <label className="block text-[11px] font-semibold text-slate-500 uppercase mb-1"> + Số điện thoại + </label> + <input + type="tel" + value={portal.vcard.phone} + onChange={(e) => + setPortal({ + ...portal, + vcard: { ...portal.vcard, phone: e.target.value }, + }) + } + placeholder="+84 901 234 567" + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + <div> + <label className="block text-[11px] font-semibold text-slate-500 uppercase mb-1"> + Email + </label> + <input + type="email" + value={portal.vcard.email} + onChange={(e) => + setPortal({ + ...portal, + vcard: { ...portal.vcard, email: e.target.value }, + }) + } + placeholder="contact@domain.com" + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + <div> + <label className="block text-[11px] font-semibold text-slate-500 uppercase mb-1"> + Công ty / Tổ chức + </label> + <input + type="text" + value={portal.vcard.company} + onChange={(e) => + setPortal({ + ...portal, + vcard: { ...portal.vcard, company: e.target.value }, + }) + } + placeholder="EZBio Tech Corp" + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + </div> + </div> + </div> + + {/* Right Column: Sticky Live Phone Mockup (5 cols) */} + <div className="lg:col-span-5 sticky top-8"> + <div className="flex flex-col items-center"> + <div className="flex items-center gap-2 mb-3 text-xs font-semibold text-slate-500 dark:text-slate-400"> + <Smartphone className="w-4 h-4 text-indigo-500" /> + Xem trước trực tiếp (Live Phone Preview) + </div> + + {/* Smartphone Frame */} + <div className="relative w-[340px] h-[680px] bg-slate-900 rounded-[3rem] p-3 shadow-2xl border-4 border-slate-800 ring-1 ring-slate-950/20 overflow-hidden flex flex-col"> + {/* Notch / Speaker */} + <div className="absolute top-4 left-1/2 -translate-x-1/2 w-28 h-5 bg-slate-900 rounded-full z-20 flex items-center justify-center"> + <div className="w-10 h-1 bg-slate-700 rounded-full" /> + </div> + + {/* Screen Content */} + <div className="w-full h-full rounded-[2.3rem] overflow-y-auto no-scrollbar bg-slate-100 z-10 pt-4"> + <PortalView + portal={portal} + user={user} + urls={sharedUrls} + isEmbedPreview={true} + /> + </div> + </div> + </div> + </div> + </div> + </div> + ); +} diff --git a/src/app/dashboard/urls/page.tsx b/src/app/dashboard/urls/page.tsx new file mode 100644 index 0000000..cbbae23 --- /dev/null +++ b/src/app/dashboard/urls/page.tsx @@ -0,0 +1,612 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Link2, + Plus, + Search, + Trash2, + Edit3, + QrCode, + BarChart2, + Eye, + EyeOff, + Copy, + Check, + X, + ExternalLink, +} from "lucide-react"; + +export default function UserUrlsPage() { + const [urls, setUrls] = useState<any[]>([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(""); + const [selectedIds, setSelectedIds] = useState<string[]>([]); + const [copiedId, setCopiedId] = useState<string | null>(null); + + // Modals + const [showCreateModal, setShowCreateModal] = useState(false); + const [editUrl, setEditUrl] = useState<any | null>(null); + const [activeQr, setActiveQr] = useState<{ short_url: string; qr_code?: string } | null>(null); + + // Form states for create + const [formUrl, setFormUrl] = useState(""); + const [formCustomCode, setFormCustomCode] = useState(""); + const [formPassword, setFormPassword] = useState(""); + const [formTags, setFormTags] = useState(""); + const [formExpiresAt, setFormExpiresAt] = useState(""); + const [formDisplayOnPortal, setFormDisplayOnPortal] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [errorMsg, setErrorMsg] = useState(""); + + async function fetchUrls() { + setLoading(true); + try { + const res = await fetch(`/api/user/urls?search=${encodeURIComponent(search)}`); + const data = await res.json(); + if (data.urls) { + setUrls(data.urls); + } + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + } + + useEffect(() => { + fetchUrls(); + }, [search]); + + async function handleCreate(e: React.FormEvent) { + e.preventDefault(); + setSubmitting(true); + setErrorMsg(""); + + try { + const res = await fetch("/api/shorten", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: formUrl, + custom_code: formCustomCode || undefined, + password: formPassword || undefined, + tags: formTags || undefined, + expires_at: formExpiresAt || undefined, + display_on_portal: formDisplayOnPortal, + }), + }); + + const data = await res.json(); + if (!res.ok || !data.success) { + throw new Error(data.message || "Lỗi tạo link rút gọn"); + } + + setShowCreateModal(false); + setFormUrl(""); + setFormCustomCode(""); + setFormPassword(""); + setFormTags(""); + setFormExpiresAt(""); + setFormDisplayOnPortal(false); + fetchUrls(); + } catch (err: any) { + setErrorMsg(err.message); + } finally { + setSubmitting(false); + } + } + + async function handleUpdate(e: React.FormEvent) { + e.preventDefault(); + if (!editUrl) return; + setSubmitting(true); + + try { + const res = await fetch(`/api/user/urls/${editUrl.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title: editUrl.title, + short_code: editUrl.short_code, + display_on_portal: editUrl.display_on_portal, + tags: editUrl.tags, + expires_at: editUrl.expires_at, + remove_password: editUrl.remove_password, + password: editUrl.new_password || undefined, + }), + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Lỗi cập nhật"); + + setEditUrl(null); + fetchUrls(); + } catch (err: any) { + alert(err.message); + } finally { + setSubmitting(false); + } + } + + async function handleToggleDisplay(id: string, currentStatus: boolean) { + // Optimistic update + setUrls((prev) => + prev.map((u) => (u.id === id ? { ...u, display_on_portal: !currentStatus } : u)) + ); + + try { + await fetch(`/api/user/urls/${id}/toggle-display`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ display_on_portal: !currentStatus }), + }); + } catch { + fetchUrls(); + } + } + + async function handleDelete(id: string) { + if (!confirm("Bạn có chắc chắn muốn xóa liên kết này?")) return; + try { + await fetch(`/api/user/urls/${id}`, { method: "DELETE" }); + fetchUrls(); + } catch (err) { + console.error(err); + } + } + + async function handleBulkDelete() { + if (!confirm(`Bạn có chắc muốn xóa ${selectedIds.length} liên kết đã chọn?`)) return; + try { + await fetch("/api/user/urls/bulk-delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ids: selectedIds }), + }); + setSelectedIds([]); + fetchUrls(); + } catch (err) { + console.error(err); + } + } + + function copyToClipboard(id: string, text: string) { + navigator.clipboard.writeText(text); + setCopiedId(id); + setTimeout(() => setCopiedId(null), 2000); + } + + async function showQr(shortUrl: string) { + setActiveQr({ short_url: shortUrl }); + try { + const res = await fetch(`/api/qr?content=${encodeURIComponent(shortUrl)}&size=300`); + const data = await res.json(); + setActiveQr({ short_url: shortUrl, qr_code: data.qr_code }); + } catch { + // + } + } + + return ( + <div className="max-w-7xl mx-auto px-4 py-8"> + {/* Header */} + <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8"> + <div> + <h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white flex items-center gap-2"> + <Link2 className="w-6 h-6 text-indigo-500" /> + Quản lý Liên kết rút gọn + </h1> + <p className="text-sm text-slate-500 dark:text-slate-400 mt-1"> + Tạo mới, chỉnh sửa, bật/tắt hiển thị trên Bio Link và theo dõi hiệu suất liên kết + </p> + </div> + + <button + onClick={() => setShowCreateModal(true)} + className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold text-sm shadow-md hover:shadow-indigo-500/25 transition-all flex items-center justify-center gap-2 shrink-0" + > + <Plus className="w-4 h-4" /> + Rút gọn link mới + </button> + </div> + + {/* Filter and Bulk Action Toolbar */} + <div className="flex flex-col sm:flex-row items-center justify-between gap-4 mb-6"> + <div className="relative w-full sm:w-80"> + <Search className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" /> + <input + type="text" + value={search} + onChange={(e) => setSearch(e.target.value)} + placeholder="Tìm theo URL, mã hoặc thẻ..." + className="w-full pl-10 pr-4 py-2 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm" + /> + </div> + + {selectedIds.length > 0 && ( + <button + onClick={handleBulkDelete} + className="px-4 py-2 rounded-xl bg-red-500/10 hover:bg-red-500/20 text-red-600 dark:text-red-400 border border-red-500/30 text-xs font-semibold flex items-center gap-2 transition-all" + > + <Trash2 className="w-4 h-4" /> + Xóa {selectedIds.length} mục đã chọn + </button> + )} + </div> + + {/* URLs Table */} + <div className="bg-white dark:bg-slate-900 rounded-3xl border border-slate-200/80 dark:border-slate-800 shadow-sm overflow-hidden"> + <div className="overflow-x-auto"> + <table className="w-full text-left text-sm"> + <thead className="bg-slate-50 dark:bg-slate-800/50 text-xs font-semibold uppercase text-slate-500 dark:text-slate-400 border-b border-slate-200 dark:border-slate-800"> + <tr> + <th className="p-4 w-10"> + <input + type="checkbox" + checked={selectedIds.length > 0 && selectedIds.length === urls.length} + onChange={(e) => { + if (e.target.checked) setSelectedIds(urls.map((u) => u.id)); + else setSelectedIds([]); + }} + className="rounded text-indigo-600 focus:ring-indigo-500" + /> + </th> + <th className="p-4">Tiêu đề & URL gốc</th> + <th className="p-4">Link ngắn</th> + <th className="p-4 text-center">Lượt click</th> + <th className="p-4 text-center">Hiện trên Bio</th> + <th className="p-4 text-right">Thao tác</th> + </tr> + </thead> + <tbody className="divide-y divide-slate-100 dark:divide-slate-800"> + {loading ? ( + <tr> + <td colSpan={6} className="p-8 text-center text-slate-400"> + Đang tải danh sách liên kết... + </td> + </tr> + ) : urls.length === 0 ? ( + <tr> + <td colSpan={6} className="p-8 text-center text-slate-400"> + Không tìm thấy liên kết nào. Hãy tạo link rút gọn đầu tiên! + </td> + </tr> + ) : ( + urls.map((u) => ( + <tr key={u.id} className="hover:bg-slate-50/60 dark:hover:bg-slate-800/30 transition-colors"> + <td className="p-4"> + <input + type="checkbox" + checked={selectedIds.includes(u.id)} + onChange={(e) => { + if (e.target.checked) setSelectedIds([...selectedIds, u.id]); + else setSelectedIds(selectedIds.filter((id) => id !== u.id)); + }} + className="rounded text-indigo-600 focus:ring-indigo-500" + /> + </td> + <td className="p-4 max-w-xs sm:max-w-md"> + <div className="font-semibold text-slate-900 dark:text-white truncate"> + {u.title} + </div> + <div className="text-xs text-slate-400 truncate flex items-center gap-1 mt-0.5"> + <span className="truncate">{u.original_url}</span> + </div> + {u.tags && u.tags.length > 0 && ( + <div className="flex flex-wrap gap-1 mt-1.5"> + {u.tags.map((t: string) => ( + <span + key={t} + className="px-1.5 py-0.5 text-[10px] rounded bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300" + > + #{t} + </span> + ))} + </div> + )} + </td> + <td className="p-4 font-mono text-xs"> + <div className="flex items-center gap-1.5"> + <a + href={u.short_url} + target="_blank" + rel="noopener noreferrer" + className="text-indigo-600 dark:text-indigo-400 hover:underline flex items-center gap-1" + > + /{u.short_code} + <ExternalLink className="w-3 h-3 opacity-60" /> + </a> + <button + onClick={() => copyToClipboard(u.id, u.short_url)} + className="p-1 text-slate-400 hover:text-slate-700 dark:hover:text-white transition-colors" + title="Sao chép" + > + {copiedId === u.id ? ( + <Check className="w-3.5 h-3.5 text-emerald-500" /> + ) : ( + <Copy className="w-3.5 h-3.5" /> + )} + </button> + </div> + </td> + <td className="p-4 text-center"> + <span className="inline-flex items-center gap-1 font-bold text-slate-800 dark:text-slate-200"> + <BarChart2 className="w-3.5 h-3.5 text-indigo-500" /> + {u.clicks_count} + </span> + </td> + <td className="p-4 text-center"> + <button + onClick={() => handleToggleDisplay(u.id, u.display_on_portal)} + className={`p-1.5 rounded-full transition-all ${ + u.display_on_portal + ? "bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" + : "bg-slate-200/50 text-slate-400 hover:bg-slate-200" + }`} + title={u.display_on_portal ? "Đang hiện trên Bio Link" : "Đang ẩn khỏi Bio Link"} + > + {u.display_on_portal ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />} + </button> + </td> + <td className="p-4 text-right"> + <div className="flex items-center justify-end gap-1"> + <button + onClick={() => showQr(u.short_url)} + className="p-2 text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 rounded-lg transition-colors" + title="Mã QR" + > + <QrCode className="w-4 h-4" /> + </button> + <button + onClick={() => setEditUrl({ ...u, tags: u.tags.join(",") })} + className="p-2 text-slate-400 hover:text-amber-600 dark:hover:text-amber-400 rounded-lg transition-colors" + title="Chỉnh sửa" + > + <Edit3 className="w-4 h-4" /> + </button> + <button + onClick={() => handleDelete(u.id)} + className="p-2 text-slate-400 hover:text-red-600 dark:hover:text-red-400 rounded-lg transition-colors" + title="Xóa" + > + <Trash2 className="w-4 h-4" /> + </button> + </div> + </td> + </tr> + )) + )} + </tbody> + </table> + </div> + </div> + + {/* Create Modal */} + {showCreateModal && ( + <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"> + <div className="relative w-full max-w-lg bg-white dark:bg-slate-900 rounded-3xl p-6 shadow-2xl border border-white/20"> + <button + onClick={() => setShowCreateModal(false)} + className="absolute top-4 right-4 p-2 text-slate-400 hover:text-slate-700 dark:hover:text-white" + > + <X className="w-5 h-5" /> + </button> + <h2 className="text-xl font-bold text-slate-900 dark:text-white mb-4"> + Tạo liên kết rút gọn mới + </h2> + + {errorMsg && ( + <div className="mb-4 p-3 rounded-xl bg-red-500/10 border border-red-500/30 text-red-600 text-xs"> + {errorMsg} + </div> + )} + + <form onSubmit={handleCreate} className="space-y-4"> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Đường dẫn gốc (URL) * + </label> + <input + type="url" + required + value={formUrl} + onChange={(e) => setFormUrl(e.target.value)} + placeholder="https://example.com/very-long-url..." + className="w-full px-4 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Mã rút gọn tùy chỉnh + </label> + <input + type="text" + value={formCustomCode} + onChange={(e) => setFormCustomCode(e.target.value)} + placeholder="VD: portfolio (tùy chọn)" + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Mật khẩu bảo vệ + </label> + <input + type="password" + value={formPassword} + onChange={(e) => setFormPassword(e.target.value)} + placeholder="Bỏ trống nếu không cần" + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Thẻ phân loại (Tags) + </label> + <input + type="text" + value={formTags} + onChange={(e) => setFormTags(e.target.value)} + placeholder="marketing, social, youtube..." + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div className="flex items-center gap-3 pt-2"> + <label className="flex items-center gap-2 cursor-pointer text-xs font-semibold text-slate-700 dark:text-slate-300"> + <input + type="checkbox" + checked={formDisplayOnPortal} + onChange={(e) => setFormDisplayOnPortal(e.target.checked)} + className="rounded text-indigo-600 focus:ring-indigo-500" + /> + Hiển thị link này trên trang Bio Link công khai + </label> + </div> + + <div className="flex justify-end gap-2 pt-4"> + <button + type="button" + onClick={() => setShowCreateModal(false)} + className="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 text-sm font-semibold" + > + Hủy + </button> + <button + type="submit" + disabled={submitting} + className="px-5 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold disabled:opacity-50" + > + {submitting ? "Đang xử lý..." : "Tạo liên kết"} + </button> + </div> + </form> + </div> + </div> + )} + + {/* Edit Modal */} + {editUrl && ( + <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"> + <div className="relative w-full max-w-lg bg-white dark:bg-slate-900 rounded-3xl p-6 shadow-2xl border border-white/20"> + <button + onClick={() => setEditUrl(null)} + className="absolute top-4 right-4 p-2 text-slate-400 hover:text-slate-700 dark:hover:text-white" + > + <X className="w-5 h-5" /> + </button> + <h2 className="text-xl font-bold text-slate-900 dark:text-white mb-4"> + Chỉnh sửa liên kết + </h2> + + <form onSubmit={handleUpdate} className="space-y-4"> + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Tiêu đề hiển thị + </label> + <input + type="text" + value={editUrl.title || ""} + onChange={(e) => setEditUrl({ ...editUrl, title: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Mã rút gọn (Short code) + </label> + <input + type="text" + value={editUrl.short_code} + onChange={(e) => setEditUrl({ ...editUrl, short_code: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div> + <label className="block text-xs font-semibold uppercase text-slate-500 mb-1"> + Tags (phân cách bằng dấu phẩy) + </label> + <input + type="text" + value={editUrl.tags || ""} + onChange={(e) => setEditUrl({ ...editUrl, tags: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + </div> + + <div className="flex items-center gap-3 pt-2"> + <label className="flex items-center gap-2 cursor-pointer text-xs font-semibold text-slate-700 dark:text-slate-300"> + <input + type="checkbox" + checked={editUrl.display_on_portal} + onChange={(e) => + setEditUrl({ ...editUrl, display_on_portal: e.target.checked }) + } + className="rounded text-indigo-600 focus:ring-indigo-500" + /> + Hiển thị trên trang Bio Link cá nhân + </label> + </div> + + <div className="flex justify-end gap-2 pt-4"> + <button + type="button" + onClick={() => setEditUrl(null)} + className="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 text-sm font-semibold" + > + Hủy + </button> + <button + type="submit" + disabled={submitting} + className="px-5 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold disabled:opacity-50" + > + {submitting ? "Đang lưu..." : "Cập nhật"} + </button> + </div> + </form> + </div> + </div> + )} + + {/* QR Code Viewer Modal */} + {activeQr && ( + <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"> + <div className="relative w-full max-w-xs bg-white dark:bg-slate-900 rounded-3xl p-6 shadow-2xl border border-white/20 text-center"> + <button + onClick={() => setActiveQr(null)} + className="absolute top-4 right-4 p-2 text-slate-400 hover:text-slate-700 dark:hover:text-white" + > + <X className="w-5 h-5" /> + </button> + <h3 className="text-base font-bold text-slate-900 dark:text-white mb-2">Mã QR Code</h3> + <p className="text-xs text-slate-500 truncate mb-4">{activeQr.short_url}</p> + + <div className="w-52 h-52 mx-auto mb-4 p-3 bg-white rounded-2xl border border-slate-200 flex items-center justify-center"> + {activeQr.qr_code ? ( + // eslint-disable-next-line @next/next/no-img-element + <img src={activeQr.qr_code} alt="QR Code" className="w-full h-full object-contain" /> + ) : ( + <div className="text-xs text-slate-400">Đang tạo QR...</div> + )} + </div> + + <button + onClick={() => setActiveQr(null)} + className="w-full py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 text-xs font-semibold" + > + Đóng + </button> + </div> + </div> + )} + </div> + ); +} diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/src/app/favicon.ico differ diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..8c4f775 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,56 @@ +@import "tailwindcss"; + +:root { + --background: #f8fafc; + --foreground: #0f172a; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-inter), system-ui, -apple-system, sans-serif; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #030712; + --foreground: #f9fafb; + } +} + +html { + scroll-behavior: smooth; +} + +body { + background-color: var(--background); + color: var(--foreground); + font-family: var(--font-inter), system-ui, -apple-system, sans-serif; +} + +/* Custom minimal scrollbar */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(156, 163, 175, 0.4); + border-radius: 9999px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(156, 163, 175, 0.7); +} + +.no-scrollbar::-webkit-scrollbar { + display: none; +} +.no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..1cba282 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import AppHeader from "@/components/AppHeader"; + +const inter = Inter({ + subsets: ["latin", "vietnamese"], + variable: "--font-inter", +}); + +export const metadata: Metadata = { + title: "EZBio - Nền tảng Rút gọn Link & Bio Link Đỉnh cao", + description: "Rút gọn liên kết thông minh, tạo mã QR tùy biến, bảo vệ link bằng mật khẩu và xây dựng trang Bio Link thương hiệu cá nhân với EZBio.", + keywords: "rut gon link, short url, bio link, linktree alternative, link in bio, tao ma qr", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <html lang="vi" className={`${inter.variable} h-full antialiased`}> + <body className="min-h-full flex flex-col font-sans bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100"> + <AppHeader /> + <main className="flex-1 flex flex-col">{children}</main> + </body> + </html> + ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..087695a --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { Link2, ArrowRight, Lock, Mail, AlertCircle } from "lucide-react"; + +export default function LoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleLogin(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + const res = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + const data = await res.json(); + if (!res.ok) { + throw new Error(data.error || "Đăng nhập thất bại"); + } + + router.push("/dashboard"); + router.refresh(); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + } + + return ( + <div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-slate-900 via-indigo-950 to-slate-900"> + <div className="w-full max-w-md bg-white/10 backdrop-blur-2xl border border-white/20 rounded-3xl p-8 shadow-2xl text-white"> + <div className="flex flex-col items-center text-center mb-8"> + <Link href="/" className="inline-flex items-center gap-2 mb-4 group"> + <div className="w-12 h-12 rounded-2xl bg-gradient-to-tr from-indigo-500 to-purple-600 flex items-center justify-center text-white shadow-lg group-hover:scale-105 transition-all"> + <Link2 className="w-6 h-6" /> + </div> + <span className="text-2xl font-black tracking-tight">EZBio</span> + </Link> + <h1 className="text-2xl font-bold tracking-tight">Chào mừng trở lại!</h1> + <p className="text-slate-300 text-sm mt-1"> + Đăng nhập để quản lý các liên kết và trang Bio của bạn + </p> + </div> + + {error && ( + <div className="mb-4 p-3.5 rounded-xl bg-red-500/20 border border-red-500/40 text-red-200 text-xs flex items-center gap-2"> + <AlertCircle className="w-4 h-4 shrink-0" /> + <span>{error}</span> + </div> + )} + + <form onSubmit={handleLogin} className="space-y-4"> + <div> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-300 mb-1.5"> + Email + </label> + <div className="relative"> + <Mail className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" /> + <input + type="email" + required + value={email} + onChange={(e) => setEmail(e.target.value)} + placeholder="you@domain.com" + className="w-full pl-10 pr-4 py-3 rounded-xl bg-white/5 border border-white/20 text-white placeholder-slate-400 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 transition-all" + /> + </div> + </div> + + <div> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-300 mb-1.5"> + Mật khẩu + </label> + <div className="relative"> + <Lock className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" /> + <input + type="password" + required + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="••••••••" + className="w-full pl-10 pr-4 py-3 rounded-xl bg-white/5 border border-white/20 text-white placeholder-slate-400 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 transition-all" + /> + </div> + </div> + + <button + type="submit" + disabled={loading} + className="w-full py-3.5 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold text-sm shadow-lg shadow-indigo-500/30 transition-all flex items-center justify-center gap-2 hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 mt-2" + > + {loading ? "Đang đăng nhập..." : "Đăng nhập ngay"} + <ArrowRight className="w-4 h-4" /> + </button> + </form> + + <div className="mt-8 pt-6 border-t border-white/10 text-center text-xs text-slate-400"> + Chưa có tài khoản?{" "} + <Link href="/register" className="text-indigo-400 hover:underline font-semibold"> + Đăng ký miễn phí + </Link> + </div> + </div> + </div> + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..ff03a4c --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,339 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { + Link2, + Sparkles, + ArrowRight, + Shield, + QrCode, + BarChart3, + Copy, + Check, + Lock, + Tag, + ChevronDown, + ChevronUp, + ExternalLink, + CheckCircle2, +} from "lucide-react"; + +export default function HomePage() { + const [url, setUrl] = useState(""); + const [customCode, setCustomCode] = useState(""); + const [password, setPassword] = useState(""); + const [tags, setTags] = useState(""); + const [showAdvanced, setShowAdvanced] = useState(false); + + const [loading, setLoading] = useState(false); + const [result, setResult] = useState<any | null>(null); + const [errorMsg, setErrorMsg] = useState(""); + const [copied, setCopied] = useState(false); + + async function handleShorten(e: React.FormEvent) { + e.preventDefault(); + if (!url) return; + setErrorMsg(""); + setLoading(true); + + try { + const res = await fetch("/api/shorten", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url, + custom_code: customCode || undefined, + password: password || undefined, + tags: tags || undefined, + }), + }); + + const data = await res.json(); + if (!res.ok || !data.success) { + throw new Error(data.message || "Lỗi tạo liên kết rút gọn"); + } + + setResult(data.data); + } catch (err: any) { + setErrorMsg(err.message); + } finally { + setLoading(false); + } + } + + function copyResult() { + if (!result?.short_url) return; + navigator.clipboard.writeText(result.short_url); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + + return ( + <div className="flex flex-col min-h-screen"> + {/* Hero Section */} + <section className="relative overflow-hidden pt-16 pb-20 md:pt-24 md:pb-28 bg-gradient-to-b from-slate-900 via-indigo-950 to-slate-900 text-white"> + {/* Glow ambient background */} + <div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-indigo-500/20 blur-[140px] pointer-events-none rounded-full" /> + <div className="absolute top-1/3 right-10 w-[400px] h-[400px] bg-purple-500/15 blur-[120px] pointer-events-none rounded-full" /> + + <div className="relative z-10 max-w-5xl mx-auto px-4 sm:px-6 text-center"> + {/* Badge */} + <div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/10 backdrop-blur-md border border-white/15 text-xs font-semibold uppercase tracking-wider text-indigo-300 mb-6"> + <Sparkles className="w-4 h-4 text-purple-400" /> + Nền tảng Rút gọn Link & Bio Link thế hệ mới + </div> + + <h1 className="text-4xl sm:text-5xl md:text-6xl font-black tracking-tight leading-tight sm:leading-tight mb-6"> + Rút gọn liên kết thông minh,{" "} + <span className="bg-gradient-to-r from-indigo-400 via-purple-300 to-pink-400 bg-clip-text text-transparent"> + Tùy biến Bio Link đỉnh cao + </span> + </h1> + + <p className="text-base sm:text-lg text-slate-300 max-w-2xl mx-auto mb-10 leading-relaxed font-normal"> + Tạo liên kết rút gọn tức thì, mã QR tùy biến màu sắc, bảo vệ bằng mật khẩu và xây dựng trang cá nhân Bio Link chuyên nghiệp cho thương hiệu của bạn. + </p> + + {/* Core Shorten Form Box */} + <div className="max-w-3xl mx-auto bg-white/10 backdrop-blur-2xl border border-white/20 p-6 sm:p-8 rounded-3xl shadow-2xl text-left"> + <form onSubmit={handleShorten} className="space-y-4"> + <div className="flex flex-col sm:flex-row gap-3"> + <div className="relative flex-1"> + <Link2 className="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" /> + <input + type="url" + required + value={url} + onChange={(e) => setUrl(e.target.value)} + placeholder="Dán đường link dài của bạn vào đây (https://...)" + className="w-full pl-12 pr-4 py-3.5 rounded-2xl bg-white/10 border border-white/20 text-white placeholder-slate-400 text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-indigo-400 transition-all" + /> + </div> + <button + type="submit" + disabled={loading} + className="px-7 py-3.5 rounded-2xl bg-gradient-to-r from-indigo-500 via-indigo-600 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-bold text-sm sm:text-base shadow-lg shadow-indigo-500/30 transition-all flex items-center justify-center gap-2 hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 shrink-0" + > + {loading ? "Đang xử lý..." : "Rút gọn link"} + <ArrowRight className="w-4 h-4" /> + </button> + </div> + + {/* Advanced options toggle */} + <div className="pt-1"> + <button + type="button" + onClick={() => setShowAdvanced(!showAdvanced)} + className="text-xs font-semibold text-indigo-300 hover:text-white flex items-center gap-1 transition-colors" + > + {showAdvanced ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />} + Tùy chọn nâng cao (Mã tùy chỉnh, Mật khẩu, Thẻ Tags) + </button> + </div> + + {/* Advanced Options Accordion */} + {showAdvanced && ( + <div className="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-3 border-t border-white/10 animate-in fade-in duration-200"> + <div> + <label className="block text-xs font-medium text-slate-300 mb-1 flex items-center gap-1"> + <Link2 className="w-3.5 h-3.5 text-indigo-400" /> + Mã rút gọn riêng + </label> + <input + type="text" + value={customCode} + onChange={(e) => setCustomCode(e.target.value)} + placeholder="VD: mypromo" + className="w-full px-3.5 py-2 rounded-xl bg-white/5 border border-white/20 text-xs text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-400" + /> + </div> + + <div> + <label className="block text-xs font-medium text-slate-300 mb-1 flex items-center gap-1"> + <Lock className="w-3.5 h-3.5 text-indigo-400" /> + Mật khẩu bảo vệ + </label> + <input + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="Nhập mật khẩu..." + className="w-full px-3.5 py-2 rounded-xl bg-white/5 border border-white/20 text-xs text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-400" + /> + </div> + + <div> + <label className="block text-xs font-medium text-slate-300 mb-1 flex items-center gap-1"> + <Tag className="w-3.5 h-3.5 text-indigo-400" /> + Thẻ phân loại + </label> + <input + type="text" + value={tags} + onChange={(e) => setTags(e.target.value)} + placeholder="tag1, tag2..." + className="w-full px-3.5 py-2 rounded-xl bg-white/5 border border-white/20 text-xs text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-400" + /> + </div> + </div> + )} + + {errorMsg && ( + <div className="p-3.5 rounded-xl bg-red-500/20 border border-red-500/40 text-red-200 text-xs"> + {errorMsg} + </div> + )} + </form> + + {/* Result Box */} + {result && ( + <div className="mt-6 p-6 rounded-2xl bg-white/15 border border-white/30 backdrop-blur-md animate-in fade-in duration-300"> + <div className="flex flex-col md:flex-row items-center justify-between gap-6"> + <div className="flex-1 w-full overflow-hidden"> + <span className="text-[11px] font-bold uppercase tracking-wider text-emerald-400 flex items-center gap-1 mb-1"> + <CheckCircle2 className="w-3.5 h-3.5" /> Link đã được rút gọn thành công + </span> + <a + href={result.short_url} + target="_blank" + rel="noopener noreferrer" + className="text-xl sm:text-2xl font-black text-white hover:text-indigo-300 transition-colors truncate block" + > + {result.short_url} + </a> + <div className="text-xs text-slate-300 truncate mt-1"> + Đích đến: {result.original_url} + </div> + + <div className="flex items-center gap-3 mt-4"> + <button + onClick={copyResult} + className="px-4 py-2 rounded-xl bg-white text-slate-900 font-bold text-xs flex items-center gap-1.5 hover:bg-slate-100 transition-all shadow-md" + > + {copied ? <Check className="w-4 h-4 text-emerald-600" /> : <Copy className="w-4 h-4" />} + {copied ? "Đã sao chép!" : "Sao chép link"} + </button> + + <a + href={result.short_url} + target="_blank" + rel="noopener noreferrer" + className="px-4 py-2 rounded-xl bg-white/10 hover:bg-white/20 text-white font-semibold text-xs border border-white/20 flex items-center gap-1.5 transition-all" + > + Mở liên kết + <ExternalLink className="w-3.5 h-3.5" /> + </a> + </div> + </div> + + {/* QR Code */} + {result.qr_code && ( + <div className="flex flex-col items-center p-3 bg-white rounded-2xl shadow-xl shrink-0"> + {/* eslint-disable-next-line @next/next/no-img-element */} + <img src={result.qr_code} alt="QR Code" className="w-32 h-32 object-contain" /> + <a + href={result.qr_code} + download={`qr_${result.short_code}.png`} + className="mt-2 text-[11px] font-bold text-indigo-600 hover:underline" + > + Tải ảnh QR (PNG) + </a> + </div> + )} + </div> + </div> + )} + </div> + </div> + </section> + + {/* Feature Highlights Grid */} + <section className="py-20 bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-white"> + <div className="max-w-7xl mx-auto px-4 sm:px-6"> + <div className="text-center max-w-2xl mx-auto mb-16"> + <h2 className="text-xs font-bold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2"> + Tính năng vượt trội + </h2> + <h3 className="text-3xl font-extrabold tracking-tight"> + Tất cả những gì bạn cần cho liên kết trực tuyến + </h3> + </div> + + <div className="grid grid-cols-1 md:grid-cols-3 gap-8"> + <div className="p-8 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm hover:shadow-xl transition-all duration-300"> + <div className="w-14 h-14 rounded-2xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center mb-6"> + <Link2 className="w-7 h-7" /> + </div> + <h4 className="text-xl font-bold mb-2">Rút gọn Link Linh hoạt</h4> + <p className="text-sm text-slate-500 dark:text-slate-400 leading-relaxed"> + Tự động sinh mã ngắn ngẫu nhiên với độ dài do Admin cấu hình, hoặc tự đặt bí danh ngắn theo phong cách thương hiệu riêng. + </p> + </div> + + <div className="p-8 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm hover:shadow-xl transition-all duration-300"> + <div className="w-14 h-14 rounded-2xl bg-purple-500/10 text-purple-600 dark:text-purple-400 flex items-center justify-center mb-6"> + <Sparkles className="w-7 h-7" /> + </div> + <h4 className="text-xl font-bold mb-2">Trang Bio Link & Landing Page</h4> + <p className="text-sm text-slate-500 dark:text-slate-400 leading-relaxed"> + Thiết kế trang Bio cá nhân đẹp mắt với hiệu ứng Gradient, xem trước thời gian thực trên giao diện điện thoại (Phone Mockup). + </p> + </div> + + <div className="p-8 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm hover:shadow-xl transition-all duration-300"> + <div className="w-14 h-14 rounded-2xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center mb-6"> + <QrCode className="w-7 h-7" /> + </div> + <h4 className="text-xl font-bold mb-2">Mã QR Code Tự Động</h4> + <p className="text-sm text-slate-500 dark:text-slate-400 leading-relaxed"> + Mỗi liên kết và mạng xã hội đều đi kèm mã QR độ nét cao. Hỗ trợ tùy biến màu sắc và tải ảnh chuẩn in ấn. + </p> + </div> + + <div className="p-8 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm hover:shadow-xl transition-all duration-300"> + <div className="w-14 h-14 rounded-2xl bg-blue-500/10 text-blue-600 dark:text-blue-400 flex items-center justify-center mb-6"> + <BarChart3 className="w-7 h-7" /> + </div> + <h4 className="text-xl font-bold mb-2">Phân Tích Chi Tiết</h4> + <p className="text-sm text-slate-500 dark:text-slate-400 leading-relaxed"> + Theo dõi hiệu suất từng liên kết: số lượt nhấp theo ngày, thiết bị (Desktop/Mobile), hệ điều hành và quốc gia truy cập. + </p> + </div> + + <div className="p-8 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm hover:shadow-xl transition-all duration-300"> + <div className="w-14 h-14 rounded-2xl bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center justify-center mb-6"> + <Shield className="w-7 h-7" /> + </div> + <h4 className="text-xl font-bold mb-2">Bảo Vệ Mật Khẩu & Hết Hạn</h4> + <p className="text-sm text-slate-500 dark:text-slate-400 leading-relaxed"> + Khóa các liên kết nhạy cảm bằng mật khẩu an toàn và đặt thời hạn hết hạn tự động cho các chiến dịch có thời hạn. + </p> + </div> + + <div className="p-8 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm hover:shadow-xl transition-all duration-300"> + <div className="w-14 h-14 rounded-2xl bg-pink-500/10 text-pink-600 dark:text-pink-400 flex items-center justify-center mb-6"> + <Sparkles className="w-7 h-7" /> + </div> + <h4 className="text-xl font-bold mb-2">Cloudflare R2 Storage</h4> + <p className="text-sm text-slate-500 dark:text-slate-400 leading-relaxed"> + Lưu trữ Avatar và ảnh nền trên Cloudflare R2 với 0đ chi phí băng thông, tích hợp hệ thống cảnh báo hạn mức thông minh cho Admin. + </p> + </div> + </div> + </div> + </section> + + {/* Footer */} + <footer className="mt-auto border-t border-slate-200 dark:border-slate-800 py-10 bg-white dark:bg-slate-900 text-center text-xs text-slate-500"> + <div className="max-w-7xl mx-auto px-4 flex flex-col sm:flex-row items-center justify-between gap-4"> + <div className="font-semibold text-slate-800 dark:text-slate-200 flex items-center gap-2"> + <Link2 className="w-4 h-4 text-indigo-500" /> + EZBio — Nền tảng Link Rút gọn & Bio Link cá nhân + </div> + <div>© {new Date().getFullYear()} EZBio. All rights reserved. Sẵn sàng deploy trên Vercel.</div> + </div> + </footer> + </div> + ); +} diff --git a/src/app/portal/[username]/page.tsx b/src/app/portal/[username]/page.tsx new file mode 100644 index 0000000..e3844b5 --- /dev/null +++ b/src/app/portal/[username]/page.tsx @@ -0,0 +1,71 @@ +import { notFound } from "next/navigation"; +import { Metadata } from "next"; +import { prisma } from "@/lib/prisma"; +import PortalView from "@/components/PortalView"; + +interface PageProps { + params: Promise<{ username: string }>; +} + +export async function generateMetadata({ params }: PageProps): Promise<Metadata> { + const { username } = await params; + const user = await prisma.user.findUnique({ + where: { username }, + include: { portal: true }, + }); + + if (!user || !user.portal) { + return { title: "Không tìm thấy trang Bio" }; + } + + const title = user.portal.title || `${user.name} - EZBio`; + const description = user.portal.description || `Xem danh sách liên kết và thông tin của ${user.name} trên EZBio.`; + + return { + title, + description, + openGraph: { + title, + description, + images: user.portal.avatar ? [user.portal.avatar] : [], + }, + }; +} + +export default async function PublicPortalPage({ params }: PageProps) { + const { username } = await params; + + const user = await prisma.user.findUnique({ + where: { username }, + include: { + portal: true, + shortUrls: { + where: { displayOnPortal: true }, + orderBy: { createdAt: "desc" }, + include: { tags: true }, + }, + }, + }); + + if (!user || !user.portal) { + return notFound(); + } + + const appUrl = (process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000").replace(/\/$/, ""); + + const formattedUrls = user.shortUrls.map((u) => ({ + id: u.id, + title: u.title, + short_code: u.shortCode, + short_url: `${appUrl}/${u.shortCode}`, + tags: u.tags.map((t) => t.name), + })); + + return ( + <PortalView + portal={user.portal} + user={{ name: user.name, username: user.username }} + urls={formattedUrls} + /> + ); +} diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx new file mode 100644 index 0000000..69972c3 --- /dev/null +++ b/src/app/register/page.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { Link2, ArrowRight, Lock, Mail, User, AlertCircle } from "lucide-react"; + +export default function RegisterPage() { + const router = useRouter(); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleRegister(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + const res = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, email, password }), + }); + + const data = await res.json(); + if (!res.ok) { + throw new Error(data.error || "Đăng ký thất bại"); + } + + router.push("/dashboard"); + router.refresh(); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + } + + return ( + <div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-slate-900 via-indigo-950 to-slate-900"> + <div className="w-full max-w-md bg-white/10 backdrop-blur-2xl border border-white/20 rounded-3xl p-8 shadow-2xl text-white"> + <div className="flex flex-col items-center text-center mb-8"> + <Link href="/" className="inline-flex items-center gap-2 mb-4 group"> + <div className="w-12 h-12 rounded-2xl bg-gradient-to-tr from-indigo-500 to-purple-600 flex items-center justify-center text-white shadow-lg group-hover:scale-105 transition-all"> + <Link2 className="w-6 h-6" /> + </div> + <span className="text-2xl font-black tracking-tight">EZBio</span> + </Link> + <h1 className="text-2xl font-bold tracking-tight">Tạo tài khoản mới</h1> + <p className="text-slate-300 text-sm mt-1"> + Bắt đầu rút gọn link và tạo trang Bio Link miễn phí không giới hạn + </p> + </div> + + {error && ( + <div className="mb-4 p-3.5 rounded-xl bg-red-500/20 border border-red-500/40 text-red-200 text-xs flex items-center gap-2"> + <AlertCircle className="w-4 h-4 shrink-0" /> + <span>{error}</span> + </div> + )} + + <form onSubmit={handleRegister} className="space-y-4"> + <div> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-300 mb-1.5"> + Họ và tên + </label> + <div className="relative"> + <User className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" /> + <input + type="text" + required + value={name} + onChange={(e) => setName(e.target.value)} + placeholder="Nguyễn Văn A" + className="w-full pl-10 pr-4 py-3 rounded-xl bg-white/5 border border-white/20 text-white placeholder-slate-400 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 transition-all" + /> + </div> + </div> + + <div> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-300 mb-1.5"> + Email + </label> + <div className="relative"> + <Mail className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" /> + <input + type="email" + required + value={email} + onChange={(e) => setEmail(e.target.value)} + placeholder="you@domain.com" + className="w-full pl-10 pr-4 py-3 rounded-xl bg-white/5 border border-white/20 text-white placeholder-slate-400 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 transition-all" + /> + </div> + </div> + + <div> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-300 mb-1.5"> + Mật khẩu (từ 6 ký tự) + </label> + <div className="relative"> + <Lock className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" /> + <input + type="password" + required + minLength={6} + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="••••••••" + className="w-full pl-10 pr-4 py-3 rounded-xl bg-white/5 border border-white/20 text-white placeholder-slate-400 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 transition-all" + /> + </div> + </div> + + <button + type="submit" + disabled={loading} + className="w-full py-3.5 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold text-sm shadow-lg shadow-indigo-500/30 transition-all flex items-center justify-center gap-2 hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 mt-2" + > + {loading ? "Đang tạo tài khoản..." : "Đăng ký tài khoản"} + <ArrowRight className="w-4 h-4" /> + </button> + </form> + + <div className="mt-8 pt-6 border-t border-white/10 text-center text-xs text-slate-400"> + Đã có tài khoản?{" "} + <Link href="/login" className="text-indigo-400 hover:underline font-semibold"> + Đăng nhập ngay + </Link> + </div> + </div> + </div> + ); +} diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx new file mode 100644 index 0000000..6e76189 --- /dev/null +++ b/src/components/AppHeader.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import Navbar from "./Navbar"; + +export default function AppHeader() { + const pathname = usePathname(); + + // Hide main navbar on public portal page + if (pathname?.startsWith("/portal/")) { + return null; + } + + return <Navbar />; +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..3e91a80 --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { Link2, Sparkles, User, LogOut, ShieldCheck, Menu, X } from "lucide-react"; + +export default function Navbar() { + const router = useRouter(); + const [user, setUser] = useState<any>(null); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + useEffect(() => { + async function checkAuth() { + try { + const res = await fetch("/api/auth/me"); + const data = await res.json(); + setUser(data.user); + } catch { + // Not logged in + } + } + checkAuth(); + }, []); + + async function handleLogout() { + try { + await fetch("/api/auth/logout", { method: "POST" }); + setUser(null); + router.push("/"); + router.refresh(); + } catch { + // + } + } + + return ( + <header className="sticky top-0 z-40 w-full border-b border-slate-200/80 dark:border-slate-800 bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl"> + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between"> + {/* Brand */} + <Link href="/" className="flex items-center gap-2.5 group"> + <div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-indigo-500 via-indigo-600 to-purple-600 flex items-center justify-center text-white shadow-md shadow-indigo-500/20 group-hover:scale-105 transition-all"> + <Link2 className="w-5 h-5" /> + </div> + <span className="text-xl font-black tracking-tight bg-gradient-to-r from-indigo-600 to-purple-600 dark:from-indigo-400 dark:to-purple-400 bg-clip-text text-transparent"> + EZBio + </span> + </Link> + + {/* Desktop Nav */} + <nav className="hidden md:flex items-center gap-6 text-sm font-medium text-slate-600 dark:text-slate-300"> + <Link href="/" className="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"> + Rút gọn link + </Link> + <Link href="/dashboard/portal" className="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors flex items-center gap-1"> + <Sparkles className="w-4 h-4 text-purple-500" /> + Bio Link + </Link> + {user && ( + <Link href="/dashboard/urls" className="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"> + Liên kết của tôi + </Link> + )} + {user?.role === "admin" && ( + <Link + href="/admin" + className="text-purple-600 dark:text-purple-400 hover:text-purple-700 font-semibold flex items-center gap-1 px-2.5 py-1 rounded-lg bg-purple-50 dark:bg-purple-950/60" + > + <ShieldCheck className="w-4 h-4" /> + Admin + </Link> + )} + </nav> + + {/* Auth CTA */} + <div className="hidden md:flex items-center gap-3"> + {user ? ( + <div className="flex items-center gap-3"> + <Link + href="/dashboard" + className="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-xs font-semibold text-slate-700 dark:text-slate-200 transition-all flex items-center gap-1.5" + > + <User className="w-3.5 h-3.5 text-indigo-500" /> + {user.name} + </Link> + <button + onClick={handleLogout} + className="p-2 rounded-xl text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/30 transition-all" + title="Đăng xuất" + > + <LogOut className="w-4 h-4" /> + </button> + </div> + ) : ( + <div className="flex items-center gap-2"> + <Link + href="/login" + className="px-4 py-2 text-xs font-semibold text-slate-700 dark:text-slate-200 hover:text-indigo-600 transition-colors" + > + Đăng nhập + </Link> + <Link + href="/register" + className="px-4 py-2 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold text-xs shadow-md shadow-indigo-500/20 transition-all hover:scale-105 active:scale-95" + > + Đăng ký miễn phí + </Link> + </div> + )} + </div> + + {/* Mobile menu trigger */} + <button + onClick={() => setMobileMenuOpen(!mobileMenuOpen)} + className="md:hidden p-2 rounded-xl text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800" + > + {mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />} + </button> + </div> + + {/* Mobile Drawer */} + {mobileMenuOpen && ( + <div className="md:hidden border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 px-4 py-4 space-y-3"> + <Link + href="/" + onClick={() => setMobileMenuOpen(false)} + className="block text-sm font-semibold text-slate-700 dark:text-slate-200 py-1.5" + > + Trang chủ & Rút gọn link + </Link> + <Link + href="/dashboard/portal" + onClick={() => setMobileMenuOpen(false)} + className="block text-sm font-semibold text-slate-700 dark:text-slate-200 py-1.5" + > + Trang Bio Link + </Link> + {user && ( + <Link + href="/dashboard/urls" + onClick={() => setMobileMenuOpen(false)} + className="block text-sm font-semibold text-slate-700 dark:text-slate-200 py-1.5" + > + Liên kết của tôi + </Link> + )} + {user?.role === "admin" && ( + <Link + href="/admin" + onClick={() => setMobileMenuOpen(false)} + className="block text-sm font-bold text-purple-600 py-1.5" + > + Trang Quản trị Admin + </Link> + )} + + <div className="pt-2 border-t border-slate-100 dark:border-slate-800 flex items-center justify-between"> + {user ? ( + <div className="flex items-center justify-between w-full"> + <span className="text-xs font-semibold text-slate-600 dark:text-slate-400"> + {user.name} ({user.email}) + </span> + <button + onClick={handleLogout} + className="px-3 py-1.5 rounded-lg bg-red-50 text-red-600 text-xs font-semibold" + > + Đăng xuất + </button> + </div> + ) : ( + <div className="flex gap-2 w-full"> + <Link + href="/login" + onClick={() => setMobileMenuOpen(false)} + className="flex-1 py-2 text-center rounded-xl bg-slate-100 dark:bg-slate-800 text-xs font-semibold" + > + Đăng nhập + </Link> + <Link + href="/register" + onClick={() => setMobileMenuOpen(false)} + className="flex-1 py-2 text-center rounded-xl bg-indigo-600 text-white text-xs font-semibold" + > + Đăng ký + </Link> + </div> + )} + </div> + </div> + )} + </header> + ); +} diff --git a/src/components/PasswordPrompt.tsx b/src/components/PasswordPrompt.tsx new file mode 100644 index 0000000..89eaf3e --- /dev/null +++ b/src/components/PasswordPrompt.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useState } from "react"; +import { Lock, ArrowRight, ShieldAlert } from "lucide-react"; + +export default function PasswordPrompt({ shortCode }: { shortCode: string }) { + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + const res = await fetch("/api/verify-password", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ shortCode, password }), + }); + + const data = await res.json(); + if (!res.ok) { + setError(data.error || "Mật khẩu không đúng"); + setLoading(false); + return; + } + + // Reload page to trigger redirect + window.location.reload(); + } catch { + setError("Đã xảy ra lỗi khi kiểm tra mật khẩu"); + setLoading(false); + } + } + + return ( + <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-indigo-950 to-slate-900 p-4"> + <div className="w-full max-w-md bg-white/10 backdrop-blur-xl border border-white/20 rounded-3xl p-8 shadow-2xl text-white"> + <div className="flex flex-col items-center text-center mb-6"> + <div className="w-16 h-16 rounded-2xl bg-indigo-500/20 border border-indigo-400/30 flex items-center justify-center mb-4 text-indigo-400 shadow-inner"> + <Lock className="w-8 h-8" /> + </div> + <h1 className="text-2xl font-bold tracking-tight">Liên kết được bảo vệ</h1> + <p className="text-slate-300 text-sm mt-1"> + Người tạo đã đặt mật khẩu cho đường link này. Vui lòng nhập mật khẩu để tiếp tục. + </p> + </div> + + {error && ( + <div className="mb-4 p-3.5 rounded-xl bg-red-500/20 border border-red-500/40 text-red-200 text-sm flex items-center gap-2"> + <ShieldAlert className="w-4 h-4 shrink-0" /> + <span>{error}</span> + </div> + )} + + <form onSubmit={handleSubmit} className="space-y-4"> + <div> + <label className="block text-xs font-semibold uppercase tracking-wider text-slate-300 mb-1.5"> + Mật khẩu truy cập + </label> + <input + type="password" + required + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="Nhập mật khẩu..." + className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/20 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:border-transparent transition-all" + /> + </div> + + <button + type="submit" + disabled={loading} + className="w-full py-3.5 px-4 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold flex items-center justify-center gap-2 shadow-lg shadow-indigo-500/30 transition-all hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50" + > + {loading ? "Đang xác thực..." : "Tiếp tục truy cập"} + <ArrowRight className="w-4 h-4" /> + </button> + </form> + </div> + </div> + ); +} diff --git a/src/components/PortalView.tsx b/src/components/PortalView.tsx new file mode 100644 index 0000000..00ae59b --- /dev/null +++ b/src/components/PortalView.tsx @@ -0,0 +1,367 @@ +"use client"; + +import { useState } from "react"; +import { + QrCode, + ExternalLink, + UserCheck, + Phone, + Mail, + Building, + Globe, + X, + Share2, +} from "lucide-react"; +import { getBackgroundStyle, getTitleGradientClass } from "@/lib/theme-presets"; + +interface PortalViewProps { + portal: { + title?: string | null; + description?: string | null; + avatar?: string | null; + backgroundImage?: string | null; + headerColor?: string | null; + titleColorStyle?: string | null; + backgroundStyle?: string | null; + vcard?: any; + socialLinks?: any; + }; + user: { + name: string; + username: string; + }; + urls: Array<{ + id: string; + title: string | null; + short_code: string; + short_url: string; + tags: string[]; + }>; + isEmbedPreview?: boolean; +} + +const SOCIAL_ICONS: Record<string, { label: string; color: string; bg: string }> = { + facebook: { label: "Facebook", color: "text-blue-500", bg: "bg-blue-500/10 hover:bg-blue-500/20" }, + twitter: { label: "Twitter / X", color: "text-sky-400", bg: "bg-sky-400/10 hover:bg-sky-400/20" }, + instagram: { label: "Instagram", color: "text-pink-500", bg: "bg-pink-500/10 hover:bg-pink-500/20" }, + threads: { label: "Threads", color: "text-slate-300", bg: "bg-slate-300/10 hover:bg-slate-300/20" }, + tiktok: { label: "TikTok", color: "text-teal-400", bg: "bg-teal-400/10 hover:bg-teal-400/20" }, + youtube: { label: "YouTube", color: "text-red-500", bg: "bg-red-500/10 hover:bg-red-500/20" }, + telegram: { label: "Telegram", color: "text-blue-400", bg: "bg-blue-400/10 hover:bg-blue-400/20" }, +}; + +export default function PortalView({ portal, user, urls, isEmbedPreview = false }: PortalViewProps) { + const [activeQr, setActiveQr] = useState<{ title: string; url: string; qrDataUrl?: string } | null>(null); + const [showVCardModal, setShowVCardModal] = useState(false); + const [loadingQr, setLoadingQr] = useState(false); + + const backgroundStyle = portal.backgroundImage + ? `url(${portal.backgroundImage}) center/cover no-repeat fixed` + : getBackgroundStyle(portal.backgroundStyle); + + const titleClass = getTitleGradientClass(portal.titleColorStyle); + + async function openQrModal(title: string, url: string) { + setActiveQr({ title, url }); + setLoadingQr(true); + try { + const res = await fetch(`/api/qr?content=${encodeURIComponent(url)}&size=300`); + const data = await res.json(); + setActiveQr({ title, url, qrDataUrl: data.qr_code }); + } catch { + // Fallback + } finally { + setLoadingQr(false); + } + } + + function downloadVCard() { + const vcard = portal.vcard || {}; + const vcardContent = [ + "BEGIN:VCARD", + "VERSION:3.0", + `FN:${vcard.fullName || user.name}`, + `TEL:${vcard.phone || ""}`, + `EMAIL:${vcard.email || ""}`, + `ORG:${vcard.company || ""}`, + `URL:${vcard.website || ""}`, + "END:VCARD", + ].join("\n"); + + const blob = new Blob([vcardContent], { type: "text/vcard;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", `${user.username}_contact.vcf`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + const socialEntries = Object.entries(portal.socialLinks || {}).filter( + ([, val]) => typeof val === "string" && val.trim().length > 0 + ); + + return ( + <div + className={`min-h-screen w-full flex flex-col items-center transition-all ${ + isEmbedPreview ? "py-4 px-3" : "py-12 px-4" + }`} + style={{ background: backgroundStyle }} + > + <div className="w-full max-w-md mx-auto flex flex-col items-center"> + {/* Header Profile */} + <div className="flex flex-col items-center text-center mb-6"> + <div className="relative mb-4"> + <div className="w-24 h-24 sm:w-28 sm:h-28 rounded-full overflow-hidden border-4 border-white/60 shadow-2xl bg-slate-200"> + {portal.avatar ? ( + // eslint-disable-next-line @next/next/no-img-element + <img + src={portal.avatar} + alt={user.name} + className="w-full h-full object-cover" + /> + ) : ( + <div className="w-full h-full flex items-center justify-center bg-gradient-to-tr from-indigo-500 to-purple-500 text-white text-3xl font-bold"> + {user.name.charAt(0).toUpperCase()} + </div> + )} + </div> + </div> + + <h1 + className={`text-2xl sm:text-3xl font-extrabold tracking-tight mb-2 ${titleClass}`} + style={{ color: portal.headerColor || "#000000" }} + > + {portal.title || user.name} + </h1> + + {portal.description && ( + <p + className="text-sm sm:text-base max-w-sm font-medium opacity-90 mb-4 px-2" + style={{ color: portal.headerColor || "#333333" }} + > + {portal.description} + </p> + )} + + {/* vCard Action Button */} + {portal.vcard && (portal.vcard.phone || portal.vcard.email || portal.vcard.website) && ( + <button + onClick={() => setShowVCardModal(true)} + className="mt-1 px-4 py-2 rounded-full bg-white/80 dark:bg-slate-900/80 backdrop-blur-md border border-white/40 shadow-md text-xs font-semibold text-slate-800 dark:text-slate-100 hover:scale-105 active:scale-95 transition-all flex items-center gap-1.5" + > + <UserCheck className="w-3.5 h-3.5 text-indigo-500" /> + Lưu danh bạ (vCard) + </button> + )} + </div> + + {/* Social Media Grid */} + {socialEntries.length > 0 && ( + <div className="w-full mb-6 p-4 rounded-2xl bg-white/70 dark:bg-slate-900/70 backdrop-blur-xl border border-white/40 shadow-lg"> + <div className="flex flex-wrap items-center justify-center gap-3"> + {socialEntries.map(([network, link]) => { + const conf = SOCIAL_ICONS[network] || { + label: network, + color: "text-slate-700", + bg: "bg-slate-100 hover:bg-slate-200", + }; + return ( + <div key={network} className="flex items-center gap-1 group"> + <a + href={String(link)} + target="_blank" + rel="noopener noreferrer" + className={`p-3 rounded-xl transition-all shadow-sm ${conf.bg} ${conf.color}`} + title={conf.label} + > + <Share2 className="w-5 h-5" /> + </a> + <button + onClick={() => openQrModal(conf.label, String(link))} + className="p-1.5 text-slate-400 hover:text-slate-700 dark:hover:text-white transition-colors" + title={`Xem mã QR của ${conf.label}`} + > + <QrCode className="w-4 h-4" /> + </button> + </div> + ); + })} + </div> + </div> + )} + + {/* Shared Links List */} + <div className="w-full space-y-3"> + {urls.length === 0 ? ( + <div className="p-6 text-center rounded-2xl bg-white/60 dark:bg-slate-900/60 backdrop-blur-md border border-white/40 text-slate-600 dark:text-slate-300 text-sm"> + Chưa có liên kết nào được chia sẻ công khai. + </div> + ) : ( + urls.map((link) => ( + <div + key={link.id} + className="group relative flex items-center justify-between p-4 rounded-2xl bg-white/80 dark:bg-slate-900/80 hover:bg-white dark:hover:bg-slate-900 backdrop-blur-xl border border-white/50 shadow-md hover:shadow-xl transition-all duration-300 hover:-translate-y-0.5" + > + <a + href={link.short_url} + target="_blank" + rel="noopener noreferrer" + className="flex-1 flex flex-col pr-3 overflow-hidden" + > + <span className="font-semibold text-slate-900 dark:text-white text-base group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors truncate"> + {link.title || link.short_code} + </span> + <span className="text-xs text-slate-500 dark:text-slate-400 truncate flex items-center gap-1 mt-0.5"> + {link.short_url} + <ExternalLink className="w-3 h-3 opacity-60" /> + </span> + {link.tags && link.tags.length > 0 && ( + <div className="flex flex-wrap gap-1 mt-1.5"> + {link.tags.map((tag) => ( + <span + key={tag} + className="px-2 py-0.5 text-[10px] font-medium rounded-md bg-indigo-50 dark:bg-indigo-950/60 text-indigo-600 dark:text-indigo-300" + > + #{tag} + </span> + ))} + </div> + )} + </a> + + <button + onClick={() => openQrModal(link.title || link.short_code, link.short_url)} + className="p-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-colors shadow-sm shrink-0" + title="Xem mã QR" + > + <QrCode className="w-5 h-5" /> + </button> + </div> + )) + )} + </div> + + {/* Footer Brand */} + <div className="mt-12 text-center"> + <a + href="/" + className="inline-flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-full bg-black/10 dark:bg-white/10 backdrop-blur-sm text-slate-800 dark:text-slate-200 hover:scale-105 transition-all" + > + ⚡ Tạo Bio Link với <span className="font-bold underline">EZBio</span> + </a> + </div> + </div> + + {/* QR Code Modal */} + {activeQr && ( + <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"> + <div className="relative w-full max-w-xs bg-white dark:bg-slate-900 rounded-3xl p-6 shadow-2xl border border-white/20 text-center"> + <button + onClick={() => setActiveQr(null)} + className="absolute top-4 right-4 p-2 rounded-full text-slate-400 hover:text-slate-700 dark:hover:text-white" + > + <X className="w-5 h-5" /> + </button> + + <h3 className="text-lg font-bold text-slate-900 dark:text-white mb-1 truncate px-4"> + {activeQr.title} + </h3> + <p className="text-xs text-slate-500 truncate mb-4">{activeQr.url}</p> + + <div className="w-56 h-56 mx-auto mb-4 p-3 bg-white rounded-2xl border border-slate-200 shadow-inner flex items-center justify-center"> + {loadingQr ? ( + <div className="text-sm text-slate-400">Đang tạo mã QR...</div> + ) : activeQr.qrDataUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + <img src={activeQr.qrDataUrl} alt="QR Code" className="w-full h-full object-contain" /> + ) : ( + <div className="text-sm text-red-500">Lỗi tải mã QR</div> + )} + </div> + + <button + onClick={() => setActiveQr(null)} + className="w-full py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 font-semibold text-sm text-slate-800 dark:text-slate-200 transition-colors" + > + Đóng + </button> + </div> + </div> + )} + + {/* vCard Modal */} + {showVCardModal && ( + <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"> + <div className="relative w-full max-w-sm bg-white dark:bg-slate-900 rounded-3xl p-6 shadow-2xl border border-white/20"> + <button + onClick={() => setShowVCardModal(false)} + className="absolute top-4 right-4 p-2 rounded-full text-slate-400 hover:text-slate-700 dark:hover:text-white" + > + <X className="w-5 h-5" /> + </button> + + <h3 className="text-xl font-bold text-slate-900 dark:text-white mb-4 flex items-center gap-2"> + <UserCheck className="w-5 h-5 text-indigo-500" /> + Thông tin liên hệ (vCard) + </h3> + + <div className="space-y-3 mb-6 text-sm"> + {portal.vcard?.fullName && ( + <div className="p-3 rounded-xl bg-slate-50 dark:bg-slate-800 flex items-center gap-3"> + <UserCheck className="w-4 h-4 text-slate-400" /> + <span className="font-semibold text-slate-800 dark:text-slate-200"> + {portal.vcard.fullName} + </span> + </div> + )} + {portal.vcard?.phone && ( + <a + href={`tel:${portal.vcard.phone}`} + className="p-3 rounded-xl bg-slate-50 dark:bg-slate-800 flex items-center gap-3 hover:bg-indigo-50 dark:hover:bg-indigo-950/40 text-slate-700 dark:text-slate-300 transition-colors" + > + <Phone className="w-4 h-4 text-emerald-500" /> + <span>{portal.vcard.phone}</span> + </a> + )} + {portal.vcard?.email && ( + <a + href={`mailto:${portal.vcard.email}`} + className="p-3 rounded-xl bg-slate-50 dark:bg-slate-800 flex items-center gap-3 hover:bg-indigo-50 dark:hover:bg-indigo-950/40 text-slate-700 dark:text-slate-300 transition-colors" + > + <Mail className="w-4 h-4 text-indigo-500" /> + <span className="truncate">{portal.vcard.email}</span> + </a> + )} + {portal.vcard?.company && ( + <div className="p-3 rounded-xl bg-slate-50 dark:bg-slate-800 flex items-center gap-3 text-slate-700 dark:text-slate-300"> + <Building className="w-4 h-4 text-amber-500" /> + <span>{portal.vcard.company}</span> + </div> + )} + {portal.vcard?.website && ( + <a + href={portal.vcard.website} + target="_blank" + rel="noopener noreferrer" + className="p-3 rounded-xl bg-slate-50 dark:bg-slate-800 flex items-center gap-3 hover:bg-indigo-50 dark:hover:bg-indigo-950/40 text-slate-700 dark:text-slate-300 transition-colors" + > + <Globe className="w-4 h-4 text-sky-500" /> + <span className="truncate">{portal.vcard.website}</span> + </a> + )} + </div> + + <button + onClick={downloadVCard} + className="w-full py-3 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold text-sm shadow-md transition-all hover:scale-[1.02] active:scale-[0.98]" + > + 📥 Tải file Danh bạ (.vcf) + </button> + </div> + </div> + )} + </div> + ); +} diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts new file mode 100644 index 0000000..ca6b120 --- /dev/null +++ b/src/lib/analytics.ts @@ -0,0 +1,67 @@ +import { UAParser } from "ua-parser-js"; +import { prisma } from "./prisma"; + +export interface ClickTrackingData { + shortUrlId: string; + ipAddress?: string; + userAgent?: string; + referrer?: string; + headers?: Headers; +} + +export async function recordUrlClick(data: ClickTrackingData): Promise<void> { + try { + const { shortUrlId, ipAddress, userAgent, referrer, headers } = data; + + // Parse User Agent + const parser = new UAParser(userAgent || ""); + const device = parser.getDevice(); + const os = parser.getOS(); + const browser = parser.getBrowser(); + + let deviceType = "desktop"; + if (device.type === "mobile") deviceType = "mobile"; + else if (device.type === "tablet") deviceType = "tablet"; + + // Detect GeoIP via Vercel / Cloudflare edge headers + let countryCode = headers?.get("x-vercel-ip-country") || headers?.get("cf-ipcountry") || null; + let city = headers?.get("x-vercel-ip-city") || null; + let countryName = countryCode; + + // Map common country codes to country names + if (countryCode && typeof countryCode === "string") { + try { + const regionNames = new Intl.DisplayNames(["en"], { type: "region" }); + countryName = regionNames.of(countryCode) || countryCode; + } catch { + countryName = countryCode; + } + } + + // Save click record + await prisma.$transaction([ + prisma.urlClick.create({ + data: { + shortUrlId, + ipAddress: ipAddress || null, + userAgent: userAgent ? userAgent.slice(0, 500) : null, + deviceType, + operatingSystem: os.name ? `${os.name} ${os.version || ""}`.trim() : "Unknown", + browser: browser.name ? `${browser.name} ${browser.version || ""}`.trim() : "Unknown", + countryName: countryName || "Unknown", + countryCode: countryCode || "XX", + city: city ? decodeURIComponent(city) : null, + referrer: referrer ? referrer.slice(0, 500) : null, + }, + }), + prisma.shortUrl.update({ + where: { id: shortUrlId }, + data: { + clicksCount: { increment: 1 }, + }, + }), + ]); + } catch (error) { + console.error("Error recording click analytics:", error); + } +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..600f8ed --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,75 @@ +import { SignJWT, jwtVerify } from "jose"; +import { cookies } from "next/headers"; +import { prisma } from "./prisma"; + +const SECRET = new TextEncoder().encode( + process.env.AUTH_SECRET || "ezbio-super-secure-jwt-secret-key-32-characters-min" +); + +const COOKIE_NAME = "ezbio_session"; + +export interface UserSession { + id: string; + email: string; + name: string; + username: string; + role: string; +} + +export async function signSession(payload: UserSession): Promise<string> { + return await new SignJWT({ ...payload }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("7d") + .sign(SECRET); +} + +export async function verifySession(token: string): Promise<UserSession | null> { + try { + const { payload } = await jwtVerify(token, SECRET); + return payload as unknown as UserSession; + } catch { + return null; + } +} + +export async function getCurrentUser(): Promise<UserSession | null> { + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; + if (!token) return null; + + const session = await verifySession(token); + if (!session) return null; + + // Optional: check if user is still active in DB + const user = await prisma.user.findUnique({ + where: { id: session.id }, + select: { id: true, email: true, name: true, username: true, role: true, isActive: true }, + }); + + if (!user || !user.isActive) return null; + + return { + id: user.id, + email: user.email, + name: user.name, + username: user.username, + role: user.role, + }; +} + +export async function setAuthCookie(token: string) { + const cookieStore = await cookies(); + cookieStore.set(COOKIE_NAME, token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: 7 * 24 * 60 * 60, // 7 days + }); +} + +export async function clearAuthCookie() { + const cookieStore = await cookies(); + cookieStore.delete(COOKIE_NAME); +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..e8a3414 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,13 @@ +import { PrismaClient } from "@prisma/client"; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + log: process.env.NODE_ENV === "development" ? ["warn", "error"] : ["error"], + }); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; diff --git a/src/lib/qr.ts b/src/lib/qr.ts new file mode 100644 index 0000000..cadcfd7 --- /dev/null +++ b/src/lib/qr.ts @@ -0,0 +1,53 @@ +import QRCode from "qrcode"; + +export interface QROptions { + color?: string; // hex foreground + background?: string; // hex background or 'transparent' + size?: number; + margin?: number; +} + +export async function generateQRCodeDataUrl(text: string, options: QROptions = {}): Promise<string> { + const { color = "#000000", background, size = 300, margin = 1 } = options; + + const qrColor = { + dark: color.startsWith("#") ? color : `#${color}`, + light: background + ? background.toLowerCase() === "transparent" + ? "#00000000" + : background.startsWith("#") + ? background + : `#${background}` + : "#00000000", + }; + + return await QRCode.toDataURL(text, { + errorCorrectionLevel: "H", + margin, + width: size, + color: qrColor, + }); +} + +export async function generateQRCodeSVG(text: string, options: QROptions = {}): Promise<string> { + const { color = "#000000", background, size = 300, margin = 1 } = options; + + const qrColor = { + dark: color.startsWith("#") ? color : `#${color}`, + light: background + ? background.toLowerCase() === "transparent" + ? "#00000000" + : background.startsWith("#") + ? background + : `#${background}` + : "#00000000", + }; + + return await QRCode.toString(text, { + type: "svg", + errorCorrectionLevel: "H", + margin, + width: size, + color: qrColor, + }); +} diff --git a/src/lib/short-code.ts b/src/lib/short-code.ts new file mode 100644 index 0000000..1a7ebdf --- /dev/null +++ b/src/lib/short-code.ts @@ -0,0 +1,62 @@ +import { prisma } from "./prisma"; + +const CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + +/** + * Get the configured default short code length from admin settings. + * Defaults to 5 if not configured. + */ +export async function getDefaultUrlLength(): Promise<number> { + try { + const setting = await prisma.setting.findUnique({ + where: { key: "default_url_length" }, + }); + if (setting && setting.value) { + const len = parseInt(setting.value, 10); + if (!isNaN(len) && len >= 3 && len <= 16) { + return len; + } + } + } catch (error) { + console.error("Error reading default_url_length:", error); + } + return 5; +} + +/** + * Generate a random short code string of a given length. + */ +export function generateRandomCode(length: number): string { + let result = ""; + for (let i = 0; i < length; i++) { + result += CHARS.charAt(Math.floor(Math.random() * CHARS.length)); + } + return result; +} + +/** + * Generate a guaranteed unique short code based on configured default length. + */ +export async function generateUniqueShortCode(customLength?: number): Promise<string> { + const length = customLength || (await getDefaultUrlLength()); + let code = ""; + let exists = true; + let attempts = 0; + + while (exists && attempts < 15) { + code = generateRandomCode(length); + const existing = await prisma.shortUrl.findUnique({ + where: { shortCode: code }, + select: { id: true }, + }); + exists = Boolean(existing); + attempts++; + } + + if (exists) { + // If collides after multiple attempts, bump length by 1 + code = generateRandomCode(length + 1); + } + + return code; +} diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 0000000..e3bdebd --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,166 @@ +import { S3Client, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3"; +import { prisma } from "./prisma"; +import fs from "fs/promises"; +import path from "path"; + +// Initialize S3 client for Cloudflare R2 +const isR2Configured = Boolean( + process.env.CLOUDFLARE_R2_ACCOUNT_ID && + process.env.CLOUDFLARE_R2_ACCESS_KEY_ID && + process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY && + process.env.CLOUDFLARE_R2_BUCKET_NAME +); + +const r2Client = isR2Configured + ? new S3Client({ + region: "auto", + endpoint: `https://${process.env.CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, + credentials: { + accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID!, + secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY!, + }, + }) + : null; + +export interface UploadOptions { + buffer: Buffer; + fileName: string; + mimeType: string; + category: "avatar" | "background" | "logo" | "favicon" | "og_image"; + userId?: string; +} + +export async function uploadFile(options: UploadOptions): Promise<{ url: string; key: string }> { + const { buffer, fileName, mimeType, category, userId } = options; + const ext = path.extname(fileName) || ""; + const uniqueKey = `${category}/${Date.now()}_${Math.random().toString(36).substring(2, 9)}${ext}`; + const sizeBytes = BigInt(buffer.length); + + let fileUrl = ""; + let storageType = "local"; + + if (isR2Configured && r2Client && process.env.STORAGE_DRIVER !== "local") { + // Upload to Cloudflare R2 + storageType = "cloudflare_r2"; + await r2Client.send( + new PutObjectCommand({ + Bucket: process.env.CLOUDFLARE_R2_BUCKET_NAME!, + Key: uniqueKey, + Body: buffer, + ContentType: mimeType, + }) + ); + + const publicBase = (process.env.CLOUDFLARE_R2_PUBLIC_URL || "").replace(/\/$/, ""); + fileUrl = publicBase ? `${publicBase}/${uniqueKey}` : `/api/storage/${uniqueKey}`; + } else { + // Fallback: Local Storage in public/uploads + storageType = "local"; + const uploadDir = path.join(process.cwd(), "public", "uploads", category); + await fs.mkdir(uploadDir, { recursive: true }); + const localFilePath = path.join(uploadDir, path.basename(uniqueKey)); + await fs.writeFile(localFilePath, buffer); + fileUrl = `/uploads/${category}/${path.basename(uniqueKey)}`; + } + + // Record in database to monitor Cloudflare R2 storage usage + try { + await prisma.uploadedFile.create({ + data: { + key: uniqueKey, + url: fileUrl, + originalName: fileName, + mimeType, + sizeBytes, + category, + storageType, + userId: userId || null, + }, + }); + } catch (error) { + console.error("Failed to record uploaded file in DB:", error); + } + + return { url: fileUrl, key: uniqueKey }; +} + +export async function deleteFile(key: string): Promise<boolean> { + try { + const fileRecord = await prisma.uploadedFile.findUnique({ + where: { key }, + }); + + if (fileRecord?.storageType === "cloudflare_r2" && r2Client) { + await r2Client.send( + new DeleteObjectCommand({ + Bucket: process.env.CLOUDFLARE_R2_BUCKET_NAME!, + Key: key, + }) + ); + } else { + // Local file + const localFilePath = path.join(process.cwd(), "public", "uploads", key); + try { + await fs.unlink(localFilePath); + } catch { + // Ignore if file doesn't exist + } + } + + await prisma.uploadedFile.deleteMany({ + where: { key }, + }); + + return true; + } catch (error) { + console.error("Error deleting file:", error); + return false; + } +} + +export interface StorageStatus { + usedBytes: number; + usedMB: number; + usedGB: number; + limitMB: number; + limitGB: number; + percentUsed: number; + isWarning: boolean; // >= 80% + isCritical: boolean; // >= 90% + totalFiles: number; + storageDriver: string; +} + +/** + * Get current storage usage and check if approaching Cloudflare R2 Free Tier limit (10GB) + */ +export async function getStorageStatus(): Promise<StorageStatus> { + const files = await prisma.uploadedFile.findMany({ + select: { sizeBytes: true }, + }); + + const totalBytes = files.reduce((acc, f) => acc + Number(f.sizeBytes), 0); + const usedMB = Number((totalBytes / (1024 * 1024)).toFixed(2)); + const usedGB = Number((totalBytes / (1024 * 1024 * 1024)).toFixed(3)); + + // Default free limit: 10 GB = 10,240 MB + const limitMB = Number(process.env.CLOUDFLARE_R2_FREE_LIMIT_MB || 10240); + const limitGB = Number((limitMB / 1024).toFixed(1)); + const percentUsed = Number(((usedMB / limitMB) * 100).toFixed(1)); + + const isWarning = percentUsed >= 80 && percentUsed < 90; + const isCritical = percentUsed >= 90; + + return { + usedBytes: totalBytes, + usedMB, + usedGB, + limitMB, + limitGB, + percentUsed, + isWarning, + isCritical, + totalFiles: files.length, + storageDriver: isR2Configured && process.env.STORAGE_DRIVER !== "local" ? "Cloudflare R2" : "Local Storage", + }; +} diff --git a/src/lib/theme-presets.ts b/src/lib/theme-presets.ts new file mode 100644 index 0000000..1766068 --- /dev/null +++ b/src/lib/theme-presets.ts @@ -0,0 +1,55 @@ +export interface GradientPreset { + id: string; + name: string; + background: string; + titleStyle: string; +} + +export const GRADIENT_PRESETS: Record<string, GradientPreset> = { + "gradient-1": { + id: "gradient-1", + name: "Cotton Candy", + background: "linear-gradient(to top, #fad0c4 0%, #ffd1ff 100%)", + titleStyle: "bg-gradient-to-r from-[#4158D0] to-[#C850C0] bg-clip-text text-transparent", + }, + "gradient-2": { + id: "gradient-2", + name: "Emerald Sky", + background: "linear-gradient(180deg, #2af598 0%, #009efd 100%)", + titleStyle: "bg-gradient-to-r from-[#85FFBD] to-[#009efd] bg-clip-text text-transparent", + }, + "gradient-3": { + id: "gradient-3", + name: "Warm Flame", + background: "linear-gradient(120deg, #f6d365 0%, #fda085 100%)", + titleStyle: "bg-gradient-to-r from-[#FBAB7E] to-[#F7CE68] bg-clip-text text-transparent", + }, + "gradient-4": { + id: "gradient-4", + name: "Lavender Mist", + background: "linear-gradient(120deg, #e0c3fc 0%, #8ec5fc 100%)", + titleStyle: "bg-gradient-to-r from-[#FFDEE9] to-[#8ec5fc] bg-clip-text text-transparent", + }, + "gradient-5": { + id: "gradient-5", + name: "Cyber Neon", + background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + titleStyle: "bg-gradient-to-r from-[#FF9A8B] to-[#FF6A88] bg-clip-text text-transparent", + }, + "gradient-6": { + id: "gradient-6", + name: "Sunset Glow", + background: "linear-gradient(120deg, #89f7fe 0%, #66a6ff 100%)", + titleStyle: "bg-gradient-to-r from-[#FFE53B] to-[#FF2525] bg-clip-text text-transparent", + }, +}; + +export function getBackgroundStyle(id?: string | null): string { + if (!id) return GRADIENT_PRESETS["gradient-1"].background; + return GRADIENT_PRESETS[id]?.background || GRADIENT_PRESETS["gradient-1"].background; +} + +export function getTitleGradientClass(id?: string | null): string { + if (!id) return GRADIENT_PRESETS["gradient-1"].titleStyle; + return GRADIENT_PRESETS[id]?.titleStyle || GRADIENT_PRESETS["gradient-1"].titleStyle; +} diff --git a/src/lib/url-metadata.ts b/src/lib/url-metadata.ts new file mode 100644 index 0000000..2a75c41 --- /dev/null +++ b/src/lib/url-metadata.ts @@ -0,0 +1,45 @@ +import * as cheerio from "cheerio"; + +/** + * Fetch and extract the page title from a URL. + */ +export async function fetchUrlTitle(targetUrl: string): Promise<string | null> { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 6000); + + const res = await fetch(targetUrl, { + signal: controller.signal, + headers: { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + }, + }); + + clearTimeout(timeout); + + if (!res.ok) return null; + + const html = await res.text(); + const $ = cheerio.load(html); + + // 1. Try standard title tag + let title = $("title").first().text().trim(); + + // 2. Fallback to OpenGraph title + if (!title) { + title = $('meta[property="og:title"]').attr("content")?.trim() || ""; + } + + // 3. Fallback to Twitter title + if (!title) { + title = $('meta[name="twitter:title"]').attr("content")?.trim() || ""; + } + + return title ? title.slice(0, 255) : null; + } catch (error) { + console.warn(`Could not fetch title for ${targetUrl}:`, error); + return null; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..416d871 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "src/**/*.ts", + "src/**/*.tsx", + "prisma/**/*.ts", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": ["node_modules", "oldversion_php"] +}