# ПАТТЕРНЫ РАЗРАБОТКИ КОМПОНЕНТОВ ## 🎯 ОБЗОР АРХИТЕКТУРЫ КОМПОНЕНТОВ SFERA использует современную модульную архитектуру React компонентов с акцентом на переиспользование, типобезопасность и производительность. Все компоненты строятся на базе Radix UI примитивов с кастомными стилями через Tailwind CSS и Class Variance Authority для типизированных вариантов. ### Ключевые принципы: - **Composition over inheritance** - предпочтение композиции наследованию - **Type-safe variants** - типизированные варианты компонентов - **Accessible by default** - доступность из коробки через Radix UI - **Glass morphism design** - современный полупрозрачный дизайн - **Real-time updates** - интеграция с GraphQL подписками ## 🏗️ СТРУКТУРА КОМПОНЕНТОВ ### Основная организация ``` src/components/ ├── ui/ # Базовые UI компоненты (кнопки, формы, модалы) ├── dashboard/ # Компоненты дашборда (сайдбар, навигация) ├── auth/ # Компоненты авторизации ├── employees/ # Управление сотрудниками (19 компонентов) ├── messenger/ # Система сообщений (5 компонентов) ├── cart/ # Корзина покупок (3 компонента) ├── favorites/ # Избранные товары (2 компонента) ├── market/ # B2B маркетплейс (12 компонентов) ├── supplies/ # Система поставок (35+ компонентов) ├── services/ # Услуги фулфилмента (4 компонента) ├── logistics/ # Логистика (1 компонент) └── admin/ # Админ панель (25+ компонентов) ``` ## 🎨 БАЗОВЫЕ UI КОМПОНЕНТЫ ### Button - Типизированная кнопка ```typescript // src/components/ui/button.tsx import { cva, type VariantProps } from 'class-variance-authority' import { Slot } from '@radix-ui/react-slot' const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', destructive: 'bg-destructive text-white shadow-xs hover:bg-destructive/90', outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', glass: 'glass-button text-white font-semibold', // Кастомный glass стиль 'glass-secondary': 'glass-secondary text-white hover:text-white/90' }, size: { default: 'h-9 px-4 py-2', sm: 'h-8 rounded-md gap-1.5 px-3', lg: 'h-10 rounded-md px-6', icon: 'size-9' } }, defaultVariants: { variant: 'default', size: 'default' } } ) function Button({ className, variant, size, asChild = false, ...props }: React.ComponentProps<'button'> & VariantProps & { asChild?: boolean }) { const Comp = asChild ? Slot : 'button' return } ``` **Паттерны использования:** - **CVA (Class Variance Authority)** для типизированных вариантов - **Radix Slot** для полиморфизма компонентов - **Glass стили** для современного дизайна - **Composition pattern** через asChild prop ### Использование Button ```tsx // Базовые варианты // Полиморфизм через asChild // Иконка кнопка ``` ## 🔗 КОМПОЗИЦИЯ И SLOT PATTERN ### Пример составного компонента ```typescript // Компонент карточки с композицией interface CardProps extends React.HTMLAttributes {} const Card = React.forwardRef( ({ className, ...props }, ref) => (
) ) const CardHeader = React.forwardRef( ({ className, ...props }, ref) => (
) ) const CardContent = React.forwardRef( ({ className, ...props }, ref) => (
) ) // Использование

Заголовок карточки

Содержимое карточки
``` ## 🎮 HOOKS ПАТТЕРНЫ ### useAuth - Централизованная авторизация ```typescript // src/hooks/useAuth.ts export const useAuth = (): UseAuthReturn => { const [user, setUser] = useState(null) const [isAuthenticated, setIsAuthenticated] = useState(() => !!getAuthToken()) const [isLoading, setIsLoading] = useState(false) // GraphQL мутации const [sendSmsCodeMutation] = useMutation(SEND_SMS_CODE) const [verifySmsCodeMutation] = useMutation(VERIFY_SMS_CODE) // Проверка авторизации const checkAuth = async () => { const token = getAuthToken() if (!token) { setIsAuthenticated(false) setUser(null) return } try { const { data } = await apolloClient.query({ query: GET_ME, fetchPolicy: 'network-only', }) if (data?.me) { setUser(data.me) setIsAuthenticated(true) setUserData(data.me) } } catch (error) { if (error.graphQLErrors?.some((e) => e.extensions?.code === 'UNAUTHENTICATED')) { logout() } } } // SMS верификация const verifySmsCode = async (phone: string, code: string) => { try { setIsLoading(true) const { data } = await verifySmsCodeMutation({ variables: { phone, code }, }) if (data.verifySmsCode.success && data.verifySmsCode.token) { setAuthToken(data.verifySmsCode.token) setUser(data.verifySmsCode.user) setIsAuthenticated(true) refreshApolloClient() return { success: true, user: data.verifySmsCode.user } } } catch (error) { console.error('SMS verification failed:', error) } finally { setIsLoading(false) } } return { user, isAuthenticated, isLoading, sendSmsCode, verifySmsCode, checkAuth, logout, } } ``` **Ключевые паттерны useAuth:** - **Lazy initialization** - проверка токена при инициализации - **Error handling** - обработка GraphQL ошибок - **Token persistence** - автоматическое сохранение токенов - **Apollo integration** - синхронизация с GraphQL клиентом ### useSidebar - Управление состоянием сайдбара ```typescript // src/hooks/useSidebar.ts export const useSidebar = () => { const [isCollapsed, setIsCollapsed] = useState(() => { if (typeof window === 'undefined') return false return localStorage.getItem('sidebar-collapsed') === 'true' }) const toggleSidebar = () => { const newState = !isCollapsed setIsCollapsed(newState) localStorage.setItem('sidebar-collapsed', newState.toString()) } const getSidebarMargin = () => { return isCollapsed ? 'ml-16' : 'ml-56' } return { isCollapsed, toggleSidebar, getSidebarMargin } } ``` ## 📱 DASHBOARD КОМПОНЕНТЫ ### Sidebar - Адаптивная навигация ```typescript // src/components/dashboard/sidebar.tsx export function Sidebar({ isRootInstance = false }: { isRootInstance?: boolean }) { const { user, logout } = useAuth() const { isCollapsed, toggleSidebar } = useSidebar() const pathname = usePathname() // Real-time данные const { data: conversationsData } = useQuery(GET_CONVERSATIONS) const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT) // Подсчет непрочитанных сообщений const unreadCount = conversationsData?.conversations?.reduce( (sum, conv) => sum + (conv.unreadCount || 0), 0 ) || 0 // Конфигурация навигации по типу организации const getNavigationItems = () => { const baseItems = [ { href: '/dashboard', icon: Home, label: 'Главная', count: 0 } ] switch (user?.organization?.type) { case 'FULFILLMENT': return [ ...baseItems, { href: '/employees', icon: Users, label: 'Сотрудники', count: 0 }, { href: '/supplies', icon: Warehouse, label: 'Поставки', count: pendingData?.pendingSuppliesCount?.supplyOrders || 0 }, { href: '/services', icon: Wrench, label: 'Услуги', count: 0 }, { href: '/messenger', icon: MessageCircle, label: 'Сообщения', count: unreadCount } ] case 'SELLER': return [ ...baseItems, { href: '/my-supplies', icon: Package, label: 'Мои поставки', count: 0 }, { href: '/market', icon: Store, label: 'Маркет', count: 0 }, { href: '/messenger', icon: MessageCircle, label: 'Сообщения', count: unreadCount } ] case 'LOGIST': return [ ...baseItems, { href: '/logistics-requests', icon: Truck, label: 'Заявки', count: pendingData?.pendingSuppliesCount?.logisticsOrders || 0 }, { href: '/messenger', icon: MessageCircle, label: 'Сообщения', count: unreadCount } ] default: return baseItems } } return ( ) } ``` **Паттерны Sidebar:** - **Conditional rendering** по типу организации - **Real-time counters** через GraphQL subscriptions - **Persistent state** через localStorage - **Responsive design** с collapsed состоянием ## 💬 REAL-TIME КОМПОНЕНТЫ ### MessengerChat - Real-time чат ```typescript // src/components/messenger/messenger-chat.tsx export function MessengerChat({ counterparty, onMessagesRead }: MessengerChatProps) { const { user } = useAuth() const [message, setMessage] = useState('') const messagesEndRef = useRef(null) // GraphQL запросы и мутации const { data: messagesData, loading, refetch } = useQuery(GET_MESSAGES, { variables: { counterpartyId: counterparty.id }, fetchPolicy: 'cache-and-network' }) const [sendMessageMutation] = useMutation(SEND_MESSAGE, { onCompleted: () => refetch() }) const [markMessagesAsReadMutation] = useMutation(MARK_MESSAGES_AS_READ) // Real-time подписка useRealtime({ onEvent: (evt) => { if (evt.type !== 'message:new') return const { senderOrgId, receiverOrgId } = evt.payload // Обновляем только если событие относится к этому чату if (senderOrgId === counterparty.id || receiverOrgId === counterparty.id) { refetch() } } }) // Автоскролл к последнему сообщению const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) } useEffect(() => { scrollToBottom() }, [messagesData?.messages]) // Отметка сообщений как прочитанных useEffect(() => { const unreadMessages = messagesData?.messages?.filter( msg => !msg.isRead && msg.senderOrganization?.id === counterparty.id ) if (unreadMessages?.length > 0) { const conversationId = `${user.organization.id}-${counterparty.id}` markMessagesAsReadMutation({ variables: { conversationId } }) } }, [messagesData?.messages]) const handleSendMessage = async () => { if (!message.trim()) return try { await sendMessageMutation({ variables: { receiverOrganizationId: counterparty.id, content: message.trim() } }) setMessage('') } catch (error) { toast.error('Ошибка отправки сообщения') } } return (
{/* Заголовок чата */} {/* История сообщений */}
{messagesData?.messages?.map((message) => ( ))}
{/* Поле ввода */}
) } ``` **Паттерны MessengerChat:** - **Real-time subscriptions** через useRealtime хук - **Optimistic updates** через Apollo Cache - **Auto-scroll** к новым сообщениям - **Message status tracking** (прочитано/не прочитано) - **Multi-media messaging** (текст, голос, файлы) ## 🛒 COMMERCE КОМПОНЕНТЫ ### ProductCard - Интерактивная карточка товара ```typescript // src/components/market/product-card.tsx export function ProductCard({ product, onAddToCart, compact = false }: ProductCardProps) { const [isModalOpen, setIsModalOpen] = useState(false) const [quantity, setQuantity] = useState(1) // GraphQL мутации const [addToCart, { loading: addingToCart }] = useMutation(ADD_TO_CART, { refetchQueries: [{ query: GET_MY_CART }], onCompleted: (data) => { if (data.addToCart.success) { toast.success(data.addToCart.message) onAddToCart?.() } } }) const [addToFavorites] = useMutation(ADD_TO_FAVORITES, { refetchQueries: [{ query: GET_MY_FAVORITES }] }) // Проверка статуса избранного const { data: favoritesData } = useQuery(GET_MY_FAVORITES) const isFavorite = favoritesData?.myFavorites?.some(fav => fav.id === product.id) const handleAddToCart = async () => { try { await addToCart({ variables: { productId: product.id, quantity } }) setQuantity(1) setIsModalOpen(false) } catch (error) { toast.error('Ошибка добавления в корзину') } } const toggleFavorite = async () => { try { if (isFavorite) { await removeFromFavorites({ variables: { productId: product.id } }) } else { await addToFavorites({ variables: { productId: product.id } }) } } catch (error) { toast.error('Ошибка изменения избранного') } } return ( <> {/* Компактная карточка */}
} />

{product.name}

{/* Модальное окно выбора количества */} setIsModalOpen(false)} product={product} quantity={quantity} onQuantityChange={setQuantity} onConfirm={handleAddToCart} isLoading={addingToCart} /> ) } ``` **Паттерны ProductCard:** - **Modal composition** для выбора количества - **Optimistic UI** для добавления в корзину - **Conditional rendering** по статусу товара - **Image fallback** для отсутствующих изображений ## 👥 EMPLOYEE КОМПОНЕНТЫ ### EmployeeCalendar - Табель учета времени ```typescript // src/components/employees/employee-calendar.tsx export function EmployeeCalendar() { const [selectedDate, setSelectedDate] = useState(new Date()) const [selectedEmployees, setSelectedEmployees] = useState([]) const [isBulkEditOpen, setIsBulkEditOpen] = useState(false) // GraphQL запросы const { data: employeesData } = useQuery(GET_MY_EMPLOYEES) const { data: scheduleData, refetch } = useQuery(GET_EMPLOYEE_SCHEDULE, { variables: { year: selectedDate.getFullYear(), month: selectedDate.getMonth() + 1 } }) const [updateSchedule] = useMutation(UPDATE_EMPLOYEE_SCHEDULE, { onCompleted: () => { refetch() toast.success('Расписание обновлено') } }) // Генерация календарной сетки const generateCalendarDays = (year: number, month: number) => { const firstDay = new Date(year, month, 1) const lastDay = new Date(year, month + 1, 0) const daysInMonth = lastDay.getDate() const days = [] for (let day = 1; day <= daysInMonth; day++) { days.push(new Date(year, month, day)) } return days } const calendarDays = generateCalendarDays( selectedDate.getFullYear(), selectedDate.getMonth() ) // Получение статуса дня для сотрудника const getDayStatus = (employeeId: string, date: Date) => { return scheduleData?.employeeSchedule?.find( record => record.employeeId === employeeId && new Date(record.date).toDateString() === date.toDateString() )?.status || 'WORK' } // Обновление статуса дня const updateDayStatus = async (employeeId: string, date: Date, status: ScheduleStatus) => { try { await updateSchedule({ variables: { input: { employeeId, date: date.toISOString(), status } } }) } catch (error) { toast.error('Ошибка обновления расписания') } } // Массовое редактирование const handleBulkEdit = async (status: ScheduleStatus, dates: Date[]) => { try { const updates = selectedEmployees.flatMap(employeeId => dates.map(date => ({ employeeId, date: date.toISOString(), status })) ) await Promise.all( updates.map(update => updateSchedule({ variables: { input: update } })) ) toast.success(`Обновлено ${updates.length} записей`) setIsBulkEditOpen(false) setSelectedEmployees([]) } catch (error) { toast.error('Ошибка массового обновления') } } return (
{/* Заголовок с навигацией по месяцам */} setIsBulkEditOpen(true)} selectedCount={selectedEmployees.length} /> {/* Календарная сетка */}
{/* Колонка сотрудников */}
Сотрудники
{employeesData?.myEmployees?.map((employee) => ( { if (selected) { setSelectedEmployees(prev => [...prev, employee.id]) } else { setSelectedEmployees(prev => prev.filter(id => id !== employee.id)) } }} /> ))}
{/* Календарные дни */}
{/* Заголовки дней */}
{calendarDays.map((date) => ( ))}
{/* Ряды сотрудников */} {employeesData?.myEmployees?.map((employee) => (
{calendarDays.map((date) => ( updateDayStatus(employee.id, date, status)} /> ))}
))}
{/* Легенда статусов */} {/* Модальное окно массового редактирования */} setIsBulkEditOpen(false)} onConfirm={handleBulkEdit} selectedEmployees={selectedEmployees} calendarDays={calendarDays} />
) } ``` **Паттерны EmployeeCalendar:** - **Grid layout** для табличного отображения - **Bulk operations** для массового редактирования - **Real-time updates** через GraphQL refetch - **Complex state management** для выделения элементов - **Modal composition** для дополнительных действий ## 🎯 ADVANCED ПАТТЕРНЫ ### Compound Components Pattern ```typescript // Составной компонент для статистики interface StatsCardProps { children: React.ReactNode } const StatsCard = ({ children }: StatsCardProps) => (
{children}
) const StatsCard.Header = ({ children }: { children: React.ReactNode }) => (
{children}
) const StatsCard.Title = ({ children }: { children: React.ReactNode }) => (

{children}

) const StatsCard.Value = ({ value, change }: { value: string; change?: number }) => (
{value}
{change !== undefined && (
0 ? "text-green-400" : change < 0 ? "text-red-400" : "text-gray-400" )}> {change > 0 ? : change < 0 ? : null} {Math.abs(change)}%
)}
) // Использование Всего заказов ``` ### Render Props Pattern ```typescript // Компонент для работы с состоянием загрузки interface DataFetcherProps { query: DocumentNode variables?: Record children: (data: { data: T | null loading: boolean error: Error | null refetch: () => void }) => React.ReactNode } function DataFetcher({ query, variables, children }: DataFetcherProps) { const { data, loading, error, refetch } = useQuery(query, { variables, errorPolicy: 'all' }) return ( <> {children({ data: data || null, loading, error, refetch })} ) } // Использование {({ data, loading, error, refetch }) => { if (loading) return if (error) return return (
{data.products.map(product => ( ))}
) }}
``` ### Custom Hook Pattern для бизнес-логики ```typescript // Хук для управления корзиной export const useCart = () => { const { data } = useQuery(GET_MY_CART) const [addToCart] = useMutation(ADD_TO_CART, { refetchQueries: [{ query: GET_MY_CART }], }) const [updateCartItem] = useMutation(UPDATE_CART_ITEM, { refetchQueries: [{ query: GET_MY_CART }], }) const [removeFromCart] = useMutation(REMOVE_FROM_CART, { refetchQueries: [{ query: GET_MY_CART }], }) const cart = data?.myCart const itemCount = cart?.totalItems || 0 const totalPrice = cart?.totalPrice || 0 const addItem = async (productId: string, quantity: number = 1) => { try { await addToCart({ variables: { productId, quantity } }) toast.success('Товар добавлен в корзину') } catch (error) { toast.error('Ошибка добавления товара') } } const updateItem = async (productId: string, quantity: number) => { try { await updateCartItem({ variables: { productId, quantity } }) } catch (error) { toast.error('Ошибка обновления корзины') } } const removeItem = async (productId: string) => { try { await removeFromCart({ variables: { productId } }) toast.success('Товар удален из корзины') } catch (error) { toast.error('Ошибка удаления товара') } } return { cart, itemCount, totalPrice, addItem, updateItem, removeItem, } } ``` ## 🎨 СТИЛИЗАЦИЯ И ТЕМИЗАЦИЯ ### Glass Morphism стили ```css /* Основные glass классы */ .glass-card { @apply bg-white/10 backdrop-blur-md border border-white/20; } .glass-card-hover { @apply hover:bg-white/15 hover:border-white/30; } .glass-button { @apply bg-white/10 backdrop-blur-md border border-white/20 hover:bg-white/20; } .glass-input { @apply bg-white/5 border border-white/20 placeholder:text-white/40 text-white; } .glass-secondary { @apply bg-black/20 backdrop-blur-md border border-white/10; } ``` ### Утилиты для состояний ```typescript // Утилиты для работы с CSS классами export const cn = (...classes: (string | undefined | null | false)[]) => { return classes.filter(Boolean).join(' ') } // Утилиты для состояний загрузки export const getLoadingState = (isLoading: boolean) => ({ opacity: isLoading ? 0.6 : 1, pointerEvents: isLoading ? 'none' : 'auto', cursor: isLoading ? 'wait' : 'default', }) // Утилиты для анимаций export const fadeInUp = { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -20 }, } ``` ## 📱 RESPONSIVE DESIGN ### Адаптивные компоненты ```typescript // Хук для отслеживания размера экрана const useBreakpoint = () => { const [breakpoint, setBreakpoint] = useState<'sm' | 'md' | 'lg' | 'xl'>('lg') useEffect(() => { const updateBreakpoint = () => { const width = window.innerWidth if (width < 640) setBreakpoint('sm') else if (width < 768) setBreakpoint('md') else if (width < 1024) setBreakpoint('lg') else setBreakpoint('xl') } updateBreakpoint() window.addEventListener('resize', updateBreakpoint) return () => window.removeEventListener('resize', updateBreakpoint) }, []) return breakpoint } // Адаптивный компонент const ResponsiveGrid = ({ children }: { children: React.ReactNode }) => { const breakpoint = useBreakpoint() const gridCols = { sm: 'grid-cols-1', md: 'grid-cols-2', lg: 'grid-cols-3', xl: 'grid-cols-4' } return (
{children}
) } ``` ## 🔧 PERFORMANCE ПАТТЕРНЫ ### Мемоизация компонентов ```typescript // Мемоизация дорогих вычислений const ExpensiveProductList = memo(({ products }: { products: Product[] }) => { const processedProducts = useMemo(() => { return products .filter(product => product.isActive) .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) .map(product => ({ ...product, formattedPrice: new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB' }).format(product.price) })) }, [products]) return (
{processedProducts.map(product => ( ))}
) }, (prevProps, nextProps) => { // Кастомная функция сравнения return prevProps.products.length === nextProps.products.length && prevProps.products.every((prod, index) => prod.id === nextProps.products[index].id) }) ``` ### Lazy Loading ```typescript // Ленивая загрузка компонентов const LazyEmployeeCalendar = lazy(() => import('./employee-calendar')) const LazyMessengerChat = lazy(() => import('./messenger-chat')) // Обертка с Suspense const LazyComponent = ({ component: Component, ...props }) => ( }> ) ``` ## 🧪 TESTING ПАТТЕРНЫ ### Тестирование компонентов ```typescript // Паттерн для тестирования с Apollo и Auth const renderWithProviders = (component: React.ReactElement) => { const mockClient = new MockedProvider({ mocks: [ { request: { query: GET_ME }, result: { data: { me: mockUser } } } ] }) return render( {component} ) } // Тест компонента describe('ProductCard', () => { it('should render product information', () => { renderWithProviders() expect(screen.getByText(mockProduct.name)).toBeInTheDocument() expect(screen.getByText(mockProduct.price)).toBeInTheDocument() }) it('should handle add to cart', async () => { const onAddToCart = jest.fn() renderWithProviders() const addButton = screen.getByRole('button', { name: /добавить/i }) fireEvent.click(addButton) await waitFor(() => { expect(onAddToCart).toHaveBeenCalled() }) }) }) ``` --- _Паттерны разработки компонентов обновлены на основе анализа 120+ React компонентов_ _React 19.1.0 • TypeScript 5 • Radix UI • Tailwind CSS 4_ _Последнее обновление: 2025-08-21_