mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 01:23:47 +00:00
Add files via upload
This commit is contained in:
42
Dockerfile
Normal file
42
Dockerfile
Normal 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
313
README.md
@@ -1,2 +1,311 @@
|
||||
# bedolaga-cabinet
|
||||
Bedolaga Web App
|
||||
# Bedolaga Cabinet - Web Interface
|
||||
|
||||
Современный веб-интерфейс личного кабинета для 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
21
docker-compose.yml
Normal 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
21
index.html
Normal 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
4600
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
package.json
Normal file
39
package.json
Normal 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
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
26889
public/miniapp/index.html
Normal file
26889
public/miniapp/index.html
Normal file
File diff suppressed because it is too large
Load Diff
148
public/miniapp/redirect.html
Normal file
148
public/miniapp/redirect.html
Normal 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
187
tailwind.config.js
Normal 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
25
tsconfig.json
Normal 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
10
tsconfig.node.json
Normal 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
26
vite.config.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user