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
|
||||||
|
|||||||
35
README.md
35
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 сети
|
||||||
@@ -194,11 +196,12 @@ services:
|
|||||||
|
|
||||||
networks:
|
networks:
|
||||||
bot_network:
|
bot_network:
|
||||||
external: true # Используем существующую сеть
|
external: true # Используем существующую сеть
|
||||||
name: remnawave-bedolaga-telegram-bot_bot_network # Пример для bedolaga bot
|
name: remnawave-bedolaga-telegram-bot_bot_network # Пример для bedolaga bot
|
||||||
```
|
```
|
||||||
|
|
||||||
**Важно:** Замените имя сети на вашу:
|
**Важно:** Замените имя сети на вашу:
|
||||||
|
|
||||||
- Если у вас 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:
|
||||||
@@ -292,18 +298,18 @@ 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` |
|
||||||
| `VITE_APP_LOGO` | Логотип (короткий текст) | `V` |
|
| `VITE_APP_LOGO` | Логотип (короткий текст) | `V` |
|
||||||
|
|
||||||
### 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 в прокси конфигурации правильное
|
||||||
|
|||||||
@@ -16,13 +16,21 @@ services:
|
|||||||
container_name: cabinet_frontend
|
container_name: cabinet_frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env # Загрузить переменные из .env файла
|
- .env # Загрузить переменные из .env файла
|
||||||
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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1974
src/locales/en.json
1974
src/locales/en.json
File diff suppressed because it is too large
Load Diff
1528
src/locales/fa.json
1528
src/locales/fa.json
File diff suppressed because it is too large
Load Diff
2290
src/locales/ru.json
2290
src/locales/ru.json
File diff suppressed because it is too large
Load Diff
1528
src/locales/zh.json
1528
src/locales/zh.json
File diff suppressed because it is too large
Load Diff
@@ -2,247 +2,341 @@ 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'
|
||||||
</svg>
|
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>
|
||||||
)
|
)
|
||||||
|
|
||||||
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'
|
||||||
</svg>
|
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>
|
||||||
)
|
)
|
||||||
|
|
||||||
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'
|
||||||
</svg>
|
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>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default function Referral() {
|
export default function Referral() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { formatAmount, currencySymbol, formatPositive } = useCurrency()
|
const { formatAmount, currencySymbol, formatPositive } = useCurrency()
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
const { data: info, isLoading } = useQuery({
|
const { data: info, isLoading } = useQuery({
|
||||||
queryKey: ['referral-info'],
|
queryKey: ['referral-info'],
|
||||||
queryFn: referralApi.getReferralInfo,
|
queryFn: referralApi.getReferralInfo,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: terms } = useQuery({
|
const { data: terms } = useQuery({
|
||||||
queryKey: ['referral-terms'],
|
queryKey: ['referral-terms'],
|
||||||
queryFn: referralApi.getReferralTerms,
|
queryFn: referralApi.getReferralTerms,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: referralList } = useQuery({
|
const { data: referralList } = useQuery({
|
||||||
queryKey: ['referral-list'],
|
queryKey: ['referral-list'],
|
||||||
queryFn: () => referralApi.getReferralList({ per_page: 10 }),
|
queryFn: () => referralApi.getReferralList({ per_page: 10 }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: earnings } = useQuery({
|
const { data: earnings } = useQuery({
|
||||||
queryKey: ['referral-earnings'],
|
queryKey: ['referral-earnings'],
|
||||||
queryFn: () => referralApi.getReferralEarnings({ per_page: 10 }),
|
queryFn: () => referralApi.getReferralEarnings({ per_page: 10 }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const copyLink = () => {
|
const { data: branding } = useQuery({
|
||||||
if (info?.referral_link) {
|
queryKey: ['branding'],
|
||||||
navigator.clipboard.writeText(info.referral_link)
|
queryFn: brandingApi.getBranding,
|
||||||
setCopied(true)
|
staleTime: 60000,
|
||||||
setTimeout(() => setCopied(false), 2000)
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shareLink = () => {
|
const copyLink = () => {
|
||||||
if (!info?.referral_link) return
|
if (info?.referral_link) {
|
||||||
const shareText = t('referral.shareMessage', {
|
navigator.clipboard.writeText(info.referral_link)
|
||||||
percent: info?.commission_percent || 0,
|
setCopied(true)
|
||||||
})
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (navigator.share) {
|
const shareLink = () => {
|
||||||
navigator
|
if (!info?.referral_link) return
|
||||||
.share({
|
const shareText = t('referral.shareMessage', {
|
||||||
title: t('referral.title'),
|
percent: info?.commission_percent || 0,
|
||||||
text: shareText,
|
botName: branding?.name || import.meta.env.VITE_APP_NAME || 'Cabinet',
|
||||||
url: info.referral_link,
|
})
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// ignore cancellation errors
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(
|
if (navigator.share) {
|
||||||
info.referral_link
|
navigator
|
||||||
)}&text=${encodeURIComponent(shareText)}`
|
.share({
|
||||||
window.open(telegramUrl, '_blank', 'noopener,noreferrer')
|
title: t('referral.title'),
|
||||||
}
|
text: shareText,
|
||||||
|
url: info.referral_link,
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// ignore cancellation errors
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(
|
||||||
return (
|
info.referral_link
|
||||||
<div className="flex items-center justify-center min-h-64">
|
)}&text=${encodeURIComponent(shareText)}`
|
||||||
<div className="w-10 h-10 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
|
window.open(telegramUrl, '_blank', 'noopener,noreferrer')
|
||||||
</div>
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
if (isLoading) {
|
||||||
<div className="space-y-6">
|
return (
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('referral.title')}</h1>
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* Stats Cards */}
|
return (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
<div className='space-y-6'>
|
||||||
<div className="card">
|
<h1 className='text-2xl sm:text-3xl font-bold text-dark-50'>
|
||||||
<div className="text-sm text-dark-400">{t('referral.stats.totalReferrals')}</div>
|
{t('referral.title')}
|
||||||
<div className="stat-value mt-1">{info?.total_referrals || 0}</div>
|
</h1>
|
||||||
<div className="text-sm text-dark-500 mt-1">
|
|
||||||
{info?.active_referrals || 0} {t('referral.stats.activeReferrals').toLowerCase()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card">
|
|
||||||
<div className="text-sm text-dark-400">{t('referral.stats.totalEarnings')}</div>
|
|
||||||
<div className="stat-value text-success-400 mt-1">
|
|
||||||
{formatPositive(info?.total_earnings_rubles || 0)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card col-span-2 sm:col-span-1">
|
|
||||||
<div className="text-sm text-dark-400">{t('referral.stats.commissionRate')}</div>
|
|
||||||
<div className="stat-value text-accent-400 mt-1">
|
|
||||||
{info?.commission_percent || 0}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Referral Link */}
|
{/* Stats Cards */}
|
||||||
<div className="card">
|
<div className='grid grid-cols-2 sm:grid-cols-3 gap-4'>
|
||||||
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.yourLink')}</h2>
|
<div className='card'>
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
<div className='text-sm text-dark-400'>
|
||||||
<input
|
{t('referral.stats.totalReferrals')}
|
||||||
type="text"
|
</div>
|
||||||
readOnly
|
<div className='stat-value mt-1'>{info?.total_referrals || 0}</div>
|
||||||
value={info?.referral_link || ''}
|
<div className='text-sm text-dark-500 mt-1'>
|
||||||
className="input flex-1"
|
{info?.active_referrals || 0}{' '}
|
||||||
/>
|
{t('referral.stats.activeReferrals').toLowerCase()}
|
||||||
<div className="flex gap-2">
|
</div>
|
||||||
<button
|
</div>
|
||||||
onClick={copyLink}
|
<div className='card'>
|
||||||
disabled={!info?.referral_link}
|
<div className='text-sm text-dark-400'>
|
||||||
className={`btn-primary px-5 ${
|
{t('referral.stats.totalEarnings')}
|
||||||
copied ? 'bg-success-500 hover:bg-success-500' : ''
|
</div>
|
||||||
} ${!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''}`}
|
<div className='stat-value text-success-400 mt-1'>
|
||||||
>
|
{formatPositive(info?.total_earnings_rubles || 0)}
|
||||||
{copied ? <CheckIcon /> : <CopyIcon />}
|
</div>
|
||||||
<span className="ml-2">{copied ? t('referral.copied') : t('referral.copyLink')}</span>
|
</div>
|
||||||
</button>
|
<div className='card col-span-2 sm:col-span-1'>
|
||||||
<button
|
<div className='text-sm text-dark-400'>
|
||||||
onClick={shareLink}
|
{t('referral.stats.commissionRate')}
|
||||||
disabled={!info?.referral_link}
|
</div>
|
||||||
className={`btn-secondary px-5 flex items-center ${
|
<div className='stat-value text-accent-400 mt-1'>
|
||||||
!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''
|
{info?.commission_percent || 0}%
|
||||||
}`}
|
</div>
|
||||||
>
|
</div>
|
||||||
<ShareIcon />
|
</div>
|
||||||
<span className="ml-2">{t('referral.shareButton')}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="mt-3 text-sm text-dark-500">
|
|
||||||
{t('referral.shareHint', { percent: info?.commission_percent || 0 })}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Program Terms */}
|
{/* Referral Link */}
|
||||||
{terms && (
|
<div className='card'>
|
||||||
<div className="card">
|
<h2 className='text-lg font-semibold text-dark-100 mb-4'>
|
||||||
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.terms.title')}</h2>
|
{t('referral.yourLink')}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
</h2>
|
||||||
<div className="p-3 rounded-xl bg-dark-800/30">
|
<div className='flex flex-col gap-2 sm:flex-row'>
|
||||||
<div className="text-sm text-dark-500">{t('referral.terms.commission')}</div>
|
<input
|
||||||
<div className="text-lg font-semibold text-dark-100 mt-1">{terms.commission_percent}%</div>
|
type='text'
|
||||||
</div>
|
readOnly
|
||||||
<div className="p-3 rounded-xl bg-dark-800/30">
|
value={info?.referral_link || ''}
|
||||||
<div className="text-sm text-dark-500">{t('referral.terms.minTopup')}</div>
|
className='input flex-1'
|
||||||
<div className="text-lg font-semibold text-dark-100 mt-1">{formatAmount(terms.minimum_topup_rubles)} {currencySymbol}</div>
|
/>
|
||||||
</div>
|
<div className='flex gap-2'>
|
||||||
<div className="p-3 rounded-xl bg-dark-800/30">
|
<button
|
||||||
<div className="text-sm text-dark-500">{t('referral.terms.newUserBonus')}</div>
|
onClick={copyLink}
|
||||||
<div className="text-lg font-semibold text-success-400 mt-1">{formatPositive(terms.first_topup_bonus_rubles)}</div>
|
disabled={!info?.referral_link}
|
||||||
</div>
|
className={`btn-primary px-5 ${
|
||||||
<div className="p-3 rounded-xl bg-dark-800/30">
|
copied ? 'bg-success-500 hover:bg-success-500' : ''
|
||||||
<div className="text-sm text-dark-500">{t('referral.terms.inviterBonus')}</div>
|
} ${!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
<div className="text-lg font-semibold text-success-400 mt-1">{formatPositive(terms.inviter_bonus_rubles)}</div>
|
>
|
||||||
</div>
|
{copied ? <CheckIcon /> : <CopyIcon />}
|
||||||
</div>
|
<span className='ml-2'>
|
||||||
</div>
|
{copied ? t('referral.copied') : t('referral.copyLink')}
|
||||||
)}
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={shareLink}
|
||||||
|
disabled={!info?.referral_link}
|
||||||
|
className={`btn-secondary px-5 flex items-center ${
|
||||||
|
!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ShareIcon />
|
||||||
|
<span className='ml-2'>{t('referral.shareButton')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className='mt-3 text-sm text-dark-500'>
|
||||||
|
{t('referral.shareHint', { percent: info?.commission_percent || 0 })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Referrals List */}
|
{/* Program Terms */}
|
||||||
<div className="card">
|
{terms && (
|
||||||
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.yourReferrals')}</h2>
|
<div className='card'>
|
||||||
{referralList?.items && referralList.items.length > 0 ? (
|
<h2 className='text-lg font-semibold text-dark-100 mb-4'>
|
||||||
<div className="space-y-3">
|
{t('referral.terms.title')}
|
||||||
{referralList.items.map((ref) => (
|
</h2>
|
||||||
<div
|
<div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
|
||||||
key={ref.id}
|
<div className='p-3 rounded-xl bg-dark-800/30'>
|
||||||
className="flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30"
|
<div className='text-sm text-dark-500'>
|
||||||
>
|
{t('referral.terms.commission')}
|
||||||
<div>
|
</div>
|
||||||
<div className="text-dark-100 font-medium">
|
<div className='text-lg font-semibold text-dark-100 mt-1'>
|
||||||
{ref.first_name || ref.username || `User #${ref.id}`}
|
{terms.commission_percent}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-dark-500 mt-0.5">
|
</div>
|
||||||
{new Date(ref.created_at).toLocaleDateString()}
|
<div className='p-3 rounded-xl bg-dark-800/30'>
|
||||||
</div>
|
<div className='text-sm text-dark-500'>
|
||||||
</div>
|
{t('referral.terms.minTopup')}
|
||||||
{ref.has_paid ? (
|
</div>
|
||||||
<span className="badge-success">{t('referral.status.paid')}</span>
|
<div className='text-lg font-semibold text-dark-100 mt-1'>
|
||||||
) : (
|
{formatAmount(terms.minimum_topup_rubles)} {currencySymbol}
|
||||||
<span className="badge-neutral">{t('referral.status.pending')}</span>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div className='p-3 rounded-xl bg-dark-800/30'>
|
||||||
))}
|
<div className='text-sm text-dark-500'>
|
||||||
</div>
|
{t('referral.terms.newUserBonus')}
|
||||||
) : (
|
</div>
|
||||||
<div className="text-center py-12">
|
<div className='text-lg font-semibold text-success-400 mt-1'>
|
||||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-800 flex items-center justify-center">
|
{formatPositive(terms.first_topup_bonus_rubles)}
|
||||||
<svg className="w-8 h-8 text-dark-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
</div>
|
||||||
<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" />
|
</div>
|
||||||
</svg>
|
<div className='p-3 rounded-xl bg-dark-800/30'>
|
||||||
</div>
|
<div className='text-sm text-dark-500'>
|
||||||
<div className="text-dark-400">{t('referral.noReferrals')}</div>
|
{t('referral.terms.inviterBonus')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className='text-lg font-semibold text-success-400 mt-1'>
|
||||||
</div>
|
{formatPositive(terms.inviter_bonus_rubles)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Earnings History */}
|
{/* Referrals List */}
|
||||||
{earnings?.items && earnings.items.length > 0 && (
|
<div className='card'>
|
||||||
<div className="card">
|
<h2 className='text-lg font-semibold text-dark-100 mb-4'>
|
||||||
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.earningsHistory')}</h2>
|
{t('referral.yourReferrals')}
|
||||||
<div className="space-y-3">
|
</h2>
|
||||||
{earnings.items.map((earning) => (
|
{referralList?.items && referralList.items.length > 0 ? (
|
||||||
<div
|
<div className='space-y-3'>
|
||||||
key={earning.id}
|
{referralList.items.map(ref => (
|
||||||
className="flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30"
|
<div
|
||||||
>
|
key={ref.id}
|
||||||
<div>
|
className='flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30'
|
||||||
<div className="text-dark-100">
|
>
|
||||||
{earning.referral_first_name || earning.referral_username || 'Referral'}
|
<div>
|
||||||
</div>
|
<div className='text-dark-100 font-medium'>
|
||||||
<div className="text-xs text-dark-500 mt-0.5">
|
{ref.first_name || ref.username || `User #${ref.id}`}
|
||||||
{t(`referral.reasons.${earning.reason}`, earning.reason)} • {new Date(earning.created_at).toLocaleDateString()}
|
</div>
|
||||||
</div>
|
<div className='text-xs text-dark-500 mt-0.5'>
|
||||||
</div>
|
{new Date(ref.created_at).toLocaleDateString()}
|
||||||
<div className="text-success-400 font-semibold">
|
</div>
|
||||||
{formatPositive(earning.amount_rubles)}
|
</div>
|
||||||
</div>
|
{ref.has_paid ? (
|
||||||
</div>
|
<span className='badge-success'>
|
||||||
))}
|
{t('referral.status.paid')}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<span className='badge-neutral'>
|
||||||
</div>
|
{t('referral.status.pending')}
|
||||||
)
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<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'>
|
||||||
|
<svg
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
<div className='text-dark-400'>{t('referral.noReferrals')}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Earnings History */}
|
||||||
|
{earnings?.items && earnings.items.length > 0 && (
|
||||||
|
<div className='card'>
|
||||||
|
<h2 className='text-lg font-semibold text-dark-100 mb-4'>
|
||||||
|
{t('referral.earningsHistory')}
|
||||||
|
</h2>
|
||||||
|
<div className='space-y-3'>
|
||||||
|
{earnings.items.map(earning => (
|
||||||
|
<div
|
||||||
|
key={earning.id}
|
||||||
|
className='flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className='text-dark-100'>
|
||||||
|
{earning.referral_first_name ||
|
||||||
|
earning.referral_username ||
|
||||||
|
'Referral'}
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-dark-500 mt-0.5'>
|
||||||
|
{t(`referral.reasons.${earning.reason}`, earning.reason)} •{' '}
|
||||||
|
{new Date(earning.created_at).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='text-success-400 font-semibold'>
|
||||||
|
{formatPositive(earning.amount_rubles)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user