marketing/src/App.tsx

256 lines
8.5 KiB
TypeScript
Raw Normal View History

2026-08-24 10:07:16 +00:00
import { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { loadInitialData, setOption } from './api/promoApi';
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';
export function App() {
const [initialData, setInitialData] = useState<InitialData | null>(null);
const [products, setProducts] = useState<Product[]>([]);
const [isCartComplete, setIsCartComplete] = useState(false);
const [isCartLoading, setIsCartLoading] = useState(true);
const [openSection, setOpenSection] = useState<string>('family');
useEffect(() => {
loadInitialData().then(data => {
setInitialData(data);
setProducts(data.cart.flat().map(product => normalizeProductImage(product)));
});
}, []);
const users = initialData?.users ?? [];
const categories = initialData?.available_groups ?? [];
const selectedCategories = initialData?.initial_groups ?? [];
const days = initialData?.days ?? 7;
const handleDaysChange = useCallback(async (nextDays: number) => {
setInitialData(current => current ? { ...current, days: nextDays } : current);
await setOption('days', nextDays);
}, []);
const handleUsersChange = useCallback(async (nextUsers: User[]) => {
setInitialData(current => current ? { ...current, users: nextUsers } : current);
await setOption('users', nextUsers);
}, []);
const handleCategoriesChange = useCallback((nextCategories: string[]) => {
setInitialData(current => current ? { ...current, initial_groups: nextCategories } : current);
}, []);
const pageContent = useMemo(() => {
if (!initialData) {
return <LoadingPage>Загрузка...</LoadingPage>;
}
return (
<Container>
<LeftColumn>
<TitleBlock>
<h1>Автокорзина</h1>
<p>Собираем продукты под ваши настройки</p>
</TitleBlock>
<Cart
products={products}
days={initialData.days}
excess={initialData.excess ?? 0}
isComplete={isCartComplete}
isLoading={isCartLoading}
onProductsChange={setProducts}
onComplete={() => {
setIsCartComplete(true);
setIsCartLoading(false);
}}
onLoadingChange={setIsCartLoading}
/>
</LeftColumn>
<RightColumn>
<FilterTitle>
<h2>Настройте корзину под себя</h2>
<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"
title="Предпочтения"
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}
onSaved={() => undefined}
/>
</Accordion>
</SettingsPanel>
</RightColumn>
</Container>
);
}, [
categories,
days,
handleCategoriesChange,
handleDaysChange,
handleUsersChange,
initialData,
isCartComplete,
isCartLoading,
openSection,
products,
selectedCategories.length,
users,
]);
return (
<>
{pageContent}
<CookiePopup />
</>
);
}
const LoadingPage = styled.div`
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: ${({ theme }) => theme.colors.white};
font-size: 20px;
`;
const Container = styled.main`
position: relative;
display: flex;
width: calc(100% - 40px);
max-width: 1228px;
height: 90vh;
margin: 40px auto 0;
background: ${({ theme }) => theme.colors.white};
border-radius: ${({ theme }) => theme.radius.page};
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
overflow: hidden;
@media (max-width: 900px) {
flex-direction: column;
height: auto;
min-height: 90vh;
}
`;
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;
border-top: 1px solid #f0f0f0;
}
}
`;
const LeftColumn = styled(Column)`
display: flex;
flex-direction: column;
height: 100%;
max-width: 490px;
`;
const RightColumn = styled(Column)``;
const TitleBlock = styled.div`
margin-bottom: 32px;
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`
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;
}
`;
const SettingsPanel = styled.div`
border: 1px solid ${({ theme }) => theme.colors.border};
border-radius: 16px;
`;