я.метрика
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 {useGlobalZustandState} from "./hooks/useGlobalZustandState";
|
||||||
import {setUsers, useAppDispatch, useAppSelector} from "./store";
|
import {setUsers, useAppDispatch, useAppSelector} from "./store";
|
||||||
import {FinitaLaCommedia} from "./components/Cart/FinitaLaCommedia";
|
import {FinitaLaCommedia} from "./components/Cart/FinitaLaCommedia";
|
||||||
import ym from "react-yandex-metrika";
|
|
||||||
|
import {reachGoal} from "./hooks/useYM";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [initialData, setInitialData] = useState<InitialData | null>(null);
|
const [initialData, setInitialData] = useState<InitialData | null>(null);
|
||||||
@ -23,7 +24,7 @@ export function App() {
|
|||||||
const [isCartComplete, setIsCartComplete] = useState(false);
|
const [isCartComplete, setIsCartComplete] = useState(false);
|
||||||
const [showEmptyPopup, setShowEmptyPopup] = useState(false);
|
const [showEmptyPopup, setShowEmptyPopup] = useState(false);
|
||||||
const [isCartLoading, setIsCartLoading] = useState(true);
|
const [isCartLoading, setIsCartLoading] = useState(true);
|
||||||
const [percent, setPercent] = useState(90);
|
const [showLoader, setShowLoader] = useState(false);
|
||||||
const [openSection, setOpenSection] = useState<string>('family');
|
const [openSection, setOpenSection] = useState<string>('family');
|
||||||
const [needRefresh, setNeedRefresh] = useState<boolean>(true);
|
const [needRefresh, setNeedRefresh] = useState<boolean>(true);
|
||||||
const [isFullnessHighlighted, setIsFullnessHighlighted] = useState<boolean>(false);
|
const [isFullnessHighlighted, setIsFullnessHighlighted] = useState<boolean>(false);
|
||||||
@ -35,13 +36,14 @@ export function App() {
|
|||||||
|
|
||||||
|
|
||||||
const loadInitialState = async () => {
|
const loadInitialState = async () => {
|
||||||
|
setShowLoader(true);
|
||||||
loadInitialData().then(data => {
|
loadInitialData().then(data => {
|
||||||
setInitialData(data);
|
setInitialData(data);
|
||||||
// setPercent(data.percent);
|
|
||||||
dispatch(setUsers(data.users));
|
dispatch(setUsers(data.users));
|
||||||
setProducts(data.cart.flat().map(product => normalizeProductImage(product)));
|
setProducts(data.cart.flat().map(product => normalizeProductImage(product)));
|
||||||
setInitial(false, data.excess || 0, data.precision || 0);
|
setInitial(false, data.excess || 0, data.precision || 0);
|
||||||
setIsCartComplete(false)
|
setIsCartComplete(false)
|
||||||
|
setShowLoader(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,22 +64,26 @@ export function App() {
|
|||||||
const days = initialData?.days ?? 7;
|
const days = initialData?.days ?? 7;
|
||||||
|
|
||||||
const handleDaysChange = useCallback(async (nextDays: number) => {
|
const handleDaysChange = useCallback(async (nextDays: number) => {
|
||||||
|
setShowLoader(true);
|
||||||
await setOption('days', nextDays);
|
await setOption('days', nextDays);
|
||||||
loadInitialState();
|
loadInitialState();
|
||||||
}, [loadInitialState]);
|
}, [loadInitialState]);
|
||||||
|
|
||||||
const handleUsersChange = useCallback(async (nextUsers: User[]) => {
|
const handleUsersChange = useCallback(async (nextUsers: User[]) => {
|
||||||
|
setShowLoader(true);
|
||||||
await setOption('users', nextUsers);
|
await setOption('users', nextUsers);
|
||||||
loadInitialState();
|
loadInitialState();
|
||||||
}, [loadInitialState]);
|
}, [loadInitialState]);
|
||||||
|
|
||||||
const handleCategoriesChange = useCallback((nextCategories: string[]) => {
|
const handleCategoriesChange = useCallback(async (nextCategories: string[]) => {
|
||||||
setInitialData(current => current ? { ...current, initial_groups: nextCategories } : current);
|
setShowLoader(true);
|
||||||
|
await setOption('initial_groups', nextCategories);
|
||||||
loadInitialState();
|
loadInitialState();
|
||||||
}, [loadInitialState]);
|
}, [loadInitialState]);
|
||||||
|
|
||||||
const handlePercentChange = useCallback((percent: number) => {
|
const handlePercentChange = useCallback(async (fullness: number) => {
|
||||||
setInitialData(current => current ? { ...current, percent } : current);
|
setShowLoader(true);
|
||||||
|
await setOption('fullness', fullness);
|
||||||
loadInitialState();
|
loadInitialState();
|
||||||
}, [loadInitialState]);
|
}, [loadInitialState]);
|
||||||
|
|
||||||
@ -90,11 +96,8 @@ export function App() {
|
|||||||
}, [isCartComplete, isCartLoading, precision, setShowEmptyPopup]);
|
}, [isCartComplete, isCartLoading, precision, setShowEmptyPopup]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ym('reachGoal', 'ac_open_settings_tab_' + openSection);
|
reachGoal('ac_open_settings_tab_' + openSection);
|
||||||
}, [openSection])
|
}, [openSection])
|
||||||
useEffect(() => {
|
|
||||||
ym('reachGoal', 'ac_fill_percent_' + percent);
|
|
||||||
}, [percent])
|
|
||||||
|
|
||||||
const pageContent = useMemo(() => {
|
const pageContent = useMemo(() => {
|
||||||
if (!initialData) {
|
if (!initialData) {
|
||||||
@ -103,9 +106,10 @@ export function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container $fullHeight={mobileShowFilters}>
|
<Container $fullHeight={mobileShowFilters}>
|
||||||
|
{showLoader && <Overlay><div /></Overlay>}
|
||||||
<LeftColumn $dataShow={!mobileShowFilters}>
|
<LeftColumn $dataShow={!mobileShowFilters}>
|
||||||
<TitleBlock>
|
<TitleBlock>
|
||||||
<h1>Хватило</h1>
|
<h1>Кладило</h1>
|
||||||
<MobileFilter onClick={() => setMobileShowFilters(true)}>
|
<MobileFilter onClick={() => setMobileShowFilters(true)}>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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"/>
|
<path d="M19 3V7M19 21V11M12 3V15M12 21V19M5 3V5M5 21V9" stroke="#5EA12D" strokeWidth="2" strokeLinecap="round"/>
|
||||||
@ -140,6 +144,7 @@ export function App() {
|
|||||||
setIsCartLoading(false);
|
setIsCartLoading(false);
|
||||||
}}
|
}}
|
||||||
onLoadingChange={setIsCartLoading}
|
onLoadingChange={setIsCartLoading}
|
||||||
|
onMistake={() => setNeedRefresh(true)}
|
||||||
/>
|
/>
|
||||||
</LeftColumn>
|
</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">
|
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"/>
|
<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>}
|
</svg>}
|
||||||
description={openSection === 'fullness' ? 'Чем меньше значение, тем меньше продуктов и калорий в корзине' : percent + '% — ваша цель. Результат может варьироваться'}
|
description={openSection === 'fullness' ? 'Чем меньше значение, тем меньше продуктов и калорий в корзине' : initialData.fullness + '% — ваша цель. Результат может варьироваться'}
|
||||||
isHighlighted={isFullnessHighlighted}
|
isHighlighted={isFullnessHighlighted}
|
||||||
isOpen={openSection === 'fullness'}
|
isOpen={openSection === 'fullness'}
|
||||||
onToggle={() => {
|
onToggle={() => {
|
||||||
@ -219,7 +224,7 @@ export function App() {
|
|||||||
setOpenSection(openSection === 'fullness' ? '' : 'fullness')
|
setOpenSection(openSection === 'fullness' ? '' : 'fullness')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CustomSlider defaultValue={percent} onChange={setPercent} />
|
<CustomSlider defaultValue={initialData.fullness} onChange={handlePercentChange} />
|
||||||
<Alert>
|
<Alert>
|
||||||
<div>
|
<div>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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`
|
const LoadingPage = styled.div`
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
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 = {
|
const fallbackInitialData: InitialData = {
|
||||||
cart: [],
|
cart: [],
|
||||||
users: [],
|
users: [],
|
||||||
initial_groups: [],
|
initial_groups: [],
|
||||||
available_groups: [],
|
available_groups: [],
|
||||||
|
fullness: 90,
|
||||||
days: 7,
|
days: 7,
|
||||||
excess: 0,
|
excess: 0,
|
||||||
precision: 0,
|
precision: 0,
|
||||||
|
|||||||
@ -9,7 +9,8 @@ import { CartItem } from './CartItem';
|
|||||||
import { CartFooter } from './CartFooter';
|
import { CartFooter } from './CartFooter';
|
||||||
import { ProductModal } from './ProductModal';
|
import { ProductModal } from './ProductModal';
|
||||||
import {useGlobalZustandState} from "../../hooks/useGlobalZustandState";
|
import {useGlobalZustandState} from "../../hooks/useGlobalZustandState";
|
||||||
import ym from "react-yandex-metrika";
|
import {NotEnoughPopup} from "./NotEnoughPopup";
|
||||||
|
import {reachGoal} from "../../hooks/useYM";
|
||||||
|
|
||||||
interface CartProps {
|
interface CartProps {
|
||||||
products: Product[];
|
products: Product[];
|
||||||
@ -18,6 +19,7 @@ interface CartProps {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onProductsChange: Dispatch<SetStateAction<Product[]>>;
|
onProductsChange: Dispatch<SetStateAction<Product[]>>;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
|
onMistake: () => void;
|
||||||
onLoadingChange: (isLoading: boolean) => void;
|
onLoadingChange: (isLoading: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +31,9 @@ export function Cart({
|
|||||||
onProductsChange,
|
onProductsChange,
|
||||||
onComplete,
|
onComplete,
|
||||||
onLoadingChange,
|
onLoadingChange,
|
||||||
|
onMistake,
|
||||||
}: CartProps) {
|
}: CartProps) {
|
||||||
|
const [showNotEnoughPopup, setShowNotEnoughPopup] = useState(false);
|
||||||
const [visibleCount, setVisibleCount] = useState(5);
|
const [visibleCount, setVisibleCount] = useState(5);
|
||||||
const [displayText, setDisplayText] = useState('');
|
const [displayText, setDisplayText] = useState('');
|
||||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||||
@ -54,10 +58,11 @@ export function Cart({
|
|||||||
onComplete,
|
onComplete,
|
||||||
onLoadingChange,
|
onLoadingChange,
|
||||||
setDisplayText,
|
setDisplayText,
|
||||||
|
onMistake,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleReplace(product: Product) {
|
async function handleReplace(product: Product) {
|
||||||
ym('reachGoal', 'ac_product_replace_' + product.name);
|
reachGoal('ac_product_replace_' + product.name);
|
||||||
setProcessingProductName(product.name);
|
setProcessingProductName(product.name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -69,7 +74,7 @@ export function Cart({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete(product: Product) {
|
async function handleDelete(product: Product) {
|
||||||
ym('reachGoal', 'ac_product_delete_' + product.name);
|
reachGoal('ac_product_delete_' + product.name);
|
||||||
setProcessingProductName(product.name);
|
setProcessingProductName(product.name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -85,7 +90,7 @@ export function Cart({
|
|||||||
<Summary>
|
<Summary>
|
||||||
<div>
|
<div>
|
||||||
<SummaryTitle>
|
<SummaryTitle>
|
||||||
Ваша корзина на <Green>{days} {declension(days, ['день', 'дня', 'дней'])}</Green>
|
Ваша закупка на <Green>{days} {declension(days, ['день', 'дня', 'дней'])}</Green>
|
||||||
{showFullRefreshButton && <AbsoluteRefreshButton>Начать заново</AbsoluteRefreshButton>}
|
{showFullRefreshButton && <AbsoluteRefreshButton>Начать заново</AbsoluteRefreshButton>}
|
||||||
</SummaryTitle>
|
</SummaryTitle>
|
||||||
|
|
||||||
@ -122,6 +127,11 @@ export function Cart({
|
|||||||
onDelete={() => handleDelete(product)}
|
onDelete={() => handleDelete(product)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{products.length === visibleProducts.length && isComplete && !isLoading && <NotEnoughWrapper>
|
||||||
|
<ShowNotEnough type="button" onClick={() => setShowNotEnoughPopup(true)}>
|
||||||
|
Не хватило
|
||||||
|
</ShowNotEnough>
|
||||||
|
</NotEnoughWrapper>}
|
||||||
</CartList>
|
</CartList>
|
||||||
|
|
||||||
{products.length === 0 && (
|
{products.length === 0 && (
|
||||||
@ -155,6 +165,8 @@ export function Cart({
|
|||||||
setSelectedProduct(null);
|
setSelectedProduct(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showNotEnoughPopup && <NotEnoughPopup onClose={() => {setShowNotEnoughPopup(false)}} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -340,3 +352,12 @@ const ShowMore = styled.button`
|
|||||||
margin-left: 6px;
|
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 {useState} from "react";
|
||||||
import {OrderPopup} from "./OrderPopup";
|
import {OrderPopup} from "./OrderPopup";
|
||||||
import {useAppSelector} from "../../store";
|
import {useAppSelector} from "../../store";
|
||||||
import ym from "react-yandex-metrika";
|
|
||||||
|
import {reachGoal} from "../../hooks/useYM";
|
||||||
|
|
||||||
|
|
||||||
interface CartFooterProps {
|
interface CartFooterProps {
|
||||||
@ -31,10 +32,10 @@ export function CartFooter({ isVisible, totalPrice }: CartFooterProps) {
|
|||||||
</TotalWrap>
|
</TotalWrap>
|
||||||
|
|
||||||
<CheckoutButton type="button" onClick={() => {
|
<CheckoutButton type="button" onClick={() => {
|
||||||
ym('reachGoal', 'ac_hvatilo')
|
reachGoal('ac_hvatilo')
|
||||||
setShowCompletePopup(true)
|
setShowCompletePopup(true)
|
||||||
}}>
|
}}>
|
||||||
{users.length > 1 ? 'Нам' : 'Мне'} хватило
|
Хватило
|
||||||
</CheckoutButton>
|
</CheckoutButton>
|
||||||
{showCompletePopup && <>
|
{showCompletePopup && <>
|
||||||
<OrderPopup setShowCompletePopup={setShowCompletePopup} />
|
<OrderPopup setShowCompletePopup={setShowCompletePopup} />
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import {useEffect, useState} from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {getProductAmount} from '../../utils/product';
|
import {getProductAmount} from '../../utils/product';
|
||||||
import type {Product} from '../../types/cart';
|
import type {Product} from '../../types/cart';
|
||||||
import ym from "react-yandex-metrika";
|
import {reachGoal} from "../../hooks/useYM";
|
||||||
|
|
||||||
interface CartItemProps {
|
interface CartItemProps {
|
||||||
product: Product;
|
product: Product;
|
||||||
@ -21,8 +21,10 @@ export function CartItem({
|
|||||||
}: CartItemProps) {
|
}: CartItemProps) {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ym('reachGoal', 'ac_product_replacement_menu_' + product.name);
|
if (isMenuOpen) {
|
||||||
}, [product.name])
|
reachGoal('ac_product_replacement_menu_' + product.name);
|
||||||
|
}
|
||||||
|
}, [product.name, isMenuOpen])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item $processing={isProcessing}>
|
<Item $processing={isProcessing}>
|
||||||
@ -148,6 +150,7 @@ const MenuButton = styled.button`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Overlay = styled.div`
|
const Overlay = styled.div`
|
||||||
|
height: 100vh;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@ -32,6 +32,7 @@ const Overlay = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
height: 100vh;
|
||||||
`;
|
`;
|
||||||
const Window = styled.div`
|
const Window = styled.div`
|
||||||
background-color: #ffffff;
|
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 styled, {css} from "styled-components";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {FinishPopupContent} from "./FinishPopupContent";
|
import {FinishPopupContent} from "./FinishPopupContent";
|
||||||
import ym from "react-yandex-metrika";
|
import {reachGoal} from "../../hooks/useYM";
|
||||||
|
|
||||||
|
|
||||||
export const OrderPopup = ({setShowCompletePopup}: any) => {
|
export const OrderPopup = ({setShowCompletePopup}: any) => {
|
||||||
const [showFinal, setShowFinal] = useState(false);
|
const [showFinal, setShowFinal] = useState(false);
|
||||||
@ -18,11 +19,11 @@ export const OrderPopup = ({setShowCompletePopup}: any) => {
|
|||||||
<ServiceFeeDescription>Включена в итоговую стоимость заказа</ServiceFeeDescription>
|
<ServiceFeeDescription>Включена в итоговую стоимость заказа</ServiceFeeDescription>
|
||||||
<ButtonBlock>
|
<ButtonBlock>
|
||||||
<ActiveButton onClick={() => {
|
<ActiveButton onClick={() => {
|
||||||
ym('reachGoal', 'ac_order_button')
|
reachGoal('ac_order_button')
|
||||||
setShowFinal(true)
|
setShowFinal(true)
|
||||||
}}>Продолжить</ActiveButton>
|
}}>Продолжить</ActiveButton>
|
||||||
<Button onClick={() => {
|
<Button onClick={() => {
|
||||||
ym('reachGoal', 'ac_no_order_button')
|
reachGoal('ac_no_order_button')
|
||||||
setShowCompletePopup(false)
|
setShowCompletePopup(false)
|
||||||
}}>Вернуться к корзине</Button>
|
}}>Вернуться к корзине</Button>
|
||||||
</ButtonBlock>
|
</ButtonBlock>
|
||||||
@ -32,6 +33,7 @@ export const OrderPopup = ({setShowCompletePopup}: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Overlay = styled.div`
|
const Overlay = styled.div`
|
||||||
|
height: 100vh;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { formatCurrency } from '../../utils/formatCurrency';
|
|||||||
import { getLargeProductImage } from '../../utils/product';
|
import { getLargeProductImage } from '../../utils/product';
|
||||||
import type { Product } from '../../types/cart';
|
import type { Product } from '../../types/cart';
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
import ym from "react-yandex-metrika";
|
|
||||||
|
import {reachGoal} from "../../hooks/useYM";
|
||||||
|
|
||||||
interface ProductModalProps {
|
interface ProductModalProps {
|
||||||
product: Product | null;
|
product: Product | null;
|
||||||
@ -20,7 +21,7 @@ export function ProductModal({
|
|||||||
}: ProductModalProps) {
|
}: ProductModalProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (product !== null) {
|
if (product !== null) {
|
||||||
ym('reachGoal', 'ac_open_popup')
|
reachGoal('ac_open_popup')
|
||||||
}
|
}
|
||||||
}, [product]);
|
}, [product]);
|
||||||
|
|
||||||
@ -62,6 +63,7 @@ export function ProductModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Overlay = styled.div`
|
const Overlay = styled.div`
|
||||||
|
height: 100vh;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { useMemo, useState } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { setOption } from '../../api/promoApi';
|
import { setOption } from '../../api/promoApi';
|
||||||
import { useDebounceBatch } from '../../hooks/useDebounceBatch';
|
import { useDebounceBatch } from '../../hooks/useDebounceBatch';
|
||||||
import ym from "react-yandex-metrika";
|
import {reachGoal} from "../../hooks/useYM";
|
||||||
|
|
||||||
|
|
||||||
interface CategoriesSectionProps {
|
interface CategoriesSectionProps {
|
||||||
categories: string[];
|
categories: string[];
|
||||||
@ -32,7 +33,7 @@ export function CategoriesSection({
|
|||||||
const sendUpdatedCategories = useDebounceBatch<string>(async categoryNames => {
|
const sendUpdatedCategories = useDebounceBatch<string>(async categoryNames => {
|
||||||
let nextCategories = [...selectedCategories];
|
let nextCategories = [...selectedCategories];
|
||||||
|
|
||||||
ym('reachGoal', 'ac_set_categories');
|
reachGoal('ac_set_categories');
|
||||||
categoryNames.forEach(categoryName => {
|
categoryNames.forEach(categoryName => {
|
||||||
if (nextCategories.includes(categoryName)) {
|
if (nextCategories.includes(categoryName)) {
|
||||||
nextCategories = nextCategories.filter(item => item !== categoryName);
|
nextCategories = nextCategories.filter(item => item !== categoryName);
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import {useEffect, useState} from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import type {Gender, User} from '../../types/user';
|
import type {Gender, User} from '../../types/user';
|
||||||
import {Tag, TagsInput, TagsWrap} from './TagsInput';
|
import {Tag, TagsInput, TagsWrap} from './TagsInput';
|
||||||
import ym from "react-yandex-metrika";
|
|
||||||
|
import {reachGoal} from "../../hooks/useYM";
|
||||||
|
|
||||||
interface PersonModalProps {
|
interface PersonModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -46,7 +47,7 @@ export function PersonModal({
|
|||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
ym('reachGoal', 'ac_family_edit');
|
reachGoal('ac_family_edit');
|
||||||
setForm({
|
setForm({
|
||||||
name: user.name,
|
name: user.name,
|
||||||
gender: user.gender,
|
gender: user.gender,
|
||||||
@ -116,22 +117,32 @@ export function PersonModal({
|
|||||||
{showExpanded ? (
|
{showExpanded ? (
|
||||||
<Content onClick={event => event.stopPropagation()}>
|
<Content onClick={event => event.stopPropagation()}>
|
||||||
<Header>
|
<Header>
|
||||||
<h2>{showExpanded == 'favorite' ? 'Любимые' : 'Не добавлять'}</h2>
|
<h2>{showExpanded == 'favorite' ? 'Любимые' : 'Исключить'}</h2>
|
||||||
<CloseButton type="button" onClick={() => setShowExpanded('')}>
|
<CloseButton type="button" onClick={() => setShowExpanded('')}>
|
||||||
×
|
×
|
||||||
</CloseButton>
|
</CloseButton>
|
||||||
</Header>
|
</Header>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<TagsWrap>
|
<TagsWrap>
|
||||||
{((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).map(tag => (
|
{/*{((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).map(tag => (*/}
|
||||||
<Tag key={tag} $type={showExpanded}>
|
{availableTags.map(tag => (
|
||||||
{tag}
|
<Tag
|
||||||
<button type="button"
|
key={tag}
|
||||||
onClick={() => (showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
|
$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)
|
((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).filter(item => item != tag)
|
||||||
)}>
|
)
|
||||||
×
|
} else {
|
||||||
</button>
|
(showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
|
||||||
|
((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).concat(tag)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
</TagsWrap>
|
</TagsWrap>
|
||||||
@ -208,7 +219,7 @@ export function PersonModal({
|
|||||||
|
|
||||||
<PrefsGrid>
|
<PrefsGrid>
|
||||||
<TagsInput
|
<TagsInput
|
||||||
title="Не добавлять"
|
title="Исключить"
|
||||||
type="avoid"
|
type="avoid"
|
||||||
value={form.avoided}
|
value={form.avoided}
|
||||||
availableTags={availableTags}
|
availableTags={availableTags}
|
||||||
@ -238,6 +249,7 @@ export function PersonModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Overlay = styled.div`
|
const Overlay = styled.div`
|
||||||
|
height: 100vh;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.4);
|
background: rgba(0, 0, 0, 0.4);
|
||||||
@ -354,6 +366,7 @@ const GenderButton = styled.button<{ $active: boolean }>`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 0.1s;
|
transition: 0.1s;
|
||||||
font-weight: 370;
|
font-weight: 370;
|
||||||
|
height: 44px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
outline: 1px solid #5EA12D;
|
outline: 1px solid #5EA12D;
|
||||||
@ -363,6 +376,7 @@ const GenderButton = styled.button<{ $active: boolean }>`
|
|||||||
|
|
||||||
const PrefsGrid = styled.div`
|
const PrefsGrid = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useState } from 'react';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import {declension} from "../../utils/declension";
|
||||||
|
|
||||||
interface TagsInputProps {
|
interface TagsInputProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -13,163 +13,140 @@ interface TagsInputProps {
|
|||||||
|
|
||||||
export function TagsInput({
|
export function TagsInput({
|
||||||
title,
|
title,
|
||||||
type,
|
|
||||||
value,
|
value,
|
||||||
availableTags,
|
|
||||||
excludedTags,
|
|
||||||
onChange,
|
|
||||||
onExpand,
|
onExpand,
|
||||||
}: TagsInputProps) {
|
}: 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 (
|
return (
|
||||||
<Column>
|
<Row>
|
||||||
<Title>{title}</Title>
|
<Column>
|
||||||
|
<Title>{title}</Title>
|
||||||
<Input
|
<Info>{value.length > 0 ? declension(value.length, ['Выбрана', 'Выбраны', 'Выбраны']) + ': ' + value.length : 'Не выбраны'}</Info>
|
||||||
type="text"
|
</Column>
|
||||||
placeholder={type === 'avoid' ? 'Например: лук' : 'Например: яблоко'}
|
<AddWrapper>
|
||||||
value={query}
|
<Button
|
||||||
onChange={event => setQuery(event.target.value)}
|
type="button"
|
||||||
/>
|
onClick={onExpand}
|
||||||
|
>
|
||||||
{matches.length > 0 && (
|
Выбрать
|
||||||
<Autocomplete>
|
</Button>
|
||||||
{matches.map(match => (
|
</AddWrapper>
|
||||||
<button
|
</Row>
|
||||||
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>
|
|
||||||
</Column>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Column = styled.div`
|
const AddWrapper = styled.div`
|
||||||
flex: 1;
|
width: 100%;
|
||||||
position: relative;
|
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`
|
const Title = styled.span`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 370;
|
font-weight: 370;
|
||||||
color: ${({ theme }) => theme.colors.darkGreen};
|
color: ${({theme}) => theme.colors.darkGreen};
|
||||||
margin-bottom: 8px;
|
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`
|
export const TagsWrap = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
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;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
background-color: ${({ $type }) => ($type === 'avoid' ? '#fce4ec' : '#f0f4c3')};
|
background-color: ${({$type,$active}) => palette[$active ? 'active' : 'inactive'][$type].bg};
|
||||||
color: ${({ $type }) => ($type === 'avoid' ? '#c62828' : '#33691e')};
|
color: ${({$type,$active}) => palette[$active ? 'active' : 'inactive'][$type].text};
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin-left: 6px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
opacity: 0.6;
|
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`
|
const Label = styled.span`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #333333;
|
color: #0C3B2E;
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, {css} from 'styled-components';
|
||||||
|
|
||||||
export type InputState = 'default' | 'success' | 'error' | 'disabled';
|
export type InputState = 'default' | 'success' | 'error' | 'disabled';
|
||||||
|
|
||||||
@ -23,91 +23,91 @@ const Container = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Label = styled.label<{ $state: InputState }>`
|
const Label = styled.label<{ $state: InputState }>`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #000;
|
color: #000;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
|
||||||
${({ $state }) => $state === 'disabled' && css`
|
${({$state}) => $state === 'disabled' && css`
|
||||||
color: #9e9e9e;
|
color: #9e9e9e;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled.div<{ $state: InputState; $allowClear: boolean }>`
|
const Wrapper = styled.div<{ $state: InputState; $allowClear: boolean }>`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
|
|
||||||
/* Default State */
|
/* Default State */
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
&:focus-within {
|
|
||||||
border-color: #b3b3b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Success State */
|
|
||||||
${({ $state }) => $state === 'success' && css`
|
|
||||||
border: 2px solid #4CAF50;
|
|
||||||
`}
|
|
||||||
|
|
||||||
/* Error State */
|
|
||||||
${({ $state }) => $state === 'error' && css`
|
|
||||||
border: 2px solid #F44336;
|
|
||||||
`}
|
|
||||||
|
|
||||||
/* Disabled State */
|
|
||||||
${({ $state }) => $state === 'disabled' && css`
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
`}
|
|
||||||
|
|
||||||
/* Padding adjustments for clear button */
|
&:focus-within {
|
||||||
${({ $allowClear }) => $allowClear && css`
|
border-color: #b3b3b3;
|
||||||
input {
|
|
||||||
padding-right: 40px;
|
|
||||||
}
|
}
|
||||||
`}
|
|
||||||
|
/* Success State */
|
||||||
|
${({$state}) => $state === 'success' && css`
|
||||||
|
border: 2px solid #4CAF50;
|
||||||
|
`} /* Error State */ ${({$state}) => $state === 'error' && css`
|
||||||
|
border: 2px solid #F44336;
|
||||||
|
`} /* Disabled State */ ${({$state}) => $state === 'disabled' && css`
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
`} /* Padding adjustments for clear button */ ${({$allowClear}) => $allowClear && css`
|
||||||
|
input {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Input = styled.input<{ $state: InputState }>`
|
const Input = styled.input<{ $state: InputState }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #333;
|
color: #0C3B2E;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: #A3ADA2;
|
color: #A3ADA2;
|
||||||
}
|
}
|
||||||
|
|
||||||
${({ $state }) => $state === 'disabled' && css`
|
&:hover {
|
||||||
color: #9e9e9e;
|
border-color: #72b541;
|
||||||
cursor: not-allowed;
|
}
|
||||||
`}
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #72b541;
|
||||||
|
}
|
||||||
|
|
||||||
|
${({$state}) => $state === 'disabled' && css`
|
||||||
|
color: #9e9e9e;
|
||||||
|
cursor: not-allowed;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ClearButton = styled.button<{ $state: InputState }>`
|
const ClearButton = styled.button<{ $state: InputState }>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: #9e9e9e;
|
color: #9e9e9e;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #333;
|
color: #0C3B2E;
|
||||||
}
|
}
|
||||||
|
|
||||||
${({ $state }) => $state === 'disabled' && css`
|
${({$state}) => $state === 'disabled' && css`
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
color: #c0c0c0;
|
color: #c0c0c0;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TextInput: React.FC<TextInputProps> = ({
|
export const TextInput: React.FC<TextInputProps> = ({
|
||||||
|
|||||||
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 { normalizeProductImage } from '../utils/product';
|
||||||
import type { Product } from '../types/cart';
|
import type { Product } from '../types/cart';
|
||||||
import {useGlobalZustandState} from "./useGlobalZustandState";
|
import {useGlobalZustandState} from "./useGlobalZustandState";
|
||||||
import ym from "react-yandex-metrika";
|
import {reachGoal} from "./useYM";
|
||||||
|
|
||||||
|
|
||||||
interface UseCartPollingParams {
|
interface UseCartPollingParams {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
onProductsChange: Dispatch<SetStateAction<Product[]>>;
|
onProductsChange: Dispatch<SetStateAction<Product[]>>;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
|
onMistake: () => void;
|
||||||
onLoadingChange: (isLoading: boolean) => void;
|
onLoadingChange: (isLoading: boolean) => void;
|
||||||
setDisplayText: Dispatch<SetStateAction<string>>;
|
setDisplayText: Dispatch<SetStateAction<string>>;
|
||||||
}
|
}
|
||||||
@ -115,6 +117,7 @@ export function useCartPolling({
|
|||||||
onComplete,
|
onComplete,
|
||||||
onLoadingChange,
|
onLoadingChange,
|
||||||
setDisplayText,
|
setDisplayText,
|
||||||
|
onMistake,
|
||||||
}: UseCartPollingParams) {
|
}: UseCartPollingParams) {
|
||||||
const timeoutRef = useRef<number | null>(null);
|
const timeoutRef = useRef<number | null>(null);
|
||||||
const {updateFromResponse, markAsComplete, markAsIncomplete} = useGlobalZustandState((state) => state);
|
const {updateFromResponse, markAsComplete, markAsIncomplete} = useGlobalZustandState((state) => state);
|
||||||
@ -161,7 +164,7 @@ export function useCartPolling({
|
|||||||
missing_scenario_element = -1;
|
missing_scenario_element = -1;
|
||||||
|
|
||||||
if (update.status === 'complete') {
|
if (update.status === 'complete') {
|
||||||
ym('reachGoal', 'ac_cart_complete');
|
reachGoal('ac_cart_complete');
|
||||||
onLoadingChange(false);
|
onLoadingChange(false);
|
||||||
onComplete();
|
onComplete();
|
||||||
updateFromResponse(true, update.payload?.excess || 0, update.payload?.precision || 0);
|
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;
|
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;
|
||||||
|
};
|
||||||
18
src/main.tsx
18
src/main.tsx
@ -16,16 +16,10 @@ if (!root) {
|
|||||||
|
|
||||||
|
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<React.StrictMode>
|
<Provider store={store}>
|
||||||
<Provider store={store}>
|
<ThemeProvider theme={theme}>
|
||||||
<ThemeProvider theme={theme}>
|
<GlobalStyle />
|
||||||
<GlobalStyle />
|
<App />
|
||||||
<App />
|
</ThemeProvider>
|
||||||
<YMInitializer
|
</Provider>,
|
||||||
accounts={[68914120]}
|
|
||||||
options={{ clickmap: true, trackLinks: true, accurateTrackBounce: true, webvisor: true }}
|
|
||||||
/>
|
|
||||||
</ThemeProvider>
|
|
||||||
</Provider>
|
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
);
|
||||||
@ -66,4 +66,9 @@ export const GlobalStyle = createGlobalStyle`
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*::placeholder {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
@ -30,5 +30,16 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script nonce="**MY_PRETTY_CSP_NONCE**" type="module" src="/src/main.tsx"></script>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -6,6 +6,7 @@ export interface InitialData {
|
|||||||
available_groups: string[];
|
available_groups: string[];
|
||||||
initial_groups: string[];
|
initial_groups: string[];
|
||||||
days: number;
|
days: number;
|
||||||
|
fullness: number;
|
||||||
users: User[];
|
users: User[];
|
||||||
cart: Product[][];
|
cart: Product[][];
|
||||||
excess?: number;
|
excess?: number;
|
||||||
@ -21,5 +22,6 @@ export interface CartUpdateResponse {
|
|||||||
excess?: number;
|
excess?: number;
|
||||||
precision?: number;
|
precision?: number;
|
||||||
chunk?: Product[];
|
chunk?: Product[];
|
||||||
|
unique_sku_cnt?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user