feat: migración a PostgreSQL, tiempo real SSE, Dockerfile y compose para Coolify
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Variables de Entorno para Coolify
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
POSTGRES_DB=encuesta_e3
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres_password_e3
|
||||
DATABASE_URL=postgresql://postgres:postgres_password_e3@db:5432/encuesta_e3
|
||||
ADMIN_SECRET=RESETEOE3
|
||||
@@ -0,0 +1,17 @@
|
||||
# Configuración del Servidor y Base de Datos PostgreSQL
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
|
||||
# Conexión PostgreSQL (Formato URL o componentes individuales)
|
||||
DATABASE_URL=postgresql://postgres:postgres_password_e3@db:5432/encuesta_e3
|
||||
|
||||
# Variables alternativas de PostgreSQL
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=encuesta_e3
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres_password_e3
|
||||
DATABASE_SSL=false
|
||||
|
||||
# Clave secreta para funciones de administrador (Restablecer votos)
|
||||
ADMIN_SECRET=RESETEOE3
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
dist/
|
||||
dist-server/
|
||||
build/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
!.env.example
|
||||
!.env.coolify.example
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# ==========================================
|
||||
# STAGE 1: Build Frontend & Server Bundle
|
||||
# ==========================================
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar dependencias completas para build
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copiar código fuente
|
||||
COPY . .
|
||||
|
||||
# Compilar frontend (Vite) y backend (esbuild)
|
||||
RUN npm run build
|
||||
|
||||
# ==========================================
|
||||
# STAGE 2: Imagen Ligera de Producción
|
||||
# ==========================================
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
# Instalar únicamente dependencias de producción
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
# Copiar artefactos compilados del builder
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/dist-server ./dist-server
|
||||
|
||||
# Exponer puerto de la aplicación
|
||||
EXPOSE 3000
|
||||
|
||||
# Health check nativo de Docker
|
||||
HEALTHCHECK --interval=20s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health || exit 1
|
||||
|
||||
# Iniciar servidor Express optimizado
|
||||
CMD ["node", "dist-server/index.js"]
|
||||
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/63074546-87c3-40fe-9162-fc39320dabba
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
@@ -0,0 +1,668 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "react-example",
|
||||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"vite": "^6.2.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/[email protected]", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
|
||||
"@babel/core": ["@babel/[email protected]", "", { "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" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
|
||||
|
||||
"@babel/generator": ["@babel/[email protected]", "", { "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" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/[email protected]", "", { "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" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/[email protected]", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/[email protected]", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/[email protected]", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/[email protected]", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/[email protected]", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
|
||||
"@babel/helpers": ["@babel/[email protected]", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
|
||||
"@babel/parser": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
|
||||
|
||||
"@babel/template": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/[email protected]", "", { "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" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@google/genai": ["@google/[email protected]", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-Cnw71bRtYXnGkN/K1YLb4Wz3yPwIe/7c5kw4VkbXAX508A9HHZCTMsBUhaAjTHDfD9Tn2veHxyJXK1Dxxtcx4g=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/[email protected]", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/[email protected]", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/[email protected]", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/[email protected]", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/[email protected]", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/[email protected]", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/[email protected]", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/[email protected]", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/[email protected]", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/[email protected]", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/[email protected]", "", { "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" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/[email protected]", "", { "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" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/[email protected]", "", { "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" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/[email protected]", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
|
||||
|
||||
"@types/babel__core": ["@types/[email protected]", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/[email protected]", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/[email protected]", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/[email protected]", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/body-parser": ["@types/[email protected]", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
|
||||
"@types/canvas-confetti": ["@types/[email protected]", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
|
||||
|
||||
"@types/connect": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/estree": ["@types/[email protected]", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/express": ["@types/[email protected]", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg=="],
|
||||
|
||||
"@types/http-errors": ["@types/[email protected]", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/mime": ["@types/[email protected]", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="],
|
||||
|
||||
"@types/qs": ["@types/[email protected]", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
|
||||
|
||||
"@types/range-parser": ["@types/[email protected]", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/retry": ["@types/[email protected]", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
||||
|
||||
"@types/send": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
|
||||
|
||||
"@types/serve-static": ["@types/[email protected]", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/[email protected]", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
|
||||
|
||||
"accepts": ["[email protected]", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"array-flatten": ["[email protected]", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
|
||||
"autoprefixer": ["[email protected]", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="],
|
||||
|
||||
"base64-js": ["[email protected]", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"baseline-browser-mapping": ["[email protected]", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww=="],
|
||||
|
||||
"bignumber.js": ["[email protected]", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"body-parser": ["[email protected]", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="],
|
||||
|
||||
"browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="],
|
||||
|
||||
"buffer-equal-constant-time": ["[email protected]", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"bytes": ["[email protected]", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"caniuse-lite": ["[email protected]", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="],
|
||||
|
||||
"canvas-confetti": ["[email protected]", "", {}, "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw=="],
|
||||
|
||||
"content-disposition": ["[email protected]", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
||||
|
||||
"content-type": ["[email protected]", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"convert-source-map": ["[email protected]", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["[email protected]", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["[email protected]", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
|
||||
|
||||
"data-uri-to-buffer": ["[email protected]", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"debug": ["[email protected]", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"depd": ["[email protected]", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"destroy": ["[email protected]", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
||||
|
||||
"detect-libc": ["[email protected]", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dotenv": ["[email protected]", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||
|
||||
"dunder-proto": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["[email protected]", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["[email protected]", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["[email protected]", "", {}, "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew=="],
|
||||
|
||||
"encodeurl": ["[email protected]", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"enhanced-resolve": ["[email protected]", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||
|
||||
"es-define-property": ["[email protected]", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["[email protected]", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
||||
|
||||
"esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escalade": ["[email protected]", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["[email protected]", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["[email protected]", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"express": ["[email protected]", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="],
|
||||
|
||||
"extend": ["[email protected]", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fetch-blob": ["[email protected]", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"finalhandler": ["[email protected]", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
||||
|
||||
"formdata-polyfill": ["[email protected]", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"forwarded": ["[email protected]", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fraction.js": ["[email protected]", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||
|
||||
"framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.43.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g=="],
|
||||
|
||||
"fresh": ["[email protected]", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["[email protected]", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gaxios": ["[email protected]", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="],
|
||||
|
||||
"gcp-metadata": ["[email protected]", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
||||
|
||||
"gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-intrinsic": ["[email protected]", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["[email protected]", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"google-auth-library": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw=="],
|
||||
|
||||
"google-logging-utils": ["[email protected]", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||
|
||||
"gopd": ["[email protected]", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-symbols": ["[email protected]", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["[email protected]", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"http-errors": ["[email protected]", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"https-proxy-agent": ["[email protected]", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"iconv-lite": ["[email protected]", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ipaddr.js": ["[email protected]", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"jiti": ["[email protected]", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||
|
||||
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-bigint": ["[email protected]", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||
|
||||
"json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jwa": ["[email protected]", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["[email protected]", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"lightningcss": ["[email protected]", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"long": ["[email protected]", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["[email protected]", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
|
||||
|
||||
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"math-intrinsics": ["[email protected]", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["[email protected]", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||
|
||||
"merge-descriptors": ["[email protected]", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
|
||||
|
||||
"methods": ["[email protected]", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
||||
|
||||
"mime": ["[email protected]", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
|
||||
"mime-db": ["[email protected]", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"motion": ["[email protected]", "", { "dependencies": { "framer-motion": "^12.43.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ=="],
|
||||
|
||||
"motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="],
|
||||
|
||||
"motion-utils": ["[email protected]", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
||||
|
||||
"ms": ["[email protected]", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||
|
||||
"negotiator": ["[email protected]", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"node-domexception": ["[email protected]", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["[email protected]", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"node-releases": ["[email protected]", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="],
|
||||
|
||||
"object-inspect": ["[email protected]", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["[email protected]", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"p-retry": ["[email protected]", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
||||
|
||||
"parseurl": ["[email protected]", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-to-regexp": ["[email protected]", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
|
||||
|
||||
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["[email protected]", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
||||
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
|
||||
|
||||
"postcss-value-parser": ["[email protected]", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"protobufjs": ["[email protected]", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
|
||||
|
||||
"proxy-addr": ["[email protected]", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["[email protected]", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
|
||||
|
||||
"range-parser": ["[email protected]", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["[email protected]", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
||||
|
||||
"react": ["[email protected]", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||
|
||||
"react-dom": ["[email protected]", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||
|
||||
"react-refresh": ["[email protected]", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||
|
||||
"retry": ["[email protected]", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||
|
||||
"rollup": ["[email protected]", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="],
|
||||
|
||||
"safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["[email protected]", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"send": ["[email protected]", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
|
||||
|
||||
"serve-static": ["[email protected]", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
||||
|
||||
"setprototypeof": ["[email protected]", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"side-channel": ["[email protected]", "", { "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" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
|
||||
|
||||
"side-channel-list": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
|
||||
"side-channel-map": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["[email protected]", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"statuses": ["[email protected]", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"tailwindcss": ["[email protected]", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||
|
||||
"tapable": ["[email protected]", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"toidentifier": ["[email protected]", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsx": ["[email protected]", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="],
|
||||
|
||||
"type-is": ["[email protected]", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["[email protected]", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unpipe": ["[email protected]", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"update-browserslist-db": ["[email protected]", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="],
|
||||
|
||||
"utils-merge": ["[email protected]", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"vary": ["[email protected]", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="],
|
||||
|
||||
"web-streams-polyfill": ["[email protected]", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
||||
|
||||
"yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@babel/core/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@babel/traverse/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/[email protected]", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@types/serve-static/@types/send": ["@types/[email protected]", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
|
||||
|
||||
"https-proxy-agent/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"send/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"tsx/esbuild": ["[email protected]", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
||||
|
||||
"@babel/core/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"@babel/traverse/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"https-proxy-agent/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres_password_e3}@db:5432/${POSTGRES_DB:-encuesta_e3}
|
||||
- ADMIN_SECRET=${ADMIN_SECRET:-RESETEOE3}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB=${POSTGRES_DB:-encuesta_e3}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres_password_e3}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-postgres} -d $${POSTGRES_DB:-encuesta_e3}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
@@ -0,0 +1,43 @@
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres_password_e3}@db:5432/${POSTGRES_DB:-encuesta_e3}
|
||||
- ADMIN_SECRET=${ADMIN_SECRET:-RESETEOE3}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_DB=${POSTGRES_DB:-encuesta_e3}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres_password_e3}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-postgres} -d $${POSTGRES_DB:-encuesta_e3}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>¿Qué proyecto debe ser el ganador? — Consultoría E3</title>
|
||||
<meta name="description" content="Encuesta interna de Consultoría E3 para elegir al proyecto ganador en tiempo real." />
|
||||
<meta property="og:title" content="¿Qué proyecto debe ser el ganador? — Consultoría E3" />
|
||||
<meta property="og:description" content="Encuesta de Votación en Tiempo Real de Consultoría E3." />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-[#000000] text-[#ffffff] antialiased selection:bg-[#00B7D4] selection:text-black min-h-screen">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Encuesta de Votación — Consultoría E3",
|
||||
"description": "Encuesta interna de Consultoría E3 para elegir al proyecto ganador en tiempo real con Upstash Serverless Redis.",
|
||||
"requestFramePermissions": [],
|
||||
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||
}
|
||||
Generated
+4171
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "encuesta-votacion-e3",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||
"dev:server": "tsx watch server/index.ts",
|
||||
"build:client": "vite build",
|
||||
"build:server": "esbuild server/index.ts --bundle --platform=node --target=node20 --format=esm --packages=external --outfile=dist-server/index.js",
|
||||
"build": "vite build && npm run build:server",
|
||||
"start": "node dist-server/index.js",
|
||||
"server": "tsx server/index.ts",
|
||||
"clean": "rm -rf dist dist-server",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"pg": "^8.13.3",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"vite": "^6.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/pg": "^8.11.11",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2"
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
import pg from 'pg';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const { Pool } = pg;
|
||||
|
||||
// Configuración flexible para PostgreSQL (DATABASE_URL o variables individuales)
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
|
||||
let pool: pg.Pool | null = null;
|
||||
let isPostgresConnected = false;
|
||||
let inMemoryVotes: Record<string, string> = {};
|
||||
|
||||
if (connectionString || process.env.POSTGRES_HOST || process.env.PGHOST) {
|
||||
const sslConfig = process.env.NODE_ENV === 'production' && process.env.DATABASE_SSL === 'true'
|
||||
? { rejectUnauthorized: false }
|
||||
: false;
|
||||
|
||||
pool = new Pool(
|
||||
connectionString
|
||||
? {
|
||||
connectionString,
|
||||
ssl: sslConfig,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
}
|
||||
: {
|
||||
host: process.env.POSTGRES_HOST || process.env.PGHOST || 'localhost',
|
||||
port: parseInt(process.env.POSTGRES_PORT || process.env.PGPORT || '5432', 10),
|
||||
user: process.env.POSTGRES_USER || process.env.PGUSER || 'postgres',
|
||||
password: process.env.POSTGRES_PASSWORD || process.env.PGPASSWORD || 'postgres',
|
||||
database: process.env.POSTGRES_DB || process.env.PGDATABASE || 'encuesta_e3',
|
||||
ssl: sslConfig,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('⚠️ Error inesperado en el pool de PostgreSQL:', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa la tabla de votos si no existe y verifica la conexión.
|
||||
*/
|
||||
export async function initDatabase(): Promise<boolean> {
|
||||
if (!pool) {
|
||||
console.warn('⚠️ No se proporcionó configuración de PostgreSQL. Usando almacenamiento en memoria.');
|
||||
isPostgresConnected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS votes (
|
||||
voter VARCHAR(255) PRIMARY KEY,
|
||||
candidate VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_votes_candidate ON votes(candidate);
|
||||
`);
|
||||
|
||||
isPostgresConnected = true;
|
||||
console.log('✅ Conexión con PostgreSQL establecida exitosamente y schema verificado.');
|
||||
return true;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error conectando a PostgreSQL durante la inicialización:', (error as Error).message);
|
||||
console.warn('⚠️ La aplicación continuará funcionando en modo híbrido/resiliente.');
|
||||
isPostgresConnected = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene todos los votos registrados (mapa de votante -> candidato)
|
||||
*/
|
||||
export async function getAllVotes(): Promise<Record<string, string>> {
|
||||
if (pool && isPostgresConnected) {
|
||||
try {
|
||||
const res = await pool.query('SELECT voter, candidate FROM votes ORDER BY updated_at ASC');
|
||||
const votes: Record<string, string> = {};
|
||||
for (const row of res.rows) {
|
||||
if (row.voter && row.candidate) {
|
||||
votes[String(row.voter).trim().toUpperCase()] = String(row.candidate).trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
// Actualizar réplica en memoria
|
||||
inMemoryVotes = { ...votes };
|
||||
return votes;
|
||||
} catch (err) {
|
||||
console.error('Error al consultar PostgreSQL getAllVotes:', (err as Error).message);
|
||||
return inMemoryVotes;
|
||||
}
|
||||
}
|
||||
return inMemoryVotes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserta o actualiza un voto atómicamente (UPSERT)
|
||||
*/
|
||||
export async function castVote(voter: string, candidate: string): Promise<{ voter: string; candidate: string; isNew: boolean }> {
|
||||
const normVoter = voter.trim().toUpperCase();
|
||||
const normCandidate = candidate.trim().toUpperCase();
|
||||
|
||||
if (!normVoter || !normCandidate) {
|
||||
throw new Error('Nombre de votante y candidato son requeridos.');
|
||||
}
|
||||
|
||||
if (pool && isPostgresConnected) {
|
||||
try {
|
||||
const query = `
|
||||
INSERT INTO votes (voter, candidate, updated_at)
|
||||
VALUES ($1, $2, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (voter)
|
||||
DO UPDATE SET
|
||||
candidate = EXCLUDED.candidate,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING (xmax = 0) AS is_new, voter, candidate;
|
||||
`;
|
||||
const res = await pool.query(query, [normVoter, normCandidate]);
|
||||
const row = res.rows[0];
|
||||
inMemoryVotes[normVoter] = normCandidate;
|
||||
return {
|
||||
voter: row.voter,
|
||||
candidate: row.candidate,
|
||||
isNew: Boolean(row.is_new),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error al insertar voto en PostgreSQL:', (err as Error).message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback memoria
|
||||
const isNew = !inMemoryVotes[normVoter];
|
||||
inMemoryVotes[normVoter] = normCandidate;
|
||||
return { voter: normVoter, candidate: normCandidate, isNew };
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina un voto (para reseteo administrativo)
|
||||
*/
|
||||
export async function deleteVote(voter: string): Promise<boolean> {
|
||||
const normVoter = voter.trim().toUpperCase();
|
||||
if (!normVoter) {
|
||||
throw new Error('Nombre de votante requerido para restablecer.');
|
||||
}
|
||||
|
||||
if (pool && isPostgresConnected) {
|
||||
try {
|
||||
const res = await pool.query('DELETE FROM votes WHERE UPPER(voter) = $1', [normVoter]);
|
||||
delete inMemoryVotes[normVoter];
|
||||
return (res.rowCount ?? 0) > 0;
|
||||
} catch (err) {
|
||||
console.error('Error al eliminar voto en PostgreSQL:', (err as Error).message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const existed = Boolean(inMemoryVotes[normVoter]);
|
||||
delete inMemoryVotes[normVoter];
|
||||
return existed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnóstico de salud de la base de datos
|
||||
*/
|
||||
export async function getDatabaseHealth(): Promise<{ connected: boolean; provider: string; error?: string }> {
|
||||
if (!pool) {
|
||||
return { connected: false, provider: 'in-memory-fallback' };
|
||||
}
|
||||
try {
|
||||
const res = await pool.query('SELECT NOW() as now, COUNT(*) as count FROM votes');
|
||||
return {
|
||||
connected: true,
|
||||
provider: 'postgresql',
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
connected: false,
|
||||
provider: 'postgresql-error',
|
||||
error: (err as Error).message,
|
||||
};
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import dotenv from 'dotenv';
|
||||
import { initDatabase, getAllVotes, castVote, deleteVote, getDatabaseHealth } from './db';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||
const ADMIN_SECRET = process.env.ADMIN_SECRET || 'RESETEOE3';
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Clientes SSE conectados para transmisión en vivo
|
||||
interface SseClient {
|
||||
id: number;
|
||||
res: Response;
|
||||
}
|
||||
let sseClients: SseClient[] = [];
|
||||
let nextClientId = 1;
|
||||
|
||||
/**
|
||||
* Transmite la actualización de votos a todos los navegadores conectados en tiempo real
|
||||
*/
|
||||
async function broadcastUpdate() {
|
||||
if (sseClients.length === 0) return;
|
||||
try {
|
||||
const votes = await getAllVotes();
|
||||
const payload = JSON.stringify({
|
||||
type: 'VOTES_UPDATED',
|
||||
timestamp: Date.now(),
|
||||
votes,
|
||||
total: Object.keys(votes).length,
|
||||
});
|
||||
|
||||
sseClients.forEach((client) => {
|
||||
try {
|
||||
client.res.write(`event: votes\ndata: ${payload}\n\n`);
|
||||
} catch (err) {
|
||||
// Ignorar clientes cerrados silenciosamente
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error transmitiendo evento SSE:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep-alive heartbeat para mantener abiertas las conexiones SSE a través de proxys/Cloudflare
|
||||
setInterval(() => {
|
||||
sseClients.forEach((client) => {
|
||||
try {
|
||||
client.res.write(': heartbeat\n\n');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
}, 15000);
|
||||
|
||||
// ==========================================
|
||||
// ENDPOINTS DE LA API
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Health check & diagnóstico
|
||||
*/
|
||||
app.get('/api/health', async (_req: Request, res: Response) => {
|
||||
const dbHealth = await getDatabaseHealth();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'encuesta-votacion-e3',
|
||||
version: '1.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
database: dbHealth,
|
||||
activeStreamClients: sseClients.length,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Stream de Server-Sent Events (SSE) para actualizaciones en vivo cero-latencia
|
||||
*/
|
||||
app.get('/api/stream', async (req: Request, res: Response) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('X-Accel-Buffering', 'no'); // Deshabilita buffering en NGINX/Traefik
|
||||
|
||||
const clientId = nextClientId++;
|
||||
const newClient: SseClient = { id: clientId, res };
|
||||
sseClients.push(newClient);
|
||||
|
||||
// Enviar estado actual de bienvenida
|
||||
const initialVotes = await getAllVotes();
|
||||
res.write(
|
||||
`event: initial\ndata: ${JSON.stringify({
|
||||
type: 'INIT',
|
||||
timestamp: Date.now(),
|
||||
votes: initialVotes,
|
||||
total: Object.keys(initialVotes).length,
|
||||
})}\n\n`
|
||||
);
|
||||
|
||||
req.on('close', () => {
|
||||
sseClients = sseClients.filter((c) => c.id !== clientId);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Obtener todos los votos actuales
|
||||
*/
|
||||
app.get('/api/votes', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const votes = await getAllVotes();
|
||||
res.json({
|
||||
success: true,
|
||||
votes,
|
||||
total: Object.keys(votes).length,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Error al obtener votos',
|
||||
detail: (error as Error).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Emitir o actualizar un voto
|
||||
*/
|
||||
app.post('/api/vote', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { voter, candidate } = req.body;
|
||||
|
||||
if (!voter || typeof voter !== 'string' || voter.trim().length < 2) {
|
||||
res.status(400).json({ success: false, error: 'Nombre de votante inválido o muy corto.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!candidate || typeof candidate !== 'string' || candidate.trim().length === 0) {
|
||||
res.status(400).json({ success: false, error: 'Debe seleccionar un candidato válido.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const normVoter = voter.trim().toUpperCase();
|
||||
const normCand = candidate.trim().toUpperCase();
|
||||
|
||||
// Validación anti-autovoto
|
||||
const voterWords = normVoter.split(/\s+/);
|
||||
if (normVoter === normCand || voterWords.includes(normCand)) {
|
||||
res.status(400).json({ success: false, error: 'No está permitido votar por uno mismo.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await castVote(normVoter, normCand);
|
||||
|
||||
// Disparar actualización en vivo para todos los clientes
|
||||
broadcastUpdate().catch(console.error);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Voto registrado exitosamente.',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error en POST /api/vote:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Error al registrar el voto en PostgreSQL.',
|
||||
detail: (error as Error).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Restablecer / eliminar un voto (Administrador)
|
||||
*/
|
||||
app.delete('/api/vote/:voter', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const voter = req.params.voter;
|
||||
const providedKey = req.headers['x-admin-key'] || req.body?.password || req.query?.key;
|
||||
|
||||
if (providedKey !== ADMIN_SECRET) {
|
||||
res.status(401).json({ success: false, error: 'Clave de administración incorrecta.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!voter) {
|
||||
res.status(400).json({ success: false, error: 'Debe especificar el nombre del votante a restablecer.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const deleted = await deleteVote(voter);
|
||||
|
||||
// Disparar actualización en vivo
|
||||
broadcastUpdate().catch(console.error);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Voto de "${voter}" restablecido exitosamente.`,
|
||||
deleted,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Error al eliminar el voto.',
|
||||
detail: (error as Error).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// SERVIR FRONTEND ESTÁTICO EN PRODUCCIÓN
|
||||
// ==========================================
|
||||
const distPath = path.resolve(process.cwd(), 'dist');
|
||||
if (fs.existsSync(distPath)) {
|
||||
app.use(express.static(distPath));
|
||||
app.get('*', (req: Request, res: Response) => {
|
||||
if (req.path.startsWith('/api')) {
|
||||
res.status(404).json({ error: 'Not Found' });
|
||||
return;
|
||||
}
|
||||
res.sendFile(path.join(distPath, 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
// Iniciar base de datos y servidor
|
||||
async function start() {
|
||||
await initDatabase();
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`🚀 Servidor de Votación E3 corriendo en http://0.0.0.0:${PORT}`);
|
||||
console.log(`📊 API disponible en http://0.0.0.0:${PORT}/api/votes`);
|
||||
console.log(`⚡ Stream en tiempo real en http://0.0.0.0:${PORT}/api/stream`);
|
||||
});
|
||||
}
|
||||
|
||||
start().catch((err) => {
|
||||
console.error('Fatal error iniciando el servidor:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Header } from './components/Header';
|
||||
import { VoterStep } from './components/VoterStep';
|
||||
import { VotingStep } from './components/VotingStep';
|
||||
import { LiveResults } from './components/LiveResults';
|
||||
import { AdminPanel } from './components/AdminPanel';
|
||||
import { Footer } from './components/Footer';
|
||||
import { ApiService } from './services/api';
|
||||
import { CANDIDATOS_DEFAULT, CandidateStats, SyncStatus } from './types';
|
||||
|
||||
export default function App() {
|
||||
const [voterName, setVoterName] = useState<string>(() => ApiService.getSavedVoter());
|
||||
const [votes, setVotes] = useState<Record<string, string>>({});
|
||||
const [syncStatus, setSyncStatus] = useState<SyncStatus>('syncing');
|
||||
const [lastSyncTime, setLastSyncTime] = useState<Date | null>(null);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isAdminOpen, setIsAdminOpen] = useState(false);
|
||||
|
||||
// Sincronización REST con la API PostgreSQL
|
||||
const fetchVotes = useCallback(async (isManual = false) => {
|
||||
if (isManual) {
|
||||
setIsRefreshing(true);
|
||||
}
|
||||
try {
|
||||
const liveVotes = await ApiService.fetchAllVotes();
|
||||
setVotes(liveVotes);
|
||||
setSyncStatus('synced');
|
||||
setLastSyncTime(new Date());
|
||||
} catch (err) {
|
||||
console.warn('Error sincronizando votos con backend PostgreSQL:', err);
|
||||
if (!navigator.onLine) {
|
||||
setSyncStatus('offline');
|
||||
} else {
|
||||
setSyncStatus('error');
|
||||
}
|
||||
} finally {
|
||||
if (isManual) {
|
||||
setTimeout(() => setIsRefreshing(false), 300);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Suscripción al stream en vivo (SSE) + sondeo de respaldo periódico
|
||||
useEffect(() => {
|
||||
// 1. Cargar votos iniciales inmediatamente
|
||||
fetchVotes();
|
||||
|
||||
// 2. Suscribirse al stream Server-Sent Events (SSE) para tiempo real instantáneo
|
||||
const unsubscribeStream = ApiService.subscribeToLiveStream(
|
||||
(liveVotes) => {
|
||||
setVotes(liveVotes);
|
||||
setSyncStatus('synced');
|
||||
setLastSyncTime(new Date());
|
||||
},
|
||||
(newStatus) => {
|
||||
setSyncStatus(newStatus);
|
||||
}
|
||||
);
|
||||
|
||||
// 3. Respaldo de sondeo periódico cada 4s y eventos reactivos del navegador
|
||||
const intervalId = setInterval(() => {
|
||||
fetchVotes();
|
||||
}, 4000);
|
||||
|
||||
const handleFocus = () => fetchVotes();
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
fetchVotes();
|
||||
}
|
||||
};
|
||||
const handleOnline = () => {
|
||||
setSyncStatus('syncing');
|
||||
fetchVotes();
|
||||
};
|
||||
const handleOffline = () => {
|
||||
setSyncStatus('offline');
|
||||
};
|
||||
|
||||
window.addEventListener('focus', handleFocus);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
unsubscribeStream();
|
||||
clearInterval(intervalId);
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, [fetchVotes]);
|
||||
|
||||
// Guardar nombre de votante
|
||||
const handleVoterNameChange = (name: string) => {
|
||||
setVoterName(name);
|
||||
ApiService.setSavedVoter(name);
|
||||
};
|
||||
|
||||
const normalizedVoter = voterName.trim().toUpperCase();
|
||||
const existingVoteFor = normalizedVoter ? votes[normalizedVoter] || null : null;
|
||||
|
||||
// Emitir voto
|
||||
const handleCastVote = async (candidate: string) => {
|
||||
if (!normalizedVoter || normalizedVoter.length < 2) {
|
||||
alert('Por favor escribe tu nombre completo en el Paso 1 antes de votar.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ApiService.castVote(normalizedVoter, candidate);
|
||||
// Actualización optimista inmediata
|
||||
setVotes((prev) => ({
|
||||
...prev,
|
||||
[normalizedVoter]: candidate.toUpperCase(),
|
||||
}));
|
||||
setLastSyncTime(new Date());
|
||||
} catch (err) {
|
||||
alert(`Error al registrar el voto en PostgreSQL: ${err instanceof Error ? err.message : 'Desconocido'}`);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// Cálculo de estadísticas en tiempo real
|
||||
const { candidateStats, totalVotes } = useMemo(() => {
|
||||
const counts: Record<string, { count: number; voters: string[] }> = {};
|
||||
|
||||
CANDIDATOS_DEFAULT.forEach((c) => {
|
||||
counts[c] = { count: 0, voters: [] };
|
||||
});
|
||||
|
||||
let total = 0;
|
||||
Object.entries(votes).forEach(([voter, cand]) => {
|
||||
const upperCand = String(cand).toUpperCase();
|
||||
if (counts[upperCand]) {
|
||||
counts[upperCand].count += 1;
|
||||
counts[upperCand].voters.push(voter);
|
||||
total += 1;
|
||||
}
|
||||
});
|
||||
|
||||
let maxVotes = 0;
|
||||
Object.values(counts).forEach(({ count }) => {
|
||||
if (count > maxVotes) maxVotes = count;
|
||||
});
|
||||
|
||||
const statsList: CandidateStats[] = CANDIDATOS_DEFAULT.map((name) => {
|
||||
const count = counts[name]?.count || 0;
|
||||
const votersList = counts[name]?.voters || [];
|
||||
const percentage = total > 0 ? (count / total) * 100 : 0;
|
||||
const isLeader = maxVotes > 0 && count === maxVotes;
|
||||
|
||||
return {
|
||||
name,
|
||||
votes: count,
|
||||
percentage,
|
||||
isLeader,
|
||||
voters: votersList,
|
||||
};
|
||||
});
|
||||
|
||||
// Orden descendente
|
||||
statsList.sort((a, b) => b.votes - a.votes);
|
||||
|
||||
return {
|
||||
candidateStats: statsList,
|
||||
totalVotes: total,
|
||||
};
|
||||
}, [votes]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#000000] text-[#ffffff] flex flex-col justify-between selection:bg-[#00B7D4] selection:text-black p-4 sm:p-6 lg:p-8">
|
||||
{/* Background ambient lighting */}
|
||||
<div className="fixed inset-0 pointer-events-none overflow-hidden z-0">
|
||||
<div className="absolute -top-40 left-1/4 w-[500px] h-[300px] bg-[#00B7D4]/10 rounded-full blur-[140px]" />
|
||||
<div className="absolute top-1/2 -right-40 w-[450px] h-[450px] bg-[#00e676]/5 rounded-full blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-7xl mx-auto flex-1 flex flex-col justify-between">
|
||||
{/* Main Immersive Dashboard Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
|
||||
{/* Panel Izquierdo: Identificación, Selección y Votación */}
|
||||
<section className="lg:col-span-5 glass-panel p-6 sm:p-8 flex flex-col justify-between shadow-2xl relative">
|
||||
<div>
|
||||
{/* Cabecera & Branding */}
|
||||
<Header
|
||||
syncStatus={syncStatus}
|
||||
lastSyncTime={lastSyncTime}
|
||||
onRefresh={() => fetchVotes(true)}
|
||||
/>
|
||||
|
||||
{/* Paso 1: Identificación */}
|
||||
<VoterStep
|
||||
voterName={voterName}
|
||||
onVoterNameChange={handleVoterNameChange}
|
||||
existingVoteFor={existingVoteFor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Paso 2: Candidatos & Acción Atómica */}
|
||||
<VotingStep
|
||||
voterName={voterName}
|
||||
existingVoteFor={existingVoteFor}
|
||||
onCastVote={handleCastVote}
|
||||
isLoading={isRefreshing}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* Panel Derecho: Resultados Real-Time y Telemetría */}
|
||||
<section className="lg:col-span-7 glass-panel p-6 sm:p-8 flex flex-col justify-between shadow-2xl relative">
|
||||
<LiveResults
|
||||
stats={candidateStats}
|
||||
totalVotes={totalVotes}
|
||||
onRefresh={() => fetchVotes(true)}
|
||||
isRefreshing={isRefreshing}
|
||||
onToggleAdmin={() => setIsAdminOpen(!isAdminOpen)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Panel de Administración (Desplegable / Modal) */}
|
||||
<div className="max-w-7xl mx-auto w-full">
|
||||
<AdminPanel
|
||||
rawVotes={votes}
|
||||
onVoteResetSuccess={() => fetchVotes(true)}
|
||||
isOpen={isAdminOpen}
|
||||
onClose={() => setIsAdminOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
import React, { useState } from 'react';
|
||||
import { KeyRound, Trash2, Copy, Check, Download, AlertTriangle, ShieldCheck, X } from 'lucide-react';
|
||||
import { ApiService } from '../services/api';
|
||||
|
||||
interface AdminPanelProps {
|
||||
rawVotes: Record<string, string>;
|
||||
onVoteResetSuccess: () => Promise<void>;
|
||||
isOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||
rawVotes,
|
||||
onVoteResetSuccess,
|
||||
isOpen: controlledOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const [selectedVoter, setSelectedVoter] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [adminMsg, setAdminMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [copiedCode, setCopiedCode] = useState(false);
|
||||
const [copiedSingleHtml, setCopiedSingleHtml] = useState(false);
|
||||
|
||||
const isPanelOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
||||
const togglePanel = () => {
|
||||
if (onClose && controlledOpen !== undefined) {
|
||||
onClose();
|
||||
} else {
|
||||
setInternalOpen(!internalOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const registeredVoters = Object.keys(rawVotes).sort();
|
||||
|
||||
const handleResetVote = async () => {
|
||||
if (!selectedVoter) {
|
||||
setAdminMsg({ text: 'Selecciona un votante de la lista.', type: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.trim() !== 'RESETEOE3') {
|
||||
setAdminMsg({ text: 'Contraseña de administración incorrecta.', type: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
setAdminMsg(null);
|
||||
|
||||
try {
|
||||
await ApiService.deleteVote(selectedVoter, password.trim());
|
||||
await onVoteResetSuccess();
|
||||
setAdminMsg({
|
||||
text: `Voto de "${selectedVoter}" restablecido y eliminado de PostgreSQL.`,
|
||||
type: 'success',
|
||||
});
|
||||
setSelectedVoter('');
|
||||
setPassword('');
|
||||
} catch (err) {
|
||||
setAdminMsg({
|
||||
text: `Error al restablecer voto: ${err instanceof Error ? err.message : 'Error desconocido'}`,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const codeSnippet = `// Votos embebidos en código fuente — Consultoría E3
|
||||
const VOTOS_EMBEBIDOS = ${JSON.stringify(rawVotes, null, 2)};`;
|
||||
|
||||
const handleCopyCode = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(codeSnippet);
|
||||
setCopiedCode(true);
|
||||
setTimeout(() => setCopiedCode(false), 2500);
|
||||
} catch {
|
||||
// fallback
|
||||
}
|
||||
};
|
||||
|
||||
const generateStandaloneHtml = () => {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>¿Qué proyecto debe ser el ganador? — Consultoría E3</title>
|
||||
<meta name="description" content="Encuesta interna de Consultoría E3 para elegir al proyecto ganador en tiempo real.">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Poppins:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--surface: #0a0a0a;
|
||||
--accent: #00B7D4;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #cfd6d9;
|
||||
--glass: rgba(255, 255, 255, 0.04);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--green-live: #00e676;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
min-height: 100vh;
|
||||
padding: 24px 16px 60px;
|
||||
}
|
||||
.wrap { max-width: 1100px; margin: 0 auto; }
|
||||
.dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 24px; }
|
||||
.glass-panel {
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(16px);
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.title-font { font-family: 'Bebas Neue', sans-serif; text-transform: uppercase; letter-spacing: 1.5px; }
|
||||
.accent-text { color: var(--accent); }
|
||||
.btn-pill {
|
||||
background: transparent; border: 1px solid var(--glass-border); color: var(--text-secondary); padding: 12px 16px; border-radius: 100px; font-size: 13px; text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s; cursor: pointer; text-align: center;
|
||||
}
|
||||
.btn-pill:hover:not(:disabled) { background: rgba(0, 183, 212, 0.1); border-color: var(--accent); color: #fff; }
|
||||
.btn-pill.active { background: var(--accent); color: var(--bg); border-color: var(--accent); font-weight: 700; box-shadow: 0 0 15px rgba(0, 183, 212, 0.4); }
|
||||
.live-indicator { display: inline-flex; align-items: center; gap: 8px; font-size: 11px; text-transform: uppercase; color: var(--green-live); letter-spacing: 1px; }
|
||||
.dot { width: 8px; height: 8px; background: var(--green-live); border-radius: 50%; box-shadow: 0 0 8px var(--green-live); animation: pulse 2s infinite; }
|
||||
@keyframes pulse { 0%,100%{ opacity:1; } 50%{ opacity:0.3; } }
|
||||
input[type="text"], input[type="password"], select {
|
||||
width: 100%; padding: 14px; background: rgba(255,255,255,0.04); border: 1px solid var(--glass-border); border-radius: 12px; color: #fff; font-size: 14px; font-family: inherit; text-transform: uppercase; outline: none; transition: 0.2s;
|
||||
}
|
||||
input:focus, select:focus { border-color: var(--accent); box-shadow: 0 0 15px rgba(0,183,212,0.25); }
|
||||
.pills-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
|
||||
.bar-row { display: grid; grid-template-columns: 100px 1fr auto; align-items: center; gap: 12px; margin-bottom: 12px; }
|
||||
.bar-track { height: 8px; background: rgba(255,255,255,0.1); border-radius: 10px; overflow: hidden; }
|
||||
.bar-fill { height: 100%; background: var(--text-secondary); border-radius: 10px; transition: width 0.5s ease; }
|
||||
.bar-fill.leader { background: var(--accent); box-shadow: 0 0 10px rgba(0, 183, 212, 0.4); }
|
||||
.badge { font-size: 9px; background: var(--accent); color: var(--bg); padding: 2px 6px; border-radius: 4px; font-weight: 800; }
|
||||
.btn-action { width: 100%; padding: 14px; border-radius: 12px; background: var(--accent); color: #000; font-weight: 800; text-transform: uppercase; letter-spacing: 1.5px; border: none; cursor: pointer; transition: 0.2s; margin-top: 16px; box-shadow: 0 0 15px rgba(0,183,212,0.3); }
|
||||
.btn-action:hover { background: #38bdf8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="dashboard">
|
||||
<section class="glass-panel">
|
||||
<div style="margin-bottom: 24px;">
|
||||
<div class="live-indicator" style="margin-bottom: 8px;"><span class="dot"></span> SINCRONIZADO EN VIVO</div>
|
||||
<h1 class="title-font" style="font-size: 38px;">CONSULTORÍA <span class="accent-text">E3</span></h1>
|
||||
<p style="font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 1px;">Sistema de Votación Global 2026</p>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display:block; font-size:11px; text-transform:uppercase; color:#888; font-weight:600; margin-bottom:6px;">Paso 1: Identificación</label>
|
||||
<input type="text" id="voterInput" placeholder="ESCRIBE TU NOMBRE COMPLETO">
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label style="display:block; font-size:11px; text-transform:uppercase; color:#888; font-weight:600; margin-bottom:8px;">Paso 2: Elige el Proyecto Ganador</label>
|
||||
<div class="pills-grid" id="pillsWrap"></div>
|
||||
</div>
|
||||
<div id="confirmBox" style="margin-top:14px;"></div>
|
||||
<button type="button" class="btn-action" id="btnCastVote">REGISTRAR VOTO ATÓMICO</button>
|
||||
</section>
|
||||
|
||||
<section class="glass-panel">
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-end; margin-bottom: 20px;">
|
||||
<div>
|
||||
<h2 class="title-font" style="font-size: 26px;">RESULTADOS <span class="accent-text">REAL-TIME</span></h2>
|
||||
<p style="font-size: 11px; color: #888; font-family: monospace;" id="totalVotosTxt">TOTAL DE VOTOS: 0</p>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<span class="title-font accent-text" style="font-size: 26px;" id="pctTxt">0%</span>
|
||||
<p style="font-size: 9px; color: #888; text-transform: uppercase;">Actividad</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="barsWrap" style="flex:1;"></div>
|
||||
<div style="margin-top:20px; border-top:1px solid var(--glass-border); pt:14px; display:flex; justify-content:space-between; align-items:center; font-size:10px; font-family:monospace; color:#666;">
|
||||
<div>ENDPOINT: WORTHY-DUCK-110283</div>
|
||||
<div style="cursor:pointer; color:#aaa;" onclick="syncVotes()">ACTUALIZAR ⟳</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const CONFIG = {
|
||||
endpoint: "https://worthy-duck-110283.upstash.io",
|
||||
token: "gQAAAAAAAa7LAQIgcDE5Yjc5NzdlMmI1ODk0NjA0ODhhZDViNTNmOTViYTdkYg",
|
||||
key: "e3_consultoria_encuesta_2026"
|
||||
};
|
||||
const CANDIDATOS = ["SERVANDO", "URIEL", "ANNA", "YAMILET", "VERONICA", "ERICK", "MELINA", "LEYMI", "GRECIA"];
|
||||
let state = { votes: ${JSON.stringify(rawVotes)}, currentVoter: localStorage.getItem('e3_voter_app') || '', selected: null };
|
||||
|
||||
async function callRedis(cmd) {
|
||||
const res = await fetch(CONFIG.endpoint, { method: "POST", headers: { "Authorization": "Bearer " + CONFIG.token, "Content-Type": "application/json" }, body: JSON.stringify(cmd) });
|
||||
const data = await res.json();
|
||||
return data.result;
|
||||
}
|
||||
|
||||
async function syncVotes() {
|
||||
try {
|
||||
const res = await callRedis(["HGETALL", CONFIG.key]);
|
||||
const votes = {};
|
||||
if (Array.isArray(res)) { for (let i = 0; i < res.length; i += 2) { if (res[i] && res[i+1]) votes[res[i].toUpperCase().trim()] = res[i+1].toUpperCase().trim(); } }
|
||||
else if (res && typeof res === 'object') { for (const [k, v] of Object.entries(res)) { if (k && v) votes[k.toUpperCase().trim()] = String(v).toUpperCase().trim(); } }
|
||||
state.votes = votes;
|
||||
render();
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function submitVote() {
|
||||
const voter = state.currentVoter.trim().toUpperCase();
|
||||
const cand = state.selected || state.votes[voter];
|
||||
if (!voter || voter.length < 2) return alert("Escribe tu nombre antes de votar.");
|
||||
if (!cand) return alert("Selecciona un candidato.");
|
||||
if (voter === cand || voter.split(/\\s+/).includes(cand)) return alert("No puedes votar por ti mismo.");
|
||||
try {
|
||||
await callRedis(["HSET", CONFIG.key, voter, cand]);
|
||||
state.votes[voter] = cand;
|
||||
localStorage.setItem('e3_voter_app', voter);
|
||||
render();
|
||||
alert("¡Voto registrado en la nube con éxito!");
|
||||
} catch (e) { alert("Error: " + e.message); }
|
||||
}
|
||||
|
||||
function render() {
|
||||
const voter = state.currentVoter.trim().toUpperCase();
|
||||
const pillsWrap = document.getElementById('pillsWrap');
|
||||
pillsWrap.innerHTML = '';
|
||||
const existingVote = state.votes[voter];
|
||||
const activeCand = state.selected || existingVote;
|
||||
|
||||
CANDIDATOS.forEach((c, idx) => {
|
||||
const isSelf = voter && (voter === c || voter.split(/\\s+/).includes(c));
|
||||
const isSelected = activeCand === c;
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn-pill' + (isSelected ? ' active' : '');
|
||||
btn.disabled = isSelf;
|
||||
if (idx === CANDIDATOS.length - 1) btn.style.gridColumn = 'span 2';
|
||||
btn.innerHTML = c + (isSelf ? ' <small style="font-size:9px;">(Tú)</small>' : '');
|
||||
btn.onclick = () => { state.selected = c; render(); };
|
||||
pillsWrap.appendChild(btn);
|
||||
});
|
||||
|
||||
const counts = {}; CANDIDATOS.forEach(c => counts[c] = 0);
|
||||
let total = 0;
|
||||
Object.values(state.votes).forEach(c => { if (counts[c] !== undefined) { counts[c]++; total++; } });
|
||||
document.getElementById('totalVotosTxt').innerText = 'TOTAL DE VOTOS: ' + total;
|
||||
document.getElementById('pctTxt').innerText = (total > 0 ? Math.min(100, Math.round((total / Math.max(total, 12)) * 100)) : 0) + '%';
|
||||
|
||||
const sorted = CANDIDATOS.map(name => ({ name, votes: counts[name], pct: total > 0 ? (counts[name] / total) * 100 : 0 })).sort((a,b) => b.votes - a.votes);
|
||||
const maxVotes = sorted[0]?.votes || 0;
|
||||
const barsWrap = document.getElementById('barsWrap');
|
||||
barsWrap.innerHTML = '';
|
||||
|
||||
sorted.forEach(item => {
|
||||
const isLeader = maxVotes > 0 && item.votes === maxVotes;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'bar-row';
|
||||
row.innerHTML = \`<span style="font-size:12px;font-weight:600;color:\${isLeader?'#fff':'#aaa'};">\${item.name}</span><div class="bar-track"><div class="bar-fill \${isLeader?'leader':''}" style="width:\${Math.max(item.pct, item.votes>0?4:0)}%;"></div></div><span style="font-size:12px;font-family:monospace;color:\${isLeader?'var(--accent)':'#aaa'};">\${item.votes} \${isLeader?'<span class="badge">LÍDER</span>':''}</span>\`;
|
||||
barsWrap.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('voterInput').addEventListener('input', e => { state.currentVoter = e.target.value.toUpperCase(); localStorage.setItem('e3_voter_app', state.currentVoter); render(); });
|
||||
document.getElementById('btnCastVote').addEventListener('click', submitVote);
|
||||
setInterval(syncVotes, 2500);
|
||||
syncVotes();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
|
||||
const handleCopySingleHtml = async () => {
|
||||
try {
|
||||
const html = generateStandaloneHtml();
|
||||
await navigator.clipboard.writeText(html);
|
||||
setCopiedSingleHtml(true);
|
||||
setTimeout(() => setCopiedSingleHtml(false), 2500);
|
||||
} catch {
|
||||
// fallback
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadSingleHtml = () => {
|
||||
const html = generateStandaloneHtml();
|
||||
const blob = new Blob([html], { type: 'text/html;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'encuesta-e3-immersive.html';
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full mt-6">
|
||||
{/* Accordion / Toggle trigger if not controlled externally */}
|
||||
{controlledOpen === undefined && (
|
||||
<div className="flex justify-end mb-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePanel}
|
||||
className="text-xs font-mono uppercase tracking-widest text-gray-500 hover:text-[#00B7D4] transition-colors flex items-center gap-1.5 cursor-pointer"
|
||||
>
|
||||
<KeyRound className="w-3.5 h-3.5" />
|
||||
<span>{isPanelOpen ? 'Cerrar Panel Admin' : 'Panel de Administración'}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPanelOpen && (
|
||||
<div className="glass-panel p-6 border border-white/10 shadow-2xl relative animate-in fade-in zoom-in-95 duration-200">
|
||||
{onClose && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="absolute top-5 right-5 text-gray-400 hover:text-white p-1 cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-8 h-8 rounded-lg bg-[#00B7D4]/10 border border-[#00B7D4]/30 flex items-center justify-center text-[#00B7D4]">
|
||||
<KeyRound className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="title-font text-lg text-white">Panel de Administración</h3>
|
||||
<p className="text-[11px] text-gray-400 font-mono">
|
||||
Operaciones Atómicas PostgreSQL & Exportación
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reset section */}
|
||||
<div className="space-y-3 pb-5 border-b border-white/5">
|
||||
<label className="block text-xs uppercase tracking-wider text-gray-400 font-semibold">
|
||||
Restablecer Voto por Error Humano (DELETE)
|
||||
</label>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<select
|
||||
value={selectedVoter}
|
||||
onChange={(e) => setSelectedVoter(e.target.value)}
|
||||
className="w-full px-3 py-2.5 bg-white/[0.04] border border-white/10 rounded-xl text-white text-xs focus:border-[#00B7D4] focus:outline-none uppercase font-mono"
|
||||
>
|
||||
<option value="">Selecciona votante...</option>
|
||||
{registeredVoters.map((v) => (
|
||||
<option key={v} value={v}>
|
||||
{v} → {rawVotes[v]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Contraseña: RESETEOE3"
|
||||
className="w-full px-3 py-2.5 bg-white/[0.04] border border-white/10 rounded-xl text-white text-xs focus:border-[#00B7D4] focus:outline-none"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetVote}
|
||||
disabled={isDeleting || !selectedVoter}
|
||||
className="px-4 py-2.5 bg-red-950/30 hover:bg-red-900/50 border border-red-500/40 text-red-300 rounded-xl text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer disabled:opacity-30 flex items-center justify-center gap-2"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
<span>{isDeleting ? 'Borrando...' : 'Restablecer'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{adminMsg && (
|
||||
<div
|
||||
className={`p-3 rounded-xl text-xs flex items-center gap-2 ${
|
||||
adminMsg.type === 'success'
|
||||
? 'bg-[#00e676]/10 text-[#00e676] border border-[#00e676]/20'
|
||||
: 'bg-red-950/40 text-red-300 border border-red-500/30'
|
||||
}`}
|
||||
>
|
||||
{adminMsg.type === 'success' ? (
|
||||
<ShieldCheck className="w-4 h-4 shrink-0" />
|
||||
) : (
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
)}
|
||||
<span>{adminMsg.text}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Export section */}
|
||||
<div className="pt-4 space-y-3">
|
||||
<div className="text-xs uppercase tracking-wider text-gray-300 font-semibold">
|
||||
Exportar Versión Autónoma Single-File (HTML5 + CSS + JS)
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopySingleHtml}
|
||||
className="px-3.5 py-2 rounded-lg bg-white/[0.04] hover:bg-white/[0.08] border border-white/10 text-xs font-mono text-[#00B7D4] transition-all flex items-center gap-1.5 cursor-pointer"
|
||||
>
|
||||
{copiedSingleHtml ? <Check className="w-3.5 h-3.5 text-[#00e676]" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
<span>{copiedSingleHtml ? 'HTML Copiado' : 'Copiar Archivo HTML Autónomo'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownloadSingleHtml}
|
||||
className="px-3.5 py-2 rounded-lg bg-[#00B7D4]/10 hover:bg-[#00B7D4]/20 border border-[#00B7D4] text-xs font-mono text-[#00B7D4] transition-all flex items-center gap-1.5 cursor-pointer"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
<span>Descargar .html</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyCode}
|
||||
className="px-3.5 py-2 rounded-lg bg-white/[0.02] hover:bg-white/[0.06] border border-white/10 text-xs font-mono text-gray-300 transition-all flex items-center gap-1.5 cursor-pointer"
|
||||
>
|
||||
{copiedCode ? <Check className="w-3.5 h-3.5 text-[#00e676]" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
<span>{copiedCode ? 'Código Copiado' : 'Copiar const VOTOS_EMBEBIDOS'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
return (
|
||||
<footer className="w-full max-w-7xl mx-auto px-4 sm:px-6 mt-8 pt-6 border-t border-white/5 text-center text-xs text-gray-500 font-mono">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-white uppercase tracking-wider title-font text-base">
|
||||
CONSULTORÍA <span className="accent-text text-[#00B7D4]">E3</span>
|
||||
</span>
|
||||
<span className="text-gray-600 hidden sm:inline">|</span>
|
||||
<span className="text-gray-400 text-[11px] hidden sm:inline">
|
||||
Digitalizando Negocios para Resultados Exponenciales
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-[11px] text-gray-400 flex items-center gap-3">
|
||||
<span>hola@consultoriae3.com</span>
|
||||
<span>•</span>
|
||||
<span>442 272 2698</span>
|
||||
<span>•</span>
|
||||
<span>Querétaro, Qro.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { RefreshCw, WifiOff, AlertTriangle, Shield } from 'lucide-react';
|
||||
import { SyncStatus } from '../types';
|
||||
|
||||
interface HeaderProps {
|
||||
syncStatus: SyncStatus;
|
||||
lastSyncTime: Date | null;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({
|
||||
syncStatus,
|
||||
lastSyncTime,
|
||||
onRefresh,
|
||||
}) => {
|
||||
return (
|
||||
<header className="w-full mb-6">
|
||||
{/* Top Telemetry and Live Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="live-indicator px-3 py-1 rounded-full bg-[#00e676]/10 border border-[#00e676]/20">
|
||||
<span className="dot pulse-dot" />
|
||||
<span>SINCRONIZADO EN VIVO</span>
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] uppercase font-mono tracking-widest text-gray-500 hidden sm:block">
|
||||
UPSTASH SERVERLESS REDIS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sync status / manual refresh */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefresh}
|
||||
title="Forzar actualización en tiempo real"
|
||||
className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/[0.03] hover:bg-white/[0.08] border border-white/10 text-xs text-gray-300 font-mono transition-all cursor-pointer group"
|
||||
>
|
||||
{syncStatus === 'syncing' ? (
|
||||
<>
|
||||
<RefreshCw className="w-3 h-3 text-[#00B7D4] animate-spin" />
|
||||
<span className="text-[#00B7D4]">SYNC...</span>
|
||||
</>
|
||||
) : syncStatus === 'offline' ? (
|
||||
<>
|
||||
<WifiOff className="w-3 h-3 text-amber-400" />
|
||||
<span className="text-amber-400">OFFLINE</span>
|
||||
</>
|
||||
) : syncStatus === 'error' ? (
|
||||
<>
|
||||
<AlertTriangle className="w-3 h-3 text-red-400" />
|
||||
<span className="text-red-400">ERROR DE RED</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#00e676]" />
|
||||
<span className="text-gray-400 group-hover:text-white">
|
||||
{lastSyncTime
|
||||
? `ACT: ${lastSyncTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}`
|
||||
: 'CONECTADO'}
|
||||
</span>
|
||||
<RefreshCw className="w-2.5 h-2.5 text-gray-500 group-hover:text-[#00B7D4] transition-colors" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Brand Title */}
|
||||
<div className="space-y-1">
|
||||
<h1 className="title-font text-3xl sm:text-4xl lg:text-5xl font-black text-white uppercase tracking-wider leading-none">
|
||||
CONSULTORÍA <span className="accent-text text-[#00B7D4] drop-shadow-[0_0_15px_rgba(0,183,212,0.4)]">E3</span>
|
||||
</h1>
|
||||
<p className="text-xs uppercase tracking-widest text-gray-400 font-medium pt-0.5">
|
||||
Sistema de Votación Global 2026 — ¿Qué proyecto debe ser el ganador?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Guideline notice */}
|
||||
<div className="mt-3 text-xs text-gray-400 leading-relaxed border-l-2 border-[#00B7D4]/40 pl-3 py-0.5">
|
||||
Vota una sola vez <span className="text-[#00B7D4] font-semibold">—</span> no puedes votar por ti mismo <span className="text-[#00B7D4] font-semibold">—</span> los resultados se actualizan en vivo para todo el equipo.
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
import React, { useState } from 'react';
|
||||
import { RefreshCw, Users, ChevronDown, ChevronUp, Shield, Database, Server } from 'lucide-react';
|
||||
import { CandidateStats } from '../types';
|
||||
|
||||
interface LiveResultsProps {
|
||||
stats: CandidateStats[];
|
||||
totalVotes: number;
|
||||
onRefresh: () => void;
|
||||
isRefreshing: boolean;
|
||||
onToggleAdmin?: () => void;
|
||||
}
|
||||
|
||||
export const LiveResults: React.FC<LiveResultsProps> = ({
|
||||
stats,
|
||||
totalVotes,
|
||||
onRefresh,
|
||||
isRefreshing,
|
||||
onToggleAdmin,
|
||||
}) => {
|
||||
const [showVoterDetails, setShowVoterDetails] = useState(false);
|
||||
|
||||
// Estimado de participación del equipo (base de 15 a 20 miembros del equipo interno)
|
||||
const estimatedTeamSize = Math.max(totalVotes, 12);
|
||||
const participationRate = totalVotes > 0 ? Math.min(100, Math.round((totalVotes / estimatedTeamSize) * 100)) : 0;
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
{/* Header Results */}
|
||||
<div>
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h2 className="title-font text-2xl sm:text-3xl text-white">
|
||||
RESULTADOS <span className="accent-text text-[#00B7D4]">REAL-TIME</span>
|
||||
</h2>
|
||||
<p className="text-xs font-mono text-gray-500 uppercase tracking-widest mt-0.5">
|
||||
TOTAL DE VOTOS REGISTRADOS: <strong className="text-white font-bold">{totalVotes}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<span className="text-2xl sm:text-3xl title-font accent-text text-[#00B7D4] font-black">
|
||||
{participationRate}%
|
||||
</span>
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-wider font-semibold">
|
||||
Actividad En Vivo
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle voter details */}
|
||||
<div className="flex justify-end mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowVoterDetails(!showVoterDetails)}
|
||||
className="text-[11px] text-gray-400 hover:text-[#00B7D4] transition-colors flex items-center gap-1.5 cursor-pointer font-mono uppercase"
|
||||
>
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
<span>{showVoterDetails ? 'Ocultar nombres' : 'Ver votantes por proyecto'}</span>
|
||||
{showVoterDetails ? (
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
) : (
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Candidate Progress Bars (Immersive UI Design) */}
|
||||
<div className="space-y-3.5" id="barsWrap">
|
||||
{stats.map((cand, index) => {
|
||||
const isTop = cand.isLeader && cand.votes > 0;
|
||||
const percentage = cand.percentage.toFixed(1);
|
||||
const opacityClass =
|
||||
index === 0
|
||||
? 'opacity-100'
|
||||
: index < 3
|
||||
? 'opacity-85'
|
||||
: index < 6
|
||||
? 'opacity-65'
|
||||
: 'opacity-45';
|
||||
|
||||
return (
|
||||
<div key={cand.name} className={`space-y-1.5 transition-all ${opacityClass}`}>
|
||||
<div className="grid grid-cols-[100px_1fr_auto] items-center gap-3">
|
||||
{/* Candidate Name */}
|
||||
<span
|
||||
className={`text-xs uppercase tracking-wider truncate font-semibold ${
|
||||
isTop ? 'text-white font-bold' : 'text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{cand.name}
|
||||
</span>
|
||||
|
||||
{/* Progress Bar Track */}
|
||||
<div className="h-2 w-full bg-white/10 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-700 ease-out ${
|
||||
isTop
|
||||
? 'bg-[#00B7D4] shadow-[0_0_12px_rgba(0,183,212,0.6)]'
|
||||
: cand.votes > 0
|
||||
? 'bg-[#cfd6d9]'
|
||||
: 'bg-transparent'
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.max(cand.percentage, cand.votes > 0 ? 4 : 0)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Votes Count and Leader Badge */}
|
||||
<div className="text-right flex items-center justify-end gap-1.5 min-w-[70px]">
|
||||
<span
|
||||
className={`text-xs font-mono font-bold ${
|
||||
isTop ? 'text-[#00B7D4]' : 'text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{cand.votes}
|
||||
</span>
|
||||
|
||||
{isTop && (
|
||||
<span className="text-[9px] bg-[#00B7D4] text-black font-extrabold px-1.5 py-0.5 rounded tracking-wider uppercase">
|
||||
LÍDER
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Optional Voter List Chip row */}
|
||||
{showVoterDetails && cand.voters.length > 0 && (
|
||||
<div className="pl-[100px] flex flex-wrap gap-1 pt-1 pb-1">
|
||||
{cand.voters.map((v) => (
|
||||
<span
|
||||
key={v}
|
||||
className="text-[10px] font-mono px-2 py-0.5 rounded bg-white/[0.04] border border-white/10 text-gray-300"
|
||||
>
|
||||
{v}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Telemetry and System Info Footer (Immersive UI Design) */}
|
||||
<div className="mt-8 border-t border-white/5 pt-5 flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-4 text-[10px] font-mono text-gray-500 uppercase tracking-tight">
|
||||
<div>
|
||||
<div className="text-white/40 flex items-center gap-1">
|
||||
<Server className="w-2.5 h-2.5 text-[#00B7D4]" /> ENDPOINT
|
||||
</div>
|
||||
<div className="text-gray-300">WORTHY-DUCK-110283</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-white/40 flex items-center gap-1">
|
||||
<Database className="w-2.5 h-2.5 text-[#00e676]" /> INSTANCIA
|
||||
</div>
|
||||
<div className="text-gray-300">UPSTASH-E3-2026</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="text-[10px] font-mono uppercase tracking-wider text-gray-400 hover:text-[#00B7D4] transition-colors flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
<RefreshCw className={`w-3 h-3 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
<span>Actualizar</span>
|
||||
</button>
|
||||
|
||||
{onToggleAdmin && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleAdmin}
|
||||
className="text-[10px] font-mono uppercase tracking-wider text-white/30 hover:text-[#00B7D4] transition-colors flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
<Shield className="w-3 h-3" />
|
||||
<span>Admin</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { User, CheckCircle2, UserCheck } from 'lucide-react';
|
||||
|
||||
interface VoterStepProps {
|
||||
voterName: string;
|
||||
onVoterNameChange: (name: string) => void;
|
||||
existingVoteFor: string | null;
|
||||
}
|
||||
|
||||
export const VoterStep: React.FC<VoterStepProps> = ({
|
||||
voterName,
|
||||
onVoterNameChange,
|
||||
existingVoteFor,
|
||||
}) => {
|
||||
const normalized = voterName.trim().toUpperCase();
|
||||
const isValid = normalized.length >= 2;
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onVoterNameChange(e.target.value.toUpperCase());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label
|
||||
htmlFor="voterInput"
|
||||
className="block text-xs uppercase tracking-wider text-gray-400 font-semibold"
|
||||
>
|
||||
Paso 1: Identificación
|
||||
</label>
|
||||
{isValid && !existingVoteFor && (
|
||||
<span className="text-[10px] uppercase font-mono text-[#00e676] flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3 h-3" /> IDENTIFICADO
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-gray-500">
|
||||
<User className="w-4 h-4 text-[#00B7D4]" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
id="voterInput"
|
||||
type="text"
|
||||
value={voterName}
|
||||
onChange={handleChange}
|
||||
placeholder="ESCRIBE TU NOMBRE COMPLETO (EJ. JUAN PÉREZ)"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
className="w-full pl-10 pr-10 py-3.5 bg-white/[0.04] border border-white/10 rounded-xl text-white text-sm sm:text-base font-medium placeholder:text-white/20 focus:outline-none focus:border-[#00B7D4] focus:ring-1 focus:ring-[#00B7D4]/40 transition-all uppercase tracking-wide"
|
||||
/>
|
||||
|
||||
{isValid && (
|
||||
<div className="absolute inset-y-0 right-0 pr-3.5 flex items-center pointer-events-none">
|
||||
<CheckCircle2 className="w-4 h-4 text-[#00e676]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info messages */}
|
||||
{isValid && existingVoteFor && (
|
||||
<div className="mt-2.5 p-2.5 rounded-lg bg-[#00B7D4]/10 border border-[#00B7D4]/30 text-xs text-gray-300 flex items-center gap-2">
|
||||
<UserCheck className="w-4 h-4 shrink-0 text-[#00B7D4]" />
|
||||
<span>
|
||||
Voto registrado previamente por:{' '}
|
||||
<strong className="text-white font-bold bg-[#00B7D4]/30 px-1.5 py-0.5 rounded uppercase">
|
||||
{existingVoteFor}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-2 text-[11px] text-gray-500">
|
||||
Escribe tu nombre para habilitar la votación y validar la regla anti-autovoto.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,183 @@
|
||||
import React, { useState } from 'react';
|
||||
import confetti from 'canvas-confetti';
|
||||
import { CheckCircle2, Sparkles, RefreshCw, Zap } from 'lucide-react';
|
||||
import { CANDIDATOS_DEFAULT } from '../types';
|
||||
|
||||
interface VotingStepProps {
|
||||
voterName: string;
|
||||
existingVoteFor: string | null;
|
||||
onCastVote: (candidate: string) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const VotingStep: React.FC<VotingStepProps> = ({
|
||||
voterName,
|
||||
existingVoteFor,
|
||||
onCastVote,
|
||||
isLoading,
|
||||
}) => {
|
||||
const [selectedCandidate, setSelectedCandidate] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const normalizedVoter = voterName.trim().toUpperCase();
|
||||
|
||||
// Función anti-autovoto
|
||||
const isSelfVote = (candidateName: string): boolean => {
|
||||
if (!normalizedVoter) return false;
|
||||
const candUpper = candidateName.toUpperCase();
|
||||
if (normalizedVoter === candUpper) return true;
|
||||
const voterWords = normalizedVoter.split(/\s+/);
|
||||
if (voterWords.includes(candUpper)) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleSelect = (candidate: string) => {
|
||||
if (isSelfVote(candidate) || isLoading || submitting) return;
|
||||
setSelectedCandidate(candidate);
|
||||
};
|
||||
|
||||
const handleConfirmVote = async () => {
|
||||
const candidateToVote = selectedCandidate || existingVoteFor;
|
||||
if (!candidateToVote || isSelfVote(candidateToVote) || submitting || isLoading) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onCastVote(candidateToVote);
|
||||
|
||||
// Confetti celebratory animation
|
||||
try {
|
||||
confetti({
|
||||
particleCount: 75,
|
||||
spread: 70,
|
||||
origin: { y: 0.6 },
|
||||
colors: ['#00B7D4', '#00e676', '#ffffff', '#38bdf8'],
|
||||
});
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error al emitir voto:', err);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<label className="block text-xs uppercase tracking-wider text-gray-400 font-semibold">
|
||||
Paso 2: Elige el Proyecto Ganador
|
||||
</label>
|
||||
<span className="text-[10px] text-gray-500 font-mono uppercase">
|
||||
9 Candidatos
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Candidatos Pills Grid (Design Theme) */}
|
||||
<div className="grid grid-cols-2 gap-2.5">
|
||||
{CANDIDATOS_DEFAULT.map((cand, index) => {
|
||||
const isSelf = isSelfVote(cand);
|
||||
const isVoted = existingVoteFor === cand;
|
||||
const isSelected = selectedCandidate === cand || (!selectedCandidate && isVoted);
|
||||
const isLast = index === CANDIDATOS_DEFAULT.length - 1 && CANDIDATOS_DEFAULT.length % 2 !== 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={cand}
|
||||
id={`btn-cand-${cand.toLowerCase()}`}
|
||||
type="button"
|
||||
disabled={isSelf || isLoading || submitting}
|
||||
onClick={() => {
|
||||
handleSelect(cand);
|
||||
// Auto-submit on tap for seamless UX if no prior selection
|
||||
if (!existingVoteFor) {
|
||||
onCastVote(cand).then(() => {
|
||||
confetti({
|
||||
particleCount: 65,
|
||||
spread: 60,
|
||||
origin: { y: 0.6 },
|
||||
colors: ['#00B7D4', '#00e676', '#ffffff'],
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
}}
|
||||
className={`btn-pill flex items-center justify-between px-3.5 py-3 ${
|
||||
isLast ? 'col-span-2' : ''
|
||||
} ${
|
||||
isVoted || (isSelected && selectedCandidate === cand)
|
||||
? 'active'
|
||||
: isSelf
|
||||
? 'opacity-30 cursor-not-allowed bg-white/[0.01]'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<span className="font-semibold tracking-wider truncate text-xs sm:text-sm">
|
||||
{cand}
|
||||
</span>
|
||||
|
||||
{isSelf ? (
|
||||
<span className="text-[9px] uppercase font-mono px-1.5 py-0.5 rounded bg-white/10 text-gray-400">
|
||||
(Tú)
|
||||
</span>
|
||||
) : isVoted ? (
|
||||
<span className="flex items-center gap-1 text-[10px] font-bold">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
VOTADO
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirmation and Atomic Action Button */}
|
||||
<div className="mt-6 pt-4 border-t border-white/5 space-y-3">
|
||||
{existingVoteFor && (
|
||||
<div className="p-3 rounded-xl bg-[#00B7D4]/10 border border-[#00B7D4]/30 text-xs text-white flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-[#00B7D4] shrink-0" />
|
||||
<span>
|
||||
Voto emitido para:{' '}
|
||||
<strong className="text-[#00B7D4] font-bold uppercase tracking-wider">
|
||||
{existingVoteFor}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-gray-400 uppercase">
|
||||
Atómico
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={!normalizedVoter || submitting || isLoading || (!selectedCandidate && !existingVoteFor)}
|
||||
onClick={handleConfirmVote}
|
||||
className="w-full py-3.5 rounded-xl bg-[#00B7D4] hover:bg-[#38bdf8] text-black font-extrabold text-xs uppercase tracking-widest transition-all duration-200 shadow-[0_0_20px_rgba(0,183,212,0.35)] hover:shadow-[0_0_25px_rgba(0,183,212,0.6)] cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center gap-2 active:scale-[0.99]"
|
||||
>
|
||||
{submitting ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
<span>REGISTRANDO EN LA NUBE...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="w-4 h-4 fill-current" />
|
||||
<span>
|
||||
{existingVoteFor && selectedCandidate && selectedCandidate !== existingVoteFor
|
||||
? `CAMBIAR VOTO A ${selectedCandidate}`
|
||||
: existingVoteFor
|
||||
? `VOTO CONFIRMADO: ${existingVoteFor}`
|
||||
: selectedCandidate
|
||||
? `REGISTRAR VOTO POR ${selectedCandidate}`
|
||||
: 'REGISTRAR VOTO ATÓMICO'}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--surface: #0a0a0a;
|
||||
--accent: #00B7D4;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #cfd6d9;
|
||||
--glass: rgba(255, 255, 255, 0.035);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--linea: rgba(255, 255, 255, 0.08);
|
||||
--green-live: #00e676;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1, h2, h3, .title-font, .font-display {
|
||||
font-family: 'Bebas Neue', 'Arial Narrow', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.accent-text {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.btn-pill {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--glass-border);
|
||||
color: var(--text-secondary);
|
||||
padding: 10px 16px;
|
||||
border-radius: 100px;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn-pill:hover:not(:disabled) {
|
||||
background: rgba(0, 183, 212, 0.1);
|
||||
border-color: rgba(0, 183, 212, 0.5);
|
||||
color: #ffffff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-pill.active {
|
||||
background: var(--accent);
|
||||
color: #000000;
|
||||
font-weight: 700;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 16px rgba(0, 183, 212, 0.45);
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: var(--green-live);
|
||||
letter-spacing: 1px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--green-live);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px var(--green-live);
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 0 rgba(0, 230, 118, 0.7);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15);
|
||||
opacity: 0.85;
|
||||
box-shadow: 0 0 0 6px rgba(0, 230, 118, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
animation: pulse-dot 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
/* Custom scrollbars */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #080808;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #1a1e22;
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,199 @@
|
||||
const LOCAL_STORAGE_KEY = 'e3_encuesta_votes_cache';
|
||||
const LAST_VOTER_KEY = 'e3_encuesta_current_voter';
|
||||
|
||||
export type LiveCallback = (votes: Record<string, string>) => void;
|
||||
export type StatusCallback = (status: 'synced' | 'syncing' | 'offline' | 'error') => void;
|
||||
|
||||
export class ApiService {
|
||||
private static eventSource: EventSource | null = null;
|
||||
private static reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/**
|
||||
* Obtiene todos los votos registrados desde la API PostgreSQL
|
||||
*/
|
||||
public static async fetchAllVotes(): Promise<Record<string, string>> {
|
||||
try {
|
||||
const response = await fetch('/api/votes');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error en la API (${response.status}): ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const votes: Record<string, string> = data.votes || {};
|
||||
|
||||
// Guardar respaldo en localStorage
|
||||
try {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(votes));
|
||||
} catch (e) {
|
||||
console.warn('No se pudo guardar en localStorage cache:', e);
|
||||
}
|
||||
|
||||
return votes;
|
||||
} catch (err) {
|
||||
console.warn('Fallo al obtener votos de API, usando cache local si existe:', err);
|
||||
try {
|
||||
const cached = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra atómicamente un voto en PostgreSQL
|
||||
*/
|
||||
public static async castVote(voterName: string, candidateName: string): Promise<boolean> {
|
||||
const voter = voterName.trim().toUpperCase();
|
||||
const candidate = candidateName.trim().toUpperCase();
|
||||
|
||||
if (!voter || !candidate) {
|
||||
throw new Error('Nombre de votante y candidato son obligatorios.');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/vote', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ voter, candidate }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(errData.error || errData.detail || `Error (${response.status}) al emitir voto`);
|
||||
}
|
||||
|
||||
// Guardar identificación local para conveniencia del usuario en este dispositivo
|
||||
try {
|
||||
localStorage.setItem(LAST_VOTER_KEY, voter);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restablece el voto de un usuario específico (Administrador)
|
||||
*/
|
||||
public static async deleteVote(voterName: string, adminKey: string = 'RESETEOE3'): Promise<boolean> {
|
||||
const voter = voterName.trim().toUpperCase();
|
||||
if (!voter) {
|
||||
throw new Error('Debes especificar el nombre del votante a restablecer.');
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/vote/${encodeURIComponent(voter)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-admin-key': adminKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(errData.error || errData.detail || `Error (${response.status}) al restablecer voto`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conecta al canal Server-Sent Events (SSE) para actualizaciones en vivo cero-latencia
|
||||
*/
|
||||
public static subscribeToLiveStream(
|
||||
onVotesUpdated: LiveCallback,
|
||||
onStatusChanged: StatusCallback
|
||||
): () => void {
|
||||
if (typeof window === 'undefined' || typeof EventSource === 'undefined') {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const connect = () => {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
}
|
||||
|
||||
onStatusChanged('syncing');
|
||||
const es = new EventSource('/api/stream');
|
||||
this.eventSource = es;
|
||||
|
||||
es.onopen = () => {
|
||||
onStatusChanged('synced');
|
||||
};
|
||||
|
||||
es.addEventListener('initial', (e: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.votes) {
|
||||
onVotesUpdated(data.votes);
|
||||
onStatusChanged('synced');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parseando mensaje inicial SSE:', err);
|
||||
}
|
||||
});
|
||||
|
||||
es.addEventListener('votes', (e: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.votes) {
|
||||
onVotesUpdated(data.votes);
|
||||
onStatusChanged('synced');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parseando evento SSE:', err);
|
||||
}
|
||||
});
|
||||
|
||||
es.onerror = () => {
|
||||
if (!navigator.onLine) {
|
||||
onStatusChanged('offline');
|
||||
} else {
|
||||
onStatusChanged('error');
|
||||
}
|
||||
es.close();
|
||||
this.eventSource = null;
|
||||
|
||||
// Intentar reconectar automáticamente después de 3 segundos
|
||||
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
connect();
|
||||
}, 3000);
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el último nombre de votante guardado localmente en este navegador
|
||||
*/
|
||||
public static getSavedVoter(): string {
|
||||
try {
|
||||
return localStorage.getItem(LAST_VOTER_KEY) || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public static setSavedVoter(name: string): void {
|
||||
try {
|
||||
localStorage.setItem(LAST_VOTER_KEY, name.trim().toUpperCase());
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
const UPSTASH_ENDPOINT = 'https://worthy-duck-110283.upstash.io';
|
||||
const UPSTASH_TOKEN = 'gQAAAAAAAa7LAQIgcDE5Yjc5NzdlMmI1ODk0NjA0ODhhZDViNTNmOTViYTdkYg';
|
||||
const REDIS_KEY = 'e3_consultoria_encuesta_2026';
|
||||
const LOCAL_STORAGE_KEY = 'e3_encuesta_votes_cache';
|
||||
const LAST_VOTER_KEY = 'e3_encuesta_current_voter';
|
||||
|
||||
export class UpstashService {
|
||||
private static async executeCommand(command: unknown[]): Promise<unknown> {
|
||||
const response = await fetch(UPSTASH_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${UPSTASH_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(command),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => '');
|
||||
throw new Error(`Error en Upstash Redis (${response.status}): ${errorText || response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene todos los votos registrados en la nube (HGETALL)
|
||||
*/
|
||||
public static async fetchAllVotes(): Promise<Record<string, string>> {
|
||||
try {
|
||||
const result = await this.executeCommand(['HGETALL', REDIS_KEY]);
|
||||
const votes: Record<string, string> = {};
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
for (let i = 0; i < result.length; i += 2) {
|
||||
const voter = result[i];
|
||||
const candidate = result[i + 1];
|
||||
if (voter && candidate) {
|
||||
votes[String(voter).trim().toUpperCase()] = String(candidate).trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
} else if (result && typeof result === 'object') {
|
||||
for (const [voter, candidate] of Object.entries(result)) {
|
||||
if (voter && candidate) {
|
||||
votes[String(voter).trim().toUpperCase()] = String(candidate).trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guardar respaldo en localStorage
|
||||
try {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(votes));
|
||||
} catch (e) {
|
||||
console.warn('No se pudo guardar en localStorage cache:', e);
|
||||
}
|
||||
|
||||
return votes;
|
||||
} catch (err) {
|
||||
console.error('Fallo al obtener votos de Upstash, recurriendo a cache local:', err);
|
||||
// Fallback a localStorage
|
||||
try {
|
||||
const cached = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra atómicamente un voto en la nube (HSET)
|
||||
*/
|
||||
public static async castVote(voterName: string, candidateName: string): Promise<boolean> {
|
||||
const voter = voterName.trim().toUpperCase();
|
||||
const candidate = candidateName.trim().toUpperCase();
|
||||
|
||||
if (!voter || !candidate) {
|
||||
throw new Error('Nombre de votante y candidato son obligatorios.');
|
||||
}
|
||||
|
||||
await this.executeCommand(['HSET', REDIS_KEY, voter, candidate]);
|
||||
|
||||
// Guardar identificación local para conveniencia del usuario en este dispositivo
|
||||
try {
|
||||
localStorage.setItem(LAST_VOTER_KEY, voter);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina/Restablece el voto de un usuario específico (HDEL)
|
||||
*/
|
||||
public static async deleteVote(voterName: string): Promise<boolean> {
|
||||
const voter = voterName.trim().toUpperCase();
|
||||
if (!voter) {
|
||||
throw new Error('Debes especificar el nombre del votante a restablecer.');
|
||||
}
|
||||
|
||||
await this.executeCommand(['HDEL', REDIS_KEY, voter]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el último nombre de votante guardado localmente en este navegador
|
||||
*/
|
||||
public static getSavedVoter(): string {
|
||||
try {
|
||||
return localStorage.getItem(LAST_VOTER_KEY) || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public static setSavedVoter(name: string): void {
|
||||
try {
|
||||
localStorage.setItem(LAST_VOTER_KEY, name.trim().toUpperCase());
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export interface VoteRecord {
|
||||
voter: string;
|
||||
candidate: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface CandidateStats {
|
||||
name: string;
|
||||
votes: number;
|
||||
percentage: number;
|
||||
isLeader: boolean;
|
||||
voters: string[];
|
||||
}
|
||||
|
||||
export type SyncStatus = 'synced' | 'syncing' | 'offline' | 'error';
|
||||
|
||||
export const CANDIDATOS_DEFAULT = [
|
||||
'SERVANDO',
|
||||
'URIEL',
|
||||
'ANNA',
|
||||
'YAMILET',
|
||||
'VERONICA',
|
||||
'ERICK',
|
||||
'MELINA',
|
||||
'LEYMI',
|
||||
'GRECIA',
|
||||
] as const;
|
||||
|
||||
export type CandidateName = (typeof CANDIDATOS_DEFAULT)[number];
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.API_TARGET || 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user