2026-08-24 10:07:16 +00:00
import { useCallback , useEffect , useMemo , useState } from 'react' ;
2026-09-02 06:41:12 +00:00
import styled , { css } from 'styled-components' ;
import { clearCart , loadInitialData , setOption } from './api/promoApi' ;
2026-08-24 10:07:16 +00:00
import { Cart } from './components/Cart/Cart' ;
import { CookiePopup } from './components/CookiePopup/CookiePopup' ;
import { FamilySection } from './components/Family/FamilySection' ;
import { CategoriesSection } from './components/Categories/CategoriesSection' ;
import { DaysSection } from './components/Days/DaysSection' ;
import { Accordion } from './components/Accordion/Accordion' ;
import { normalizeProductImage } from './utils/product' ;
import type { InitialData } from './types/api' ;
import type { Product } from './types/cart' ;
import type { User } from './types/user' ;
2026-08-30 11:47:34 +00:00
import CustomSlider from "./components/Fullness/FullnessSlider" ;
import { useGlobalZustandState } from "./hooks/useGlobalZustandState" ;
2026-09-02 06:41:12 +00:00
import { setUsers , useAppDispatch , useAppSelector } from "./store" ;
import { FinitaLaCommedia } from "./components/Cart/FinitaLaCommedia" ;
2026-09-04 15:57:54 +00:00
import { reachGoal } from "./hooks/useYM" ;
2026-08-24 10:07:16 +00:00
export function App() {
const [ initialData , setInitialData ] = useState < InitialData | null > ( null ) ;
const [ products , setProducts ] = useState < Product [ ] > ( [ ] ) ;
const [ isCartComplete , setIsCartComplete ] = useState ( false ) ;
2026-09-02 06:41:12 +00:00
const [ showEmptyPopup , setShowEmptyPopup ] = useState ( false ) ;
2026-08-24 10:07:16 +00:00
const [ isCartLoading , setIsCartLoading ] = useState ( true ) ;
2026-09-04 15:57:54 +00:00
const [ showLoader , setShowLoader ] = useState ( false ) ;
2026-08-24 10:07:16 +00:00
const [ openSection , setOpenSection ] = useState < string > ( 'family' ) ;
2026-08-30 11:47:34 +00:00
const [ needRefresh , setNeedRefresh ] = useState < boolean > ( true ) ;
const [ isFullnessHighlighted , setIsFullnessHighlighted ] = useState < boolean > ( false ) ;
2026-09-02 06:41:12 +00:00
const [ mobileShowFilters , setMobileShowFilters ] = useState < boolean > ( true ) ;
2026-08-24 10:07:16 +00:00
2026-09-02 06:41:12 +00:00
const { setInitial , precision } = useGlobalZustandState ( ( state ) = > state ) ;
const dispatch = useAppDispatch ( ) ;
const users = useAppSelector ( state = > state . family . users ) ;
2026-08-30 11:47:34 +00:00
const loadInitialState = async ( ) = > {
2026-09-04 15:57:54 +00:00
setShowLoader ( true ) ;
2026-08-24 10:07:16 +00:00
loadInitialData ( ) . then ( data = > {
setInitialData ( data ) ;
2026-09-02 06:41:12 +00:00
dispatch ( setUsers ( data . users ) ) ;
2026-08-24 10:07:16 +00:00
setProducts ( data . cart . flat ( ) . map ( product = > normalizeProductImage ( product ) ) ) ;
2026-08-30 11:47:34 +00:00
setInitial ( false , data . excess || 0 , data . precision || 0 ) ;
setIsCartComplete ( false )
2026-09-05 17:35:33 +00:00
} ) . finally ( ( ) = > {
2026-09-04 15:57:54 +00:00
setShowLoader ( false ) ;
2026-08-24 10:07:16 +00:00
} ) ;
2026-08-30 11:47:34 +00:00
} ;
2026-09-02 06:41:12 +00:00
const emptyCart = async ( ) = > {
return clearCart ( ) . finally ( ( ) = > {
return loadInitialState ( ) ;
} ) ;
} ;
2026-08-30 11:47:34 +00:00
useEffect ( ( ) = > {
if ( needRefresh ) {
setNeedRefresh ( false ) ;
loadInitialState ( ) ;
}
} , [ needRefresh , setNeedRefresh , loadInitialState ] ) ;
2026-08-24 10:07:16 +00:00
const categories = initialData ? . available_groups ? ? [ ] ;
const selectedCategories = initialData ? . initial_groups ? ? [ ] ;
const days = initialData ? . days ? ? 7 ;
const handleDaysChange = useCallback ( async ( nextDays : number ) = > {
2026-09-04 15:57:54 +00:00
setShowLoader ( true ) ;
2026-09-05 17:35:33 +00:00
setOption ( 'days' , nextDays )
. finally ( ( ) = > {
loadInitialState ( ) ;
} ) ;
2026-08-30 11:47:34 +00:00
} , [ loadInitialState ] ) ;
2026-08-24 10:07:16 +00:00
const handleUsersChange = useCallback ( async ( nextUsers : User [ ] ) = > {
2026-09-04 15:57:54 +00:00
setShowLoader ( true ) ;
2026-09-05 17:35:33 +00:00
setOption ( 'users' , nextUsers )
. finally ( ( ) = > {
loadInitialState ( ) ;
} ) ;
2026-08-30 11:47:34 +00:00
} , [ loadInitialState ] ) ;
2026-08-24 10:07:16 +00:00
2026-09-04 15:57:54 +00:00
const handleCategoriesChange = useCallback ( async ( nextCategories : string [ ] ) = > {
setShowLoader ( true ) ;
2026-09-05 17:35:33 +00:00
setOption ( 'initial_groups' , nextCategories )
. finally ( ( ) = > {
loadInitialState ( ) ;
} ) ;
2026-08-30 11:47:34 +00:00
} , [ loadInitialState ] ) ;
2026-09-04 15:57:54 +00:00
const handlePercentChange = useCallback ( async ( fullness : number ) = > {
setShowLoader ( true ) ;
2026-09-05 17:35:33 +00:00
setOption ( 'fullness' , fullness )
. finally ( ( ) = > {
loadInitialState ( ) ;
} )
2026-08-30 11:47:34 +00:00
} , [ loadInitialState ] ) ;
2026-08-24 10:07:16 +00:00
2026-09-02 06:41:12 +00:00
useEffect ( ( ) = > {
if ( isCartComplete && ! isCartLoading && precision < 80 ) {
setShowEmptyPopup ( true ) ;
} else {
setShowEmptyPopup ( false ) ;
}
} , [ isCartComplete , isCartLoading , precision , setShowEmptyPopup ] ) ;
2026-09-03 10:50:12 +00:00
useEffect ( ( ) = > {
2026-09-04 15:57:54 +00:00
reachGoal ( 'ac_open_settings_tab_' + openSection ) ;
2026-09-03 10:50:12 +00:00
} , [ openSection ] )
2026-08-24 10:07:16 +00:00
const pageContent = useMemo ( ( ) = > {
if ( ! initialData ) {
return < LoadingPage > З а г р у з к а . . . < / LoadingPage > ;
}
return (
2026-09-02 06:41:12 +00:00
< Container $ fullHeight = { mobileShowFilters } >
2026-09-04 15:57:54 +00:00
{ showLoader && < Overlay > < div / > < / Overlay > }
2026-09-02 06:41:12 +00:00
< LeftColumn $ dataShow = { ! mobileShowFilters } >
2026-08-24 10:07:16 +00:00
< TitleBlock >
2026-09-05 07:59:33 +00:00
< h1 > Х в а т и л о < / h1 >
2026-09-02 06:41:12 +00:00
< MobileFilter onClick = { ( ) = > setMobileShowFilters ( true ) } >
< svg width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" xmlns = "http://www.w3.org/2000/svg" >
2026-09-03 10:50:12 +00:00
< path d = "M19 3V7M19 21V11M12 3V15M12 21V19M5 3V5M5 21V9" stroke = "#5EA12D" strokeWidth = "2" strokeLinecap = "round" / >
< path d = "M17 9C17 10.1046 17.8954 11 19 11C20.1046 11 21 10.1046 21 9C21 7.89543 20.1046 7 19 7C17.8954 7 17 7.89543 17 9Z" stroke = "#5EA12D" strokeWidth = "2" strokeLinecap = "round" / >
< path d = "M10 17C10 18.1046 10.8954 19 12 19C13.1046 19 14 18.1046 14 17C14 15.8954 13.1046 15 12 15C10.8954 15 10 15.8954 10 17Z" stroke = "#5EA12D" strokeWidth = "2" strokeLinecap = "round" / >
< path d = "M3 7C3 8.10457 3.89543 9 5 9C6.10457 9 7 8.10457 7 7C7 5.89543 6.10457 5 5 5C3.89543 5 3 5.89543 3 7Z" stroke = "#5EA12D" strokeWidth = "2" strokeLinecap = "round" / >
2026-09-02 06:41:12 +00:00
< / svg >
< / MobileFilter >
2026-08-30 11:47:34 +00:00
< p > З а б о т и м с я о б а л а н с е н у т р и е н т о в < / p >
2026-08-24 10:07:16 +00:00
< / TitleBlock >
< Cart
products = { products }
days = { initialData . days }
isComplete = { isCartComplete }
isLoading = { isCartLoading }
2026-08-30 11:47:34 +00:00
onProductsChange = { ( valueOrArrUpdater ) = > {
const newProducts = typeof valueOrArrUpdater === 'function'
? valueOrArrUpdater ( products )
: valueOrArrUpdater ;
if ( newProducts . length < products . length ) {
setIsCartComplete ( false ) ;
setTimeout ( ( ) = > {
setIsCartComplete ( false ) ;
} , 3000 ) ;
}
setProducts ( newProducts ) ;
} }
2026-08-24 10:07:16 +00:00
onComplete = { ( ) = > {
setIsCartComplete ( true ) ;
setIsCartLoading ( false ) ;
} }
onLoadingChange = { setIsCartLoading }
2026-09-04 15:57:54 +00:00
onMistake = { ( ) = > setNeedRefresh ( true ) }
2026-08-24 10:07:16 +00:00
/ >
< / LeftColumn >
2026-09-02 06:41:12 +00:00
< RightColumn $ dataShow = { mobileShowFilters } >
2026-08-24 10:07:16 +00:00
< FilterTitle >
< h2 > Н а с т р о й т е к о р з и н у п о д с е б я < / h2 >
2026-09-02 06:41:12 +00:00
< MobileBack onClick = { ( ) = > setMobileShowFilters ( false ) } >
< svg width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" xmlns = "http://www.w3.org/2000/svg" >
2026-09-03 10:50:12 +00:00
< path d = "M20 12L5 12M10.9231 6L5 12L10.9231 18" stroke = "#5EA12D" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" / >
2026-09-02 06:41:12 +00:00
< / svg >
< / MobileBack >
2026-08-24 10:07:16 +00:00
< p > Ч е м б о л ь ш е в ы р а с с к а ж е т е , т е м т о ч н е е б у д е т п о д б о р < / p >
< / FilterTitle >
< SettingsPanel >
< Accordion
id = "days"
title = "Н а сколько дней собираем корзину?"
icon = { < img src = "/images/calendar.svg" alt = "календарь" / > }
isOpen = { openSection === 'days' }
onToggle = { ( ) = > setOpenSection ( openSection === 'days' ? '' : 'days' ) }
>
< DaysSection value = { days } onChange = { handleDaysChange } / >
< / Accordion >
< Accordion
id = "family"
title = "Кто будет есть?"
description = {
users . length > 0
? ` ${ users . length } человек `
: 'Н е указаны'
}
icon = { < img src = "/images/family.svg" alt = "семья" / > }
isOpen = { openSection === 'family' }
onToggle = { ( ) = > setOpenSection ( openSection === 'family' ? '' : 'family' ) }
>
< FamilySection
users = { users }
availableTags = { categories }
onChange = { handleUsersChange }
/ >
< / Accordion >
< Accordion
id = "preferences"
2026-08-30 11:47:34 +00:00
title = "Обязательные продукты"
2026-08-24 10:07:16 +00:00
description = {
selectedCategories . length > 0
? ` Выбраны ${ selectedCategories . length } `
: 'Н е указаны'
}
isHighlighted = { selectedCategories . length > 0 }
isOpen = { openSection === 'preferences' }
onToggle = { ( ) = > setOpenSection ( openSection === 'preferences' ? '' : 'preferences' ) }
>
< CategoriesSection
categories = { categories }
selectedCategories = { selectedCategories }
onLocalChange = { handleCategoriesChange }
2026-09-02 06:41:12 +00:00
onSaved = { ( ) = > setNeedRefresh ( true ) }
2026-08-24 10:07:16 +00:00
/ >
< / Accordion >
2026-08-30 11:47:34 +00:00
< Accordion
id = "fullness"
2026-09-02 06:41:12 +00:00
title = "Наполненность корзины"
2026-08-30 11:47:34 +00:00
icon = { < svg width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" xmlns = "http://www.w3.org/2000/svg" >
2026-09-03 10:50:12 +00:00
< 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" / >
2026-08-30 11:47:34 +00:00
< / svg > }
2026-09-04 15:57:54 +00:00
description = { openSection === 'fullness' ? 'Чем меньше значение, тем меньше продуктов и калорий в корзине' : initialData . fullness + '% — ваша цель. Результат может варьироваться' }
2026-08-30 11:47:34 +00:00
isHighlighted = { isFullnessHighlighted }
isOpen = { openSection === 'fullness' }
onToggle = { ( ) = > {
if ( openSection !== 'fullness' ) {
setIsFullnessHighlighted ( true ) ;
}
setOpenSection ( openSection === 'fullness' ? '' : 'fullness' )
} }
>
2026-09-04 15:57:54 +00:00
< CustomSlider defaultValue = { initialData . fullness } onChange = { handlePercentChange } / >
2026-08-30 11:47:34 +00:00
< Alert >
< div >
< svg width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" xmlns = "http://www.w3.org/2000/svg" >
< rect x = "3.5" y = "3.5" width = "17" height = "17" rx = "8.5" stroke = "#5EA12D" / >
< circle cx = "12.1111" cy = "11.8924" r = "7.11111" fill = "#5EA12D" / >
< path d = "M9.61488 13.1843C8.90453 11.7621 9.3189 8.83864 9.61488 7.55469L9.72587 7.73247L9.9996 8.08802L10.0551 8.14728L10.2808 8.38432L10.5583 8.68061L10.8358 8.97691L11.2798 9.33247L11.5572 9.51024L12.0567 9.80654L12.2787 9.92506L12.3859 9.98432C12.4969 10.0436 13.1675 10.3991 13.519 10.5177C13.6077 10.5651 14.0175 10.7152 14.221 10.8139C14.6206 10.9562 15.2755 11.5843 15.5529 11.8806C15.9969 12.3073 16.3669 13.2041 16.4964 13.5991C16.7627 14.3102 16.5704 15.5942 16.4409 16.1473L16.1079 17.0954L15.7194 17.8658L15.22 18.5769L14.8315 18.9917L14.443 19.3473L13.9991 19.7028H13.8881L14.1655 19.3473C14.2099 19.2999 14.369 19.0905 14.443 18.9917C14.5762 18.8021 14.8315 18.3991 14.9425 18.2214C15.1645 17.9369 15.4419 16.9177 15.5529 16.4436C15.7157 15.9102 15.5122 15.1004 15.3827 14.7843C15.0719 14.0258 14.5355 13.3818 14.2765 13.1251C14.2765 13.0777 13.7216 12.5917 13.4441 12.3547L12.7226 11.8806L11.8902 11.4065L11.2243 11.051H11.1133V11.1102L11.8902 11.7028L13.1111 12.888C13.5107 13.1725 14.2395 14.3893 14.554 14.9621C14.8204 15.3414 14.776 16.3448 14.7205 16.7991C13.7438 16.8939 12.2047 16.0485 11.5572 15.6139C11.2058 15.3967 10.3252 14.6065 9.61488 13.1843Z" fill = "#F1FAF0" / >
< / svg >
< / div >
< div >
П р о ц е н т в л и я е т н а к о л и ч е с т в о к а л о р и й , б а з о в ы й р а ц и о н & mdash ; < span > 100 % < / span > , е с л и ч а с т ь е д ы п о к у п а е т с я и с ъ е д а е т с я з а
п р е д е л а м и д о м а — п о д в и н ь т е п о л з у н о к в л е в о , е с л и в ы ж д ё т е г о с т е й — в п р а в о
< / div >
< / Alert >
< / Accordion >
2026-08-24 10:07:16 +00:00
< / SettingsPanel >
< / RightColumn >
< / Container >
) ;
} , [
categories ,
days ,
handleCategoriesChange ,
handleDaysChange ,
handleUsersChange ,
initialData ,
isCartComplete ,
isCartLoading ,
openSection ,
products ,
selectedCategories . length ,
users ,
] ) ;
return (
< >
{ pageContent }
< CookiePopup / >
2026-09-02 06:41:12 +00:00
{ showEmptyPopup && < FinitaLaCommedia onClose = { ( ) = > setShowEmptyPopup ( false ) } onRefresh = { emptyCart } / > }
2026-08-24 10:07:16 +00:00
< / >
) ;
}
2026-09-04 15:57:54 +00:00
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 1 s linear infinite ;
}
@keyframes rotation {
0 % {
transform : rotate ( 0 deg ) ;
}
100 % {
transform : rotate ( 360 deg ) ;
}
}
` ;
2026-08-24 10:07:16 +00:00
const LoadingPage = styled . div `
min - height : 100vh ;
display : flex ;
align - items : center ;
justify - content : center ;
color : $ { ( { theme } ) = > theme . colors . white } ;
font - size : 20px ;
` ;
2026-09-02 06:41:12 +00:00
const Container = styled . main < { $fullHeight : boolean } > `
2026-08-24 10:07:16 +00:00
position : relative ;
display : flex ;
width : calc ( 100 % - 40 px ) ;
max - width : 1228px ;
height : 90vh ;
margin : 40px auto 0 ;
background : $ { ( { theme } ) = > theme . colors . white } ;
border - radius : $ { ( { theme } ) = > theme . radius . page } ;
box - shadow : 0 4 px 20 px rgba ( 0 , 0 , 0 , 0.04 ) ;
overflow : hidden ;
@media ( max - width : 900px ) {
flex - direction : column ;
2026-09-02 06:41:12 +00:00
height : $ { $fullHeight = > $fullHeight ? 'auto' : '90vh' } ;
2026-08-24 10:07:16 +00:00
}
2026-09-02 06:41:12 +00:00
$ { ( { 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' } ;
` )}
2026-08-24 10:07:16 +00:00
` ;
const Column = styled . section `
padding : 40px ;
flex : 1 ;
width : 50 % ;
overflow - y : auto ;
& + & {
border - left : 1px solid # f0f0f0 ;
}
@media ( max - width : 900px ) {
width : 100 % ;
padding : 20px ;
& + & {
border - left : none ;
}
}
` ;
2026-09-02 06:41:12 +00:00
const LeftColumn = styled ( Column ) < { $dataShow : boolean } > `
2026-08-24 10:07:16 +00:00
display : flex ;
flex - direction : column ;
height : 100 % ;
max - width : 490px ;
2026-09-02 06:41:12 +00:00
position : relative ;
$ { props = > props . theme . media . mobile ( css `
display : $ { props . $dataShow ? 'flex' : 'none' } ;
` )}
2026-08-24 10:07:16 +00:00
` ;
2026-09-02 06:41:12 +00:00
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 ;
` )}
` ;
2026-08-24 10:07:16 +00:00
const TitleBlock = styled . div `
margin - bottom : 32px ;
2026-09-02 06:41:12 +00:00
position : relative ;
2026-08-24 10:07:16 +00:00
h1 {
font - size : 32px ;
line - height : 38px ;
font - weight : 700 ;
color : $ { ( { theme } ) = > theme . colors . darkGreen } ;
margin - bottom : 8px ;
}
p {
font - size : 16px ;
line - height : 19px ;
color : $ { ( { theme } ) = > theme . colors . gray } ;
font - weight : 350 ;
}
` ;
const FilterTitle = styled . div `
2026-09-02 06:41:12 +00:00
position : relative ;
2026-08-24 10:07:16 +00:00
margin - bottom : 40px ;
h2 {
font - size : 20px ;
line - height : 24px ;
margin - bottom : 8px ;
color : $ { ( { theme } ) = > theme . colors . darkGreen } ;
}
p {
font - size : 16px ;
line - height : 19px ;
color : $ { ( { theme } ) = > theme . colors . gray } ;
font - weight : 350 ;
}
2026-09-02 06:41:12 +00:00
$ { ( { 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 ;
}
` )}
2026-08-24 10:07:16 +00:00
` ;
const SettingsPanel = styled . div `
border : 1px solid $ { ( { theme } ) = > theme . colors . border } ;
2026-08-31 08:55:33 +00:00
border - radius : 12px ;
2026-08-30 11:47:34 +00:00
` ;
const Alert = styled . div `
gap : 8px ;
display : flex ;
flex - direction : row ;
font - weight : 370 ;
background - color : # F1FAF0 ;
border - radius : 8px ;
padding : 12px ;
svg {
width : 24px ;
}
span {
font - weight : 450 ;
color : # 5 EA12D ;
}
2026-09-02 06:41:12 +00:00
`
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 ;
` )}
2026-08-30 11:47:34 +00:00
`