From 820f12d33a9de67d9bc289860b9537604fc6bc6b Mon Sep 17 00:00:00 2001 From: Zakhar Date: Wed, 2 Sep 2026 09:41:12 +0300 Subject: [PATCH] mobile --- package-lock.json | 109 +++++++++++++++ package.json | 2 + src/App.tsx | 132 +++++++++++++++--- src/api/promoApi.ts | 7 + src/components/Accordion/Accordion.tsx | 24 ++-- src/components/Cart/Cart.tsx | 4 + src/components/Cart/CartFooter.tsx | 4 +- src/components/Cart/CartItem.tsx | 1 + src/components/Cart/FinishPopupContent.tsx | 10 +- src/components/Cart/FinitaLaCommedia.tsx | 132 ++++++++++++++++++ src/components/Cart/OrderPopup.tsx | 7 +- src/components/Cart/ProductModal.tsx | 2 + .../Categories/CategoriesSection.tsx | 4 +- src/components/CookiePopup/CookiePopup.tsx | 48 ++++--- src/components/Days/DaysSection.tsx | 5 +- src/components/Family/FamilySection.tsx | 35 ++++- src/components/Family/PersonModal.tsx | 11 +- src/components/Family/TagsInput.tsx | 36 ----- src/main.tsx | 13 +- src/store/family.ts | 28 ++++ src/store/index.ts | 23 +++ src/styled-components.d.ts | 2 + src/styles/GlobalStyle.ts | 16 ++- src/styles/theme.ts | 18 +++ 24 files changed, 567 insertions(+), 106 deletions(-) create mode 100644 src/components/Cart/FinitaLaCommedia.tsx create mode 100644 src/store/family.ts create mode 100644 src/store/index.ts diff --git a/package-lock.json b/package-lock.json index d00b1da..953a3aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,11 @@ "packages": { "": { "dependencies": { + "@reduxjs/toolkit": "^2.12.0", "http-server": "^14.1.1", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-redux": "^9.3.0", "styled-components": "^6.5.3", "zustand": "^5.0.15" }, @@ -225,6 +227,32 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", @@ -487,6 +515,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -524,6 +564,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.10", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", @@ -1894,6 +1940,16 @@ "node": ">=0.10.0" } }, + "node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2844,6 +2900,44 @@ "react": "^19.2.8" } }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -2857,6 +2951,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, + "node_modules/reselect": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", + "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -3301,6 +3401,15 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "license": "MIT" }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/package.json b/package.json index 32281e3..33869cd 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,11 @@ "build": "vite build" }, "dependencies": { + "@reduxjs/toolkit": "^2.12.0", "http-server": "^14.1.1", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-redux": "^9.3.0", "styled-components": "^6.5.3", "zustand": "^5.0.15" }, diff --git a/src/App.tsx b/src/App.tsx index a3effc1..739cb47 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { loadInitialData, setOption } from './api/promoApi'; +import styled, {css} from 'styled-components'; +import {clearCart, loadInitialData, setOption} from './api/promoApi'; import { Cart } from './components/Cart/Cart'; import { CookiePopup } from './components/CookiePopup/CookiePopup'; import { FamilySection } from './components/Family/FamilySection'; @@ -13,27 +13,40 @@ import type { Product } from './types/cart'; import type { User } from './types/user'; import CustomSlider from "./components/Fullness/FullnessSlider"; import {useGlobalZustandState} from "./hooks/useGlobalZustandState"; +import {setUsers, useAppDispatch, useAppSelector} from "./store"; +import {FinitaLaCommedia} from "./components/Cart/FinitaLaCommedia"; export function App() { const [initialData, setInitialData] = useState(null); const [products, setProducts] = useState([]); const [isCartComplete, setIsCartComplete] = useState(false); + const [showEmptyPopup, setShowEmptyPopup] = useState(false); const [isCartLoading, setIsCartLoading] = useState(true); const [openSection, setOpenSection] = useState('family'); const [needRefresh, setNeedRefresh] = useState(true); const [isFullnessHighlighted, setIsFullnessHighlighted] = useState(false); + const [mobileShowFilters, setMobileShowFilters] = useState(true); - const {setInitial} = useGlobalZustandState((state) => state); + const {setInitial, precision} = useGlobalZustandState((state) => state); + const dispatch = useAppDispatch(); + const users = useAppSelector(state => state.family.users); const loadInitialState = async () => { loadInitialData().then(data => { setInitialData(data); + dispatch(setUsers(data.users)); setProducts(data.cart.flat().map(product => normalizeProductImage(product))); setInitial(false, data.excess || 0, data.precision || 0); setIsCartComplete(false) }); }; + + const emptyCart = async () => { + return clearCart().finally(() => { + return loadInitialState(); + }); + }; useEffect(() => { if (needRefresh) { setNeedRefresh(false); @@ -41,7 +54,6 @@ export function App() { } }, [needRefresh, setNeedRefresh, loadInitialState]); - const users = initialData?.users ?? []; const categories = initialData?.available_groups ?? []; const selectedCategories = initialData?.initial_groups ?? []; const days = initialData?.days ?? 7; @@ -66,16 +78,32 @@ export function App() { loadInitialState(); }, [loadInitialState]); + useEffect(() => { + if (isCartComplete && !isCartLoading && precision < 80) { + setShowEmptyPopup(true); + } else { + setShowEmptyPopup(false); + } + }, [isCartComplete, isCartLoading, precision, setShowEmptyPopup]); + const pageContent = useMemo(() => { if (!initialData) { return Загрузка...; } return ( - - + + -

Семейные покупки

+

Хватило

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

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

@@ -105,9 +133,14 @@ export function App() { />
- +

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

+ setMobileShowFilters(false)}> + + + +

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

@@ -157,12 +190,12 @@ export function App() { categories={categories} selectedCategories={selectedCategories} onLocalChange={handleCategoriesChange} - onSaved={() => undefined} + onSaved={() => setNeedRefresh(true)} /> } @@ -214,6 +247,7 @@ export function App() { <> {pageContent} + {showEmptyPopup && setShowEmptyPopup(false)} onRefresh={emptyCart} />} ); } @@ -227,7 +261,7 @@ const LoadingPage = styled.div` font-size: 20px; `; -const Container = styled.main` +const Container = styled.main<{$fullHeight: boolean}>` position: relative; display: flex; width: calc(100% - 40px); @@ -241,9 +275,18 @@ const Container = styled.main` @media (max-width: 900px) { flex-direction: column; - height: auto; - min-height: 90vh; + height: ${$fullHeight => $fullHeight ? 'auto' : '90vh'}; } + + ${({ theme }) => theme.media.mobile(css<{$fullHeight: boolean}>` + margin: 0; + width: 100%; + h1 { + padding-top: 10px; + } + height: ${({ $fullHeight }) => $fullHeight ? 'auto' : '100%'}; + border-radius: ${({ $fullHeight }) => $fullHeight ? '0 0 8px 8px' : '0'}; + `)} `; const Column = styled.section` @@ -262,22 +305,34 @@ const Column = styled.section` & + & { border-left: none; - border-top: 1px solid #f0f0f0; } } `; -const LeftColumn = styled(Column)` +const LeftColumn = styled(Column)<{$dataShow: boolean}>` display: flex; flex-direction: column; height: 100%; max-width: 490px; + position: relative; + ${props => props.theme.media.mobile(css` + display: ${props.$dataShow ? 'flex' : 'none'}; + `)} `; -const RightColumn = styled(Column)``; +const RightColumn = styled(Column)<{$dataShow: boolean}>` + ${({ theme }) => theme.media.mobile(css` + display: none; + `)} + ${props => props.theme.media.mobile(css` + display: ${props.$dataShow ? 'flex' : 'none'}; + flex-direction: column; + `)} +`; const TitleBlock = styled.div` margin-bottom: 32px; + position: relative; h1 { font-size: 32px; @@ -296,6 +351,7 @@ const TitleBlock = styled.div` `; const FilterTitle = styled.div` + position: relative; margin-bottom: 40px; h2 { @@ -311,6 +367,23 @@ const FilterTitle = styled.div` color: ${({ theme }) => theme.colors.gray}; font-weight: 350; } + + ${({ theme }) => theme.media.mobile(css` + margin-bottom: 32px; + h2 { + padding-top: 10px; + text-align: center; + font-size: 16px; + line-height: 19px; + } + p { + font-size: 14px; + line-height: 16px; + max-width: 271px; + text-align: center; + margin: 0 auto; + } + `)} `; const SettingsPanel = styled.div` @@ -333,4 +406,31 @@ const Alert = styled.div` font-weight: 450; color: #5EA12D; } +` + +const MobileFilter = styled.div` + display: none; + position: absolute; + top:0; + right:0; + width: 40px; + height: 40px; + justify-content: center; + align-items: center; + ${({ theme }) => theme.media.mobile(css` + display: flex; + `)} +` +const MobileBack = styled.div` + display: none; + position: absolute; + top:0; + left:0; + width: 40px; + height: 40px; + justify-content: center; + align-items: center; + ${({ theme }) => theme.media.mobile(css` + display: flex; + `)} ` \ No newline at end of file diff --git a/src/api/promoApi.ts b/src/api/promoApi.ts index 008470a..a78e292 100644 --- a/src/api/promoApi.ts +++ b/src/api/promoApi.ts @@ -95,6 +95,13 @@ export function deleteProduct(productName: string): Promise { }); } +export function clearCart(): Promise { + return requestWithRefresh(apiUrl('/api/promo/clear_cart'), { + method: 'POST', + headers: jsonHeaders, + }); +} + export function setOption(key: string, value: TValue): Promise { return requestWithRefresh(apiUrl('/api/promo/set_option'), { method: 'POST', diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx index 25b05c2..e1d3737 100644 --- a/src/components/Accordion/Accordion.tsx +++ b/src/components/Accordion/Accordion.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import styled from 'styled-components'; +import styled, {css} from 'styled-components'; import { ArrowIcon } from '../Icons/ArrowIcon'; import { HeartIcon } from '../Icons/HeartIcon'; @@ -15,7 +15,6 @@ interface AccordionProps { } export function Accordion({ - id, title, description, icon, @@ -70,6 +69,9 @@ const Header = styled.div` background: transparent; border: 0; text-align: left; + ${props => props.theme.media.mobile(css` + padding: 10px 20px 16px 20px; + `)}; `; const HeaderLeft = styled.div` @@ -78,8 +80,8 @@ const HeaderLeft = styled.div` `; const IconCircle = styled.div<{ $highlighted: boolean }>` - width: 44px; - height: 44px; + width: 40px; + height: 40px; border-radius: 50%; display: flex; align-items: center; @@ -89,8 +91,8 @@ const IconCircle = styled.div<{ $highlighted: boolean }>` background: ${({ $highlighted }) => ($highlighted ? '#E6F696' : '#E7EDE7')}; img { - width: 44px; - height: 44px; + width: 40px; + height: 40px; } svg rect { @@ -119,10 +121,13 @@ const ArrowWrap = styled.div<{ $open: boolean }>` color: ${({ theme }) => theme.colors.gray}; transition: transform 0.3s ease; transform: ${({ $open }) => ($open ? 'rotate(180deg)' : 'rotate(0deg)')}; + width: 24px; + height: 24px; + margin-left:8px; svg { - width: 16px; - height: 16px; + width: 24px; + height: 24px; } `; @@ -134,4 +139,7 @@ const Content = styled.div<{ $open: boolean }>` const ContentInner = styled.div` padding: 0 24px 20px; + ${props => props.theme.media.mobile(css` + padding: 0 20px 20px; + `)}; `; \ No newline at end of file diff --git a/src/components/Cart/Cart.tsx b/src/components/Cart/Cart.tsx index 1c3fb14..33eb15a 100644 --- a/src/components/Cart/Cart.tsx +++ b/src/components/Cart/Cart.tsx @@ -323,6 +323,10 @@ const ShowMore = styled.button` opacity: 0.8; } + span { + color: ${({ theme }) => theme.colors.green}; + } + svg { width: 24px; height: 24px; diff --git a/src/components/Cart/CartFooter.tsx b/src/components/Cart/CartFooter.tsx index 9cbc315..56ff118 100644 --- a/src/components/Cart/CartFooter.tsx +++ b/src/components/Cart/CartFooter.tsx @@ -2,6 +2,7 @@ import styled from 'styled-components'; import {BagIcon} from "../Icons/BagIcon"; import {useState} from "react"; import {OrderPopup} from "./OrderPopup"; +import {useAppSelector} from "../../store"; interface CartFooterProps { @@ -11,6 +12,7 @@ interface CartFooterProps { export function CartFooter({ isVisible, totalPrice }: CartFooterProps) { const [showCompletePopup, setShowCompletePopup] = useState(false); + const users = useAppSelector(state => state.family.users); return (