Add files via upload

This commit is contained in:
Egor
2026-01-15 19:20:17 +03:00
committed by GitHub
parent 7be6b5c0ae
commit 2274a23b62
13 changed files with 32325 additions and 2 deletions

42
Dockerfile Normal file
View File

@@ -0,0 +1,42 @@
# Stage 1: Build the React application
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json package-lock.json* ./
# Install dependencies
# Use npm install if no lock file, npm ci if lock file exists
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
# Copy source code
COPY . .
# Build arguments for environment variables
ARG VITE_API_URL=/api
ARG VITE_TELEGRAM_BOT_USERNAME
ARG VITE_APP_NAME=Cabinet
ARG VITE_APP_LOGO=V
# Set environment variables for build
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_TELEGRAM_BOT_USERNAME=$VITE_TELEGRAM_BOT_USERNAME
ENV VITE_APP_NAME=$VITE_APP_NAME
ENV VITE_APP_LOGO=$VITE_APP_LOGO
# Build the application
RUN npm run build
# Stage 2: Serve with Nginx
FROM nginx:alpine
# Copy built assets from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1

313
README.md
View File

@@ -1,2 +1,311 @@
# bedolaga-cabinet # Bedolaga Cabinet - Web Interface
Bedolaga Web App
Современный веб-интерфейс личного кабинета для VPN бота на базе [Remnawave Bedolaga Telegram Bot](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot).
## Возможности
- 🔐 Авторизация через Telegram
- 💳 Пополнение баланса (YooKassa, CryptoBot, Stars и др.)
- 📊 Управление подписками и ключами
- 🎫 Система тикетов поддержки
- 🌐 Мультиязычность (EN/RU)
- 📱 Адаптивный дизайн
- 🎨 Настраиваемый брендинг
- ⚡ Fast - React + Vite + TypeScript
## Требования
- Node.js 18+ (для разработки)
- Docker и Docker Compose (для production)
- Запущенный backend бота с включенным Cabinet API
## Быстрый старт
### Вариант 1: Готовый Docker образ
```bash
# Из GitHub Container Registry
docker pull ghcr.io/bedolaga-dev/bedolaga-cabinet:latest
# Или из Docker Hub
docker pull bedolaga/bedolaga-cabinet:latest
```
### Вариант 2: Сборка из исходников
#### 1. Клонирование репозитория
```bash
git clone https://github.com/BEDOLAGA-DEV/bedolaga-cabinet.git
cd bedolaga-cabinet
```
#### 2. Настройка окружения
Скопируйте `.env.example` в `.env` и настройте переменные:
```bash
cp .env.example .env
```
**Основные переменные:**
```env
# API URL - путь к backend API
# Используйте /api если прокси на том же домене
# Или полный URL если backend на другом сервере
VITE_API_URL=/api
# Telegram Bot Username (без @)
VITE_TELEGRAM_BOT_USERNAME=your_bot_username
# Брендинг (опционально)
VITE_APP_NAME=My VPN Cabinet
VITE_APP_LOGO=V
# Порт для Docker контейнера
CABINET_PORT=3000
```
#### 3. Запуск в Docker
```bash
docker compose up -d --build
```
Приложение будет доступно на `http://localhost:3000`
## Настройка backend
В `.env` файле вашего бота добавьте:
```env
# Включить Cabinet API
CABINET_ENABLED=true
# JWT секрет для авторизации (сгенерируйте случайную строку)
CABINET_JWT_SECRET=your_random_secret_key_here
# Разрешенные origins для CORS
CABINET_ALLOWED_ORIGINS=http://localhost:3000,https://cabinet.yourdomain.com
```
После изменений перезапустите бота.
## Настройка прокси для production
Frontend раздает только статические файлы. Для работы с API нужно настроить reverse proxy, который будет проксировать запросы `/api/*` на backend бота.
### Вариант 1: Caddy
Добавьте в ваш Caddyfile:
```caddyfile
cabinet.yourdomain.com {
# Проксировать API запросы на backend
handle /api/* {
uri strip_prefix /api
reverse_proxy backend_bot:8080
}
# Остальное - на frontend контейнер
handle {
reverse_proxy cabinet_frontend:80
}
}
```
### Вариант 2: Nginx
Добавьте в конфигурацию Nginx:
```nginx
server {
listen 443 ssl http2;
server_name cabinet.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# API запросы проксируем на backend
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://backend_bot:8080;
proxy_http_version 1.1;
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;
}
# Frontend контейнер
location / {
proxy_pass http://cabinet_frontend:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
### Вариант 3: Статика + прямое проксирование
Если хотите раздавать статику напрямую без Docker:
```bash
# Соберите проект
npm install
npm run build
# Скопируйте dist на сервер
scp -r dist/* user@server:/var/www/cabinet/
```
Конфигурация Nginx для статики:
```nginx
server {
listen 443 ssl http2;
server_name cabinet.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
root /var/www/cabinet;
index index.html;
# Gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
# API на backend
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://backend_bot:8080;
proxy_http_version 1.1;
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;
}
# SPA routing
location / {
try_files $uri $uri/ /index.html;
}
# Кэширование статики
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
```
## Разработка
### Установка зависимостей
```bash
npm install
```
### Запуск dev сервера
```bash
npm run dev
```
Откроется на `http://localhost:5173`
### Сборка для production
```bash
npm run build
```
Результат в папке `dist/`
### Проверка типов
```bash
npm run type-check
```
### Линтинг
```bash
npm run lint
```
## Переменные окружения
### Build-time (используются при сборке)
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `VITE_API_URL` | Путь к API (`/api` или полный URL) | `/api` |
| `VITE_TELEGRAM_BOT_USERNAME` | Username Telegram бота (без @) | - |
| `VITE_APP_NAME` | Название приложения | `Cabinet` |
| `VITE_APP_LOGO` | Логотип (короткий текст) | `V` |
### Runtime (только для Docker)
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `CABINET_PORT` | Порт контейнера | `3000` |
## Структура проекта
```
bedolaga-cabinet/
├── src/
│ ├── api/ # API клиенты
│ ├── components/ # React компоненты
│ ├── contexts/ # React контексты
│ ├── hooks/ # Custom hooks
│ ├── locales/ # Переводы (i18n)
│ ├── pages/ # Страницы приложения
│ ├── types/ # TypeScript типы
│ └── utils/ # Утилиты
├── public/ # Статические файлы
├── Dockerfile # Docker образ
├── docker-compose.yml # Docker Compose конфигурация
└── .env.example # Пример переменных окружения
```
## Устранение проблем
### Ошибка CORS
Убедитесь, что домен frontend добавлен в `CABINET_ALLOWED_ORIGINS` в настройках бота.
### API возвращает HTML вместо JSON
Проверьте настройку прокси - запросы на `/api/*` должны попадать на backend, а не на frontend.
### 502 Bad Gateway
Убедитесь что:
1. Backend бот запущен и работает
2. Контейнеры находятся в одной Docker сети
3. Имя сервиса backend в прокси конфигурации правильное
### Telegram авторизация не работает
1. Проверьте `VITE_TELEGRAM_BOT_USERNAME` - должен быть без `@`
2. Убедитесь что домен добавлен в Bot Settings → Domain
## Связанные проекты
- [Remnawave Bedolaga Telegram Bot](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot) - Backend бота
- [Bedolaga Chat](https://t.me/+wTdMtSWq8YdmZmVi) - Чат поддержки
## Лицензия
Apache-2.0 License - см. [LICENSE](LICENSE)
## Контакты
- Telegram: [@fringg](https://t.me/fringg)
- Telegram: [@pedzeo](https://t.me/pedzeo)
- Чат: [Bedolaga Chat](https://t.me/+wTdMtSWq8YdmZmVi)

21
docker-compose.yml Normal file
View File

@@ -0,0 +1,21 @@
services:
cabinet-frontend:
build:
context: .
dockerfile: Dockerfile
args:
# API URL - use /api for same-server deployment or full URL for external backend
VITE_API_URL: ${VITE_API_URL:-/api}
VITE_TELEGRAM_BOT_USERNAME: ${VITE_TELEGRAM_BOT_USERNAME}
VITE_APP_NAME: ${VITE_APP_NAME:-Cabinet}
VITE_APP_LOGO: ${VITE_APP_LOGO:-V}
container_name: cabinet_frontend
restart: unless-stopped
ports:
- "${CABINET_PORT:-3000}:80"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s

21
index.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="ru" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#0a0f1a" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="format-detection" content="telephone=no" />
<title>Loading...</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<style>
body { background-color: #0a0f1a; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4600
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "cabinet-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@lottiefiles/dotlottie-react": "^0.8.0",
"axios": "^1.6.0",
"i18next": "^23.7.0",
"i18next-browser-languagedetector": "^7.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^13.5.0",
"react-router-dom": "^6.20.0",
"zustand": "^4.4.0",
"@tanstack/react-query": "^5.8.0"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.53.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

26889
public/miniapp/index.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redirecting...</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
@media (prefers-color-scheme: light) {
body {
background: linear-gradient(135deg, #f5f5f7 0%, #e5e7eb 100%);
color: #1d1d1f;
}
.spinner {
border-color: rgba(0,0,0,0.1);
border-top-color: #3b82f6;
}
.error {
background: rgba(0,0,0,0.05);
}
.btn {
background: #3b82f6;
color: #fff;
}
}
.container {
text-align: center;
padding: 2rem;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid rgba(255,255,255,0.2);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1.5rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
h1 {
font-size: 1.25rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
p {
font-size: 0.875rem;
opacity: 0.7;
}
.error {
display: none;
margin-top: 1.5rem;
padding: 1rem;
background: rgba(255,255,255,0.1);
border-radius: 8px;
}
.error.show {
display: block;
}
.btn {
display: inline-block;
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background: #fff;
color: #1a1a2e;
text-decoration: none;
border-radius: 8px;
font-weight: 500;
}
</style>
</head>
<body>
<div class="container">
<div class="spinner"></div>
<h1 id="title">Opening app...</h1>
<p id="subtitle">Please wait</p>
<div class="error" id="errorBlock">
<p id="errorText">If the app didn't open, click the button below:</p>
<a class="btn" id="manualBtn" href="#">Open App</a>
</div>
</div>
<script>
(function() {
const params = new URLSearchParams(window.location.search);
const url = params.get('url');
const lang = params.get('lang') || 'en';
const texts = {
en: {
title: 'Opening app...',
subtitle: 'Please wait',
error: "If the app didn't open, click the button below:",
btn: 'Open App',
noUrl: 'No URL provided'
},
ru: {
title: 'Открываем приложение...',
subtitle: 'Пожалуйста, подождите',
error: 'Если приложение не открылось, нажмите кнопку:',
btn: 'Открыть приложение',
noUrl: 'URL не указан'
}
};
const t = texts[lang] || texts.en;
document.getElementById('title').textContent = t.title;
document.getElementById('subtitle').textContent = t.subtitle;
document.getElementById('errorText').textContent = t.error;
document.getElementById('manualBtn').textContent = t.btn;
if (!url) {
document.getElementById('title').textContent = t.noUrl;
document.getElementById('subtitle').textContent = '';
document.querySelector('.spinner').style.display = 'none';
return;
}
const manualBtn = document.getElementById('manualBtn');
manualBtn.href = url;
// Try to open the URL scheme directly
// This page is opened in external browser, so custom schemes work
window.location.href = url;
// Show manual button after a delay in case redirect didn't work
setTimeout(function() {
document.getElementById('errorBlock').classList.add('show');
}, 2000);
})();
</script>
</body>
</html>

187
tailwind.config.js Normal file
View File

@@ -0,0 +1,187 @@
/** @type {import('tailwindcss').Config} */
// Helper function to create color with opacity support using CSS variables
const withOpacity = (variableName, fallback) => {
return ({ opacityValue }) => {
if (opacityValue !== undefined) {
return `rgba(var(${variableName}), ${opacityValue})`
}
return `rgb(var(${variableName}))`
}
}
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
// Modern neutral palette
dark: {
50: withOpacity('--color-dark-50'),
100: withOpacity('--color-dark-100'),
200: withOpacity('--color-dark-200'),
300: withOpacity('--color-dark-300'),
400: withOpacity('--color-dark-400'),
500: withOpacity('--color-dark-500'),
600: withOpacity('--color-dark-600'),
700: withOpacity('--color-dark-700'),
800: withOpacity('--color-dark-800'),
850: withOpacity('--color-dark-850'),
900: withOpacity('--color-dark-900'),
950: withOpacity('--color-dark-950'),
},
// Champagne light theme palette
champagne: {
50: withOpacity('--color-champagne-50'),
100: withOpacity('--color-champagne-100'),
200: withOpacity('--color-champagne-200'),
300: withOpacity('--color-champagne-300'),
400: withOpacity('--color-champagne-400'),
500: withOpacity('--color-champagne-500'),
600: withOpacity('--color-champagne-600'),
700: withOpacity('--color-champagne-700'),
800: withOpacity('--color-champagne-800'),
900: withOpacity('--color-champagne-900'),
950: withOpacity('--color-champagne-950'),
},
// Accent - dynamic color scheme
accent: {
50: withOpacity('--color-accent-50'),
100: withOpacity('--color-accent-100'),
200: withOpacity('--color-accent-200'),
300: withOpacity('--color-accent-300'),
400: withOpacity('--color-accent-400'),
500: withOpacity('--color-accent-500'),
600: withOpacity('--color-accent-600'),
700: withOpacity('--color-accent-700'),
800: withOpacity('--color-accent-800'),
900: withOpacity('--color-accent-900'),
950: withOpacity('--color-accent-950'),
},
// Success - green
success: {
50: withOpacity('--color-success-50'),
100: withOpacity('--color-success-100'),
200: withOpacity('--color-success-200'),
300: withOpacity('--color-success-300'),
400: withOpacity('--color-success-400'),
500: withOpacity('--color-success-500'),
600: withOpacity('--color-success-600'),
700: withOpacity('--color-success-700'),
800: withOpacity('--color-success-800'),
900: withOpacity('--color-success-900'),
950: withOpacity('--color-success-950'),
},
// Warning - amber
warning: {
50: withOpacity('--color-warning-50'),
100: withOpacity('--color-warning-100'),
200: withOpacity('--color-warning-200'),
300: withOpacity('--color-warning-300'),
400: withOpacity('--color-warning-400'),
500: withOpacity('--color-warning-500'),
600: withOpacity('--color-warning-600'),
700: withOpacity('--color-warning-700'),
800: withOpacity('--color-warning-800'),
900: withOpacity('--color-warning-900'),
950: withOpacity('--color-warning-950'),
},
// Error - red
error: {
50: withOpacity('--color-error-50'),
100: withOpacity('--color-error-100'),
200: withOpacity('--color-error-200'),
300: withOpacity('--color-error-300'),
400: withOpacity('--color-error-400'),
500: withOpacity('--color-error-500'),
600: withOpacity('--color-error-600'),
700: withOpacity('--color-error-700'),
800: withOpacity('--color-error-800'),
900: withOpacity('--color-error-900'),
950: withOpacity('--color-error-950'),
},
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
},
fontSize: {
'2xs': ['0.625rem', { lineHeight: '0.875rem' }],
},
boxShadow: {
'glow': '0 0 20px rgba(var(--color-accent-500), 0.15)',
'glow-lg': '0 0 40px rgba(var(--color-accent-500), 0.2)',
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -4px rgba(0, 0, 0, 0.2)',
'card': '0 4px 24px -4px rgba(0, 0, 0, 0.4)',
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-subtle': 'linear-gradient(135deg, var(--tw-gradient-stops))',
},
animation: {
'fade-in': 'fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
'fade-in-fast': 'fadeIn 0.2s cubic-bezier(0.16, 1, 0.3, 1)',
'slide-up': 'slideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1)',
'slide-down': 'slideDown 0.5s cubic-bezier(0.16, 1, 0.3, 1)',
'slide-in-right': 'slideInRight 0.5s cubic-bezier(0.16, 1, 0.3, 1)',
'slide-in-left': 'slideInLeft 0.5s cubic-bezier(0.16, 1, 0.3, 1)',
'scale-in': 'scaleIn 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
'scale-in-bounce': 'scaleInBounce 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)',
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'float': 'float 3s ease-in-out infinite',
'glow-pulse': 'glowPulse 2s ease-in-out infinite',
'spotlight': 'spotlight 2s ease-in-out infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideDown: {
'0%': { opacity: '0', transform: 'translateY(-20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideInRight: {
'0%': { opacity: '0', transform: 'translateX(20px)' },
'100%': { opacity: '1', transform: 'translateX(0)' },
},
slideInLeft: {
'0%': { opacity: '0', transform: 'translateX(-20px)' },
'100%': { opacity: '1', transform: 'translateX(0)' },
},
scaleIn: {
'0%': { opacity: '0', transform: 'scale(0.95)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
scaleInBounce: {
'0%': { opacity: '0', transform: 'scale(0.9)' },
'50%': { transform: 'scale(1.02)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-5px)' },
},
glowPulse: {
'0%, 100%': { boxShadow: '0 0 20px rgba(var(--color-accent-500), 0.3)' },
'50%': { boxShadow: '0 0 40px rgba(var(--color-accent-500), 0.6)' },
},
spotlight: {
'0%, 100%': { boxShadow: '0 0 0 4px rgba(var(--color-accent-500), 0.4), 0 0 20px rgba(var(--color-accent-500), 0.3)' },
'50%': { boxShadow: '0 0 0 8px rgba(var(--color-accent-500), 0.2), 0 0 40px rgba(var(--color-accent-500), 0.5)' },
},
},
transitionTimingFunction: {
'smooth': 'cubic-bezier(0.4, 0, 0.2, 1)',
},
},
},
plugins: [],
}

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

26
vite.config.ts Normal file
View File

@@ -0,0 +1,26 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
// Base path - use '/' for standalone Docker deployment
// Change to '/cabinet/' if serving from a sub-path
base: '/',
server: {
port: 5173,
host: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
// Strip /api prefix: /api/cabinet/auth -> /cabinet/auth
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
build: {
outDir: 'dist',
sourcemap: false,
},
})