Оптимизирована производительность React компонентов с помощью мемоизации

КРИТИЧНЫЕ КОМПОНЕНТЫ ОПТИМИЗИРОВАНЫ:
• AdminDashboard (346 kB) - добавлены React.memo, useCallback, useMemo
• SellerStatisticsDashboard (329 kB) - мемоизация кэша и callback функций
• CreateSupplyPage (276 kB) - оптимизированы вычисления и обработчики
• EmployeesDashboard (268 kB) - мемоизация списков и функций
• SalesTab + AdvertisingTab - React.memo обертка

ТЕХНИЧЕСКИЕ УЛУЧШЕНИЯ:
 React.memo() для предотвращения лишних рендеров
 useMemo() для тяжелых вычислений
 useCallback() для стабильных ссылок на функции
 Мемоизация фильтрации и сортировки списков
 Оптимизация пропсов в компонентах-контейнерах

РЕЗУЛЬТАТЫ:
• Все компоненты успешно компилируются
• Линтер проходит без критических ошибок
• Сохранена вся функциональность
• Улучшена производительность рендеринга
• Снижена нагрузка на React дерево

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-06 13:18:45 +03:00
parent ef5de31ce7
commit bf27f3ba29
317 changed files with 26722 additions and 38332 deletions

View File

@ -1,16 +1,18 @@
"use client"
'use client'
import { useQuery } from '@apollo/client'
import { Card } from '@/components/ui/card'
import { ShoppingCart, Package } from 'lucide-react'
import { Sidebar } from '@/components/dashboard/sidebar'
import { Card } from '@/components/ui/card'
import { GET_MY_CART } from '@/graphql/queries'
import { CartItems } from './cart-items'
import { CartSummary } from './cart-summary'
import { GET_MY_CART } from '@/graphql/queries'
import { ShoppingCart, Package } from 'lucide-react'
export function CartDashboard() {
const { data, loading, error } = useQuery(GET_MY_CART)
const cart = data?.myCart
const hasItems = cart?.items && cart.items.length > 0
@ -58,10 +60,9 @@ export function CartDashboard() {
<div>
<h1 className="text-2xl font-bold text-white">Корзина</h1>
<p className="text-white/60">
{hasItems
{hasItems
? `${cart.totalItems} товаров на сумму ${new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB' }).format(cart.totalPrice)}`
: 'Ваша корзина пуста'
}
: 'Ваша корзина пуста'}
</p>
</div>
</div>
@ -89,11 +90,9 @@ export function CartDashboard() {
<div className="h-full flex flex-col items-center justify-center text-center">
<Package className="h-24 w-24 text-white/20 mb-6" />
<h2 className="text-xl font-semibold text-white mb-2">Корзина пуста</h2>
<p className="text-white/60 mb-6 max-w-md">
Добавьте товары из маркета, чтобы оформить заказ
</p>
<p className="text-white/60 mb-6 max-w-md">Добавьте товары из маркета, чтобы оформить заказ</p>
<button
onClick={() => window.location.href = '/market'}
onClick={() => (window.location.href = '/market')}
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white px-6 py-3 rounded-lg font-medium transition-all"
>
Перейти в маркет
@ -106,4 +105,4 @@ export function CartDashboard() {
</main>
</div>
)
}
}

View File

@ -1,23 +1,17 @@
"use client"
'use client'
import { useState } from 'react'
import { useMutation } from '@apollo/client'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Trash2,
AlertTriangle,
Package,
Store,
Minus,
Plus
} from 'lucide-react'
import { OrganizationAvatar } from '@/components/market/organization-avatar'
import { Input } from '@/components/ui/input'
import { Trash2, AlertTriangle, Package, Store, Minus, Plus } from 'lucide-react'
import Image from 'next/image'
import { useState } from 'react'
import { toast } from 'sonner'
import { OrganizationAvatar } from '@/components/market/organization-avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { UPDATE_CART_ITEM, REMOVE_FROM_CART, CLEAR_CART } from '@/graphql/mutations'
import { GET_MY_CART } from '@/graphql/queries'
import { toast } from 'sonner'
interface CartItem {
id: string
@ -69,7 +63,7 @@ export function CartItems({ cart }: CartItemsProps) {
onError: (error) => {
toast.error('Ошибка при обновлении заявки')
console.error('Error updating cart item:', error)
}
},
})
const [removeFromCart] = useMutation(REMOVE_FROM_CART, {
@ -84,7 +78,7 @@ export function CartItems({ cart }: CartItemsProps) {
onError: (error) => {
toast.error('Ошибка при удалении заявки')
console.error('Error removing from cart:', error)
}
},
})
const [clearCart] = useMutation(CLEAR_CART, {
@ -95,29 +89,29 @@ export function CartItems({ cart }: CartItemsProps) {
onError: (error) => {
toast.error('Ошибка при очистке заявок')
console.error('Error clearing cart:', error)
}
},
})
const getQuantity = (productId: string, defaultQuantity: number) => quantities[productId] || defaultQuantity
const setQuantity = (productId: string, quantity: number) => {
setQuantities(prev => ({ ...prev, [productId]: quantity }))
setQuantities((prev) => ({ ...prev, [productId]: quantity }))
}
const updateQuantity = async (productId: string, newQuantity: number) => {
if (newQuantity <= 0) return
setLoadingItems(prev => new Set(prev).add(productId))
setLoadingItems((prev) => new Set(prev).add(productId))
try {
await updateCartItem({
variables: {
productId,
quantity: newQuantity
}
quantity: newQuantity,
},
})
} finally {
setLoadingItems(prev => {
setLoadingItems((prev) => {
const newSet = new Set(prev)
newSet.delete(productId)
return newSet
@ -126,14 +120,14 @@ export function CartItems({ cart }: CartItemsProps) {
}
const removeItem = async (productId: string) => {
setLoadingItems(prev => new Set(prev).add(productId))
setLoadingItems((prev) => new Set(prev).add(productId))
try {
await removeFromCart({
variables: { productId }
variables: { productId },
})
} finally {
setLoadingItems(prev => {
setLoadingItems((prev) => {
const newSet = new Set(prev)
newSet.delete(productId)
return newSet
@ -150,33 +144,39 @@ export function CartItems({ cart }: CartItemsProps) {
const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
currency: 'RUB',
}).format(price)
}
const unavailableItems = cart.items.filter(item => !item.isAvailable)
const unavailableItems = cart.items.filter((item) => !item.isAvailable)
// Группировка товаров по поставщикам
const groupedItems = cart.items.reduce((groups, item) => {
const orgId = item.product.organization.id
if (!groups[orgId]) {
groups[orgId] = {
organization: item.product.organization,
items: [],
totalPrice: 0,
totalItems: 0
const groupedItems = cart.items.reduce(
(groups, item) => {
const orgId = item.product.organization.id
if (!groups[orgId]) {
groups[orgId] = {
organization: item.product.organization,
items: [],
totalPrice: 0,
totalItems: 0,
}
}
}
groups[orgId].items.push(item)
groups[orgId].totalPrice += item.totalPrice
groups[orgId].totalItems += item.quantity
return groups
}, {} as Record<string, {
organization: CartItem['product']['organization'],
items: CartItem[],
totalPrice: number,
totalItems: number
}>)
groups[orgId].items.push(item)
groups[orgId].totalPrice += item.totalPrice
groups[orgId].totalItems += item.quantity
return groups
},
{} as Record<
string,
{
organization: CartItem['product']['organization']
items: CartItem[]
totalPrice: number
totalItems: number
}
>,
)
const supplierGroups = Object.values(groupedItems)
@ -202,9 +202,7 @@ export function CartItems({ cart }: CartItemsProps) {
<div className="mb-6 p-4 bg-orange-500/10 border border-orange-500/20 rounded-lg">
<div className="flex items-center space-x-2 text-orange-400">
<AlertTriangle className="h-4 w-4" />
<span className="text-sm font-medium">
{unavailableItems.length} заявок недоступно для оформления
</span>
<span className="text-sm font-medium">{unavailableItems.length} заявок недоступно для оформления</span>
</div>
</div>
)}
@ -218,26 +216,21 @@ export function CartItems({ cart }: CartItemsProps) {
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<OrganizationAvatar organization={group.organization} size="md" />
<div>
<div>
<h3 className="text-lg font-semibold text-white mb-1">
{group.organization.name || group.organization.fullName || `ИНН ${group.organization.inn}`}
</h3>
{group.organization.name || group.organization.fullName || `ИНН ${group.organization.inn}`}
</h3>
<div className="flex items-center space-x-3 text-sm text-white/60">
<span className="flex items-center space-x-1">
<Package className="h-4 w-4" />
<span>{group.totalItems} товаров</span>
</span>
<span></span>
<span className="font-medium text-purple-300">
{formatPrice(group.totalPrice)}
</span>
<span className="font-medium text-purple-300">{formatPrice(group.totalPrice)}</span>
</div>
</div>
</div>
<Badge
variant="secondary"
className="bg-purple-500/20 text-purple-300 px-3 py-1 text-sm font-medium"
>
<Badge variant="secondary" className="bg-purple-500/20 text-purple-300 px-3 py-1 text-sm font-medium">
{group.items.length} заявок
</Badge>
</div>
@ -246,28 +239,30 @@ export function CartItems({ cart }: CartItemsProps) {
{/* Товары этого поставщика */}
<div className="space-y-3">
{group.items.map((item) => {
const isLoading = loadingItems.has(item.product.id)
const mainImage = item.product.images?.[0] || item.product.mainImage
const isLoading = loadingItems.has(item.product.id)
const mainImage = item.product.images?.[0] || item.product.mainImage
return (
<div
key={item.id}
return (
<div
key={item.id}
className={`bg-white/5 backdrop-blur border border-white/10 rounded-xl transition-all hover:bg-white/8 hover:border-white/20 ${
!item.isAvailable ? 'opacity-60' : ''
}`}
>
!item.isAvailable ? 'opacity-60' : ''
}`}
>
{/* Информация о поставщике в карточке товара */}
<div className="px-4 py-2 bg-white/5 border-b border-white/10 rounded-t-xl">
<div className="flex items-center space-x-2 text-xs text-white/60">
<Store className="h-3 w-3" />
<span>Поставщик:</span>
<span className="text-white/80 font-medium">
{item.product.organization.name || item.product.organization.fullName || `ИНН ${item.product.organization.inn}`}
{item.product.organization.name ||
item.product.organization.fullName ||
`ИНН ${item.product.organization.inn}`}
</span>
</div>
</div>
{/* Основное содержимое карточки */}
{/* Основное содержимое карточки */}
<div className="p-4">
<div className="flex items-center space-x-4">
{/* Изображение товара */}
@ -292,18 +287,12 @@ export function CartItems({ cart }: CartItemsProps) {
{/* Информация о товаре */}
<div className="flex-1 min-w-0">
{/* Название и артикул */}
<h4 className="text-sm font-semibold text-white mb-1 line-clamp-1">
{item.product.name}
</h4>
<p className="text-xs text-white/50 mb-2">
Арт: {item.product.article}
</p>
<h4 className="text-sm font-semibold text-white mb-1 line-clamp-1">{item.product.name}</h4>
<p className="text-xs text-white/50 mb-2">Арт: {item.product.article}</p>
{/* Статус и наличие */}
<div className="flex items-center space-x-2">
<span className="text-xs text-white/60">
{item.availableQuantity} шт.
</span>
<span className="text-xs text-white/60">{item.availableQuantity} шт.</span>
{item.isAvailable ? (
<Badge className="bg-green-500/20 text-green-300 text-xs border border-green-500/30 px-1 py-0">
В наличии
@ -318,112 +307,106 @@ export function CartItems({ cart }: CartItemsProps) {
{/* Управление количеством */}
<div className="flex-shrink-0">
<div className="flex items-center space-x-1 mb-2">
<Button
onClick={() => updateQuantity(item.product.id, item.quantity - 1)}
disabled={isLoading || !item.isAvailable || item.quantity <= 1}
size="sm"
className="h-7 w-7 p-0 bg-gradient-to-r from-purple-500/20 to-pink-500/20 hover:from-purple-500/30 hover:to-pink-500/30 border border-purple-500/30 text-purple-300 hover:text-white transition-all"
>
<Minus className="h-3 w-3" />
</Button>
<Input
type="text"
value={getQuantity(item.product.id, item.quantity)}
onChange={(e) => {
const value = e.target.value
// Разрешаем только цифры и пустое поле
if (value === '' || /^\d+$/.test(value)) {
const numValue = value === '' ? 0 : parseInt(value)
// Временно сохраняем даже если 0 или больше лимита для удобства ввода
if (value === '' || (numValue >= 0 && numValue <= 99999)) {
setQuantity(item.product.id, numValue || 1)
}
}
}}
onFocus={(e) => {
// При фокусе выделяем весь текст для удобного редактирования
e.target.select()
}}
onBlur={(e) => {
// При потере фокуса проверяем и корректируем значение, отправляем запрос
let value = parseInt(e.target.value)
if (isNaN(value) || value < 1) {
value = 1
} else if (value > item.availableQuantity) {
value = item.availableQuantity
}
setQuantity(item.product.id, value)
updateQuantity(item.product.id, value)
}}
onKeyDown={(e) => {
// Enter для быстрого обновления
if (e.key === 'Enter') {
let value = parseInt(e.currentTarget.value)
if (isNaN(value) || value < 1) {
value = 1
} else if (value > item.availableQuantity) {
value = item.availableQuantity
}
setQuantity(item.product.id, value)
updateQuantity(item.product.id, value)
e.currentTarget.blur()
}
}}
disabled={isLoading || !item.isAvailable}
className="w-16 h-7 text-xs text-center glass-input text-white border-white/20 bg-white/5"
placeholder="1"
/>
<Button
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
disabled={isLoading || !item.isAvailable || item.quantity >= item.availableQuantity}
size="sm"
className="h-7 w-7 p-0 bg-gradient-to-r from-purple-500/20 to-pink-500/20 hover:from-purple-500/30 hover:to-pink-500/30 border border-purple-500/30 text-purple-300 hover:text-white transition-all"
>
<Plus className="h-3 w-3" />
</Button>
</div>
<div className="text-xs text-white/40 text-center">
до {item.availableQuantity}
<div className="flex items-center space-x-1 mb-2">
<Button
onClick={() => updateQuantity(item.product.id, item.quantity - 1)}
disabled={isLoading || !item.isAvailable || item.quantity <= 1}
size="sm"
className="h-7 w-7 p-0 bg-gradient-to-r from-purple-500/20 to-pink-500/20 hover:from-purple-500/30 hover:to-pink-500/30 border border-purple-500/30 text-purple-300 hover:text-white transition-all"
>
<Minus className="h-3 w-3" />
</Button>
<Input
type="text"
value={getQuantity(item.product.id, item.quantity)}
onChange={(e) => {
const value = e.target.value
// Разрешаем только цифры и пустое поле
if (value === '' || /^\d+$/.test(value)) {
const numValue = value === '' ? 0 : parseInt(value)
// Временно сохраняем даже если 0 или больше лимита для удобства ввода
if (value === '' || (numValue >= 0 && numValue <= 99999)) {
setQuantity(item.product.id, numValue || 1)
}
}
}}
onFocus={(e) => {
// При фокусе выделяем весь текст для удобного редактирования
e.target.select()
}}
onBlur={(e) => {
// При потере фокуса проверяем и корректируем значение, отправляем запрос
let value = parseInt(e.target.value)
if (isNaN(value) || value < 1) {
value = 1
} else if (value > item.availableQuantity) {
value = item.availableQuantity
}
setQuantity(item.product.id, value)
updateQuantity(item.product.id, value)
}}
onKeyDown={(e) => {
// Enter для быстрого обновления
if (e.key === 'Enter') {
let value = parseInt(e.currentTarget.value)
if (isNaN(value) || value < 1) {
value = 1
} else if (value > item.availableQuantity) {
value = item.availableQuantity
}
setQuantity(item.product.id, value)
updateQuantity(item.product.id, value)
e.currentTarget.blur()
}
}}
disabled={isLoading || !item.isAvailable}
className="w-16 h-7 text-xs text-center glass-input text-white border-white/20 bg-white/5"
placeholder="1"
/>
<Button
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
disabled={isLoading || !item.isAvailable || item.quantity >= item.availableQuantity}
size="sm"
className="h-7 w-7 p-0 bg-gradient-to-r from-purple-500/20 to-pink-500/20 hover:from-purple-500/30 hover:to-pink-500/30 border border-purple-500/30 text-purple-300 hover:text-white transition-all"
>
<Plus className="h-3 w-3" />
</Button>
</div>
<div className="text-xs text-white/40 text-center">до {item.availableQuantity}</div>
</div>
{/* Правая часть: цена и кнопка удаления */}
<div className="flex-shrink-0 text-right">
{/* Цена */}
<div className="mb-2">
<div className="text-base font-bold text-purple-300">
{formatPrice(item.totalPrice)}
</div>
<div className="text-xs text-white/50">
{formatPrice(item.product.price)} за шт.
</div>
<div className="text-base font-bold text-purple-300">{formatPrice(item.totalPrice)}</div>
<div className="text-xs text-white/50">{formatPrice(item.product.price)} за шт.</div>
</div>
{/* Кнопка удаления */}
<Button
onClick={() => removeItem(item.product.id)}
disabled={isLoading}
size="sm"
className="h-7 w-7 p-0 bg-gradient-to-r from-red-500/20 to-pink-500/20 hover:from-red-500/30 hover:to-pink-500/30 border border-red-500/30 text-red-400 hover:text-white transition-all"
>
<Trash2 className="h-3 w-3" />
</Button>
{/* Кнопка удаления */}
<Button
onClick={() => removeItem(item.product.id)}
disabled={isLoading}
size="sm"
className="h-7 w-7 p-0 bg-gradient-to-r from-red-500/20 to-pink-500/20 hover:from-red-500/30 hover:to-pink-500/30 border border-red-500/30 text-red-400 hover:text-white transition-all"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
</div>
</div>
)
})}
</div>
)
})}
</div>
</div>
))}
</div>
</div>
)
}
}

View File

@ -1,15 +1,10 @@
"use client"
'use client'
import { ShoppingCart, AlertTriangle, CheckCircle, Info } from 'lucide-react'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import {
ShoppingCart,
AlertTriangle,
CheckCircle,
Info
} from 'lucide-react'
interface CartItem {
id: string
@ -49,35 +44,41 @@ export function CartSummary({ cart }: CartSummaryProps) {
const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
currency: 'RUB',
}).format(price)
}
// Анализ товаров в корзине
const availableItems = cart.items.filter(item => item.isAvailable)
const unavailableItems = cart.items.filter(item => !item.isAvailable)
const availableItems = cart.items.filter((item) => item.isAvailable)
const unavailableItems = cart.items.filter((item) => !item.isAvailable)
const availableTotal = availableItems.reduce((sum, item) => sum + item.totalPrice, 0)
const availableItemsCount = availableItems.reduce((sum, item) => sum + item.quantity, 0)
// Группировка по продавцам
const sellerGroups = availableItems.reduce((groups, item) => {
const sellerId = item.product.organization.id
if (!groups[sellerId]) {
groups[sellerId] = {
organization: item.product.organization,
items: [],
total: 0
const sellerGroups = availableItems.reduce(
(groups, item) => {
const sellerId = item.product.organization.id
if (!groups[sellerId]) {
groups[sellerId] = {
organization: item.product.organization,
items: [],
total: 0,
}
}
}
groups[sellerId].items.push(item)
groups[sellerId].total += item.totalPrice
return groups
}, {} as Record<string, {
organization: CartItem['product']['organization']
items: CartItem[]
total: number
}>)
groups[sellerId].items.push(item)
groups[sellerId].total += item.totalPrice
return groups
},
{} as Record<
string,
{
organization: CartItem['product']['organization']
items: CartItem[]
total: number
}
>,
)
const sellerCount = Object.keys(sellerGroups).length
const canOrder = availableItems.length > 0
@ -129,27 +130,18 @@ export function CartSummary({ cart }: CartSummaryProps) {
<Info className="h-4 w-4" />
<span>Поставщики ({sellerCount}):</span>
</div>
<div className="space-y-2">
{Object.values(sellerGroups).map((group) => (
<div
key={group.organization.id}
className="bg-white/5 p-3 rounded-lg text-xs"
>
<div key={group.organization.id} className="bg-white/5 p-3 rounded-lg text-xs">
<div className="flex justify-between items-start mb-1">
<span className="text-white font-medium line-clamp-1">
{getOrganizationName(group.organization)}
</span>
<span className="text-purple-300 font-medium">
{formatPrice(group.total)}
</span>
</div>
<div className="text-white/50">
ИНН: {group.organization.inn}
</div>
<div className="text-white/50">
Заявок: {group.items.length}
<span className="text-purple-300 font-medium">{formatPrice(group.total)}</span>
</div>
<div className="text-white/50">ИНН: {group.organization.inn}</div>
<div className="text-white/50">Заявок: {group.items.length}</div>
</div>
))}
</div>
@ -188,24 +180,18 @@ export function CartSummary({ cart }: CartSummaryProps) {
{unavailableItems.length > 0 && (
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-3 text-xs">
<div className="text-orange-400 font-medium mb-1">
Внимание!
</div>
<div className="text-orange-400 font-medium mb-1">Внимание!</div>
<div className="text-orange-300">
{unavailableItems.length} заявок недоступно.
Они будут исключены при отправке.
{unavailableItems.length} заявок недоступно. Они будут исключены при отправке.
</div>
</div>
)}
{sellerCount > 1 && (
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 text-xs">
<div className="text-blue-400 font-medium mb-1">
Несколько продавцов
</div>
<div className="text-blue-400 font-medium mb-1">Несколько продавцов</div>
<div className="text-blue-300">
Ваши заявки будут отправлены {sellerCount} разным продавцам
для рассмотрения.
Ваши заявки будут отправлены {sellerCount} разным продавцам для рассмотрения.
</div>
</div>
)}
@ -222,12 +208,11 @@ export function CartSummary({ cart }: CartSummaryProps) {
}`}
>
<ShoppingCart className="h-4 w-4 mr-2" />
{isProcessingOrder
? 'Отправляем заявки...'
: canOrder
{isProcessingOrder
? 'Отправляем заявки...'
: canOrder
? `Отправить заявки • ${formatPrice(availableTotal)}`
: 'Невозможно отправить заявки'
}
: 'Невозможно отправить заявки'}
</Button>
{/* Дополнительная информация */}
@ -239,4 +224,4 @@ export function CartSummary({ cart }: CartSummaryProps) {
</div>
</div>
)
}
}