feat: Add QR code generation feature to AdminPage, including UI for input and download options, and integrate QR code library for generating codes. Update package dependencies and adjust Navigation and ProfilePage components to streamline user data handling.

This commit is contained in:
NikitolProject 2025-09-18 01:27:46 +03:00
parent 46ffe69b57
commit 90eeda2edf
23 changed files with 1112 additions and 13 deletions

14
.env.backend Normal file
View File

@ -0,0 +1,14 @@
# Server port
PORT=8080
# PostgreSQL connection string
DATABASE_URL=postgres://user:password@localhost:5432/quiz_app?sslmode=disable
# Redis connection string
REDIS_URL=redis://localhost:6379/0
# Secret key for signing JWT or validating Telegram data
SECRET_KEY=your-very-secret-key
# Bot token
BOT_TOKEN=8130773519:AAG__5YpiFROrdJovts6xm5Edl0EEvD0bxo

17
.env.bot Normal file
View File

@ -0,0 +1,17 @@
# Telegram Bot Token
BOT_TOKEN=8130773519:AAG__5YpiFROrdJovts6xm5Edl0EEvD0bxo
# Backend API URL
BACKEND_API_URL=http://localhost:8080
# Frontend URL (for mini app)
FRONTEND_URL=https://www.google.com
# Bot username (without @)
BOT_USERNAME=volsu_sno_bot
# Admin user IDs (comma-separated)
ADMIN_USER_IDS=6113992941
# Operator user IDs (comma-separated)
OPERATOR_USER_IDS=6968371470

5
.env.db Normal file
View File

@ -0,0 +1,5 @@
# Database Configuration
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_DB=quiz_app
POSTGRES_PORT=5432

26
.env.example Normal file
View File

@ -0,0 +1,26 @@
# PostgreSQL Configuration
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_DB=quiz_app
POSTGRES_PORT=5432
# Redis Configuration
REDIS_PORT=6379
# Nginx Configuration
NGINX_PORT=80
# Frontend Configuration
FRONTEND_API_URL=http://localhost:80
# Backend Configuration
PORT=8080
SECRET_KEY=your-very-secret-key
# Bot Configuration
BOT_TOKEN=your-telegram-bot-token
BACKEND_API_URL=http://backend:8080
FRONTEND_URL=http://localhost
BOT_USERNAME=your_bot_username
ADMIN_USER_IDS=123456789
OPERATOR_USER_IDS=123456789

2
.env.redis Normal file
View File

@ -0,0 +1,2 @@
# Redis Configuration
REDIS_PORT=6379

51
backend/.dockerignore Normal file
View File

@ -0,0 +1,51 @@
# Build outputs
*.exe
*.exe~
*.dll
*.so
*.dylib
main
main.exe
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# Go workspace files
go.work
# Environment files
.env
.env.local
.env.production
.env.development
# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Git files
.git
.gitignore
# Temporary files
*.tmp
*.temp
*.log
# Dependencies
vendor/

48
backend/Dockerfile Normal file
View File

@ -0,0 +1,48 @@
# Build stage
FROM golang:1.24-alpine AS builder
WORKDIR /app
# Install git and other dependencies
RUN apk add --no-cache git
# Copy go mod files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Install goose
RUN go install github.com/pressly/goose/v3/cmd/goose@latest
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/server/main.go
# Production stage
FROM alpine:latest
# Install ca-certificates for HTTPS and postgresql client
RUN apk --no-cache add ca-certificates tzdata postgresql-client
WORKDIR /app
# Copy binary from builder stage
COPY --from=builder /app/main .
# Copy goose for migrations
COPY --from=builder /go/bin/goose /usr/local/bin/goose
# Copy migrations
COPY migrations ./migrations
# Copy environment file if exists
COPY .env.example .env
# Expose port
EXPOSE 8080
# Run the application
CMD ["./main"]

145
docker-compose.yml Normal file
View File

@ -0,0 +1,145 @@
version: '3.8'
services:
# База данных PostgreSQL
postgres:
image: postgres:14-alpine
container_name: sno_postgres
env_file:
- .env.db
environment:
POSTGRES_USER: ${POSTGRES_USER:-user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_DB:-quiz_app}
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- sno_network
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-user} -d ${POSTGRES_DB:-quiz_app}"]
interval: 10s
timeout: 5s
retries: 5
# Migration runner
migrations:
build:
context: ./backend
dockerfile: Dockerfile
container_name: sno_migrations
env_file:
- .env.db
- .env.backend
environment:
- DATABASE_URL=postgres://${POSTGRES_USER:-user}:${POSTGRES_PASSWORD:-password}@postgres:5432/${POSTGRES_DB:-quiz_app}?sslmode=disable
depends_on:
postgres:
condition: service_healthy
networks:
- sno_network
command: >
sh -c "
echo 'Running database migrations...'
goose -dir ./migrations postgres \"$$DATABASE_URL\" up
echo 'Running setup admin script...'
psql \"$$DATABASE_URL\" < /app/setup_admin.sql
echo 'Migrations completed successfully'
"
volumes:
- ./setup_admin.sql:/app/setup_admin.sql:ro
profiles:
- migrations
# Redis для кэширования
redis:
image: redis:7-alpine
container_name: sno_redis
env_file:
- .env.redis
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
networks:
- sno_network
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
# Backend на Go
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: sno_backend
env_file:
- ./backend/.env
- .env.backend
environment:
- DATABASE_URL=postgres://${POSTGRES_USER:-user}:${POSTGRES_PASSWORD:-password}@postgres:5432/${POSTGRES_DB:-quiz_app}?sslmode=disable
- REDIS_URL=redis://redis:6379/0
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- sno_network
restart: unless-stopped
# Telegram бот на Python
bot:
build:
context: ./bot
dockerfile: Dockerfile
container_name: sno_bot
env_file:
- ./bot/.env
- .env.bot
environment:
- DATABASE_URL=postgres://${POSTGRES_USER:-user}:${POSTGRES_PASSWORD:-password}@postgres:5432/${POSTGRES_DB:-quiz_app}?sslmode=disable
- REDIS_URL=redis://redis:6379/0
- BACKEND_API_URL=http://backend:8080
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- sno_network
restart: unless-stopped
volumes:
- ./bot:/app
working_dir: /app
# Frontend на React (production build)
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: sno_frontend
environment:
- VITE_API_BASE_URL=http://localhost/api
ports:
- "${NGINX_PORT:-80}:80"
depends_on:
- backend
networks:
- sno_network
restart: unless-stopped
volumes:
postgres_data:
redis_data:
networks:
sno_network:
driver: bridge

38
frontend/.dockerignore Normal file
View File

@ -0,0 +1,38 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
dist/
build/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
*.log
# Editor directories and files
.idea/
.vscode/
*.swp
*.swo
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
*.tmp
*.temp

34
frontend/Dockerfile Normal file
View File

@ -0,0 +1,34 @@
# Build stage
FROM node:20-alpine AS builder
# Install pnpm
RUN npm install -g pnpm
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN pnpm install
# Copy source code
COPY . .
# Build the application
RUN pnpm run build
# Production stage
FROM nginx:alpine
# Copy built files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

48
frontend/nginx.conf Normal file
View File

@ -0,0 +1,48 @@
server {
listen 80;
server_name localhost;
# Frontend (React build)
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
# Cache static files
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# Backend API
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CORS headers
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization";
# Handle OPTIONS requests
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization";
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

View File

@ -16,9 +16,11 @@
"@mui/material": "^7.3.2", "@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.11.2", "@mui/x-data-grid": "^8.11.2",
"@types/node": "^24.5.1", "@types/node": "^24.5.1",
"@types/qrcode": "^1.5.5",
"axios": "^1.12.2", "axios": "^1.12.2",
"html5-qrcode": "^2.3.8", "html5-qrcode": "^2.3.8",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"qrcode": "^1.5.4",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.9.1" "react-router-dom": "^7.9.1"

View File

@ -26,6 +26,9 @@ importers:
'@types/node': '@types/node':
specifier: ^24.5.1 specifier: ^24.5.1
version: 24.5.1 version: 24.5.1
'@types/qrcode':
specifier: ^1.5.5
version: 1.5.5
axios: axios:
specifier: ^1.12.2 specifier: ^1.12.2
version: 1.12.2 version: 1.12.2
@ -35,6 +38,9 @@ importers:
lucide-react: lucide-react:
specifier: ^0.544.0 specifier: ^0.544.0
version: 0.544.0(react@19.1.1) version: 0.544.0(react@19.1.1)
qrcode:
specifier: ^1.5.4
version: 1.5.4
react: react:
specifier: ^19.1.1 specifier: ^19.1.1
version: 19.1.1 version: 19.1.1
@ -718,6 +724,9 @@ packages:
'@types/prop-types@15.7.15': '@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
'@types/qrcode@1.5.5':
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
'@types/react-dom@19.1.9': '@types/react-dom@19.1.9':
resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==}
peerDependencies: peerDependencies:
@ -809,6 +818,10 @@ packages:
ajv@6.12.6: ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0: ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -856,6 +869,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001743: caniuse-lite@1.0.30001743:
resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==} resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==}
@ -863,6 +880,9 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
clsx@2.1.1: clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -911,6 +931,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
deep-is@0.1.4: deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@ -918,6 +942,9 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
dom-helpers@5.2.1: dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
@ -928,6 +955,9 @@ packages:
electron-to-chromium@1.5.218: electron-to-chromium@1.5.218:
resolution: {integrity: sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==} resolution: {integrity: sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
error-ex@1.3.4: error-ex@1.3.4:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
@ -1049,6 +1079,10 @@ packages:
find-root@1.1.0: find-root@1.1.0:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
find-up@5.0.0: find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1085,6 +1119,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1165,6 +1203,10 @@ packages:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-glob@4.0.3: is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -1215,6 +1257,10 @@ packages:
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
locate-path@6.0.0: locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1283,14 +1329,26 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-limit@3.1.0: p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-locate@5.0.0: p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
parent-module@1.0.1: parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1325,6 +1383,10 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
postcss@8.5.6: postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@ -1343,6 +1405,11 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -1388,6 +1455,13 @@ packages:
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
reselect@5.1.1: reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
@ -1424,6 +1498,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
set-cookie-parser@2.7.1: set-cookie-parser@2.7.1:
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
@ -1443,6 +1520,14 @@ packages:
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-json-comments@3.1.1: strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1545,6 +1630,9 @@ packages:
yaml: yaml:
optional: true optional: true
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -1554,6 +1642,13 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yallist@3.1.1: yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@ -1561,6 +1656,14 @@ packages:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
yocto-queue@0.1.0: yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2154,6 +2257,10 @@ snapshots:
'@types/prop-types@15.7.15': {} '@types/prop-types@15.7.15': {}
'@types/qrcode@1.5.5':
dependencies:
'@types/node': 24.5.1
'@types/react-dom@19.1.9(@types/react@19.1.13)': '@types/react-dom@19.1.9(@types/react@19.1.13)':
dependencies: dependencies:
'@types/react': 19.1.13 '@types/react': 19.1.13
@ -2284,6 +2391,8 @@ snapshots:
json-schema-traverse: 0.4.1 json-schema-traverse: 0.4.1
uri-js: 4.4.1 uri-js: 4.4.1
ansi-regex@5.0.1: {}
ansi-styles@4.3.0: ansi-styles@4.3.0:
dependencies: dependencies:
color-convert: 2.0.1 color-convert: 2.0.1
@ -2338,6 +2447,8 @@ snapshots:
callsites@3.1.0: {} callsites@3.1.0: {}
camelcase@5.3.1: {}
caniuse-lite@1.0.30001743: {} caniuse-lite@1.0.30001743: {}
chalk@4.1.2: chalk@4.1.2:
@ -2345,6 +2456,12 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
clsx@2.1.1: {} clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
@ -2385,10 +2502,14 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
deep-is@0.1.4: {} deep-is@0.1.4: {}
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
dijkstrajs@1.0.3: {}
dom-helpers@5.2.1: dom-helpers@5.2.1:
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
@ -2402,6 +2523,8 @@ snapshots:
electron-to-chromium@1.5.218: {} electron-to-chromium@1.5.218: {}
emoji-regex@8.0.0: {}
error-ex@1.3.4: error-ex@1.3.4:
dependencies: dependencies:
is-arrayish: 0.2.1 is-arrayish: 0.2.1
@ -2561,6 +2684,11 @@ snapshots:
find-root@1.1.0: {} find-root@1.1.0: {}
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
find-up@5.0.0: find-up@5.0.0:
dependencies: dependencies:
locate-path: 6.0.0 locate-path: 6.0.0
@ -2590,6 +2718,8 @@ snapshots:
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@ -2661,6 +2791,8 @@ snapshots:
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
is-glob@4.0.3: is-glob@4.0.3:
dependencies: dependencies:
is-extglob: 2.1.1 is-extglob: 2.1.1
@ -2698,6 +2830,10 @@ snapshots:
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
@ -2758,14 +2894,24 @@ snapshots:
type-check: 0.4.0 type-check: 0.4.0
word-wrap: 1.2.5 word-wrap: 1.2.5
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-limit@3.1.0: p-limit@3.1.0:
dependencies: dependencies:
yocto-queue: 0.1.0 yocto-queue: 0.1.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-locate@5.0.0: p-locate@5.0.0:
dependencies: dependencies:
p-limit: 3.1.0 p-limit: 3.1.0
p-try@2.2.0: {}
parent-module@1.0.1: parent-module@1.0.1:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0
@ -2791,6 +2937,8 @@ snapshots:
picomatch@4.0.3: {} picomatch@4.0.3: {}
pngjs@5.0.0: {}
postcss@8.5.6: postcss@8.5.6:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
@ -2809,6 +2957,12 @@ snapshots:
punycode@2.3.1: {} punycode@2.3.1: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
react-dom@19.1.1(react@19.1.1): react-dom@19.1.1(react@19.1.1):
@ -2847,6 +3001,10 @@ snapshots:
react@19.1.1: {} react@19.1.1: {}
require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
reselect@5.1.1: {} reselect@5.1.1: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
@ -2896,6 +3054,8 @@ snapshots:
semver@7.7.2: {} semver@7.7.2: {}
set-blocking@2.0.0: {}
set-cookie-parser@2.7.1: {} set-cookie-parser@2.7.1: {}
shebang-command@2.0.0: shebang-command@2.0.0:
@ -2908,6 +3068,16 @@ snapshots:
source-map@0.5.7: {} source-map@0.5.7: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
stylis@4.2.0: {} stylis@4.2.0: {}
@ -2976,14 +3146,43 @@ snapshots:
'@types/node': 24.5.1 '@types/node': 24.5.1
fsevents: 2.3.3 fsevents: 2.3.3
which-module@2.0.1: {}
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
y18n@4.0.3: {}
yallist@3.1.1: {} yallist@3.1.1: {}
yaml@1.10.2: {} yaml@1.10.2: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}

View File

@ -0,0 +1,103 @@
import React from 'react';
import QRCode from 'qrcode';
import { Box, Typography, Button } from '@mui/material';
import { Download } from '@mui/icons-material';
interface QRCodeDisplayProps {
token: string;
type: string;
value: string;
index: number;
}
export const QRCodeDisplay: React.FC<QRCodeDisplayProps> = ({ token, type, value, index }) => {
const [qrCodeDataUrl, setQrCodeDataUrl] = React.useState<string>('');
React.useEffect(() => {
const generateQRCode = async () => {
try {
const dataUrl = await QRCode.toDataURL(token, {
width: 200,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
});
setQrCodeDataUrl(dataUrl);
} catch (err) {
console.error('Error generating QR code:', err);
}
};
generateQRCode();
}, [token]);
const downloadQRCode = () => {
if (!qrCodeDataUrl) return;
const link = document.createElement('a');
link.download = `qr_${type}_${value}_${index + 1}.png`;
link.href = qrCodeDataUrl;
link.click();
};
return (
<Box sx={{ textAlign: 'center', p: 2, border: '1px solid #333', borderRadius: 1, backgroundColor: '#2a2a2a' }}>
<Typography variant="body2" sx={{ color: '#ffffff', mb: 1 }}>
QR #{index + 1}
</Typography>
<Typography variant="caption" sx={{ color: '#666', mb: 2, display: 'block' }}>
{token}
</Typography>
{qrCodeDataUrl ? (
<Box sx={{ mb: 2 }}>
<img
src={qrCodeDataUrl}
alt={`QR Code ${index + 1}`}
style={{
width: '200px',
height: '200px',
border: '2px solid #FFD700',
borderRadius: '8px'
}}
/>
</Box>
) : (
<Box sx={{
width: '200px',
height: '200px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '2px dashed #666',
borderRadius: '8px',
margin: '0 auto 16px'
}}>
<Typography variant="body2" sx={{ color: '#666' }}>
Генерация...
</Typography>
</Box>
)}
<Button
onClick={downloadQRCode}
disabled={!qrCodeDataUrl}
startIcon={<Download />}
size="small"
sx={{
backgroundColor: '#FFD700',
color: '#000000',
'&:hover': {
backgroundColor: '#FFC700',
},
'&:disabled': {
backgroundColor: '#666',
color: '#999',
}
}}
>
Скачать PNG
</Button>
</Box>
);
};

View File

@ -17,7 +17,7 @@ import { useAuth } from '../../context/AuthContext';
export const Navigation: React.FC = () => { export const Navigation: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { user, isAdmin } = useAuth(); const { isAdmin } = useAuth();
const getValue = () => { const getValue = () => {
if (location.pathname === '/' || location.pathname === '/home') return 0; if (location.pathname === '/' || location.pathname === '/home') return 0;

View File

@ -104,7 +104,7 @@ export const AnswerOption: React.FC<AnswerOptionProps> = ({
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
cursor: !disabled && !showResult ? 'pointer' : 'default', pointerEvents: !disabled && !showResult ? 'auto' : 'none',
zIndex: 1, zIndex: 1,
...getCardStyles(), ...getCardStyles(),
'&:hover': !disabled && !showResult ? { '&:hover': !disabled && !showResult ? {

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import QRCode from 'qrcode';
import { import {
Box, Box,
Typography, Typography,
@ -24,6 +25,7 @@ import {
Chip, Chip,
} from '@mui/material'; } from '@mui/material';
import { GridItem } from '../components/GridItem'; import { GridItem } from '../components/GridItem';
import { QRCodeDisplay } from '../components/QRCodeDisplay';
import { import {
Dashboard, Dashboard,
Quiz as QuizIcon, Quiz as QuizIcon,
@ -35,6 +37,8 @@ import {
Delete, Delete,
BarChart, BarChart,
DragIndicator, DragIndicator,
QrCode,
Download,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { apiService } from '../services/api'; import { apiService } from '../services/api';
@ -106,10 +110,19 @@ export const AdminPage: React.FC = () => {
// Dialog states // Dialog states
const [quizDialogOpen, setQuizDialogOpen] = useState(false); const [quizDialogOpen, setQuizDialogOpen] = useState(false);
const [rewardDialogOpen, setRewardDialogOpen] = useState(false); const [rewardDialogOpen, setRewardDialogOpen] = useState(false);
const [qrDialogOpen, setQrDialogOpen] = useState(false);
const [editingQuiz, setEditingQuiz] = useState<Quiz | null>(null); const [editingQuiz, setEditingQuiz] = useState<Quiz | null>(null);
const [editingReward, setEditingReward] = useState<Reward | null>(null); const [editingReward, setEditingReward] = useState<Reward | null>(null);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
// QR form states
const [qrForm, setQrForm] = useState({
type: 'reward',
value: '',
count: 1,
});
const [generatedQRCodes, setGeneratedQRCodes] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
@ -311,6 +324,116 @@ export const AdminPage: React.FC = () => {
} }
}; };
const handleGenerateQRCodes = async () => {
if (!qrForm.value || !qrForm.count) {
setError('Заполните все поля');
return;
}
setSubmitting(true);
try {
const response = await apiService.generateQRCodes(qrForm);
if (response.success) {
setGeneratedQRCodes(response.data.tokens || []);
} else {
setError(response.message || 'Не удалось сгенерировать QR-коды');
}
} catch (err) {
console.error('Error generating QR codes:', err);
setError('Произошла ошибка при генерации QR-кодов');
} finally {
setSubmitting(false);
}
};
const downloadQRCodes = async () => {
try {
// Create a zip-like file with all QR codes as base64 images
const qrData = await Promise.all(
generatedQRCodes.map(async (token, index) => {
try {
const dataUrl = await QRCode.toDataURL(token, {
width: 300,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
});
// Convert base64 to blob
const base64Data = dataUrl.split(',')[1];
const binaryData = atob(base64Data);
const bytes = new Uint8Array(binaryData.length);
for (let i = 0; i < binaryData.length; i++) {
bytes[i] = binaryData.charCodeAt(i);
}
return {
blob: new Blob([bytes], { type: 'image/png' }),
filename: `qr_${qrForm.type}_${qrForm.value}_${index + 1}.png`,
token
};
} catch (err) {
console.error('Error generating QR code:', err);
return null;
}
})
);
const validQrData = qrData.filter(item => item !== null);
if (validQrData.length === 0) {
setError('Не удалось сгенерировать QR-коды для скачивания');
return;
}
// Download each QR code individually
validQrData.forEach((qr, index) => {
if (qr) {
setTimeout(() => {
const url = URL.createObjectURL(qr.blob);
const a = document.createElement('a');
a.href = url;
a.download = qr.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, index * 200); // Small delay between downloads
}
});
// Also create a text file with all tokens
const textContent = generatedQRCodes.map((token, index) =>
`QR Code ${index + 1}\nType: ${qrForm.type}\nValue: ${qrForm.value}\nToken: ${token}\nFile: qr_${qrForm.type}_${qrForm.value}_${index + 1}.png\n${'-'.repeat(60)}`
).join('\n');
const textBlob = new Blob([textContent], { type: 'text/plain' });
const textUrl = URL.createObjectURL(textBlob);
const textA = document.createElement('a');
textA.href = textUrl;
textA.download = `qrcodes_tokens_${qrForm.type}_${qrForm.value}_${new Date().toISOString().split('T')[0]}.txt`;
document.body.appendChild(textA);
textA.click();
document.body.removeChild(textA);
URL.revokeObjectURL(textUrl);
} catch (err) {
console.error('Error downloading QR codes:', err);
setError('Произошла ошибка при скачивании QR-кодов');
}
};
const resetQRForm = () => {
setQrForm({
type: 'reward',
value: '',
count: 1,
});
setGeneratedQRCodes([]);
};
const openEditQuizDialog = async (quiz: Quiz) => { const openEditQuizDialog = async (quiz: Quiz) => {
setEditingQuiz(quiz); setEditingQuiz(quiz);
@ -566,11 +689,17 @@ export const AdminPage: React.FC = () => {
aria-controls="admin-tabpanel-2" aria-controls="admin-tabpanel-2"
/> />
<Tab <Tab
icon={<Settings />} icon={<QrCode />}
label="Настройки" label="QR-коды"
id="admin-tab-3" id="admin-tab-3"
aria-controls="admin-tabpanel-3" aria-controls="admin-tabpanel-3"
/> />
<Tab
icon={<Settings />}
label="Настройки"
id="admin-tab-4"
aria-controls="admin-tabpanel-4"
/>
</Tabs> </Tabs>
</Box> </Box>
@ -644,6 +773,17 @@ export const AdminPage: React.FC = () => {
> >
<Edit /> <Edit />
</IconButton> </IconButton>
<IconButton
size="small"
sx={{ color: '#2196F3' }}
onClick={() => {
setQrForm({ type: 'quiz', value: quiz.id.toString(), count: 1 });
setQrDialogOpen(true);
}}
title="Сгенерировать QR-код"
>
<QrCode />
</IconButton>
<IconButton <IconButton
size="small" size="small"
sx={{ color: '#f44336' }} sx={{ color: '#f44336' }}
@ -717,6 +857,17 @@ export const AdminPage: React.FC = () => {
> >
<Edit /> <Edit />
</IconButton> </IconButton>
<IconButton
size="small"
sx={{ color: '#2196F3' }}
onClick={() => {
setQrForm({ type: 'reward', value: reward.price_stars.toString(), count: 1 });
setQrDialogOpen(true);
}}
title="Сгенерировать QR-код"
>
<QrCode />
</IconButton>
<IconButton <IconButton
size="small" size="small"
sx={{ color: '#f44336' }} sx={{ color: '#f44336' }}
@ -733,6 +884,42 @@ export const AdminPage: React.FC = () => {
</TabPanel> </TabPanel>
<TabPanel value={value} index={3}> <TabPanel value={value} index={3}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ color: '#ffffff' }}>
Генерация QR-кодов
</Typography>
<Button
variant="contained"
onClick={() => setQrDialogOpen(true)}
startIcon={<Add />}
sx={{
backgroundColor: '#FFD700',
color: '#000000',
}}
>
Сгенерировать QR-коды
</Button>
</Box>
<Card sx={{ backgroundColor: '#1a1a1a', border: '1px solid #333' }}>
<CardContent>
<Typography variant="body1" sx={{ color: '#ffffff', mb: 2 }}>
Используйте эту функцию для создания QR-кодов для:
</Typography>
<Box component="ul" sx={{ color: '#888', pl: 2, mb: 2 }}>
<li>Начисления звезд (reward)</li>
<li>Доступа к викторинам (quiz)</li>
<li>Специальных акций в магазине (shop)</li>
</Box>
<Typography variant="body2" sx={{ color: '#666' }}>
Каждый QR-код содержит уникальный токен и может быть использован только один раз.
Срок действия токена - 30 дней.
</Typography>
</CardContent>
</Card>
</TabPanel>
<TabPanel value={value} index={4}>
<Typography variant="h6" sx={{ color: '#ffffff', mb: 2 }}> <Typography variant="h6" sx={{ color: '#ffffff', mb: 2 }}>
Настройки системы Настройки системы
</Typography> </Typography>
@ -1227,6 +1414,165 @@ export const AdminPage: React.FC = () => {
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
{/* Generate QR Codes Dialog */}
<Dialog
open={qrDialogOpen}
onClose={() => {
setQrDialogOpen(false);
resetQRForm();
}}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
backgroundColor: '#1a1a1a',
border: '1px solid #333',
},
}}
>
<DialogTitle sx={{ color: '#ffffff' }}>
Генерация QR-кодов
</DialogTitle>
<DialogContent>
<TextField
fullWidth
select
label="Тип QR-кода"
value={qrForm.type}
onChange={(e) => setQrForm(prev => ({ ...prev, type: e.target.value }))}
margin="normal"
SelectProps={{
native: true,
}}
sx={{
'& .MuiOutlinedInput-root': {
'& fieldset': { borderColor: '#333' },
'&:hover fieldset': { borderColor: '#FFD700' },
},
'& .MuiInputLabel-root': { color: '#888' },
'& .MuiInputBase-input': { color: '#ffffff' },
'& .MuiSelect-select': { color: '#ffffff' },
}}
>
<option value="reward">Начисление звезд</option>
<option value="quiz">Доступ к викторине</option>
<option value="shop">Акция магазина</option>
</TextField>
<TextField
fullWidth
label="Значение"
value={qrForm.value}
onChange={(e) => setQrForm(prev => ({ ...prev, value: e.target.value }))}
margin="normal"
helperText={
qrForm.type === 'reward' ? 'Количество звезд для начисления (например: 50)' :
qrForm.type === 'quiz' ? 'ID викторины (например: 1)' :
'Код акции (например: discount_10)'
}
sx={{
'& .MuiOutlinedInput-root': {
'& fieldset': { borderColor: '#333' },
'&:hover fieldset': { borderColor: '#FFD700' },
},
'& .MuiInputLabel-root': { color: '#888' },
'& .MuiInputBase-input': { color: '#ffffff' },
}}
/>
<TextField
fullWidth
label="Количество QR-кодов"
type="number"
value={qrForm.count}
onChange={(e) => setQrForm(prev => ({ ...prev, count: parseInt(e.target.value) || 1 }))}
margin="normal"
inputProps={{ min: 1, max: 100 }}
sx={{
'& .MuiOutlinedInput-root': {
'& fieldset': { borderColor: '#333' },
'&:hover fieldset': { borderColor: '#FFD700' },
},
'& .MuiInputLabel-root': { color: '#888' },
'& .MuiInputBase-input': { color: '#ffffff' },
}}
/>
{generatedQRCodes.length > 0 && (
<Box sx={{ mt: 3 }}>
<Typography variant="h6" sx={{ color: '#ffffff', mb: 2 }}>
Сгенерированные QR-коды ({generatedQRCodes.length})
</Typography>
{/* Grid of QR codes */}
<Box sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: '2fr', md: '3fr' },
gap: 2,
mb: 3,
maxHeight: 400,
overflow: 'auto',
p: 1,
backgroundColor: '#1a1a1a',
borderRadius: 1,
border: '1px solid #333'
}}>
{generatedQRCodes.map((token, index) => (
<QRCodeDisplay
key={index}
token={token}
type={qrForm.type}
value={qrForm.value}
index={index}
/>
))}
</Box>
{/* Bulk download button */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button
onClick={downloadQRCodes}
startIcon={<Download />}
sx={{
backgroundColor: '#FFD700',
color: '#000000',
'&:hover': {
backgroundColor: '#FFC700',
}
}}
>
Скачать все QR-коды (PNG + токены)
</Button>
<Typography variant="caption" sx={{ color: '#666' }}>
{generatedQRCodes.length} QR-кодов
</Typography>
</Box>
</Box>
)}
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setQrDialogOpen(false);
resetQRForm();
}}
sx={{ color: '#888' }}
>
Отмена
</Button>
<Button
onClick={handleGenerateQRCodes}
disabled={submitting || !qrForm.value || !qrForm.count}
sx={{
backgroundColor: '#FFD700',
color: '#000000',
}}
>
{submitting ? 'Генерация...' : 'Сгенерировать'}
</Button>
</DialogActions>
</Dialog>
</Box> </Box>
); );
}; };

View File

@ -13,7 +13,6 @@ import {
Chip, Chip,
CircularProgress, CircularProgress,
Alert, Alert,
Button,
} from '@mui/material'; } from '@mui/material';
import { import {
Person, Person,
@ -47,7 +46,7 @@ const TabPanel: React.FC<TabPanelProps> = ({ children, value, index }) => {
}; };
export const ProfilePage: React.FC = () => { export const ProfilePage: React.FC = () => {
const { user, logout } = useAuth(); const { user } = useAuth();
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const [transactions, setTransactions] = useState<Transaction[]>([]); const [transactions, setTransactions] = useState<Transaction[]>([]);
const [purchases, setPurchases] = useState<Purchase[]>([]); const [purchases, setPurchases] = useState<Purchase[]>([]);

View File

@ -44,8 +44,10 @@ export const QRScannerPage: React.FC = () => {
}, []); }, []);
const startScanning = async () => { const startScanning = async () => {
console.log('🎥 Starting QR scanner...');
if (scannerRef.current) { if (scannerRef.current) {
try { try {
console.log('🧹 Clearing existing scanner...');
await scannerRef.current.clear(); await scannerRef.current.clear();
} catch (err) { } catch (err) {
console.error('Error clearing scanner:', err); console.error('Error clearing scanner:', err);
@ -56,6 +58,7 @@ export const QRScannerPage: React.FC = () => {
setError(null); setError(null);
try { try {
console.log('📱 Creating new Html5Qrcode scanner...');
const scanner = new Html5Qrcode('qr-reader'); const scanner = new Html5Qrcode('qr-reader');
scannerRef.current = scanner; scannerRef.current = scanner;
@ -64,18 +67,21 @@ export const QRScannerPage: React.FC = () => {
qrbox: { width: 250, height: 250 }, qrbox: { width: 250, height: 250 },
}; };
console.log('🚀 Starting camera scanner...');
await scanner.start( await scanner.start(
{ facingMode: 'environment' }, { facingMode: 'environment' },
config, config,
async (decodedText: string) => { async (decodedText: string) => {
console.log('🔍 QR CODE DETECTED BY SCANNER:', decodedText);
await handleQRScan(decodedText); await handleQRScan(decodedText);
}, },
(_errorMessage: string) => { (_errorMessage: string) => {
// Handle scan errors silently // Handle scan errors silently
} }
); );
console.log('✅ Scanner started successfully');
} catch (err) { } catch (err) {
console.error('Error starting scanner:', err); console.error('💥 Error starting scanner:', err);
setError('Не удалось получить доступ к камере. Пожалуйста, проверьте разрешения.'); setError('Не удалось получить доступ к камере. Пожалуйста, проверьте разрешения.');
setScanning(false); setScanning(false);
} }
@ -93,13 +99,18 @@ export const QRScannerPage: React.FC = () => {
}; };
const handleQRScan = async (decodedText: string) => { const handleQRScan = async (decodedText: string) => {
console.log('🚀 QR SCAN DETECTED:', decodedText);
await stopScanning(); await stopScanning();
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
console.log('📡 Sending QR validation request...');
const response = await apiService.validateQR(decodedText); const response = await apiService.validateQR(decodedText);
console.log('📨 QR validation response:', response);
if (response.success && response.data) { if (response.success && response.data) {
console.log('✅ QR validation successful:', response.data);
// Transform backend response types to frontend format // Transform backend response types to frontend format
const transformedData: any = { const transformedData: any = {
type: response.data.type.toLowerCase(), // Convert "REWARD" -> "reward", "OPEN_QUIZ" -> "open_quiz" type: response.data.type.toLowerCase(), // Convert "REWARD" -> "reward", "OPEN_QUIZ" -> "open_quiz"
@ -121,10 +132,11 @@ export const QRScannerPage: React.FC = () => {
setResult(transformedData); setResult(transformedData);
setShowResult(true); setShowResult(true);
} else { } else {
console.log('❌ QR validation failed:', response.message);
setError(response.message || 'Недействительный QR-код'); setError(response.message || 'Недействительный QR-код');
} }
} catch (err) { } catch (err) {
console.error('Error validating QR:', err); console.error('💥 Error validating QR:', err);
setError('Произошла ошибка при проверке QR-кода'); setError('Произошла ошибка при проверке QR-кода');
} finally { } finally {
setLoading(false); setLoading(false);

View File

@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
import { import {
Box, Box,
Typography, Typography,
Card,
CardContent,
Button, Button,
CircularProgress, CircularProgress,
Alert, Alert,

View File

@ -17,28 +17,38 @@ class ApiService {
// Request interceptor to add auth token // Request interceptor to add auth token
this.api.interceptors.request.use( this.api.interceptors.request.use(
(config) => { (config) => {
console.log('🚀 API Request:', config.method?.toUpperCase(), config.url, config.data);
// For Telegram Web App, use X-Telegram-WebApp-Init-Data header // For Telegram Web App, use X-Telegram-WebApp-Init-Data header
const telegramInitData = localStorage.getItem('telegram_init_data'); const telegramInitData = localStorage.getItem('telegram_init_data');
if (telegramInitData) { if (telegramInitData) {
config.headers['X-Telegram-WebApp-Init-Data'] = telegramInitData; config.headers['X-Telegram-WebApp-Init-Data'] = telegramInitData;
console.log('🔑 Using Telegram auth headers');
} else { } else {
// Fallback to Bearer token for non-Telegram auth // Fallback to Bearer token for non-Telegram auth
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
console.log('🔑 Using Bearer token auth');
} else {
console.log('❌ No auth data found!');
} }
} }
return config; return config;
}, },
(error) => { (error) => {
console.error('💥 API Request error:', error);
return Promise.reject(error); return Promise.reject(error);
} }
); );
// Response interceptor to handle errors // Response interceptor to handle errors
this.api.interceptors.response.use( this.api.interceptors.response.use(
(response) => response, (response) => {
console.log('✅ API Response:', response.config.method?.toUpperCase(), response.config.url, response.status, response.data);
return response;
},
(error) => { (error) => {
console.error('❌ API Response error:', error.config?.method?.toUpperCase(), error.config?.url, error.response?.status, error.response?.data);
if (error.response?.status === 401) { if (error.response?.status === 401) {
localStorage.removeItem('auth_token'); localStorage.removeItem('auth_token');
window.location.href = '/'; window.location.href = '/';
@ -109,7 +119,9 @@ class ApiService {
// QR methods // QR methods
async validateQR(payload: string): Promise<ApiResponse<any>> { async validateQR(payload: string): Promise<ApiResponse<any>> {
console.log('🌐 API validateQR called with payload:', payload);
const response = await this.api.post('/qr/validate', { payload }); const response = await this.api.post('/qr/validate', { payload });
console.log('🌐 API validateQR response:', response.data);
return response.data; return response.data;
} }

View File

@ -131,7 +131,7 @@ export interface GrantStarsRequest {
} }
export interface GenerateQRCodesRequest { export interface GenerateQRCodesRequest {
type: 'reward' | 'quiz'; type: 'reward' | 'quiz' | 'shop';
value: string; value: string;
count: number; count: number;
} }

View File

@ -5,6 +5,6 @@ import react from '@vitejs/plugin-react'
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
allowedHosts: [] allowedHosts: ["*"]
} }
}) })