mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
Merge pull request #7 from evansvl/main
Пофиксил UX хедера для десктопа и исправлен текстовый баг при попытке поделиться реф. ссылкой
This commit is contained in:
@@ -20,5 +20,5 @@ VITE_APP_LOGO=V
|
|||||||
# ===== RUNTIME VARIABLES =====
|
# ===== RUNTIME VARIABLES =====
|
||||||
|
|
||||||
# Port to expose the frontend on host machine
|
# Port to expose the frontend on host machine
|
||||||
# Default: 3000 (http://localhost:3000)
|
# Default: 3020 (http://localhost:3020)
|
||||||
CABINET_PORT=3000
|
CABINET_PORT=3020
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -70,7 +70,7 @@ VITE_APP_NAME=My VPN Cabinet
|
|||||||
VITE_APP_LOGO=V
|
VITE_APP_LOGO=V
|
||||||
|
|
||||||
# Порт для Docker контейнера
|
# Порт для Docker контейнера
|
||||||
CABINET_PORT=3000
|
CABINET_PORT=3020
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. Запуск в Docker
|
#### 3. Запуск в Docker
|
||||||
@@ -101,6 +101,7 @@ CABINET_ALLOWED_ORIGINS=http://localhost:3000,https://cabinet.yourdomain.com
|
|||||||
## Настройка прокси для production
|
## Настройка прокси для production
|
||||||
|
|
||||||
Frontend - это статические файлы (HTML, JS, CSS). Для работы нужно:
|
Frontend - это статические файлы (HTML, JS, CSS). Для работы нужно:
|
||||||
|
|
||||||
1. Раздавать статику через веб-сервер
|
1. Раздавать статику через веб-сервер
|
||||||
2. Проксировать `/api/*` запросы на backend бота
|
2. Проксировать `/api/*` запросы на backend бота
|
||||||
|
|
||||||
@@ -153,8 +154,8 @@ services:
|
|||||||
- ./cabinet-dist:/srv/cabinet:ro
|
- ./cabinet-dist:/srv/cabinet:ro
|
||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- '80:80'
|
||||||
- "443:443"
|
- '443:443'
|
||||||
networks:
|
networks:
|
||||||
- bot_network
|
- bot_network
|
||||||
```
|
```
|
||||||
@@ -164,6 +165,7 @@ services:
|
|||||||
Если хотите использовать готовый Docker контейнер с nginx внутри.
|
Если хотите использовать готовый Docker контейнер с nginx внутри.
|
||||||
|
|
||||||
**⚠️ Важно:**
|
**⚠️ Важно:**
|
||||||
|
|
||||||
- Порт `80` в примерах - это **внутренний порт контейнера** (nginx внутри), не хост-порт!
|
- Порт `80` в примерах - это **внутренний порт контейнера** (nginx внутри), не хост-порт!
|
||||||
- Контейнеры должны быть в одной Docker сети для связи друг с другом
|
- Контейнеры должны быть в одной Docker сети для связи друг с другом
|
||||||
- Имена контейнеров используются как DNS внутри Docker сети
|
- Имена контейнеров используются как DNS внутри Docker сети
|
||||||
@@ -199,6 +201,7 @@ networks:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Важно:** Замените имя сети на вашу:
|
**Важно:** Замените имя сети на вашу:
|
||||||
|
|
||||||
- Если у вас bot + caddy: используйте сеть бота (обычно `<название_проекта>_bot_network`)
|
- Если у вас bot + caddy: используйте сеть бота (обычно `<название_проекта>_bot_network`)
|
||||||
- Если отдельный Caddy: узнайте через `docker network ls`
|
- Если отдельный Caddy: узнайте через `docker network ls`
|
||||||
- Если используете Traefik: обычно `traefik` или `web`
|
- Если используете Traefik: обычно `traefik` или `web`
|
||||||
@@ -212,6 +215,7 @@ docker compose up -d
|
|||||||
**Шаг 4:** Добавьте в конфигурацию Caddy/Nginx:
|
**Шаг 4:** Добавьте в конфигурацию Caddy/Nginx:
|
||||||
|
|
||||||
Caddy проксирует на контейнер:
|
Caddy проксирует на контейнер:
|
||||||
|
|
||||||
```caddyfile
|
```caddyfile
|
||||||
cabinet.yourdomain.com {
|
cabinet.yourdomain.com {
|
||||||
# API на backend
|
# API на backend
|
||||||
@@ -228,6 +232,7 @@ cabinet.yourdomain.com {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Nginx (добавьте в существующий конфиг):
|
Nginx (добавьте в существующий конфиг):
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 ssl http2;
|
||||||
@@ -269,6 +274,7 @@ docker exec <nginx_container> nginx -s reload
|
|||||||
#### B. Если Caddy/Nginx ещё НЕ запущен:
|
#### B. Если Caddy/Nginx ещё НЕ запущен:
|
||||||
|
|
||||||
docker-compose.yml:
|
docker-compose.yml:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
cabinet-frontend:
|
cabinet-frontend:
|
||||||
@@ -293,7 +299,7 @@ networks:
|
|||||||
### Build-time (используются при сборке)
|
### Build-time (используются при сборке)
|
||||||
|
|
||||||
| Переменная | Описание | По умолчанию |
|
| Переменная | Описание | По умолчанию |
|
||||||
|------------|----------|--------------|
|
| ---------------------------- | ---------------------------------- | ------------ |
|
||||||
| `VITE_API_URL` | Путь к API (`/api` или полный URL) | `/api` |
|
| `VITE_API_URL` | Путь к API (`/api` или полный URL) | `/api` |
|
||||||
| `VITE_TELEGRAM_BOT_USERNAME` | Username Telegram бота (без @) | - |
|
| `VITE_TELEGRAM_BOT_USERNAME` | Username Telegram бота (без @) | - |
|
||||||
| `VITE_APP_NAME` | Название приложения | `Cabinet` |
|
| `VITE_APP_NAME` | Название приложения | `Cabinet` |
|
||||||
@@ -302,7 +308,7 @@ networks:
|
|||||||
### Runtime (только для Docker)
|
### Runtime (только для Docker)
|
||||||
|
|
||||||
| Переменная | Описание | По умолчанию |
|
| Переменная | Описание | По умолчанию |
|
||||||
|------------|----------|--------------|
|
| -------------- | --------------- | ------------ |
|
||||||
| `CABINET_PORT` | Порт контейнера | `3000` |
|
| `CABINET_PORT` | Порт контейнера | `3000` |
|
||||||
|
|
||||||
## Структура проекта
|
## Структура проекта
|
||||||
@@ -337,6 +343,7 @@ bedolaga-cabinet/
|
|||||||
### 502 Bad Gateway
|
### 502 Bad Gateway
|
||||||
|
|
||||||
Убедитесь что:
|
Убедитесь что:
|
||||||
|
|
||||||
1. Backend бот запущен и работает
|
1. Backend бот запущен и работает
|
||||||
2. Контейнеры находятся в одной Docker сети
|
2. Контейнеры находятся в одной Docker сети
|
||||||
3. Имя сервиса backend в прокси конфигурации правильное
|
3. Имя сервиса backend в прокси конфигурации правильное
|
||||||
|
|||||||
@@ -20,9 +20,17 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
# Маппинг: <хост-порт>:<контейнер-порт>
|
# Маппинг: <хост-порт>:<контейнер-порт>
|
||||||
# Внутри контейнера nginx слушает на порту 80
|
# Внутри контейнера nginx слушает на порту 80
|
||||||
- "${CABINET_PORT:-3000}:80"
|
- '${CABINET_PORT:-3020}:80'
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/"]
|
test:
|
||||||
|
[
|
||||||
|
'CMD',
|
||||||
|
'wget',
|
||||||
|
'--no-verbose',
|
||||||
|
'--tries=1',
|
||||||
|
'--spider',
|
||||||
|
'http://localhost:80/',
|
||||||
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -21,100 +21,264 @@ interface LayoutProps {
|
|||||||
|
|
||||||
// Icons as simple SVG components
|
// Icons as simple SVG components
|
||||||
const HomeIcon = () => (
|
const HomeIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const SubscriptionIcon = () => (
|
const SubscriptionIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const WalletIcon = () => (
|
const WalletIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const UsersIcon = () => (
|
const UsersIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const ChatIcon = () => (
|
const ChatIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const UserIcon = () => (
|
const UserIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const LogoutIcon = () => (
|
const LogoutIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
// Theme toggle icons
|
// Theme toggle icons
|
||||||
const SunIcon = () => (
|
const SunIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const MoonIcon = () => (
|
const MoonIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const MenuIcon = () => (
|
const MenuIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
className='w-6 h-6'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const CloseIcon = () => (
|
const CloseIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
className='w-6 h-6'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M6 18L18 6M6 6l12 12'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const GamepadIcon = () => (
|
const GamepadIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 01-.657.643 48.39 48.39 0 01-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 01-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 00-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 01-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 00.657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 005.427-.63 48.05 48.05 0 00.582-4.717.532.532 0 00-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.959.401v0a.656.656 0 00.659-.663 47.703 47.703 0 00-.31-4.82.78.78 0 01.79-.869" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 01-.657.643 48.39 48.39 0 01-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 01-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 00-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 01-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 00.657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 005.427-.63 48.05 48.05 0 00.582-4.717.532.532 0 00-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.959.401v0a.656.656 0 00.659-.663 47.703 47.703 0 00-.31-4.82.78.78 0 01.79-.869'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const ClipboardIcon = () => (
|
const ClipboardIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const InfoIcon = () => (
|
const InfoIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const CogIcon = () => (
|
const CogIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
className='w-5 h-5'
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M15 12a3 3 0 11-6 0 3 3 0 016 0z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const WheelIcon = () => (
|
const WheelIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
className='w-5 h-5'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -211,7 +375,11 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
const navItems = useMemo(() => {
|
const navItems = useMemo(() => {
|
||||||
const items = [
|
const items = [
|
||||||
{ path: '/', label: t('nav.dashboard'), icon: HomeIcon },
|
{ path: '/', label: t('nav.dashboard'), icon: HomeIcon },
|
||||||
{ path: '/subscription', label: t('nav.subscription'), icon: SubscriptionIcon },
|
{
|
||||||
|
path: '/subscription',
|
||||||
|
label: t('nav.subscription'),
|
||||||
|
icon: SubscriptionIcon,
|
||||||
|
},
|
||||||
{ path: '/balance', label: t('nav.balance'), icon: WalletIcon },
|
{ path: '/balance', label: t('nav.balance'), icon: WalletIcon },
|
||||||
{ path: '/referral', label: t('nav.referral'), icon: UsersIcon },
|
{ path: '/referral', label: t('nav.referral'), icon: UsersIcon },
|
||||||
{ path: '/support', label: t('nav.support'), icon: ChatIcon },
|
{ path: '/support', label: t('nav.support'), icon: ChatIcon },
|
||||||
@@ -219,7 +387,11 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
|
|
||||||
// Only show contests if there are available contests
|
// Only show contests if there are available contests
|
||||||
if (contestsCount && contestsCount.count > 0) {
|
if (contestsCount && contestsCount.count > 0) {
|
||||||
items.push({ path: '/contests', label: t('nav.contests'), icon: GamepadIcon })
|
items.push({
|
||||||
|
path: '/contests',
|
||||||
|
label: t('nav.contests'),
|
||||||
|
icon: GamepadIcon,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show polls if there are available polls
|
// Only show polls if there are available polls
|
||||||
@@ -239,7 +411,11 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
if (wheelConfig?.is_enabled) {
|
if (wheelConfig?.is_enabled) {
|
||||||
const infoIndex = items.findIndex(item => item.path === '/info')
|
const infoIndex = items.findIndex(item => item.path === '/info')
|
||||||
if (infoIndex !== -1) {
|
if (infoIndex !== -1) {
|
||||||
items.splice(infoIndex, 0, { path: '/wheel', label: t('nav.wheel'), icon: WheelIcon })
|
items.splice(infoIndex, 0, {
|
||||||
|
path: '/wheel',
|
||||||
|
label: t('nav.wheel'),
|
||||||
|
icon: WheelIcon,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
items.push({ path: '/wheel', label: t('nav.wheel'), icon: WheelIcon })
|
items.push({ path: '/wheel', label: t('nav.wheel'), icon: WheelIcon })
|
||||||
}
|
}
|
||||||
@@ -255,30 +431,41 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
const isAdminActive = () => location.pathname.startsWith('/admin')
|
const isAdminActive = () => location.pathname.startsWith('/admin')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className='min-h-screen flex flex-col'>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="sticky top-0 z-50 glass border-b border-dark-800/50">
|
<header className='sticky top-0 z-50 glass border-b border-dark-800/50'>
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6">
|
<div className='w-full mx-auto px-4 sm:px-6'>
|
||||||
<div className="flex justify-between items-center h-16 lg:h-20">
|
<div className='flex justify-between items-center h-16 lg:h-20'>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link to="/" className={`flex items-center gap-2.5 flex-shrink-0 ${!appName ? 'lg:mr-4' : ''}`}>
|
<Link
|
||||||
<div className="w-10 h-10 sm:w-12 sm:h-12 lg:w-14 lg:h-14 rounded-xl bg-gradient-to-br from-accent-400 to-accent-600 flex items-center justify-center overflow-hidden shadow-lg shadow-accent-500/20 flex-shrink-0">
|
to='/'
|
||||||
|
className={`flex items-center gap-2.5 flex-shrink-0 pl-4 ${
|
||||||
|
!appName ? 'lg:mr-4' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className='w-10 h-10 sm:w-12 sm:h-12 lg:w-14 lg:h-14 rounded-xl bg-gradient-to-br from-accent-400 to-accent-600 flex items-center justify-center overflow-hidden shadow-lg shadow-accent-500/20 flex-shrink-0'>
|
||||||
{hasCustomLogo && logoUrl ? (
|
{hasCustomLogo && logoUrl ? (
|
||||||
<img src={logoUrl} alt={appName || 'Logo'} className="w-full h-full object-contain" />
|
<img
|
||||||
|
src={logoUrl}
|
||||||
|
alt={appName || 'Logo'}
|
||||||
|
className='w-full h-full object-contain'
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-white font-bold text-lg sm:text-xl lg:text-2xl">{logoLetter}</span>
|
<span className='text-white font-bold text-lg sm:text-xl lg:text-2xl'>
|
||||||
|
{logoLetter}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{appName && (
|
{appName && (
|
||||||
<span className="text-base lg:text-lg font-semibold text-dark-100 whitespace-nowrap">
|
<span className='text-base lg:text-lg font-semibold text-dark-100 whitespace-nowrap'>
|
||||||
{appName}
|
{appName}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<nav className="hidden lg:flex items-center gap-1">
|
<nav className='hidden lg:flex items-center gap-1'>
|
||||||
{desktopNavItems.map((item) => (
|
{desktopNavItems.map(item => (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
@@ -294,8 +481,8 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
))}
|
))}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<>
|
<>
|
||||||
<div className="w-px h-6 bg-dark-700 mx-2" />
|
<div className='w-px h-6 bg-dark-700 mx-2' />
|
||||||
{adminNavItems.map((item) => (
|
{adminNavItems.map(item => (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
@@ -314,21 +501,33 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Right side */}
|
{/* Right side */}
|
||||||
<div className="flex items-center gap-2 sm:gap-3">
|
<div className='flex items-center gap-2 sm:gap-3 pr-4'>
|
||||||
{/* Theme toggle button - only show if both themes are enabled */}
|
{/* Theme toggle button - only show if both themes are enabled */}
|
||||||
{canToggle && (
|
{canToggle && (
|
||||||
<button
|
<button
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
className="relative p-2.5 rounded-xl transition-all duration-300 hover:scale-110 active:scale-95
|
className='relative p-2.5 rounded-xl transition-all duration-300 hover:scale-110 active:scale-95
|
||||||
dark:text-dark-400 dark:hover:text-dark-100 dark:hover:bg-dark-800
|
dark:text-dark-400 dark:hover:text-dark-100 dark:hover:bg-dark-800
|
||||||
text-champagne-500 hover:text-champagne-800 hover:bg-champagne-200/50"
|
text-champagne-500 hover:text-champagne-800 hover:bg-champagne-200/50'
|
||||||
title={isDark ? t('theme.light') || 'Light mode' : t('theme.dark') || 'Dark mode'}
|
title={
|
||||||
|
isDark
|
||||||
|
? t('theme.light') || 'Light mode'
|
||||||
|
: t('theme.dark') || 'Dark mode'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='relative w-5 h-5'>
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 transition-all duration-300 ${
|
||||||
|
isDark ? 'opacity-100 rotate-0' : 'opacity-0 rotate-90'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-5 h-5">
|
|
||||||
<div className={`absolute inset-0 transition-all duration-300 ${isDark ? 'opacity-100 rotate-0' : 'opacity-0 rotate-90'}`}>
|
|
||||||
<MoonIcon />
|
<MoonIcon />
|
||||||
</div>
|
</div>
|
||||||
<div className={`absolute inset-0 transition-all duration-300 ${isDark ? 'opacity-0 -rotate-90' : 'opacity-100 rotate-0'}`}>
|
<div
|
||||||
|
className={`absolute inset-0 transition-all duration-300 ${
|
||||||
|
isDark ? 'opacity-0 -rotate-90' : 'opacity-100 rotate-0'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<SunIcon />
|
<SunIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,21 +537,23 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
|
|
||||||
{/* Profile - Desktop */}
|
{/* Profile - Desktop */}
|
||||||
<div className="hidden sm:flex items-center gap-3">
|
<div className='hidden sm:flex items-center gap-3'>
|
||||||
<Link
|
<Link
|
||||||
to="/profile"
|
to='/profile'
|
||||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-dark-800/50 transition-colors"
|
className='flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-dark-800/50 transition-colors'
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 rounded-full bg-dark-700 flex items-center justify-center">
|
<div className='w-8 h-8 rounded-full bg-dark-700 flex items-center justify-center'>
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-dark-300">
|
<span className='text-sm text-dark-300'>
|
||||||
{user?.first_name || user?.username || `#${user?.telegram_id}`}
|
{user?.first_name ||
|
||||||
|
user?.username ||
|
||||||
|
`#${user?.telegram_id}`}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="btn-icon"
|
className='btn-icon'
|
||||||
title={t('nav.logout')}
|
title={t('nav.logout')}
|
||||||
>
|
>
|
||||||
<LogoutIcon />
|
<LogoutIcon />
|
||||||
@@ -362,62 +563,72 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
{/* Mobile menu button */}
|
{/* Mobile menu button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
className="lg:hidden btn-icon"
|
className='lg:hidden btn-icon'
|
||||||
>
|
>
|
||||||
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
|
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Mobile menu - fixed overlay */}
|
{/* Mobile menu - fixed overlay */}
|
||||||
{mobileMenuOpen && (
|
{mobileMenuOpen && (
|
||||||
<div className="lg:hidden fixed inset-0 z-40 animate-fade-in" style={{ top: '64px' }}>
|
<div
|
||||||
|
className='lg:hidden fixed inset-0 z-40 animate-fade-in'
|
||||||
|
style={{ top: '64px' }}
|
||||||
|
>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
className='absolute inset-0 bg-black/50 backdrop-blur-sm'
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Menu content */}
|
{/* Menu content */}
|
||||||
<div className="absolute inset-x-0 top-0 bottom-0 bg-dark-900 border-t border-dark-800/50 overflow-y-auto pb-[calc(5rem+env(safe-area-inset-bottom,0px))]">
|
<div className='absolute inset-x-0 top-0 bottom-0 bg-dark-900 border-t border-dark-800/50 overflow-y-auto pb-[calc(5rem+env(safe-area-inset-bottom,0px))]'>
|
||||||
<div className="max-w-6xl mx-auto px-4 py-4">
|
<div className='max-w-6xl mx-auto px-4 py-4'>
|
||||||
{/* User info */}
|
{/* User info */}
|
||||||
<div className="flex items-center gap-3 pb-4 mb-4 border-b border-dark-800/50">
|
<div className='flex items-center gap-3 pb-4 mb-4 border-b border-dark-800/50'>
|
||||||
{userPhotoUrl ? (
|
{userPhotoUrl ? (
|
||||||
<img
|
<img
|
||||||
src={userPhotoUrl}
|
src={userPhotoUrl}
|
||||||
alt="Avatar"
|
alt='Avatar'
|
||||||
className="w-10 h-10 rounded-full object-cover"
|
className='w-10 h-10 rounded-full object-cover'
|
||||||
onError={(e) => {
|
onError={e => {
|
||||||
e.currentTarget.style.display = 'none'
|
e.currentTarget.style.display = 'none'
|
||||||
e.currentTarget.nextElementSibling?.classList.remove('hidden')
|
e.currentTarget.nextElementSibling?.classList.remove(
|
||||||
|
'hidden'
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={`w-10 h-10 rounded-full bg-dark-700 flex items-center justify-center ${userPhotoUrl ? 'hidden' : ''}`}>
|
<div
|
||||||
|
className={`w-10 h-10 rounded-full bg-dark-700 flex items-center justify-center ${
|
||||||
|
userPhotoUrl ? 'hidden' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-dark-100">
|
<div className='text-sm font-medium text-dark-100'>
|
||||||
{user?.first_name || user?.username}
|
{user?.first_name || user?.username}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-dark-500">
|
<div className='text-xs text-dark-500'>
|
||||||
@{user?.username || `ID: ${user?.telegram_id}`}
|
@{user?.username || `ID: ${user?.telegram_id}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav items */}
|
{/* Nav items */}
|
||||||
<nav className="space-y-1">
|
<nav className='space-y-1'>
|
||||||
{desktopNavItems.map((item) => (
|
{desktopNavItems.map(item => (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
className={isActive(item.path) ? 'nav-item-active' : 'nav-item'}
|
className={
|
||||||
|
isActive(item.path) ? 'nav-item-active' : 'nav-item'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<item.icon />
|
<item.icon />
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -426,16 +637,20 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<>
|
<>
|
||||||
<div className="divider my-3" />
|
<div className='divider my-3' />
|
||||||
<div className="px-4 py-1 text-xs font-medium text-dark-500 uppercase tracking-wider">
|
<div className='px-4 py-1 text-xs font-medium text-dark-500 uppercase tracking-wider'>
|
||||||
{t('admin.nav.title')}
|
{t('admin.nav.title')}
|
||||||
</div>
|
</div>
|
||||||
{adminNavItems.map((item) => (
|
{adminNavItems.map(item => (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
className={`nav-item ${isAdminActive() ? 'text-warning-400 bg-warning-500/10' : 'text-warning-500/70'}`}
|
className={`nav-item ${
|
||||||
|
isAdminActive()
|
||||||
|
? 'text-warning-400 bg-warning-500/10'
|
||||||
|
: 'text-warning-500/70'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<item.icon />
|
<item.icon />
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -444,12 +659,14 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="divider my-3" />
|
<div className='divider my-3' />
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/profile"
|
to='/profile'
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
className={isActive('/profile') ? 'nav-item-active' : 'nav-item'}
|
className={
|
||||||
|
isActive('/profile') ? 'nav-item-active' : 'nav-item'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
{t('nav.profile')}
|
{t('nav.profile')}
|
||||||
@@ -460,7 +677,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
logout()
|
logout()
|
||||||
}}
|
}}
|
||||||
className="nav-item w-full text-error-400"
|
className='nav-item w-full text-error-400'
|
||||||
>
|
>
|
||||||
<LogoutIcon />
|
<LogoutIcon />
|
||||||
{t('nav.logout')}
|
{t('nav.logout')}
|
||||||
@@ -472,25 +689,37 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 max-w-6xl w-full mx-auto px-4 sm:px-6 py-6 pb-24 lg:pb-8">
|
<main className='flex-1 max-w-6xl w-full mx-auto px-4 sm:px-6 py-6 pb-24 lg:pb-8'>
|
||||||
<div className="animate-fade-in">
|
<div className='animate-fade-in'>{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Mobile Bottom Navigation - only core items */}
|
{/* Mobile Bottom Navigation - only core items */}
|
||||||
<nav className="bottom-nav lg:hidden">
|
<nav className='bottom-nav lg:hidden'>
|
||||||
<div className="flex justify-around">
|
<div className='flex justify-around'>
|
||||||
{navItems.filter(item =>
|
{navItems
|
||||||
['/', '/subscription', '/balance', '/referral', '/support'].includes(item.path)
|
.filter(item =>
|
||||||
).map((item) => (
|
[
|
||||||
|
'/',
|
||||||
|
'/subscription',
|
||||||
|
'/balance',
|
||||||
|
'/referral',
|
||||||
|
'/support',
|
||||||
|
].includes(item.path)
|
||||||
|
)
|
||||||
|
.map(item => (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
className={isActive(item.path) ? 'bottom-nav-item-active' : 'bottom-nav-item'}
|
className={
|
||||||
|
isActive(item.path)
|
||||||
|
? 'bottom-nav-item-active'
|
||||||
|
: 'bottom-nav-item'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<item.icon />
|
<item.icon />
|
||||||
<span className="text-2xs mt-1 whitespace-nowrap">{item.label}</span>
|
<span className='text-2xs mt-1 whitespace-nowrap'>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -319,7 +319,7 @@
|
|||||||
"copyLink": "Copy Link",
|
"copyLink": "Copy Link",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"shareButton": "Share",
|
"shareButton": "Share",
|
||||||
"shareMessage": "Join me on and earn up to {{percent}}% cashback!",
|
"shareMessage": "Join me on {{botName}} and earn up to {{percent}}% cashback!",
|
||||||
"shareHint": "Share this link with friends. When they sign up and make a purchase, you'll earn {{percent}}% commission!",
|
"shareHint": "Share this link with friends. When they sign up and make a purchase, you'll earn {{percent}}% commission!",
|
||||||
"stats": {
|
"stats": {
|
||||||
"totalReferrals": "Total Referrals",
|
"totalReferrals": "Total Referrals",
|
||||||
|
|||||||
@@ -220,7 +220,7 @@
|
|||||||
"copyLink": "کپی لینک",
|
"copyLink": "کپی لینک",
|
||||||
"copied": "کپی شد!",
|
"copied": "کپی شد!",
|
||||||
"shareButton": "اشتراکگذاری",
|
"shareButton": "اشتراکگذاری",
|
||||||
"shareMessage": "از طریق لینک من به Cabinet بپیوند و تا {{percent}}٪ پاداش بگیر!",
|
"shareMessage": "از طریق لینک من به {{botName}} بپیوند و تا {{percent}}٪ پاداش بگیر!",
|
||||||
"shareHint": "این لینک را با دوستان به اشتراک بگذارید. وقتی ثبت نام کنند و خرید کنند، {{percent}}% کمیسیون دریافت میکنید!",
|
"shareHint": "این لینک را با دوستان به اشتراک بگذارید. وقتی ثبت نام کنند و خرید کنند، {{percent}}% کمیسیون دریافت میکنید!",
|
||||||
"stats": {
|
"stats": {
|
||||||
"totalReferrals": "کل معرفیها",
|
"totalReferrals": "کل معرفیها",
|
||||||
|
|||||||
@@ -319,7 +319,7 @@
|
|||||||
"copyLink": "Копировать ссылку",
|
"copyLink": "Копировать ссылку",
|
||||||
"copied": "Скопировано!",
|
"copied": "Скопировано!",
|
||||||
"shareButton": "Поделиться",
|
"shareButton": "Поделиться",
|
||||||
"shareMessage": "Заходи в Cabinet по моей ссылке и получай до {{percent}}% кешбэка!",
|
"shareMessage": "Заходи в {{botName}} по моей ссылке и получай до {{percent}}% кешбэка!",
|
||||||
"shareHint": "Поделитесь ссылкой с друзьями. Когда они зарегистрируются и оплатят, вы получите {{percent}}% комиссии!",
|
"shareHint": "Поделитесь ссылкой с друзьями. Когда они зарегистрируются и оплатят, вы получите {{percent}}% комиссии!",
|
||||||
"stats": {
|
"stats": {
|
||||||
"totalReferrals": "Всего рефералов",
|
"totalReferrals": "Всего рефералов",
|
||||||
|
|||||||
@@ -220,7 +220,7 @@
|
|||||||
"copyLink": "复制链接",
|
"copyLink": "复制链接",
|
||||||
"copied": "已复制!",
|
"copied": "已复制!",
|
||||||
"shareButton": "分享",
|
"shareButton": "分享",
|
||||||
"shareMessage": "通过我的链接加入 Cabinet,获取高达 {{percent}}% 返现!",
|
"shareMessage": "通过我的链接加入 {{botName}},获取高达 {{percent}}% 返现!",
|
||||||
"shareHint": "分享链接给朋友。当他们注册并付款后,您将获得 {{percent}}% 佣金!",
|
"shareHint": "分享链接给朋友。当他们注册并付款后,您将获得 {{percent}}% 佣金!",
|
||||||
"stats": {
|
"stats": {
|
||||||
"totalReferrals": "总推荐数",
|
"totalReferrals": "总推荐数",
|
||||||
|
|||||||
@@ -2,24 +2,59 @@ import { useState } from 'react'
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { referralApi } from '../api/referral'
|
import { referralApi } from '../api/referral'
|
||||||
|
import { brandingApi } from '../api/branding'
|
||||||
import { useCurrency } from '../hooks/useCurrency'
|
import { useCurrency } from '../hooks/useCurrency'
|
||||||
|
|
||||||
const CopyIcon = () => (
|
const CopyIcon = () => (
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
|
className='w-4 h-4'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const CheckIcon = () => (
|
const CheckIcon = () => (
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
className='w-4 h-4'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M4.5 12.75l6 6 9-13.5'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const ShareIcon = () => (
|
const ShareIcon = () => (
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M7 8l5-5m0 0l5 5m-5-5v12" />
|
className='w-4 h-4'
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 15v3a2 2 0 002 2h12a2 2 0 002-2v-3" />
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M7 8l5-5m0 0l5 5m-5-5v12'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M4 15v3a2 2 0 002 2h12a2 2 0 002-2v-3'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,6 +83,12 @@ export default function Referral() {
|
|||||||
queryFn: () => referralApi.getReferralEarnings({ per_page: 10 }),
|
queryFn: () => referralApi.getReferralEarnings({ per_page: 10 }),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { data: branding } = useQuery({
|
||||||
|
queryKey: ['branding'],
|
||||||
|
queryFn: brandingApi.getBranding,
|
||||||
|
staleTime: 60000,
|
||||||
|
})
|
||||||
|
|
||||||
const copyLink = () => {
|
const copyLink = () => {
|
||||||
if (info?.referral_link) {
|
if (info?.referral_link) {
|
||||||
navigator.clipboard.writeText(info.referral_link)
|
navigator.clipboard.writeText(info.referral_link)
|
||||||
@@ -60,6 +101,7 @@ export default function Referral() {
|
|||||||
if (!info?.referral_link) return
|
if (!info?.referral_link) return
|
||||||
const shareText = t('referral.shareMessage', {
|
const shareText = t('referral.shareMessage', {
|
||||||
percent: info?.commission_percent || 0,
|
percent: info?.commission_percent || 0,
|
||||||
|
botName: branding?.name || import.meta.env.VITE_APP_NAME || 'Cabinet',
|
||||||
})
|
})
|
||||||
|
|
||||||
if (navigator.share) {
|
if (navigator.share) {
|
||||||
@@ -83,50 +125,61 @@ export default function Referral() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-64">
|
<div className='flex items-center justify-center min-h-64'>
|
||||||
<div className="w-10 h-10 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
|
<div className='w-10 h-10 border-2 border-accent-500 border-t-transparent rounded-full animate-spin' />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className='space-y-6'>
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('referral.title')}</h1>
|
<h1 className='text-2xl sm:text-3xl font-bold text-dark-50'>
|
||||||
|
{t('referral.title')}
|
||||||
|
</h1>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
<div className='grid grid-cols-2 sm:grid-cols-3 gap-4'>
|
||||||
<div className="card">
|
<div className='card'>
|
||||||
<div className="text-sm text-dark-400">{t('referral.stats.totalReferrals')}</div>
|
<div className='text-sm text-dark-400'>
|
||||||
<div className="stat-value mt-1">{info?.total_referrals || 0}</div>
|
{t('referral.stats.totalReferrals')}
|
||||||
<div className="text-sm text-dark-500 mt-1">
|
</div>
|
||||||
{info?.active_referrals || 0} {t('referral.stats.activeReferrals').toLowerCase()}
|
<div className='stat-value mt-1'>{info?.total_referrals || 0}</div>
|
||||||
|
<div className='text-sm text-dark-500 mt-1'>
|
||||||
|
{info?.active_referrals || 0}{' '}
|
||||||
|
{t('referral.stats.activeReferrals').toLowerCase()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card">
|
<div className='card'>
|
||||||
<div className="text-sm text-dark-400">{t('referral.stats.totalEarnings')}</div>
|
<div className='text-sm text-dark-400'>
|
||||||
<div className="stat-value text-success-400 mt-1">
|
{t('referral.stats.totalEarnings')}
|
||||||
|
</div>
|
||||||
|
<div className='stat-value text-success-400 mt-1'>
|
||||||
{formatPositive(info?.total_earnings_rubles || 0)}
|
{formatPositive(info?.total_earnings_rubles || 0)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card col-span-2 sm:col-span-1">
|
<div className='card col-span-2 sm:col-span-1'>
|
||||||
<div className="text-sm text-dark-400">{t('referral.stats.commissionRate')}</div>
|
<div className='text-sm text-dark-400'>
|
||||||
<div className="stat-value text-accent-400 mt-1">
|
{t('referral.stats.commissionRate')}
|
||||||
|
</div>
|
||||||
|
<div className='stat-value text-accent-400 mt-1'>
|
||||||
{info?.commission_percent || 0}%
|
{info?.commission_percent || 0}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Referral Link */}
|
{/* Referral Link */}
|
||||||
<div className="card">
|
<div className='card'>
|
||||||
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.yourLink')}</h2>
|
<h2 className='text-lg font-semibold text-dark-100 mb-4'>
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
{t('referral.yourLink')}
|
||||||
|
</h2>
|
||||||
|
<div className='flex flex-col gap-2 sm:flex-row'>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type='text'
|
||||||
readOnly
|
readOnly
|
||||||
value={info?.referral_link || ''}
|
value={info?.referral_link || ''}
|
||||||
className="input flex-1"
|
className='input flex-1'
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className='flex gap-2'>
|
||||||
<button
|
<button
|
||||||
onClick={copyLink}
|
onClick={copyLink}
|
||||||
disabled={!info?.referral_link}
|
disabled={!info?.referral_link}
|
||||||
@@ -135,7 +188,9 @@ export default function Referral() {
|
|||||||
} ${!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''}`}
|
} ${!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
>
|
>
|
||||||
{copied ? <CheckIcon /> : <CopyIcon />}
|
{copied ? <CheckIcon /> : <CopyIcon />}
|
||||||
<span className="ml-2">{copied ? t('referral.copied') : t('referral.copyLink')}</span>
|
<span className='ml-2'>
|
||||||
|
{copied ? t('referral.copied') : t('referral.copyLink')}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={shareLink}
|
onClick={shareLink}
|
||||||
@@ -145,97 +200,136 @@ export default function Referral() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<ShareIcon />
|
<ShareIcon />
|
||||||
<span className="ml-2">{t('referral.shareButton')}</span>
|
<span className='ml-2'>{t('referral.shareButton')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3 text-sm text-dark-500">
|
<p className='mt-3 text-sm text-dark-500'>
|
||||||
{t('referral.shareHint', { percent: info?.commission_percent || 0 })}
|
{t('referral.shareHint', { percent: info?.commission_percent || 0 })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Program Terms */}
|
{/* Program Terms */}
|
||||||
{terms && (
|
{terms && (
|
||||||
<div className="card">
|
<div className='card'>
|
||||||
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.terms.title')}</h2>
|
<h2 className='text-lg font-semibold text-dark-100 mb-4'>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
{t('referral.terms.title')}
|
||||||
<div className="p-3 rounded-xl bg-dark-800/30">
|
</h2>
|
||||||
<div className="text-sm text-dark-500">{t('referral.terms.commission')}</div>
|
<div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
|
||||||
<div className="text-lg font-semibold text-dark-100 mt-1">{terms.commission_percent}%</div>
|
<div className='p-3 rounded-xl bg-dark-800/30'>
|
||||||
|
<div className='text-sm text-dark-500'>
|
||||||
|
{t('referral.terms.commission')}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-xl bg-dark-800/30">
|
<div className='text-lg font-semibold text-dark-100 mt-1'>
|
||||||
<div className="text-sm text-dark-500">{t('referral.terms.minTopup')}</div>
|
{terms.commission_percent}%
|
||||||
<div className="text-lg font-semibold text-dark-100 mt-1">{formatAmount(terms.minimum_topup_rubles)} {currencySymbol}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-xl bg-dark-800/30">
|
|
||||||
<div className="text-sm text-dark-500">{t('referral.terms.newUserBonus')}</div>
|
|
||||||
<div className="text-lg font-semibold text-success-400 mt-1">{formatPositive(terms.first_topup_bonus_rubles)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-xl bg-dark-800/30">
|
<div className='p-3 rounded-xl bg-dark-800/30'>
|
||||||
<div className="text-sm text-dark-500">{t('referral.terms.inviterBonus')}</div>
|
<div className='text-sm text-dark-500'>
|
||||||
<div className="text-lg font-semibold text-success-400 mt-1">{formatPositive(terms.inviter_bonus_rubles)}</div>
|
{t('referral.terms.minTopup')}
|
||||||
|
</div>
|
||||||
|
<div className='text-lg font-semibold text-dark-100 mt-1'>
|
||||||
|
{formatAmount(terms.minimum_topup_rubles)} {currencySymbol}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='p-3 rounded-xl bg-dark-800/30'>
|
||||||
|
<div className='text-sm text-dark-500'>
|
||||||
|
{t('referral.terms.newUserBonus')}
|
||||||
|
</div>
|
||||||
|
<div className='text-lg font-semibold text-success-400 mt-1'>
|
||||||
|
{formatPositive(terms.first_topup_bonus_rubles)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='p-3 rounded-xl bg-dark-800/30'>
|
||||||
|
<div className='text-sm text-dark-500'>
|
||||||
|
{t('referral.terms.inviterBonus')}
|
||||||
|
</div>
|
||||||
|
<div className='text-lg font-semibold text-success-400 mt-1'>
|
||||||
|
{formatPositive(terms.inviter_bonus_rubles)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Referrals List */}
|
{/* Referrals List */}
|
||||||
<div className="card">
|
<div className='card'>
|
||||||
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.yourReferrals')}</h2>
|
<h2 className='text-lg font-semibold text-dark-100 mb-4'>
|
||||||
|
{t('referral.yourReferrals')}
|
||||||
|
</h2>
|
||||||
{referralList?.items && referralList.items.length > 0 ? (
|
{referralList?.items && referralList.items.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className='space-y-3'>
|
||||||
{referralList.items.map((ref) => (
|
{referralList.items.map(ref => (
|
||||||
<div
|
<div
|
||||||
key={ref.id}
|
key={ref.id}
|
||||||
className="flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30"
|
className='flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30'
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-dark-100 font-medium">
|
<div className='text-dark-100 font-medium'>
|
||||||
{ref.first_name || ref.username || `User #${ref.id}`}
|
{ref.first_name || ref.username || `User #${ref.id}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-dark-500 mt-0.5">
|
<div className='text-xs text-dark-500 mt-0.5'>
|
||||||
{new Date(ref.created_at).toLocaleDateString()}
|
{new Date(ref.created_at).toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ref.has_paid ? (
|
{ref.has_paid ? (
|
||||||
<span className="badge-success">{t('referral.status.paid')}</span>
|
<span className='badge-success'>
|
||||||
|
{t('referral.status.paid')}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="badge-neutral">{t('referral.status.pending')}</span>
|
<span className='badge-neutral'>
|
||||||
|
{t('referral.status.pending')}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12">
|
<div className='text-center py-12'>
|
||||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-800 flex items-center justify-center">
|
<div className='w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-800 flex items-center justify-center'>
|
||||||
<svg className="w-8 h-8 text-dark-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
className='w-8 h-8 text-dark-500'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z'
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-dark-400">{t('referral.noReferrals')}</div>
|
<div className='text-dark-400'>{t('referral.noReferrals')}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Earnings History */}
|
{/* Earnings History */}
|
||||||
{earnings?.items && earnings.items.length > 0 && (
|
{earnings?.items && earnings.items.length > 0 && (
|
||||||
<div className="card">
|
<div className='card'>
|
||||||
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.earningsHistory')}</h2>
|
<h2 className='text-lg font-semibold text-dark-100 mb-4'>
|
||||||
<div className="space-y-3">
|
{t('referral.earningsHistory')}
|
||||||
{earnings.items.map((earning) => (
|
</h2>
|
||||||
|
<div className='space-y-3'>
|
||||||
|
{earnings.items.map(earning => (
|
||||||
<div
|
<div
|
||||||
key={earning.id}
|
key={earning.id}
|
||||||
className="flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30"
|
className='flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30'
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-dark-100">
|
<div className='text-dark-100'>
|
||||||
{earning.referral_first_name || earning.referral_username || 'Referral'}
|
{earning.referral_first_name ||
|
||||||
|
earning.referral_username ||
|
||||||
|
'Referral'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-dark-500 mt-0.5">
|
<div className='text-xs text-dark-500 mt-0.5'>
|
||||||
{t(`referral.reasons.${earning.reason}`, earning.reason)} • {new Date(earning.created_at).toLocaleDateString()}
|
{t(`referral.reasons.${earning.reason}`, earning.reason)} •{' '}
|
||||||
|
{new Date(earning.created_at).toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-success-400 font-semibold">
|
<div className='text-success-400 font-semibold'>
|
||||||
{formatPositive(earning.amount_rubles)}
|
{formatPositive(earning.amount_rubles)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user