я.метрика
This commit is contained in:
parent
74f9885677
commit
80824b0494
BIN
images/broc-fail.png
Normal file
BIN
images/broc-fail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 392 KiB |
67
src/App.tsx
67
src/App.tsx
@ -15,7 +15,8 @@ import CustomSlider from "./components/Fullness/FullnessSlider";
|
||||
import {useGlobalZustandState} from "./hooks/useGlobalZustandState";
|
||||
import {setUsers, useAppDispatch, useAppSelector} from "./store";
|
||||
import {FinitaLaCommedia} from "./components/Cart/FinitaLaCommedia";
|
||||
import ym from "react-yandex-metrika";
|
||||
|
||||
import {reachGoal} from "./hooks/useYM";
|
||||
|
||||
export function App() {
|
||||
const [initialData, setInitialData] = useState<InitialData | null>(null);
|
||||
@ -23,7 +24,7 @@ export function App() {
|
||||
const [isCartComplete, setIsCartComplete] = useState(false);
|
||||
const [showEmptyPopup, setShowEmptyPopup] = useState(false);
|
||||
const [isCartLoading, setIsCartLoading] = useState(true);
|
||||
const [percent, setPercent] = useState(90);
|
||||
const [showLoader, setShowLoader] = useState(false);
|
||||
const [openSection, setOpenSection] = useState<string>('family');
|
||||
const [needRefresh, setNeedRefresh] = useState<boolean>(true);
|
||||
const [isFullnessHighlighted, setIsFullnessHighlighted] = useState<boolean>(false);
|
||||
@ -35,13 +36,14 @@ export function App() {
|
||||
|
||||
|
||||
const loadInitialState = async () => {
|
||||
setShowLoader(true);
|
||||
loadInitialData().then(data => {
|
||||
setInitialData(data);
|
||||
// setPercent(data.percent);
|
||||
dispatch(setUsers(data.users));
|
||||
setProducts(data.cart.flat().map(product => normalizeProductImage(product)));
|
||||
setInitial(false, data.excess || 0, data.precision || 0);
|
||||
setIsCartComplete(false)
|
||||
setShowLoader(false);
|
||||
});
|
||||
};
|
||||
|
||||
@ -62,22 +64,26 @@ export function App() {
|
||||
const days = initialData?.days ?? 7;
|
||||
|
||||
const handleDaysChange = useCallback(async (nextDays: number) => {
|
||||
setShowLoader(true);
|
||||
await setOption('days', nextDays);
|
||||
loadInitialState();
|
||||
}, [loadInitialState]);
|
||||
|
||||
const handleUsersChange = useCallback(async (nextUsers: User[]) => {
|
||||
setShowLoader(true);
|
||||
await setOption('users', nextUsers);
|
||||
loadInitialState();
|
||||
}, [loadInitialState]);
|
||||
|
||||
const handleCategoriesChange = useCallback((nextCategories: string[]) => {
|
||||
setInitialData(current => current ? { ...current, initial_groups: nextCategories } : current);
|
||||
const handleCategoriesChange = useCallback(async (nextCategories: string[]) => {
|
||||
setShowLoader(true);
|
||||
await setOption('initial_groups', nextCategories);
|
||||
loadInitialState();
|
||||
}, [loadInitialState]);
|
||||
|
||||
const handlePercentChange = useCallback((percent: number) => {
|
||||
setInitialData(current => current ? { ...current, percent } : current);
|
||||
const handlePercentChange = useCallback(async (fullness: number) => {
|
||||
setShowLoader(true);
|
||||
await setOption('fullness', fullness);
|
||||
loadInitialState();
|
||||
}, [loadInitialState]);
|
||||
|
||||
@ -90,11 +96,8 @@ export function App() {
|
||||
}, [isCartComplete, isCartLoading, precision, setShowEmptyPopup]);
|
||||
|
||||
useEffect(() => {
|
||||
ym('reachGoal', 'ac_open_settings_tab_' + openSection);
|
||||
reachGoal('ac_open_settings_tab_' + openSection);
|
||||
}, [openSection])
|
||||
useEffect(() => {
|
||||
ym('reachGoal', 'ac_fill_percent_' + percent);
|
||||
}, [percent])
|
||||
|
||||
const pageContent = useMemo(() => {
|
||||
if (!initialData) {
|
||||
@ -103,9 +106,10 @@ export function App() {
|
||||
|
||||
return (
|
||||
<Container $fullHeight={mobileShowFilters}>
|
||||
{showLoader && <Overlay><div /></Overlay>}
|
||||
<LeftColumn $dataShow={!mobileShowFilters}>
|
||||
<TitleBlock>
|
||||
<h1>Хватило</h1>
|
||||
<h1>Кладило</h1>
|
||||
<MobileFilter onClick={() => setMobileShowFilters(true)}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 3V7M19 21V11M12 3V15M12 21V19M5 3V5M5 21V9" stroke="#5EA12D" strokeWidth="2" strokeLinecap="round"/>
|
||||
@ -140,6 +144,7 @@ export function App() {
|
||||
setIsCartLoading(false);
|
||||
}}
|
||||
onLoadingChange={setIsCartLoading}
|
||||
onMistake={() => setNeedRefresh(true)}
|
||||
/>
|
||||
</LeftColumn>
|
||||
|
||||
@ -209,7 +214,7 @@ export function App() {
|
||||
icon={<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 13.23C17 13.23 16.09 12.77 15.182 12.77C13.818 12.77 12 14.615 12 17.385C12 20.154 14.49 22 17 22C19.51 22 22 20.154 22 17.385C22 14.616 20.182 12.769 18.818 12.769C17.909 12.769 17 13.231 17 13.231C17 11.847 17.91 10.001 19.727 10.001M10.655 5C11.551 5 12.278 4.328 12.278 3.5C12.278 2.672 11.55 2 10.655 2H5.245C4.349 2 3.623 2.672 3.623 3.5C3.623 4.328 4.349 5 5.246 5M11.169 4.923C12.125 6.689 12.909 8.283 13.389 10C13.4283 10.14 13.4653 10.281 13.5 10.423M10.428 22H6.328C2.747 22 2 21.31 2 18V13.777C2 10.377 3.098 7.886 4.705 4.915" stroke="#859D96" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>}
|
||||
description={openSection === 'fullness' ? 'Чем меньше значение, тем меньше продуктов и калорий в корзине' : percent + '% — ваша цель. Результат может варьироваться'}
|
||||
description={openSection === 'fullness' ? 'Чем меньше значение, тем меньше продуктов и калорий в корзине' : initialData.fullness + '% — ваша цель. Результат может варьироваться'}
|
||||
isHighlighted={isFullnessHighlighted}
|
||||
isOpen={openSection === 'fullness'}
|
||||
onToggle={() => {
|
||||
@ -219,7 +224,7 @@ export function App() {
|
||||
setOpenSection(openSection === 'fullness' ? '' : 'fullness')
|
||||
}}
|
||||
>
|
||||
<CustomSlider defaultValue={percent} onChange={setPercent} />
|
||||
<CustomSlider defaultValue={initialData.fullness} onChange={handlePercentChange} />
|
||||
<Alert>
|
||||
<div>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@ -262,6 +267,40 @@ export function App() {
|
||||
);
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9;
|
||||
overflow: auto;
|
||||
height: 100vh;
|
||||
|
||||
div {
|
||||
--color-1: #ffa0a0;
|
||||
--size: 1.2px;
|
||||
width: calc(48 * var(--size));
|
||||
height: calc(48 * var(--size));
|
||||
border: calc(5 * var(--size)) solid var(--color-1);
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const LoadingPage = styled.div`
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
|
||||
@ -124,11 +124,20 @@ export function subscribeOnFinish(email: string): Promise<unknown> {
|
||||
});
|
||||
}
|
||||
|
||||
export function subscribeOnFail(email: string, comment: string): Promise<unknown> {
|
||||
return requestWithRefresh(apiUrl('/api/promo/subscribe_fail'), {
|
||||
method: 'POST',
|
||||
headers: jsonHeaders,
|
||||
body: JSON.stringify({ email, comment }),
|
||||
});
|
||||
}
|
||||
|
||||
const fallbackInitialData: InitialData = {
|
||||
cart: [],
|
||||
users: [],
|
||||
initial_groups: [],
|
||||
available_groups: [],
|
||||
fullness: 90,
|
||||
days: 7,
|
||||
excess: 0,
|
||||
precision: 0,
|
||||
|
||||
@ -9,7 +9,8 @@ import { CartItem } from './CartItem';
|
||||
import { CartFooter } from './CartFooter';
|
||||
import { ProductModal } from './ProductModal';
|
||||
import {useGlobalZustandState} from "../../hooks/useGlobalZustandState";
|
||||
import ym from "react-yandex-metrika";
|
||||
import {NotEnoughPopup} from "./NotEnoughPopup";
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
interface CartProps {
|
||||
products: Product[];
|
||||
@ -18,6 +19,7 @@ interface CartProps {
|
||||
isLoading: boolean;
|
||||
onProductsChange: Dispatch<SetStateAction<Product[]>>;
|
||||
onComplete: () => void;
|
||||
onMistake: () => void;
|
||||
onLoadingChange: (isLoading: boolean) => void;
|
||||
}
|
||||
|
||||
@ -29,7 +31,9 @@ export function Cart({
|
||||
onProductsChange,
|
||||
onComplete,
|
||||
onLoadingChange,
|
||||
onMistake,
|
||||
}: CartProps) {
|
||||
const [showNotEnoughPopup, setShowNotEnoughPopup] = useState(false);
|
||||
const [visibleCount, setVisibleCount] = useState(5);
|
||||
const [displayText, setDisplayText] = useState('');
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||
@ -54,10 +58,11 @@ export function Cart({
|
||||
onComplete,
|
||||
onLoadingChange,
|
||||
setDisplayText,
|
||||
onMistake,
|
||||
});
|
||||
|
||||
async function handleReplace(product: Product) {
|
||||
ym('reachGoal', 'ac_product_replace_' + product.name);
|
||||
reachGoal('ac_product_replace_' + product.name);
|
||||
setProcessingProductName(product.name);
|
||||
|
||||
try {
|
||||
@ -69,7 +74,7 @@ export function Cart({
|
||||
}
|
||||
|
||||
async function handleDelete(product: Product) {
|
||||
ym('reachGoal', 'ac_product_delete_' + product.name);
|
||||
reachGoal('ac_product_delete_' + product.name);
|
||||
setProcessingProductName(product.name);
|
||||
|
||||
try {
|
||||
@ -85,7 +90,7 @@ export function Cart({
|
||||
<Summary>
|
||||
<div>
|
||||
<SummaryTitle>
|
||||
Ваша корзина на <Green>{days} {declension(days, ['день', 'дня', 'дней'])}</Green>
|
||||
Ваша закупка на <Green>{days} {declension(days, ['день', 'дня', 'дней'])}</Green>
|
||||
{showFullRefreshButton && <AbsoluteRefreshButton>Начать заново</AbsoluteRefreshButton>}
|
||||
</SummaryTitle>
|
||||
|
||||
@ -122,6 +127,11 @@ export function Cart({
|
||||
onDelete={() => handleDelete(product)}
|
||||
/>
|
||||
))}
|
||||
{products.length === visibleProducts.length && isComplete && !isLoading && <NotEnoughWrapper>
|
||||
<ShowNotEnough type="button" onClick={() => setShowNotEnoughPopup(true)}>
|
||||
Не хватило
|
||||
</ShowNotEnough>
|
||||
</NotEnoughWrapper>}
|
||||
</CartList>
|
||||
|
||||
{products.length === 0 && (
|
||||
@ -155,6 +165,8 @@ export function Cart({
|
||||
setSelectedProduct(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{showNotEnoughPopup && <NotEnoughPopup onClose={() => {setShowNotEnoughPopup(false)}} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -340,3 +352,12 @@ const ShowMore = styled.button`
|
||||
margin-left: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
const NotEnoughWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
const ShowNotEnough = styled(ShowMore)`
|
||||
text-align: center;
|
||||
`;
|
||||
@ -3,7 +3,8 @@ import {BagIcon} from "../Icons/BagIcon";
|
||||
import {useState} from "react";
|
||||
import {OrderPopup} from "./OrderPopup";
|
||||
import {useAppSelector} from "../../store";
|
||||
import ym from "react-yandex-metrika";
|
||||
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
|
||||
interface CartFooterProps {
|
||||
@ -31,10 +32,10 @@ export function CartFooter({ isVisible, totalPrice }: CartFooterProps) {
|
||||
</TotalWrap>
|
||||
|
||||
<CheckoutButton type="button" onClick={() => {
|
||||
ym('reachGoal', 'ac_hvatilo')
|
||||
reachGoal('ac_hvatilo')
|
||||
setShowCompletePopup(true)
|
||||
}}>
|
||||
{users.length > 1 ? 'Нам' : 'Мне'} хватило
|
||||
Хватило
|
||||
</CheckoutButton>
|
||||
{showCompletePopup && <>
|
||||
<OrderPopup setShowCompletePopup={setShowCompletePopup} />
|
||||
|
||||
@ -2,7 +2,7 @@ import {useEffect, useState} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {getProductAmount} from '../../utils/product';
|
||||
import type {Product} from '../../types/cart';
|
||||
import ym from "react-yandex-metrika";
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
interface CartItemProps {
|
||||
product: Product;
|
||||
@ -21,8 +21,10 @@ export function CartItem({
|
||||
}: CartItemProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
ym('reachGoal', 'ac_product_replacement_menu_' + product.name);
|
||||
}, [product.name])
|
||||
if (isMenuOpen) {
|
||||
reachGoal('ac_product_replacement_menu_' + product.name);
|
||||
}
|
||||
}, [product.name, isMenuOpen])
|
||||
|
||||
return (
|
||||
<Item $processing={isProcessing}>
|
||||
@ -148,6 +150,7 @@ const MenuButton = styled.button`
|
||||
`;
|
||||
|
||||
const Overlay = styled.div`
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
|
||||
@ -32,6 +32,7 @@ const Overlay = styled.div`
|
||||
align-items: center;
|
||||
z-index: 4;
|
||||
overflow: auto;
|
||||
height: 100vh;
|
||||
`;
|
||||
const Window = styled.div`
|
||||
background-color: #ffffff;
|
||||
|
||||
161
src/components/Cart/NotEnoughPopup.tsx
Normal file
161
src/components/Cart/NotEnoughPopup.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import styled, {css} from "styled-components";
|
||||
import {TextInput} from "../components/TextInput";
|
||||
import {useState} from "react";
|
||||
import FeedbackTextarea from "../components/Textarea";
|
||||
import {subscribeOnFail} from "../../api/promoApi";
|
||||
|
||||
export const NotEnoughPopup = ({onClose}: any) => {
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [value, setValue] = useState('');
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
return <Overlay>
|
||||
<Window>
|
||||
<CloseButton type="button" aria-label="Закрыть" onClick={() => onClose()}>
|
||||
×
|
||||
</CloseButton>
|
||||
<ModalContent>
|
||||
{sent ? <>
|
||||
<h2>Сообщение отправлено</h2>
|
||||
<V8/>
|
||||
<p>
|
||||
Спасибо, ваша помощь крайне важна для нас, всё записали!
|
||||
</p>
|
||||
<img src="/images/broc-fail.png" alt=""/>
|
||||
</> : <>
|
||||
<h2>Чего-то не хватило?</h2>
|
||||
<V8/>
|
||||
<p>
|
||||
Расскажите, что не так с текущей корзиной — мы постараемся учесть ваши пожелания. За отзыв
|
||||
подарим <strong>3 бесплатные корзины</strong>
|
||||
</p>
|
||||
<V24/>
|
||||
<TextInput placeholder={'Почта'} value={email} onChange={setEmail}/>
|
||||
<V16/>
|
||||
<FeedbackTextarea maxLength={400} value={value} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setValue(e.target.value);
|
||||
}}></FeedbackTextarea>
|
||||
<V16/>
|
||||
<V24/>
|
||||
<ActiveButton onClick={() => subscribeOnFail(email, value).then(() => {setSent(true)})}>Отправить</ActiveButton>
|
||||
</>}
|
||||
</ModalContent>
|
||||
</Window>
|
||||
</Overlay>
|
||||
};
|
||||
|
||||
const V8 = styled.div`height: 8px;`;
|
||||
const V16 = styled.div`height: 16px;`;
|
||||
const V24 = styled.div`height: 24px;`;
|
||||
|
||||
const Overlay = styled.div`
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 4;
|
||||
`;
|
||||
const Window = styled.div`
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
width: 515px;
|
||||
max-width: 100%;
|
||||
padding: 20px 24px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
z-index: 9;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-weight: 300;
|
||||
line-height: 12px;
|
||||
color: #CEDFD0;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #ACBAAE;
|
||||
}
|
||||
|
||||
${({theme}) => theme.media.mobile(css`
|
||||
top: 40px;
|
||||
`)};
|
||||
`;
|
||||
|
||||
const ModalContent = styled.div`
|
||||
padding: 20px;
|
||||
color: #0C3B2E;
|
||||
box-sizing: content-box;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
`;
|
||||
const ButtonBlock = styled.div`
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-direction: column;
|
||||
`;
|
||||
const Button = styled.div`
|
||||
height: 54px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
color: #5EA12D;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #F7FBF6;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: #EEF8EB;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
`;
|
||||
const ActiveButton = styled(Button)`
|
||||
background-color: #5EA12D;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: ${({theme}) => theme.colors.greenHover};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #4A8D19;
|
||||
}
|
||||
`;
|
||||
@ -1,7 +1,8 @@
|
||||
import styled, {css} from "styled-components";
|
||||
import {useState} from "react";
|
||||
import {FinishPopupContent} from "./FinishPopupContent";
|
||||
import ym from "react-yandex-metrika";
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
|
||||
export const OrderPopup = ({setShowCompletePopup}: any) => {
|
||||
const [showFinal, setShowFinal] = useState(false);
|
||||
@ -18,11 +19,11 @@ export const OrderPopup = ({setShowCompletePopup}: any) => {
|
||||
<ServiceFeeDescription>Включена в итоговую стоимость заказа</ServiceFeeDescription>
|
||||
<ButtonBlock>
|
||||
<ActiveButton onClick={() => {
|
||||
ym('reachGoal', 'ac_order_button')
|
||||
reachGoal('ac_order_button')
|
||||
setShowFinal(true)
|
||||
}}>Продолжить</ActiveButton>
|
||||
<Button onClick={() => {
|
||||
ym('reachGoal', 'ac_no_order_button')
|
||||
reachGoal('ac_no_order_button')
|
||||
setShowCompletePopup(false)
|
||||
}}>Вернуться к корзине</Button>
|
||||
</ButtonBlock>
|
||||
@ -32,6 +33,7 @@ export const OrderPopup = ({setShowCompletePopup}: any) => {
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
|
||||
@ -3,7 +3,8 @@ import { formatCurrency } from '../../utils/formatCurrency';
|
||||
import { getLargeProductImage } from '../../utils/product';
|
||||
import type { Product } from '../../types/cart';
|
||||
import {useEffect} from "react";
|
||||
import ym from "react-yandex-metrika";
|
||||
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
interface ProductModalProps {
|
||||
product: Product | null;
|
||||
@ -20,7 +21,7 @@ export function ProductModal({
|
||||
}: ProductModalProps) {
|
||||
useEffect(() => {
|
||||
if (product !== null) {
|
||||
ym('reachGoal', 'ac_open_popup')
|
||||
reachGoal('ac_open_popup')
|
||||
}
|
||||
}, [product]);
|
||||
|
||||
@ -62,6 +63,7 @@ export function ProductModal({
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
|
||||
@ -2,7 +2,8 @@ import { useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { setOption } from '../../api/promoApi';
|
||||
import { useDebounceBatch } from '../../hooks/useDebounceBatch';
|
||||
import ym from "react-yandex-metrika";
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
|
||||
interface CategoriesSectionProps {
|
||||
categories: string[];
|
||||
@ -32,7 +33,7 @@ export function CategoriesSection({
|
||||
const sendUpdatedCategories = useDebounceBatch<string>(async categoryNames => {
|
||||
let nextCategories = [...selectedCategories];
|
||||
|
||||
ym('reachGoal', 'ac_set_categories');
|
||||
reachGoal('ac_set_categories');
|
||||
categoryNames.forEach(categoryName => {
|
||||
if (nextCategories.includes(categoryName)) {
|
||||
nextCategories = nextCategories.filter(item => item !== categoryName);
|
||||
|
||||
@ -2,7 +2,8 @@ import {useEffect, useState} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type {Gender, User} from '../../types/user';
|
||||
import {Tag, TagsInput, TagsWrap} from './TagsInput';
|
||||
import ym from "react-yandex-metrika";
|
||||
|
||||
import {reachGoal} from "../../hooks/useYM";
|
||||
|
||||
interface PersonModalProps {
|
||||
isOpen: boolean;
|
||||
@ -46,7 +47,7 @@ export function PersonModal({
|
||||
if (!isOpen) return;
|
||||
|
||||
if (user) {
|
||||
ym('reachGoal', 'ac_family_edit');
|
||||
reachGoal('ac_family_edit');
|
||||
setForm({
|
||||
name: user.name,
|
||||
gender: user.gender,
|
||||
@ -116,22 +117,32 @@ export function PersonModal({
|
||||
{showExpanded ? (
|
||||
<Content onClick={event => event.stopPropagation()}>
|
||||
<Header>
|
||||
<h2>{showExpanded == 'favorite' ? 'Любимые' : 'Не добавлять'}</h2>
|
||||
<h2>{showExpanded == 'favorite' ? 'Любимые' : 'Исключить'}</h2>
|
||||
<CloseButton type="button" onClick={() => setShowExpanded('')}>
|
||||
×
|
||||
</CloseButton>
|
||||
</Header>
|
||||
<FormGroup>
|
||||
<TagsWrap>
|
||||
{((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).map(tag => (
|
||||
<Tag key={tag} $type={showExpanded}>
|
||||
{tag}
|
||||
<button type="button"
|
||||
onClick={() => (showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
|
||||
{/*{((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).map(tag => (*/}
|
||||
{availableTags.map(tag => (
|
||||
<Tag
|
||||
key={tag}
|
||||
$type={showExpanded}
|
||||
$active={(showExpanded == 'favorite' ? form.favorite : form.avoided).indexOf(tag) > -1}
|
||||
onClick={() => {
|
||||
if ((showExpanded == 'favorite' ? form.favorite : form.avoided).indexOf(tag) > -1) {
|
||||
(showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
|
||||
((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).filter(item => item != tag)
|
||||
)}>
|
||||
×
|
||||
</button>
|
||||
)
|
||||
} else {
|
||||
(showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
|
||||
((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).concat(tag)
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</TagsWrap>
|
||||
@ -208,7 +219,7 @@ export function PersonModal({
|
||||
|
||||
<PrefsGrid>
|
||||
<TagsInput
|
||||
title="Не добавлять"
|
||||
title="Исключить"
|
||||
type="avoid"
|
||||
value={form.avoided}
|
||||
availableTags={availableTags}
|
||||
@ -238,6 +249,7 @@ export function PersonModal({
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
@ -354,6 +366,7 @@ const GenderButton = styled.button<{ $active: boolean }>`
|
||||
cursor: pointer;
|
||||
transition: 0.1s;
|
||||
font-weight: 370;
|
||||
height: 44px;
|
||||
|
||||
&:hover {
|
||||
outline: 1px solid #5EA12D;
|
||||
@ -363,6 +376,7 @@ const GenderButton = styled.button<{ $active: boolean }>`
|
||||
|
||||
const PrefsGrid = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {declension} from "../../utils/declension";
|
||||
|
||||
interface TagsInputProps {
|
||||
title: string;
|
||||
@ -13,88 +13,92 @@ interface TagsInputProps {
|
||||
|
||||
export function TagsInput({
|
||||
title,
|
||||
type,
|
||||
value,
|
||||
availableTags,
|
||||
excludedTags,
|
||||
onChange,
|
||||
onExpand,
|
||||
}: TagsInputProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const matches = useMemo(() => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
|
||||
if (!normalizedQuery) return [];
|
||||
|
||||
const selected = new Set([...value, ...excludedTags]);
|
||||
|
||||
return availableTags.filter(tag =>
|
||||
tag.toLowerCase().includes(normalizedQuery) && !selected.has(tag),
|
||||
);
|
||||
}, [availableTags, excludedTags, query, value]);
|
||||
|
||||
function addTag(tag: string) {
|
||||
if (value.includes(tag)) return;
|
||||
|
||||
onChange([...value, tag]);
|
||||
setQuery('');
|
||||
}
|
||||
|
||||
function removeTag(tag: string) {
|
||||
onChange(value.filter(item => item !== tag));
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Column>
|
||||
<Title>{title}</Title>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={type === 'avoid' ? 'Например: лук' : 'Например: яблоко'}
|
||||
value={query}
|
||||
onChange={event => setQuery(event.target.value)}
|
||||
/>
|
||||
|
||||
{matches.length > 0 && (
|
||||
<Autocomplete>
|
||||
{matches.map(match => (
|
||||
<button
|
||||
key={match}
|
||||
type="button"
|
||||
onClick={() => addTag(match)}
|
||||
>
|
||||
{match}
|
||||
</button>
|
||||
))}
|
||||
</Autocomplete>
|
||||
)}
|
||||
|
||||
<TagsWrap>
|
||||
{value.slice(0, 4).map(tag => (
|
||||
<Tag key={tag} $type={type}>
|
||||
{tag}
|
||||
<button type="button" onClick={() => removeTag(tag)}>
|
||||
×
|
||||
</button>
|
||||
</Tag>
|
||||
))}
|
||||
{value.length > 4 && <TagCollapse $type={'favorite'} $collapsed={true} onClick={() => onExpand()}>
|
||||
+{value.length - 4}
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<polyline points="18 15 12 9 6 15" />
|
||||
</svg>
|
||||
</TagCollapse>}
|
||||
</TagsWrap>
|
||||
<Info>{value.length > 0 ? declension(value.length, ['Выбрана', 'Выбраны', 'Выбраны']) + ': ' + value.length : 'Не выбраны'}</Info>
|
||||
</Column>
|
||||
<AddWrapper>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onExpand}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</AddWrapper>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const Column = styled.div`
|
||||
flex: 1;
|
||||
position: relative;
|
||||
const AddWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const Button = styled.button`
|
||||
padding: 18px 23px;
|
||||
border-radius: 12px;
|
||||
min-width: 160px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${({theme}) => theme.colors.green};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
span {
|
||||
color: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #F7FBF6;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: #EEF8EB;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 20px;
|
||||
margin-left: 8px;
|
||||
font-weight: 300;
|
||||
}
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
flex: 1 1 100%;
|
||||
justify-content: center;
|
||||
`
|
||||
const Column = styled.div`
|
||||
flex: 1 1 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const Info = styled.span`
|
||||
color: ${({theme}) => theme.colors.gray};
|
||||
`;
|
||||
const Title = styled.span`
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
@ -103,73 +107,46 @@ const Title = styled.span`
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
margin-bottom: 6px;
|
||||
height: 44px;
|
||||
|
||||
&::placeholder {
|
||||
color:#A3ADA2;
|
||||
}
|
||||
&:hover {
|
||||
border-color: ${({theme}) => theme.colors.gray};
|
||||
}
|
||||
&:focus {
|
||||
border-color: ${({ theme }) => theme.colors.green};
|
||||
}
|
||||
`;
|
||||
|
||||
const Autocomplete = styled.div`
|
||||
position: absolute;
|
||||
top: 65px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
z-index: 20;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: 0.2s;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TagsWrap = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
export const Tag = styled.div<{ $type: 'avoid' | 'favorite' }>`
|
||||
const palette = {
|
||||
inactive: {
|
||||
avoid: {
|
||||
bg: '#FDE3E3',
|
||||
text: '#0c3b2e',
|
||||
},
|
||||
favorite: {
|
||||
bg: '#F1F9D7',
|
||||
text: '#0c3b2e',
|
||||
}
|
||||
},
|
||||
active: {
|
||||
avoid: {
|
||||
bg: '#9F2518',
|
||||
text: '#FDE3E3',
|
||||
},
|
||||
favorite: {
|
||||
bg: '#5EA12D',
|
||||
text: '#F1F9D7',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const Tag = styled.div<{ $type: 'avoid' | 'favorite', $active: boolean }>`
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
background-color: ${({ $type }) => ($type === 'avoid' ? '#fce4ec' : '#f0f4c3')};
|
||||
color: ${({ $type }) => ($type === 'avoid' ? '#c62828' : '#33691e')};
|
||||
background-color: ${({$type,$active}) => palette[$active ? 'active' : 'inactive'][$type].bg};
|
||||
color: ${({$type,$active}) => palette[$active ? 'active' : 'inactive'][$type].text};
|
||||
|
||||
button {
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
opacity: 0.6;
|
||||
@ -182,26 +159,3 @@ export const Tag = styled.div<{ $type: 'avoid' | 'favorite' }>`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const TagCollapse = styled(Tag)<{$collapsed: boolean}>`
|
||||
border: 1px solid #E7EDE7;
|
||||
background: transparent;
|
||||
color: ${({ theme }) => theme.colors.darkGreen};
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f4c3;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke: ${({ theme }) => theme.colors.darkGreen};
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
transition: transform 0.3s ease;
|
||||
transform: ${({ $collapsed }) => ($collapsed ? 'rotate(180deg)' : 'none')};
|
||||
}
|
||||
`
|
||||
@ -91,7 +91,7 @@ const LabelsContainer = styled.div`
|
||||
const Label = styled.span`
|
||||
position: absolute;
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
color: #0C3B2E;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
|
||||
@ -42,6 +42,7 @@ const Wrapper = styled.div<{ $state: InputState; $allowClear: boolean }>`
|
||||
|
||||
/* Default State */
|
||||
border: 1px solid #e0e0e0;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
@ -49,21 +50,12 @@ const Wrapper = styled.div<{ $state: InputState; $allowClear: boolean }>`
|
||||
/* Success State */
|
||||
${({$state}) => $state === 'success' && css`
|
||||
border: 2px solid #4CAF50;
|
||||
`}
|
||||
|
||||
/* Error State */
|
||||
${({ $state }) => $state === 'error' && css`
|
||||
`} /* Error State */ ${({$state}) => $state === 'error' && css`
|
||||
border: 2px solid #F44336;
|
||||
`}
|
||||
|
||||
/* Disabled State */
|
||||
${({ $state }) => $state === 'disabled' && css`
|
||||
`} /* Disabled State */ ${({$state}) => $state === 'disabled' && css`
|
||||
background-color: #f2f2f2;
|
||||
border: 1px solid #e0e0e0;
|
||||
`}
|
||||
|
||||
/* Padding adjustments for clear button */
|
||||
${({ $allowClear }) => $allowClear && css`
|
||||
`} /* Padding adjustments for clear button */ ${({$allowClear}) => $allowClear && css`
|
||||
input {
|
||||
padding-right: 40px;
|
||||
}
|
||||
@ -77,12 +69,20 @@ const Input = styled.input<{ $state: InputState }>`
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
color: #0C3B2E;
|
||||
|
||||
&::placeholder {
|
||||
color: #A3ADA2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #72b541;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #72b541;
|
||||
}
|
||||
|
||||
${({$state}) => $state === 'disabled' && css`
|
||||
color: #9e9e9e;
|
||||
cursor: not-allowed;
|
||||
@ -101,7 +101,7 @@ const ClearButton = styled.button<{ $state: InputState }>`
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
color: #0C3B2E;
|
||||
}
|
||||
|
||||
${({$state}) => $state === 'disabled' && css`
|
||||
|
||||
82
src/components/components/Textarea.tsx
Normal file
82
src/components/components/Textarea.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
// Стилизованные компоненты
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`;
|
||||
|
||||
const StyledTextarea = styled.textarea`
|
||||
width: 100%;
|
||||
min-height: 224px;
|
||||
/* Добавляем нижний отступ, чтобы текст не заезжал под счетчик */
|
||||
padding: 16px 16px 40px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
color: #0C3B2E;
|
||||
background: white;
|
||||
resize: vertical; /* Разрешаем растягивать по высоте */
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease-in-out;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #a0a0a0;
|
||||
}
|
||||
&:focus {
|
||||
border-color: #72b541;
|
||||
}
|
||||
`;
|
||||
|
||||
const Counter = styled.div`
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
right: 0;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
// Интерфейс пропсов
|
||||
interface FeedbackTextareaProps {
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
interface FeedbackTextareaProps {
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
const FeedbackTextarea: React.FC<FeedbackTextareaProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Напишите, что бы вы хотели поменять',
|
||||
maxLength = 250,
|
||||
}) => {
|
||||
return (
|
||||
<Container>
|
||||
<StyledTextarea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
<Counter>
|
||||
{value.length}/{maxLength}
|
||||
</Counter>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackTextarea;
|
||||
@ -3,12 +3,14 @@ import { getCartUpdates } from '../api/promoApi';
|
||||
import { normalizeProductImage } from '../utils/product';
|
||||
import type { Product } from '../types/cart';
|
||||
import {useGlobalZustandState} from "./useGlobalZustandState";
|
||||
import ym from "react-yandex-metrika";
|
||||
import {reachGoal} from "./useYM";
|
||||
|
||||
|
||||
interface UseCartPollingParams {
|
||||
enabled: boolean;
|
||||
onProductsChange: Dispatch<SetStateAction<Product[]>>;
|
||||
onComplete: () => void;
|
||||
onMistake: () => void;
|
||||
onLoadingChange: (isLoading: boolean) => void;
|
||||
setDisplayText: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
@ -115,6 +117,7 @@ export function useCartPolling({
|
||||
onComplete,
|
||||
onLoadingChange,
|
||||
setDisplayText,
|
||||
onMistake,
|
||||
}: UseCartPollingParams) {
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
const {updateFromResponse, markAsComplete, markAsIncomplete} = useGlobalZustandState((state) => state);
|
||||
@ -161,7 +164,7 @@ export function useCartPolling({
|
||||
missing_scenario_element = -1;
|
||||
|
||||
if (update.status === 'complete') {
|
||||
ym('reachGoal', 'ac_cart_complete');
|
||||
reachGoal('ac_cart_complete');
|
||||
onLoadingChange(false);
|
||||
onComplete();
|
||||
updateFromResponse(true, update.payload?.excess || 0, update.payload?.precision || 0);
|
||||
@ -195,6 +198,10 @@ export function useCartPolling({
|
||||
}
|
||||
});
|
||||
|
||||
if (update.payload?.unique_sku_cnt && update.payload?.unique_sku_cnt !== next.length) {
|
||||
onMistake();
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
|
||||
9
src/hooks/useYM.ts
Normal file
9
src/hooks/useYM.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const reachGoal = (goalName: string) => {
|
||||
// @ts-ignore
|
||||
if (typeof window.yaCounter68914120 !== undefined) {
|
||||
// @ts-ignore
|
||||
return window.yaCounter68914120.reachGoal(goalName);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -16,16 +16,10 @@ if (!root) {
|
||||
|
||||
|
||||
createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyle />
|
||||
<App />
|
||||
<YMInitializer
|
||||
accounts={[68914120]}
|
||||
options={{ clickmap: true, trackLinks: true, accurateTrackBounce: true, webvisor: true }}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
</Provider>,
|
||||
);
|
||||
@ -66,4 +66,9 @@ export const GlobalStyle = createGlobalStyle`
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
*::placeholder {
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
}
|
||||
`;
|
||||
@ -30,5 +30,16 @@
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script nonce="**MY_PRETTY_CSP_NONCE**" type="module" src="/src/main.tsx"></script>
|
||||
<script nonce="**MY_PRETTY_CSP_NONCE**" type="text/javascript">
|
||||
(function(m,e,t,r,i,k,a){
|
||||
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||
m[i].l=1*new Date();
|
||||
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
||||
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js', 'ym');
|
||||
|
||||
ym(68914120, 'init', {webvisor:true, clickmap:true, referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
|
||||
</script>
|
||||
<noscript><div><img src="https://mc.yandex.ru/watch/68914120" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||
</body>
|
||||
</html>
|
||||
@ -6,6 +6,7 @@ export interface InitialData {
|
||||
available_groups: string[];
|
||||
initial_groups: string[];
|
||||
days: number;
|
||||
fullness: number;
|
||||
users: User[];
|
||||
cart: Product[][];
|
||||
excess?: number;
|
||||
@ -21,5 +22,6 @@ export interface CartUpdateResponse {
|
||||
excess?: number;
|
||||
precision?: number;
|
||||
chunk?: Product[];
|
||||
unique_sku_cnt?: number;
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user