256 lines
8.5 KiB
TypeScript
256 lines
8.5 KiB
TypeScript
|
|
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;
|
|||
|
|
`;
|