Унификация UI раздела Партнеры и создание системы документирования

🎨 Унификация UI:
- Полная унификация визуала вкладок Рефералы и Мои контрагенты
- Исправлены React Hooks ошибки в sidebar.tsx
- Убрана лишняя обертка glass-card в partners-dashboard.tsx
- Исправлена цветовая схема (purple → yellow)
- Табличный формат вместо карточного grid-layout
- Компактные блоки статистики (4 метрики в ряд)
- Правильная прозрачность glass-morphism эффектов

📚 Документация:
- Переименован referral-system-rules.md → partners-rules.md
- Детальные UI/UX правила в partners-rules.md
- Правила унификации в visual-design-rules.md
- Обновлен current-session.md
- Создан development-diary.md

🚀 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-11 15:38:23 +03:00
parent 8f7ec70fe6
commit 6b425d075f
27 changed files with 4825 additions and 463 deletions

View File

@ -43,11 +43,11 @@ export function AuthGuard({ children, fallback }: AuthGuardProps) {
)
}
// Если не авторизован, показываем форму авторизации
if (!isAuthenticated) {
// Если не авторизован ИЛИ нет организации (незавершенная регистрация), показываем форму авторизации
if (!isAuthenticated || (isAuthenticated && user && !user.organization)) {
return fallback || <AuthFlow />
}
// Если авторизован, показываем защищенный контент
// Если авторизован И у пользователя есть организация, показываем защищенный контент
return <>{children}</>
}

View File

@ -55,9 +55,26 @@ export function AuthFlow({ partnerCode, referralCode }: AuthFlowProps = {}) {
console.log('🎢 AuthFlow - Полученные props:', { partnerCode, referralCode })
console.log('🎢 AuthFlow - Статус авторизации:', { isAuthenticated, hasUser: !!user })
// Определяем начальный шаг в зависимости от авторизации
const initialStep = isAuthenticated ? 'cabinet-select' : 'phone'
const [step, setStep] = useState<AuthStep>(initialStep)
// Проверяем незавершенную регистрацию: если есть токен, но нет организации - очищаем токен
useEffect(() => {
// Выполняем только на клиенте после гидрации
if (typeof window === 'undefined') return
if (isAuthenticated && user && !user.organization) {
console.log('🧹 AuthFlow - Обнаружена незавершенная регистрация, очищаем токен')
// Очищаем токен и данные пользователя
localStorage.removeItem('authToken')
localStorage.removeItem('userData')
// Перезагружаем страницу чтобы сбросить состояние useAuth
window.location.reload()
return
}
}, [isAuthenticated, user])
// Начинаем всегда с 'phone' для избежания гидрации,
// а затем обновляем в useEffect после загрузки клиента
const [step, setStep] = useState<AuthStep>('phone')
// Определяем тип регистрации на основе параметров
// Только один из них должен быть активен (валидация уже прошла в RegisterPage)
@ -84,9 +101,27 @@ export function AuthFlow({ partnerCode, referralCode }: AuthFlowProps = {}) {
console.log('🎢 AuthFlow - Сохраненные в authData:', {
partnerCode: authData.partnerCode,
referralCode: authData.referralCode
referralCode: authData.referralCode,
})
// Определяем правильный шаг после гидрации
useEffect(() => {
if (typeof window === 'undefined') return // Только на клиенте
// Если у пользователя есть токен и организация - переходим к завершению
if (isAuthenticated && user?.organization) {
setStep('complete')
}
// Если есть токен но нет организации - переходим к выбору кабинета
else if (isAuthenticated && !user?.organization) {
setStep('cabinet-select')
}
// Иначе остаемся на шаге телефона
else {
setStep('phone')
}
}, [isAuthenticated, user])
// Обновляем шаг при изменении статуса авторизации
useEffect(() => {
if (isAuthenticated && step === 'phone') {
@ -231,6 +266,7 @@ export function AuthFlow({ partnerCode, referralCode }: AuthFlowProps = {}) {
return (
<>
{step === 'phone' && (
<PhoneStep
onNext={handlePhoneNext}

View File

@ -2,7 +2,6 @@
import { Package, ShoppingCart, ArrowLeft, Truck, Building2 } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { AuthLayout } from './auth-layout'
@ -87,13 +86,12 @@ export function CabinetSelectStep({ onNext, onBack }: CabinetSelectStepProps) {
<div className="flex flex-wrap gap-1 justify-center">
{cabinet.features.slice(0, 2).map((feature, index) => (
<Badge
<div
key={index}
variant="outline"
className="glass-secondary text-white/60 border-white/20 text-xs px-1 py-0"
className="inline-flex items-center justify-center rounded-md border glass-secondary text-white/60 border-white/20 text-xs px-1 py-0"
>
{feature}
</Badge>
</div>
))}
</div>
</div>

View File

@ -72,7 +72,7 @@ export function ConfirmationStep({ data, onConfirm, onBack }: ConfirmationStepPr
cabinetType: data.cabinetType,
inn: data.inn,
referralCode: data.referralCode,
partnerCode: data.partnerCode
partnerCode: data.partnerCode,
})
try {
@ -84,7 +84,7 @@ export function ConfirmationStep({ data, onConfirm, onBack }: ConfirmationStepPr
) {
console.log('📝 ConfirmationStep - Вызов registerFulfillmentOrganization с кодами:', {
referralCode: data.referralCode,
partnerCode: data.partnerCode
partnerCode: data.partnerCode,
})
result = await registerFulfillmentOrganization(

View File

@ -17,7 +17,7 @@ interface PhoneStepProps {
referrerCode?: string | null
}
export function PhoneStep({ onNext, registrationType, referrerCode }: PhoneStepProps) {
export function PhoneStep({ onNext, registrationType }: PhoneStepProps) {
const [phone, setPhone] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)

View File

@ -90,10 +90,6 @@ declare global {
}
export function Sidebar({ isRootInstance = false }: { isRootInstance?: boolean } = {}) {
// Если уже есть корневой сайдбар и это не корневой экземпляр — не рендерим дубликат
if (typeof window !== 'undefined' && !isRootInstance && (window as any).__SIDEBAR_ROOT_MOUNTED__) {
return null
}
const { user, logout } = useAuth()
const router = useRouter()
const pathname = usePathname()
@ -115,6 +111,11 @@ export function Sidebar({ isRootInstance = false }: { isRootInstance?: boolean }
notifyOnNetworkStatusChange: false,
})
// Если уже есть корневой сайдбар и это не корневой экземпляр — не рендерим дубликат
if (typeof window !== 'undefined' && !isRootInstance && (window as any).__SIDEBAR_ROOT_MOUNTED__) {
return null
}
const conversations = conversationsData?.conversations || []
const incomingRequests = incomingRequestsData?.incomingRequests || []
const totalUnreadCount = conversations.reduce(

View File

@ -17,6 +17,7 @@ import {
X,
Copy,
Gift,
TrendingUp,
} from 'lucide-react'
import React, { useState, useMemo } from 'react'
import { toast } from 'sonner'
@ -37,7 +38,6 @@ import {
} from '@/graphql/queries'
import { OrganizationAvatar } from './organization-avatar'
import { OrganizationCard } from './organization-card'
interface Organization {
id: string
@ -119,7 +119,7 @@ export function MarketCounterparties() {
}
await navigator.clipboard.writeText(partnerLink)
toast.success('Партнерская ссылка скопирована!', {
description: 'Поделитесь ей для прямого делового сотрудничества'
description: 'Поделитесь ей для прямого делового сотрудничества',
})
} catch {
toast.error('Не удалось скопировать ссылку')
@ -321,34 +321,121 @@ export function MarketCounterparties() {
</TabsList>
<TabsContent value="counterparties" className="flex-1 overflow-hidden mt-3 flex flex-col">
{/* Блок с партнерской ссылкой */}
<Card className="glass-card p-4 mb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-purple-500/20 border border-purple-500/30">
<Gift className="h-5 w-5 text-purple-400" />
<div className="h-full flex flex-col space-y-4">
{/* Компактный блок с партнерской ссылкой */}
<Card className="glass-card p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<Gift className="h-4 w-4 text-yellow-400" />
</div>
<h3 className="text-base font-semibold text-white">Партнерская ссылка</h3>
</div>
<div>
<h3 className="text-white font-medium">Пригласить партнера</h3>
<p className="text-white/60 text-sm">Прямое деловое сотрудничество с автоматическим добавлением в партнеры</p>
<div className="text-xs text-white/60">
Прямое деловое сотрудничество с автоматическим добавлением
</div>
</div>
<Button
onClick={copyPartnerLink}
className="glass-button hover:bg-white/20 transition-all duration-200"
>
<Copy className="h-4 w-4 mr-2" />
Копировать ссылку
</Button>
</div>
</Card>
<div className="flex items-center gap-3">
<div className="flex-1 px-3 py-2 glass-input rounded-lg text-white/60 font-mono text-sm truncate">
{partnerLinkData?.myPartnerLink || 'http://localhost:3000/register?partner=LOADING'}
</div>
<Button
size="sm"
onClick={copyPartnerLink}
className="glass-button hover:bg-white/20 transition-all duration-200 px-3"
>
<Copy className="h-4 w-4 mr-1" />
Копировать
</Button>
</div>
</Card>
{/* Компактная панель фильтров */}
<div className="glass-card p-3 mb-3 space-y-3">
<div className="flex flex-col xl:flex-row gap-3">
{/* Поиск */}
<div className="flex-1 min-w-0">
<div className="relative">
{/* Компактная статистика */}
<div className="grid grid-cols-4 gap-3">
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-blue-500/20 border border-blue-500/30">
<Users className="h-4 w-4 text-blue-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Партнеров</p>
<p className="text-xl font-bold text-white">
{counterpartiesLoading ? (
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
) : (
counterparties.length
)}
</p>
</div>
</div>
</Card>
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<ArrowDownCircle className="h-4 w-4 text-yellow-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Заявок</p>
<p className="text-xl font-bold text-white">
{incomingLoading ? (
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
) : (
incomingRequests.length
)}
</p>
</div>
</div>
</Card>
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-green-500/20 border border-green-500/30">
<TrendingUp className="h-4 w-4 text-green-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">За месяц</p>
<p className="text-xl font-bold text-white">
{counterpartiesLoading ? (
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
) : (
counterparties.filter(org => {
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
return new Date(org.createdAt) > monthAgo;
}).length
)}
</p>
</div>
</div>
</Card>
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<ArrowUpCircle className="h-4 w-4 text-yellow-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Исходящих</p>
<p className="text-xl font-bold text-white">
{outgoingLoading ? (
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
) : (
outgoingRequests.length
)}
</p>
</div>
</div>
</Card>
</div>
{/* Компактные фильтры */}
<Card className="glass-card p-3">
<div className="flex flex-col xl:flex-row gap-3">
{/* Поиск */}
<div className="flex-1 min-w-0">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
<GlassInput
placeholder="Поиск..."
@ -356,16 +443,16 @@ export function MarketCounterparties() {
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 h-9"
/>
</div>
</div>
</div>
{/* Фильтры и сортировка */}
<div className="flex gap-2">
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="glass-input text-white border-white/20 h-9 min-w-[120px]">
<Filter className="h-3 w-3 mr-1" />
<SelectValue placeholder="Тип" />
</SelectTrigger>
{/* Фильтры и сортировка */}
<div className="flex gap-2">
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="glass-input text-white border-white/20 h-9 min-w-[120px]">
<Filter className="h-3 w-3 mr-1" />
<SelectValue placeholder="Тип" />
</SelectTrigger>
<SelectContent className="glass-card border-white/20">
<SelectItem value="all">Все</SelectItem>
<SelectItem value="FULFILLMENT">Фулфилмент</SelectItem>
@ -437,102 +524,137 @@ export function MarketCounterparties() {
)
})}
</div>
</div>
</div>
{/* Список контрагентов */}
<div className="flex-1 overflow-auto">
{counterpartiesLoading ? (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Загрузка...</div>
</div>
) : filteredAndSortedCounterparties.length === 0 ? (
<div className="glass-card p-8">
<div className="text-center">
{counterparties.length === 0 ? (
<>
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">У вас пока нет контрагентов</p>
<p className="text-white/40 text-sm mt-2">Перейдите на другие вкладки, чтобы найти партнеров</p>
</>
) : (
<>
<Search className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">Ничего не найдено</p>
<p className="text-white/40 text-sm mt-2">
Попробуйте изменить параметры поиска или фильтрации
</p>
</>
)}
</Card>
{/* Таблица контрагентов */}
<Card className="glass-card flex-1 overflow-hidden">
<div className="h-full overflow-auto">
<div className="p-6 space-y-3">
{/* Заголовок таблицы */}
<div className="p-4 rounded-xl bg-gradient-to-r from-white/5 to-white/10 border border-white/10">
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-white/80">
<div className="col-span-2 flex items-center gap-2">
<Calendar className="h-4 w-4 text-blue-400" />
<span>Дата добавления</span>
</div>
<div className="col-span-3 flex items-center gap-2">
<Building className="h-4 w-4 text-green-400" />
<span>Организация</span>
</div>
<div className="col-span-1 text-center flex items-center justify-center">
<span>Тип</span>
</div>
<div className="col-span-3 flex items-center gap-2">
<Phone className="h-4 w-4 text-purple-400" />
<span>Контакты</span>
</div>
<div className="col-span-2 flex items-center gap-2">
<MapPin className="h-4 w-4 text-orange-400" />
<span>Адрес</span>
</div>
<div className="col-span-1 text-center flex items-center justify-center">
<span>Действия</span>
</div>
</div>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredAndSortedCounterparties.map((organization: Organization) => (
<div key={organization.id} className="glass-card p-4 w-full hover:bg-white/5 transition-colors">
<div className="flex flex-col space-y-4">
<div className="flex items-start space-x-3">
<OrganizationAvatar organization={organization} size="md" />
<div className="flex-1 min-w-0">
<div className="flex flex-col space-y-2 mb-3">
<h4 className="text-white font-medium text-lg leading-tight">
{organization.name || organization.fullName}
</h4>
<div className="flex items-center space-x-2">
<Badge className={getTypeBadgeStyles(organization.type)}>
{getTypeLabel(organization.type)}
</Badge>
</div>
{/* Строки таблицы */}
{counterpartiesLoading ? (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Загрузка...</div>
</div>
) : filteredAndSortedCounterparties.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64">
{counterparties.length === 0 ? (
<>
<Users className="h-12 w-12 text-white/20 mb-2" />
<p className="text-white/60">У вас пока нет контрагентов</p>
<p className="text-white/40 text-sm mt-1">Перейдите на другие вкладки, чтобы найти партнеров</p>
</>
) : (
<>
<Search className="h-12 w-12 text-white/20 mb-2" />
<p className="text-white/60">Ничего не найдено</p>
<p className="text-white/40 text-sm mt-1">
Попробуйте изменить параметры поиска или фильтрации
</p>
</>
)}
</div>
) : (
filteredAndSortedCounterparties.map((organization: Organization) => (
<div key={organization.id} className="p-4 rounded-xl bg-white/5 hover:bg-white/10 transition-all duration-200 border border-white/10">
<div className="grid grid-cols-12 gap-4 items-center">
<div className="col-span-2 text-white/80">
<div className="flex items-center gap-2">
<Calendar className="h-3 w-3 text-white/40" />
<span className="text-sm">{formatDate(organization.createdAt)}</span>
</div>
<div className="space-y-2">
<div className="flex items-center text-white/60 text-sm">
<Building className="h-3 w-3 mr-2 flex-shrink-0" />
<span>ИНН: {organization.inn}</span>
</div>
{organization.address && (
<div className="flex items-start text-white/60 text-sm">
<MapPin className="h-3 w-3 mr-2 mt-0.5 flex-shrink-0" />
<span className="line-clamp-2">{organization.address}</span>
</div>
)}
{organization.phones && organization.phones.length > 0 && (
<div className="flex items-center text-white/60 text-sm">
<Phone className="h-3 w-3 mr-2 flex-shrink-0" />
<span>{organization.phones[0].value}</span>
</div>
)}
{organization.emails && organization.emails.length > 0 && (
<div className="flex items-center text-white/60 text-sm">
<Mail className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">{organization.emails[0].value}</span>
</div>
)}
<div className="flex items-center text-white/40 text-xs pt-2">
<Calendar className="h-3 w-3 mr-2 flex-shrink-0" />
<span>Добавлен {formatDate(organization.createdAt)}</span>
</div>
<div className="col-span-3">
<div className="flex items-center gap-3">
<OrganizationAvatar organization={organization} size="sm" />
<div>
<p className="text-white font-medium text-sm">
{organization.name || organization.fullName}
</p>
<p className="text-white/60 text-xs flex items-center gap-1">
<Building className="h-3 w-3" />
{organization.inn}
</p>
</div>
</div>
</div>
<div className="col-span-1 text-center">
<Badge className={getTypeBadgeStyles(organization.type) + ' text-xs'}>
{getTypeLabel(organization.type)}
</Badge>
</div>
<div className="col-span-3">
<div className="space-y-1">
{organization.phones && organization.phones.length > 0 && (
<div className="flex items-center text-white/60 text-xs">
<Phone className="h-3 w-3 mr-2" />
<span>{organization.phones[0].value}</span>
</div>
)}
{organization.emails && organization.emails.length > 0 && (
<div className="flex items-center text-white/60 text-xs">
<Mail className="h-3 w-3 mr-2" />
<span className="truncate">{organization.emails[0].value}</span>
</div>
)}
{!organization.phones?.length && !organization.emails?.length && (
<span className="text-white/40 text-xs">Нет контактов</span>
)}
</div>
</div>
<div className="col-span-2">
{organization.address ? (
<p className="text-white/60 text-xs line-clamp-2">{organization.address}</p>
) : (
<span className="text-white/40 text-xs">Не указан</span>
)}
</div>
<div className="col-span-1 text-center">
<Button
size="sm"
variant="ghost"
onClick={() => handleRemoveCounterparty(organization.id)}
className="hover:bg-red-500/20 text-white/60 hover:text-red-300 h-8 w-8 p-0"
title="Удалить из контрагентов"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<Button
size="sm"
variant="outline"
onClick={() => handleRemoveCounterparty(organization.id)}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 cursor-pointer w-full"
>
Удалить из контрагентов
</Button>
</div>
</div>
))}
))
)}
</div>
)}
</div>
</Card>
</div>
</TabsContent>

View File

@ -13,6 +13,7 @@ import { MarketFulfillment } from '../market/market-fulfillment'
import { MarketLogistics } from '../market/market-logistics'
import { MarketSellers } from '../market/market-sellers'
import { MarketSuppliers } from '../market/market-suppliers'
import { ReferralsTab } from './referrals-tab'
export function PartnersDashboard() {
@ -85,9 +86,7 @@ export function PartnersDashboard() {
</TabsList>
<TabsContent value="counterparties" className="flex-1 overflow-hidden mt-6">
<Card className="glass-card h-full overflow-hidden p-6">
<MarketCounterparties />
</Card>
<MarketCounterparties />
</TabsContent>
<TabsContent value="fulfillment" className="flex-1 overflow-hidden mt-6">

View File

@ -28,24 +28,27 @@ import { GET_REFERRAL_DASHBOARD_DATA } from '@/graphql/referral-queries'
export function ReferralsTab() {
console.log('🚀 ReferralsTab COMPONENT RENDERED!')
const [searchQuery, setSearchQuery] = useState('')
const [typeFilter, setTypeFilter] = useState<string>('all')
const [sourceFilter, setSourceFilter] = useState<string>('all')
// GraphQL запрос для получения данных
// Основной запрос для получения всех данных рефералов
const { data, loading, error } = useQuery(GET_REFERRAL_DASHBOARD_DATA, {
fetchPolicy: 'cache-and-network',
fetchPolicy: 'network-only', // Принудительно загружаем данные с сервера
errorPolicy: 'all',
})
console.log('🔥 ReferralsTab - useQuery result:', {
loading,
hasData: !!data,
error: error?.message,
data
})
// Отладка для понимания что приходит в data (только в dev режиме)
if (process.env.NODE_ENV === 'development') {
console.log('🔍 ReferralsTab - полные данные:', {
loading,
error: error?.message,
data,
myReferralLink: data?.myReferralLink,
myReferralStats: data?.myReferralStats,
})
}
// Извлекаем данные из GraphQL ответа или используем fallback для разработки
const referralLink = data?.myReferralLink || 'http://localhost:3000/register?ref=LOADING'
@ -121,47 +124,49 @@ export function ReferralsTab() {
const hasActiveFilters = searchQuery || typeFilter !== 'all' || sourceFilter !== 'all'
return (
<div className="h-full flex flex-col space-y-6">
{/* Блок с реферальной ссылкой */}
<Card className="glass-card p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<Gift className="h-5 w-5 text-yellow-400" />
<div className="h-full flex flex-col space-y-4">
{/* Компактный блок с реферальной ссылкой */}
<Card className="glass-card p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<Gift className="h-4 w-4 text-yellow-400" />
</div>
<h3 className="text-base font-semibold text-white">Реферальная ссылка</h3>
</div>
<div className="text-xs text-white/60">
<span className="text-yellow-400 font-medium">100 сфер</span> за регистрацию +
<span className="text-yellow-400 font-medium"> 100 сфер</span> за первую сделку
</div>
<h3 className="text-lg font-semibold text-white">Ваша реферальная ссылка</h3>
</div>
<div className="flex items-center gap-4 mb-4">
<div className="flex-1 px-4 py-3 glass-input rounded-lg text-white/60 font-mono text-sm">
<div className="flex items-center gap-3">
<div className="flex-1 px-3 py-2 glass-input rounded-lg text-white/60 font-mono text-sm truncate">
{referralLink}
</div>
<Button
size="sm"
onClick={copyReferralLink}
className="glass-button hover:bg-white/20 transition-all duration-200"
className="glass-button hover:bg-white/20 transition-all duration-200 px-3"
>
<Copy className="h-4 w-4 mr-2" />
Копировать ссылку
<Copy className="h-4 w-4 mr-1" />
Копировать
</Button>
</div>
<p className="text-sm text-white/60">
Поделитесь ссылкой с партнерами и получайте <span className="text-yellow-400 font-medium">100 сфер </span> за каждую регистрацию.
Также получайте <span className="text-yellow-400 font-medium">50 сфер </span> при одобрении заявок от новых клиентов.
</p>
</Card>
{/* Статистика */}
<div className="grid grid-cols-4 gap-4">
<Card className="glass-card p-4 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-blue-500/20 border border-blue-500/30">
{/* Компактная статистика */}
<div className="grid grid-cols-4 gap-3">
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-blue-500/20 border border-blue-500/30">
<Users className="h-4 w-4 text-blue-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Всего партнеров</p>
<p className="text-2xl font-bold text-white">
<p className="text-xs text-white/60 uppercase tracking-wide">Партнеров</p>
<p className="text-xl font-bold text-white">
{loading ? (
<span className="inline-block h-8 w-12 bg-white/10 rounded animate-pulse" />
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
) : (
stats.totalPartners
)}
@ -170,37 +175,37 @@ export function ReferralsTab() {
</div>
</Card>
<Card className="glass-card p-4 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<Zap className="h-4 w-4 text-yellow-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Сфер заработано</p>
<p className="text-xs text-white/60 uppercase tracking-wide">Заработано</p>
<div className="flex items-center gap-1">
<p className="text-2xl font-bold text-white">
<p className="text-xl font-bold text-white">
{loading ? (
<span className="inline-block h-8 w-12 bg-white/10 rounded animate-pulse" />
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
) : (
stats.totalSpheres
)}
</p>
<Zap className="h-5 w-5 text-yellow-400" />
<Zap className="h-4 w-4 text-yellow-400" />
</div>
</div>
</div>
</Card>
<Card className="glass-card p-4 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-green-500/20 border border-green-500/30">
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-green-500/20 border border-green-500/30">
<TrendingUp className="h-4 w-4 text-green-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Партнеров за месяц</p>
<p className="text-2xl font-bold text-white">
<p className="text-xs text-white/60 uppercase tracking-wide">За месяц</p>
<p className="text-xl font-bold text-white">
{loading ? (
<span className="inline-block h-8 w-12 bg-white/10 rounded animate-pulse" />
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
) : (
stats.monthlyPartners
)}
@ -209,31 +214,31 @@ export function ReferralsTab() {
</div>
</Card>
<Card className="glass-card p-4 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<Zap className="h-4 w-4 text-yellow-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Сфер за месяц</p>
<p className="text-xs text-white/60 uppercase tracking-wide">Сфер/мес</p>
<div className="flex items-center gap-1">
<p className="text-2xl font-bold text-white">
<p className="text-xl font-bold text-white">
{loading ? (
<span className="inline-block h-8 w-12 bg-white/10 rounded animate-pulse" />
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
) : (
stats.monthlySpheres
)}
</p>
<Zap className="h-5 w-5 text-yellow-400" />
<Zap className="h-4 w-4 text-yellow-400" />
</div>
</div>
</div>
</Card>
</div>
{/* Фильтры */}
<Card className="glass-card p-4">
<div className="flex flex-col xl:flex-row gap-4">
{/* Компактные фильтры */}
<Card className="glass-card p-3">
<div className="flex flex-col xl:flex-row gap-3">
{/* Поиск */}
<div className="flex-1">
<div className="relative">
@ -307,7 +312,7 @@ export function ReferralsTab() {
</div>
</Card>
{/* Таблица партнеров */}
{/* Таблица рефералов */}
<Card className="glass-card flex-1 overflow-hidden">
<div className="h-full overflow-auto">
<div className="p-6 space-y-3">

View File

@ -6,7 +6,7 @@ import {
BarChart3,
Eye,
Minimize2,
TrendingUp
TrendingUp,
} from 'lucide-react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
@ -15,7 +15,7 @@ import {
CartesianGrid,
ResponsiveContainer,
XAxis,
YAxis
YAxis,
} from 'recharts'
import { Alert, AlertDescription } from '@/components/ui/alert'

View File

@ -6,7 +6,7 @@ import {
Package,
Plus,
Search,
Trash2
Trash2,
} from 'lucide-react'
import { useState } from 'react'

View File

@ -8,7 +8,7 @@ import {
Plus,
Search,
ShoppingCart,
X
X,
} from 'lucide-react'
import Image from 'next/image'
import { useRouter } from 'next/navigation'