From 74f98856775e5590cce7c0aa951bb46240d07f21 Mon Sep 17 00:00:00 2001 From: Zakhar Date: Thu, 3 Sep 2026 13:50:12 +0300 Subject: [PATCH] =?UTF-8?q?=D1=8F.=D0=BC=D0=B5=D1=82=D1=80=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 60 +++++++++++++++++++ package.json | 1 + src/App.tsx | 20 +++++-- src/components/Cart/Cart.tsx | 5 +- src/components/Cart/CartFooter.tsx | 6 +- src/components/Cart/CartItem.tsx | 6 +- src/components/Cart/OrderPopup.tsx | 11 +++- src/components/Cart/ProductModal.tsx | 8 +++ .../Categories/CategoriesSection.tsx | 2 + src/components/Family/FamilySection.tsx | 2 +- src/components/Family/PersonModal.tsx | 2 + src/components/Icons/BagIcon.tsx | 8 +-- src/hooks/useCartPolling.ts | 2 + src/main.tsx | 5 ++ 14 files changed, 122 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 953a3aa..92b60d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "react": "^19.2.8", "react-dom": "^19.2.8", "react-redux": "^9.3.0", + "react-yandex-metrika": "^2.6.0", "styled-components": "^6.5.3", "zustand": "^5.0.15" }, @@ -2205,6 +2206,13 @@ "node": ">= 0.8" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT", + "peer": true + }, "node_modules/levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -2487,6 +2495,19 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2624,6 +2645,16 @@ "node": ">=8" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -2864,6 +2895,18 @@ "node": ">= 0.6.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -2900,6 +2943,13 @@ "react": "^19.2.8" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT", + "peer": true + }, "node_modules/react-redux": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", @@ -2923,6 +2973,16 @@ } } }, + "node_modules/react-yandex-metrika": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-yandex-metrika/-/react-yandex-metrika-2.6.0.tgz", + "integrity": "sha512-8K4wExsNZtY3DTxh1G8a+zWH9Pg8fw23MJcoJ4I/562qrHRnh7L5nteq3lnNL58dnNQbuuHIRoGgMjIo+r1GjA==", + "license": "MIT", + "peerDependencies": { + "prop-types": "*", + "react": "*" + } + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", diff --git a/package.json b/package.json index 33869cd..dc990ac 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "react": "^19.2.8", "react-dom": "^19.2.8", "react-redux": "^9.3.0", + "react-yandex-metrika": "^2.6.0", "styled-components": "^6.5.3", "zustand": "^5.0.15" }, diff --git a/src/App.tsx b/src/App.tsx index ec039f4..ed574cb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ 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"; export function App() { const [initialData, setInitialData] = useState(null); @@ -88,6 +89,13 @@ export function App() { } }, [isCartComplete, isCartLoading, precision, setShowEmptyPopup]); + useEffect(() => { + ym('reachGoal', 'ac_open_settings_tab_' + openSection); + }, [openSection]) + useEffect(() => { + ym('reachGoal', 'ac_fill_percent_' + percent); + }, [percent]) + const pageContent = useMemo(() => { if (!initialData) { return Загрузка...; @@ -100,10 +108,10 @@ export function App() {

Хватило

setMobileShowFilters(true)}> - - - - + + + +

Заботимся о балансе нутриентов

@@ -140,7 +148,7 @@ export function App() {

Настройте корзину под себя

setMobileShowFilters(false)}> - +

Чем больше вы расскажете, тем точнее будет подбор

@@ -199,7 +207,7 @@ export function App() { id="fullness" title="Наполненность корзины" icon={ - + } description={openSection === 'fullness' ? 'Чем меньше значение, тем меньше продуктов и калорий в корзине' : percent + '% — ваша цель. Результат может варьироваться'} isHighlighted={isFullnessHighlighted} diff --git a/src/components/Cart/Cart.tsx b/src/components/Cart/Cart.tsx index c5a8478..86fdef9 100644 --- a/src/components/Cart/Cart.tsx +++ b/src/components/Cart/Cart.tsx @@ -9,6 +9,7 @@ import { CartItem } from './CartItem'; import { CartFooter } from './CartFooter'; import { ProductModal } from './ProductModal'; import {useGlobalZustandState} from "../../hooks/useGlobalZustandState"; +import ym from "react-yandex-metrika"; interface CartProps { products: Product[]; @@ -56,6 +57,7 @@ export function Cart({ }); async function handleReplace(product: Product) { + ym('reachGoal', 'ac_product_replace_' + product.name); setProcessingProductName(product.name); try { @@ -67,6 +69,7 @@ export function Cart({ } async function handleDelete(product: Product) { + ym('reachGoal', 'ac_product_delete_' + product.name); setProcessingProductName(product.name); try { @@ -130,7 +133,7 @@ export function Cart({ setVisibleCount(products.length)}> Показать ещё {remaining} товаров - + )} diff --git a/src/components/Cart/CartFooter.tsx b/src/components/Cart/CartFooter.tsx index 56ff118..d0804a9 100644 --- a/src/components/Cart/CartFooter.tsx +++ b/src/components/Cart/CartFooter.tsx @@ -3,6 +3,7 @@ import {BagIcon} from "../Icons/BagIcon"; import {useState} from "react"; import {OrderPopup} from "./OrderPopup"; import {useAppSelector} from "../../store"; +import ym from "react-yandex-metrika"; interface CartFooterProps { @@ -29,7 +30,10 @@ export function CartFooter({ isVisible, totalPrice }: CartFooterProps) { - setShowCompletePopup(true)}> + { + ym('reachGoal', 'ac_hvatilo') + setShowCompletePopup(true) + }}> {users.length > 1 ? 'Нам' : 'Мне'} хватило {showCompletePopup && <> diff --git a/src/components/Cart/CartItem.tsx b/src/components/Cart/CartItem.tsx index 40ba9c1..f8b691f 100644 --- a/src/components/Cart/CartItem.tsx +++ b/src/components/Cart/CartItem.tsx @@ -1,7 +1,8 @@ -import {useState} from 'react'; +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"; interface CartItemProps { product: Product; @@ -19,6 +20,9 @@ export function CartItem({ onDelete, }: CartItemProps) { const [isMenuOpen, setIsMenuOpen] = useState(false); + useEffect(() => { + ym('reachGoal', 'ac_product_replacement_menu_' + product.name); + }, [product.name]) return ( diff --git a/src/components/Cart/OrderPopup.tsx b/src/components/Cart/OrderPopup.tsx index 13c32df..31e5503 100644 --- a/src/components/Cart/OrderPopup.tsx +++ b/src/components/Cart/OrderPopup.tsx @@ -1,6 +1,7 @@ import styled, {css} from "styled-components"; import {useState} from "react"; import {FinishPopupContent} from "./FinishPopupContent"; +import ym from "react-yandex-metrika"; export const OrderPopup = ({setShowCompletePopup}: any) => { const [showFinal, setShowFinal] = useState(false); @@ -16,8 +17,14 @@ export const OrderPopup = ({setShowCompletePopup}: any) => { 199 ₽ Включена в итоговую стоимость заказа - setShowFinal(true)}>Продолжить - + { + ym('reachGoal', 'ac_order_button') + setShowFinal(true) + }}>Продолжить + } diff --git a/src/components/Cart/ProductModal.tsx b/src/components/Cart/ProductModal.tsx index 84c5f55..baea0ac 100644 --- a/src/components/Cart/ProductModal.tsx +++ b/src/components/Cart/ProductModal.tsx @@ -2,6 +2,8 @@ import styled from 'styled-components'; 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"; interface ProductModalProps { product: Product | null; @@ -16,6 +18,12 @@ export function ProductModal({ onReplace, onDelete, }: ProductModalProps) { + useEffect(() => { + if (product !== null) { + ym('reachGoal', 'ac_open_popup') + } + }, [product]); + if (!product) return null; return ( diff --git a/src/components/Categories/CategoriesSection.tsx b/src/components/Categories/CategoriesSection.tsx index 6253338..6cfc46e 100644 --- a/src/components/Categories/CategoriesSection.tsx +++ b/src/components/Categories/CategoriesSection.tsx @@ -2,6 +2,7 @@ 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"; interface CategoriesSectionProps { categories: string[]; @@ -31,6 +32,7 @@ export function CategoriesSection({ const sendUpdatedCategories = useDebounceBatch(async categoryNames => { let nextCategories = [...selectedCategories]; + ym('reachGoal', 'ac_set_categories'); categoryNames.forEach(categoryName => { if (nextCategories.includes(categoryName)) { nextCategories = nextCategories.filter(item => item !== categoryName); diff --git a/src/components/Family/FamilySection.tsx b/src/components/Family/FamilySection.tsx index 157e38e..ecc84f8 100644 --- a/src/components/Family/FamilySection.tsx +++ b/src/components/Family/FamilySection.tsx @@ -65,7 +65,7 @@ export function FamilySection({ }} > - + diff --git a/src/components/Family/PersonModal.tsx b/src/components/Family/PersonModal.tsx index 7141286..d209dbe 100644 --- a/src/components/Family/PersonModal.tsx +++ b/src/components/Family/PersonModal.tsx @@ -2,6 +2,7 @@ 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"; interface PersonModalProps { isOpen: boolean; @@ -45,6 +46,7 @@ export function PersonModal({ if (!isOpen) return; if (user) { + ym('reachGoal', 'ac_family_edit'); setForm({ name: user.name, gender: user.gender, diff --git a/src/components/Icons/BagIcon.tsx b/src/components/Icons/BagIcon.tsx index 2533a7f..7190fc6 100644 --- a/src/components/Icons/BagIcon.tsx +++ b/src/components/Icons/BagIcon.tsx @@ -1,9 +1,9 @@ export function BagIcon() { return - - - - + + + + ; } \ No newline at end of file diff --git a/src/hooks/useCartPolling.ts b/src/hooks/useCartPolling.ts index 0939d7f..6af3aa8 100644 --- a/src/hooks/useCartPolling.ts +++ b/src/hooks/useCartPolling.ts @@ -3,6 +3,7 @@ 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"; interface UseCartPollingParams { enabled: boolean; @@ -160,6 +161,7 @@ export function useCartPolling({ missing_scenario_element = -1; if (update.status === 'complete') { + ym('reachGoal', 'ac_cart_complete'); onLoadingChange(false); onComplete(); updateFromResponse(true, update.payload?.excess || 0, update.payload?.precision || 0); diff --git a/src/main.tsx b/src/main.tsx index a25df2c..f3cf0d8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,6 +6,7 @@ import { GlobalStyle } from './styles/GlobalStyle'; import { theme } from './styles/theme'; import { Provider } from 'react-redux' import {store} from "./store"; +import { YMInitializer } from 'react-yandex-metrika'; const root = document.getElementById('root'); @@ -20,6 +21,10 @@ createRoot(root).render( + ,