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:
parent
46ffe69b57
commit
90eeda2edf
14
.env.backend
Normal file
14
.env.backend
Normal 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
17
.env.bot
Normal 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
5
.env.db
Normal 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
26
.env.example
Normal 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
2
.env.redis
Normal file
@ -0,0 +1,2 @@
|
||||
# Redis Configuration
|
||||
REDIS_PORT=6379
|
||||
51
backend/.dockerignore
Normal file
51
backend/.dockerignore
Normal 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
48
backend/Dockerfile
Normal 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
145
docker-compose.yml
Normal 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
38
frontend/.dockerignore
Normal 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
34
frontend/Dockerfile
Normal 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
48
frontend/nginx.conf
Normal 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;
|
||||
}
|
||||
@ -16,9 +16,11 @@
|
||||
"@mui/material": "^7.3.2",
|
||||
"@mui/x-data-grid": "^8.11.2",
|
||||
"@types/node": "^24.5.1",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"axios": "^1.12.2",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"lucide-react": "^0.544.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.1"
|
||||
|
||||
@ -26,6 +26,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^24.5.1
|
||||
version: 24.5.1
|
||||
'@types/qrcode':
|
||||
specifier: ^1.5.5
|
||||
version: 1.5.5
|
||||
axios:
|
||||
specifier: ^1.12.2
|
||||
version: 1.12.2
|
||||
@ -35,6 +38,9 @@ importers:
|
||||
lucide-react:
|
||||
specifier: ^0.544.0
|
||||
version: 0.544.0(react@19.1.1)
|
||||
qrcode:
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
react:
|
||||
specifier: ^19.1.1
|
||||
version: 19.1.1
|
||||
@ -718,6 +724,9 @@ packages:
|
||||
'@types/prop-types@15.7.15':
|
||||
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':
|
||||
resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==}
|
||||
peerDependencies:
|
||||
@ -809,6 +818,10 @@ packages:
|
||||
ajv@6.12.6:
|
||||
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:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
@ -856,6 +869,10 @@ packages:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
camelcase@5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001743:
|
||||
resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==}
|
||||
|
||||
@ -863,6 +880,9 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
cliui@6.0.0:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
@ -911,6 +931,10 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decamelize@1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
@ -918,6 +942,9 @@ packages:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
dijkstrajs@1.0.3:
|
||||
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
|
||||
@ -928,6 +955,9 @@ packages:
|
||||
electron-to-chromium@1.5.218:
|
||||
resolution: {integrity: sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
error-ex@1.3.4:
|
||||
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||
|
||||
@ -1049,6 +1079,10 @@ packages:
|
||||
find-root@1.1.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
@ -1085,6 +1119,10 @@ packages:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1165,6 +1203,10 @@ packages:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -1215,6 +1257,10 @@ packages:
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
locate-path@5.0.0:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
locate-path@6.0.0:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -1283,14 +1329,26 @@ packages:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
p-locate@4.1.0:
|
||||
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-locate@5.0.0:
|
||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
p-try@2.2.0:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@ -1325,6 +1383,10 @@ packages:
|
||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pngjs@5.0.0:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
postcss@8.5.6:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@ -1343,6 +1405,11 @@ packages:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
qrcode@1.5.4:
|
||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@ -1388,6 +1455,13 @@ packages:
|
||||
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||
|
||||
@ -1424,6 +1498,9 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
set-blocking@2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
set-cookie-parser@2.7.1:
|
||||
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
|
||||
|
||||
@ -1443,6 +1520,14 @@ packages:
|
||||
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
@ -1545,6 +1630,9 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
which-module@2.0.1:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -1554,6 +1642,13 @@ packages:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
@ -1561,6 +1656,14 @@ packages:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@ -2154,6 +2257,10 @@ snapshots:
|
||||
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@types/react': 19.1.13
|
||||
@ -2284,6 +2391,8 @@ snapshots:
|
||||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
@ -2338,6 +2447,8 @@ snapshots:
|
||||
|
||||
callsites@3.1.0: {}
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001743: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
@ -2345,6 +2456,12 @@ snapshots:
|
||||
ansi-styles: 4.3.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: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
@ -2385,10 +2502,14 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
dijkstrajs@1.0.3: {}
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
@ -2402,6 +2523,8 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.218: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
error-ex@1.3.4:
|
||||
dependencies:
|
||||
is-arrayish: 0.2.1
|
||||
@ -2561,6 +2684,11 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
locate-path: 6.0.0
|
||||
@ -2590,6 +2718,8 @@ snapshots:
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@ -2661,6 +2791,8 @@ snapshots:
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
@ -2698,6 +2830,10 @@ snapshots:
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
locate-path@5.0.0:
|
||||
dependencies:
|
||||
p-locate: 4.1.0
|
||||
|
||||
locate-path@6.0.0:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
@ -2758,14 +2894,24 @@ snapshots:
|
||||
type-check: 0.4.0
|
||||
word-wrap: 1.2.5
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
|
||||
p-limit@3.1.0:
|
||||
dependencies:
|
||||
yocto-queue: 0.1.0
|
||||
|
||||
p-locate@4.1.0:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
p-locate@5.0.0:
|
||||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@ -2791,6 +2937,8 @@ snapshots:
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
@ -2809,6 +2957,12 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
react-dom@19.1.1(react@19.1.1):
|
||||
@ -2847,6 +3001,10 @@ snapshots:
|
||||
|
||||
react@19.1.1: {}
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
reselect@5.1.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
@ -2896,6 +3054,8 @@ snapshots:
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
set-cookie-parser@2.7.1: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
@ -2908,6 +3068,16 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
stylis@4.2.0: {}
|
||||
@ -2976,14 +3146,43 @@ snapshots:
|
||||
'@types/node': 24.5.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
which-module@2.0.1: {}
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
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: {}
|
||||
|
||||
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: {}
|
||||
|
||||
103
frontend/src/components/QRCodeDisplay.tsx
Normal file
103
frontend/src/components/QRCodeDisplay.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -17,7 +17,7 @@ import { useAuth } from '../../context/AuthContext';
|
||||
export const Navigation: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user, isAdmin } = useAuth();
|
||||
const { isAdmin } = useAuth();
|
||||
|
||||
const getValue = () => {
|
||||
if (location.pathname === '/' || location.pathname === '/home') return 0;
|
||||
|
||||
@ -104,7 +104,7 @@ export const AnswerOption: React.FC<AnswerOptionProps> = ({
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: !disabled && !showResult ? 'pointer' : 'default',
|
||||
pointerEvents: !disabled && !showResult ? 'auto' : 'none',
|
||||
zIndex: 1,
|
||||
...getCardStyles(),
|
||||
'&:hover': !disabled && !showResult ? {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import QRCode from 'qrcode';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@ -24,6 +25,7 @@ import {
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import { GridItem } from '../components/GridItem';
|
||||
import { QRCodeDisplay } from '../components/QRCodeDisplay';
|
||||
import {
|
||||
Dashboard,
|
||||
Quiz as QuizIcon,
|
||||
@ -35,6 +37,8 @@ import {
|
||||
Delete,
|
||||
BarChart,
|
||||
DragIndicator,
|
||||
QrCode,
|
||||
Download,
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { apiService } from '../services/api';
|
||||
@ -106,10 +110,19 @@ export const AdminPage: React.FC = () => {
|
||||
// Dialog states
|
||||
const [quizDialogOpen, setQuizDialogOpen] = useState(false);
|
||||
const [rewardDialogOpen, setRewardDialogOpen] = useState(false);
|
||||
const [qrDialogOpen, setQrDialogOpen] = useState(false);
|
||||
const [editingQuiz, setEditingQuiz] = useState<Quiz | null>(null);
|
||||
const [editingReward, setEditingReward] = useState<Reward | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// QR form states
|
||||
const [qrForm, setQrForm] = useState({
|
||||
type: 'reward',
|
||||
value: '',
|
||||
count: 1,
|
||||
});
|
||||
const [generatedQRCodes, setGeneratedQRCodes] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
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) => {
|
||||
setEditingQuiz(quiz);
|
||||
|
||||
@ -566,11 +689,17 @@ export const AdminPage: React.FC = () => {
|
||||
aria-controls="admin-tabpanel-2"
|
||||
/>
|
||||
<Tab
|
||||
icon={<Settings />}
|
||||
label="Настройки"
|
||||
icon={<QrCode />}
|
||||
label="QR-коды"
|
||||
id="admin-tab-3"
|
||||
aria-controls="admin-tabpanel-3"
|
||||
/>
|
||||
<Tab
|
||||
icon={<Settings />}
|
||||
label="Настройки"
|
||||
id="admin-tab-4"
|
||||
aria-controls="admin-tabpanel-4"
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@ -644,6 +773,17 @@ export const AdminPage: React.FC = () => {
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ color: '#2196F3' }}
|
||||
onClick={() => {
|
||||
setQrForm({ type: 'quiz', value: quiz.id.toString(), count: 1 });
|
||||
setQrDialogOpen(true);
|
||||
}}
|
||||
title="Сгенерировать QR-код"
|
||||
>
|
||||
<QrCode />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ color: '#f44336' }}
|
||||
@ -717,6 +857,17 @@ export const AdminPage: React.FC = () => {
|
||||
>
|
||||
<Edit />
|
||||
</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
|
||||
size="small"
|
||||
sx={{ color: '#f44336' }}
|
||||
@ -733,6 +884,42 @@ export const AdminPage: React.FC = () => {
|
||||
</TabPanel>
|
||||
|
||||
<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>
|
||||
@ -1227,6 +1414,165 @@ export const AdminPage: React.FC = () => {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@ -13,7 +13,6 @@ import {
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Button,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Person,
|
||||
@ -47,7 +46,7 @@ const TabPanel: React.FC<TabPanelProps> = ({ children, value, index }) => {
|
||||
};
|
||||
|
||||
export const ProfilePage: React.FC = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const [value, setValue] = useState(0);
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [purchases, setPurchases] = useState<Purchase[]>([]);
|
||||
|
||||
@ -44,8 +44,10 @@ export const QRScannerPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const startScanning = async () => {
|
||||
console.log('🎥 Starting QR scanner...');
|
||||
if (scannerRef.current) {
|
||||
try {
|
||||
console.log('🧹 Clearing existing scanner...');
|
||||
await scannerRef.current.clear();
|
||||
} catch (err) {
|
||||
console.error('Error clearing scanner:', err);
|
||||
@ -56,6 +58,7 @@ export const QRScannerPage: React.FC = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
console.log('📱 Creating new Html5Qrcode scanner...');
|
||||
const scanner = new Html5Qrcode('qr-reader');
|
||||
scannerRef.current = scanner;
|
||||
|
||||
@ -64,18 +67,21 @@ export const QRScannerPage: React.FC = () => {
|
||||
qrbox: { width: 250, height: 250 },
|
||||
};
|
||||
|
||||
console.log('🚀 Starting camera scanner...');
|
||||
await scanner.start(
|
||||
{ facingMode: 'environment' },
|
||||
config,
|
||||
async (decodedText: string) => {
|
||||
console.log('🔍 QR CODE DETECTED BY SCANNER:', decodedText);
|
||||
await handleQRScan(decodedText);
|
||||
},
|
||||
(_errorMessage: string) => {
|
||||
// Handle scan errors silently
|
||||
}
|
||||
);
|
||||
console.log('✅ Scanner started successfully');
|
||||
} catch (err) {
|
||||
console.error('Error starting scanner:', err);
|
||||
console.error('💥 Error starting scanner:', err);
|
||||
setError('Не удалось получить доступ к камере. Пожалуйста, проверьте разрешения.');
|
||||
setScanning(false);
|
||||
}
|
||||
@ -93,13 +99,18 @@ export const QRScannerPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleQRScan = async (decodedText: string) => {
|
||||
console.log('🚀 QR SCAN DETECTED:', decodedText);
|
||||
await stopScanning();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
console.log('📡 Sending QR validation request...');
|
||||
const response = await apiService.validateQR(decodedText);
|
||||
console.log('📨 QR validation response:', response);
|
||||
|
||||
if (response.success && response.data) {
|
||||
console.log('✅ QR validation successful:', response.data);
|
||||
// Transform backend response types to frontend format
|
||||
const transformedData: any = {
|
||||
type: response.data.type.toLowerCase(), // Convert "REWARD" -> "reward", "OPEN_QUIZ" -> "open_quiz"
|
||||
@ -121,10 +132,11 @@ export const QRScannerPage: React.FC = () => {
|
||||
setResult(transformedData);
|
||||
setShowResult(true);
|
||||
} else {
|
||||
console.log('❌ QR validation failed:', response.message);
|
||||
setError(response.message || 'Недействительный QR-код');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error validating QR:', err);
|
||||
console.error('💥 Error validating QR:', err);
|
||||
setError('Произошла ошибка при проверке QR-кода');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
|
||||
@ -17,28 +17,38 @@ class ApiService {
|
||||
// Request interceptor to add auth token
|
||||
this.api.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log('🚀 API Request:', config.method?.toUpperCase(), config.url, config.data);
|
||||
// For Telegram Web App, use X-Telegram-WebApp-Init-Data header
|
||||
const telegramInitData = localStorage.getItem('telegram_init_data');
|
||||
if (telegramInitData) {
|
||||
config.headers['X-Telegram-WebApp-Init-Data'] = telegramInitData;
|
||||
console.log('🔑 Using Telegram auth headers');
|
||||
} else {
|
||||
// Fallback to Bearer token for non-Telegram auth
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
console.log('🔑 Using Bearer token auth');
|
||||
} else {
|
||||
console.log('❌ No auth data found!');
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
console.error('💥 API Request error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor to handle errors
|
||||
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) => {
|
||||
console.error('❌ API Response error:', error.config?.method?.toUpperCase(), error.config?.url, error.response?.status, error.response?.data);
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('auth_token');
|
||||
window.location.href = '/';
|
||||
@ -109,7 +119,9 @@ class ApiService {
|
||||
|
||||
// QR methods
|
||||
async validateQR(payload: string): Promise<ApiResponse<any>> {
|
||||
console.log('🌐 API validateQR called with payload:', payload);
|
||||
const response = await this.api.post('/qr/validate', { payload });
|
||||
console.log('🌐 API validateQR response:', response.data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
@ -131,7 +131,7 @@ export interface GrantStarsRequest {
|
||||
}
|
||||
|
||||
export interface GenerateQRCodesRequest {
|
||||
type: 'reward' | 'quiz';
|
||||
type: 'reward' | 'quiz' | 'shop';
|
||||
value: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
@ -5,6 +5,6 @@ import react from '@vitejs/plugin-react'
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
allowedHosts: []
|
||||
allowedHosts: ["*"]
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user