2026-08-24 10:07:16 +00:00
|
|
|
|
import {Dispatch, SetStateAction, useMemo, useState} from 'react';
|
|
|
|
|
|
import styled from 'styled-components';
|
|
|
|
|
|
import { deleteProduct, replaceProduct } from '../../api/promoApi';
|
|
|
|
|
|
import { useCartPolling } from '../../hooks/useCartPolling';
|
|
|
|
|
|
import { declension } from '../../utils/declension';
|
|
|
|
|
|
import { formatCurrency } from '../../utils/formatCurrency';
|
|
|
|
|
|
import type { Product } from '../../types/cart';
|
|
|
|
|
|
import { CartItem } from './CartItem';
|
|
|
|
|
|
import { CartFooter } from './CartFooter';
|
|
|
|
|
|
import { ProductModal } from './ProductModal';
|
2026-08-30 11:47:34 +00:00
|
|
|
|
import {useGlobalZustandState} from "../../hooks/useGlobalZustandState";
|
2026-08-24 10:07:16 +00:00
|
|
|
|
|
|
|
|
|
|
interface CartProps {
|
|
|
|
|
|
products: Product[];
|
|
|
|
|
|
days: number;
|
|
|
|
|
|
isComplete: boolean;
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
onProductsChange: Dispatch<SetStateAction<Product[]>>;
|
|
|
|
|
|
onComplete: () => void;
|
|
|
|
|
|
onLoadingChange: (isLoading: boolean) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function Cart({
|
|
|
|
|
|
products,
|
|
|
|
|
|
days,
|
|
|
|
|
|
isComplete,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
onProductsChange,
|
|
|
|
|
|
onComplete,
|
|
|
|
|
|
onLoadingChange,
|
|
|
|
|
|
}: CartProps) {
|
|
|
|
|
|
const [visibleCount, setVisibleCount] = useState(5);
|
|
|
|
|
|
const [displayText, setDisplayText] = useState('');
|
|
|
|
|
|
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
|
|
|
|
|
const [processingProductName, setProcessingProductName] = useState<string | null>(null);
|
|
|
|
|
|
|
2026-08-30 11:54:25 +00:00
|
|
|
|
const {showFullRefreshButton, excess} = useGlobalZustandState((state) => state);
|
2026-08-30 11:47:34 +00:00
|
|
|
|
|
2026-08-24 10:07:16 +00:00
|
|
|
|
const visibleProducts = products.slice(0, visibleCount);
|
|
|
|
|
|
const remaining = products.length - visibleProducts.length;
|
|
|
|
|
|
|
|
|
|
|
|
const totalPrice = useMemo(
|
|
|
|
|
|
() => products.reduce((sum, product) => sum + product.total_price, 0),
|
|
|
|
|
|
[products],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const formattedTotalPrice = formatCurrency(totalPrice);
|
|
|
|
|
|
const stockDays = Math.round(days * excess + days);
|
|
|
|
|
|
|
|
|
|
|
|
useCartPolling({
|
|
|
|
|
|
enabled: !isComplete,
|
|
|
|
|
|
onProductsChange,
|
|
|
|
|
|
onComplete,
|
|
|
|
|
|
onLoadingChange,
|
|
|
|
|
|
setDisplayText,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
async function handleReplace(product: Product) {
|
|
|
|
|
|
setProcessingProductName(product.name);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await replaceProduct(product.name);
|
|
|
|
|
|
onProductsChange(products.filter(item => item.name !== product.name));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setProcessingProductName(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleDelete(product: Product) {
|
|
|
|
|
|
setProcessingProductName(product.name);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await deleteProduct(product.name);
|
|
|
|
|
|
onProductsChange(products.filter(item => item.name !== product.name));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setProcessingProductName(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Summary>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<SummaryTitle>
|
|
|
|
|
|
Ваша корзина на <Green>{days} {declension(days, ['день', 'дня', 'дней'])}</Green>
|
2026-08-30 11:47:34 +00:00
|
|
|
|
{showFullRefreshButton && <AbsoluteRefreshButton>Начать заново</AbsoluteRefreshButton>}
|
2026-08-24 10:07:16 +00:00
|
|
|
|
</SummaryTitle>
|
|
|
|
|
|
|
|
|
|
|
|
<SummaryDetails $hidden={isLoading}>
|
|
|
|
|
|
{products.length > 0 && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<span>
|
|
|
|
|
|
{products.length} {declension(products.length, ['товар', 'товара', 'товаров'])}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<b>•</b>
|
|
|
|
|
|
<span>{formattedTotalPrice}</span>
|
|
|
|
|
|
<SummaryExcess>
|
2026-08-31 08:55:33 +00:00
|
|
|
|
с запасами на {stockDays} {declension(stockDays, ['день', 'дня', 'дней'])}
|
2026-08-24 10:07:16 +00:00
|
|
|
|
</SummaryExcess>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</SummaryDetails>
|
|
|
|
|
|
|
|
|
|
|
|
<Loader $hidden={!isLoading} data-display-text={displayText}>
|
|
|
|
|
|
{displayText}
|
|
|
|
|
|
</Loader>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Summary>
|
|
|
|
|
|
|
|
|
|
|
|
<ScrollWrapper>
|
|
|
|
|
|
<CartList>
|
|
|
|
|
|
{visibleProducts.map(product => (
|
|
|
|
|
|
<CartItem
|
|
|
|
|
|
key={product.name}
|
|
|
|
|
|
product={product}
|
|
|
|
|
|
isProcessing={processingProductName === product.name}
|
|
|
|
|
|
onOpen={() => setSelectedProduct(product)}
|
|
|
|
|
|
onReplace={() => handleReplace(product)}
|
|
|
|
|
|
onDelete={() => handleDelete(product)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</CartList>
|
|
|
|
|
|
|
|
|
|
|
|
{products.length === 0 && (
|
|
|
|
|
|
<EmptyLoader src="/images/empty-cart-loader.svg" alt="сбор корзины" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</ScrollWrapper>
|
|
|
|
|
|
|
|
|
|
|
|
{remaining > 0 && (
|
|
|
|
|
|
<ShowMore type="button" onClick={() => setVisibleCount(products.length)}>
|
|
|
|
|
|
Показать ещё <span>{remaining}</span> товаров
|
2026-08-31 08:55:33 +00:00
|
|
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
|
<path d="M19 9L12 16L5 9" stroke="#5EA12D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
2026-08-24 10:07:16 +00:00
|
|
|
|
</svg>
|
|
|
|
|
|
</ShowMore>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<CartFooter
|
|
|
|
|
|
isVisible={isComplete}
|
|
|
|
|
|
totalPrice={formattedTotalPrice}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<ProductModal
|
|
|
|
|
|
product={selectedProduct}
|
|
|
|
|
|
onClose={() => setSelectedProduct(null)}
|
2026-08-30 11:47:34 +00:00
|
|
|
|
onReplace={(product) => {
|
|
|
|
|
|
handleReplace(product);
|
|
|
|
|
|
setSelectedProduct(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDelete={(product) => {
|
|
|
|
|
|
handleDelete(product);
|
|
|
|
|
|
setSelectedProduct(null);
|
|
|
|
|
|
}}
|
2026-08-24 10:07:16 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 11:47:34 +00:00
|
|
|
|
const AbsoluteRefreshButton = styled.button`
|
|
|
|
|
|
background-color: ${({ theme }) => theme.colors.green};
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 370;
|
|
|
|
|
|
line-height: 20px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover,
|
|
|
|
|
|
&:focus {
|
|
|
|
|
|
background-color: ${({ theme }) => theme.colors.greenHover};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
background-color: #4A8D19;
|
|
|
|
|
|
}
|
|
|
|
|
|
`
|
|
|
|
|
|
|
2026-08-24 10:07:16 +00:00
|
|
|
|
const Summary = styled.div`
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
width: 100%;
|
2026-08-30 11:47:34 +00:00
|
|
|
|
position: relative;
|
2026-08-24 10:07:16 +00:00
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const SummaryTitle = styled.div`
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
line-height: 24px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: ${({ theme }) => theme.colors.darkGreen};
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const Green = styled.span`
|
|
|
|
|
|
color: ${({ theme }) => theme.colors.green};
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const SummaryDetails = styled.div<{ $hidden: boolean }>`
|
|
|
|
|
|
min-height: 24px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: ${({ theme }) => theme.colors.gray};
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: start;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
visibility: ${({ $hidden }) => ($hidden ? 'hidden' : 'visible')};
|
|
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
|
font-weight: 350;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
b {
|
|
|
|
|
|
font-family: initial;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
line-height: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const SummaryExcess = styled.div`
|
|
|
|
|
|
background-color: ${({ theme }) => theme.colors.paleGreen};
|
|
|
|
|
|
padding: 3px 6px 5px;
|
|
|
|
|
|
color: #6D8982;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 350;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const Loader = styled.div<{ $hidden: boolean, "data-display-text": string }>`
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
line-height: 19px;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
color: transparent;
|
|
|
|
|
|
background-clip: text;
|
|
|
|
|
|
position: absolute;
|
2026-08-30 11:47:34 +00:00
|
|
|
|
top:32px;
|
2026-08-24 10:07:16 +00:00
|
|
|
|
visibility: ${({ $hidden }) => ($hidden ? 'hidden' : 'visible')};
|
|
|
|
|
|
|
|
|
|
|
|
&:before,
|
|
|
|
|
|
&:after {
|
|
|
|
|
|
content: '${({"data-display-text": displayText}) => displayText}';
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:before {
|
|
|
|
|
|
background: linear-gradient(79deg, #FFBA00 0%, #FFEC47 31%, #C4E30F 63%, #5EA12D 98%) 100%;
|
|
|
|
|
|
background-clip: text;
|
|
|
|
|
|
animation: opacityAnim 1s ease-in-out 0s infinite alternate;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:after {
|
|
|
|
|
|
background: linear-gradient(79deg, #5EA12D 2%, #C4E30F 38%, #FFEC47 69%, #FFBA00 100%) 100%;
|
|
|
|
|
|
background-clip: text;
|
|
|
|
|
|
animation: opacityAnim 1s ease-in-out -1s infinite alternate;
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const ScrollWrapper = styled.div`
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
|
padding-right: 4px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-track {
|
|
|
|
|
|
background: #f1f1f1;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: #d1d5db;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const CartList = styled.ul`
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const EmptyLoader = styled.img`
|
|
|
|
|
|
animation: spin 1.4s cubic-bezier(0.4, 0.2, 0.35, 0.7) infinite;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
margin-left: -30px;
|
|
|
|
|
|
align-self: center;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const ShowMore = styled.button`
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 20px 0 10px;
|
|
|
|
|
|
color: ${({ theme }) => theme.colors.green};
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
transition: 0.2s;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
svg {
|
2026-08-31 08:55:33 +00:00
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
2026-08-24 10:07:16 +00:00
|
|
|
|
stroke: ${({ theme }) => theme.colors.green};
|
|
|
|
|
|
stroke-width: 2;
|
|
|
|
|
|
stroke-linecap: round;
|
|
|
|
|
|
stroke-linejoin: round;
|
2026-08-31 08:55:33 +00:00
|
|
|
|
margin-left: 6px;
|
2026-08-24 10:07:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
`;
|