mobile
This commit is contained in:
parent
099755d516
commit
820f12d33a
109
package-lock.json
generated
109
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
132
src/App.tsx
132
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<InitialData | null>(null);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [isCartComplete, setIsCartComplete] = useState(false);
|
||||
const [showEmptyPopup, setShowEmptyPopup] = useState(false);
|
||||
const [isCartLoading, setIsCartLoading] = useState(true);
|
||||
const [openSection, setOpenSection] = useState<string>('family');
|
||||
const [needRefresh, setNeedRefresh] = useState<boolean>(true);
|
||||
const [isFullnessHighlighted, setIsFullnessHighlighted] = useState<boolean>(false);
|
||||
const [mobileShowFilters, setMobileShowFilters] = useState<boolean>(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 <LoadingPage>Загрузка...</LoadingPage>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LeftColumn>
|
||||
<Container $fullHeight={mobileShowFilters}>
|
||||
<LeftColumn $dataShow={!mobileShowFilters}>
|
||||
<TitleBlock>
|
||||
<h1>Семейные покупки</h1>
|
||||
<h1>Хватило</h1>
|
||||
<MobileFilter onClick={() => setMobileShowFilters(true)}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 3V7M19 21V11M12 3V15M12 21V19M5 3V5M5 21V9" stroke="#5EA12D" stroke-width="2" stroke-linecap="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" stroke-width="2" stroke-linecap="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" stroke-width="2" stroke-linecap="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" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</MobileFilter>
|
||||
<p>Заботимся о балансе нутриентов</p>
|
||||
</TitleBlock>
|
||||
|
||||
@ -105,9 +133,14 @@ export function App() {
|
||||
/>
|
||||
</LeftColumn>
|
||||
|
||||
<RightColumn>
|
||||
<RightColumn $dataShow={mobileShowFilters}>
|
||||
<FilterTitle>
|
||||
<h2>Настройте корзину под себя</h2>
|
||||
<MobileBack onClick={() => setMobileShowFilters(false)}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 12L5 12M10.9231 6L5 12L10.9231 18" stroke="#5EA12D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</MobileBack>
|
||||
<p>Чем больше вы расскажете, тем точнее будет подбор</p>
|
||||
</FilterTitle>
|
||||
|
||||
@ -157,12 +190,12 @@ export function App() {
|
||||
categories={categories}
|
||||
selectedCategories={selectedCategories}
|
||||
onLocalChange={handleCategoriesChange}
|
||||
onSaved={() => undefined}
|
||||
onSaved={() => setNeedRefresh(true)}
|
||||
/>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
id="fullness"
|
||||
title="Заполненность корзины"
|
||||
title="Наполненность корзины"
|
||||
icon={<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<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" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>}
|
||||
@ -214,6 +247,7 @@ export function App() {
|
||||
<>
|
||||
{pageContent}
|
||||
<CookiePopup />
|
||||
{showEmptyPopup && <FinitaLaCommedia onClose={() => 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`
|
||||
@ -334,3 +407,30 @@ const Alert = styled.div`
|
||||
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;
|
||||
`)}
|
||||
`
|
||||
@ -95,6 +95,13 @@ export function deleteProduct(productName: string): Promise<unknown> {
|
||||
});
|
||||
}
|
||||
|
||||
export function clearCart(): Promise<unknown> {
|
||||
return requestWithRefresh(apiUrl('/api/promo/clear_cart'), {
|
||||
method: 'POST',
|
||||
headers: jsonHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
export function setOption<TValue>(key: string, value: TValue): Promise<unknown> {
|
||||
return requestWithRefresh(apiUrl('/api/promo/set_option'), {
|
||||
method: 'POST',
|
||||
|
||||
@ -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;
|
||||
`)};
|
||||
`;
|
||||
@ -323,6 +323,10 @@ const ShowMore = styled.button`
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
span {
|
||||
color: ${({ theme }) => theme.colors.green};
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
@ -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 (
|
||||
<Footer $visible={isVisible}>
|
||||
@ -28,7 +30,7 @@ export function CartFooter({ isVisible, totalPrice }: CartFooterProps) {
|
||||
</TotalWrap>
|
||||
|
||||
<CheckoutButton type="button" onClick={() => setShowCompletePopup(true)}>
|
||||
Оформить заказ
|
||||
{users.length > 1 ? 'Нам' : 'Мне'} хватило
|
||||
</CheckoutButton>
|
||||
{showCompletePopup && <>
|
||||
<OrderPopup setShowCompletePopup={setShowCompletePopup} />
|
||||
|
||||
@ -151,6 +151,7 @@ const Overlay = styled.div`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const Dropdown = styled.div`
|
||||
|
||||
@ -13,14 +13,17 @@ export const FinishPopupContent = ({setShowCompletePopup}: any) => {
|
||||
<img src="/images/broc-gifts.png" alt=""/>
|
||||
<h2>Спасибо!</h2>
|
||||
<p>
|
||||
Вы помогли нам протестировать автокорзину. Сейчас сервис ещё разрабатывается, поэтому оформить заказ пока нельзя.
|
||||
Оставьте адрес электронной почты, и после запуска мы подарим вам <strong>3 бесплатные корзины</strong>.
|
||||
Вы помогли нам протестировать автокорзину. Сейчас сервис ещё разрабатывается, поэтому оформить заказ пока не получится.
|
||||
</p>
|
||||
<p>
|
||||
Если вы оставите адрес электронной почты - после запуска мы подарим вам <strong>3 бесплатные корзины</strong>.
|
||||
</p>
|
||||
<TextInput placeholder={'Почта'} value={email} onChange={setEmail} />
|
||||
<FinishButton onClick={() => {
|
||||
if (buttonContent === 'Получить подарок') {
|
||||
setButtonContent('...')
|
||||
subscribeOnFinish(email).then((data) => {
|
||||
// @ts-ignore
|
||||
if (data && data.status === 200) {
|
||||
setButtonContent('Письмо отправлено')
|
||||
} else {
|
||||
@ -37,9 +40,12 @@ export const FinishPopupContent = ({setShowCompletePopup}: any) => {
|
||||
|
||||
const FinishPopupContentWrap = styled.div`
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 24px;
|
||||
|
||||
132
src/components/Cart/FinitaLaCommedia.tsx
Normal file
132
src/components/Cart/FinitaLaCommedia.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const FinitaLaCommedia = ({onClose, onRefresh}: any) => {
|
||||
return <Overlay onClick={onClose}>
|
||||
<Window onClick={event => event.stopPropagation()}>
|
||||
<CloseButton type="button" aria-label="Закрыть" onClick={onClose}>
|
||||
×
|
||||
</CloseButton>
|
||||
<ModalContent>
|
||||
<img src="/images/broc-finita-la-commedia.png" alt="не смогли собрать корзину"/>
|
||||
<div>
|
||||
<h2>Больше подходящих вариантов нет</h2>
|
||||
<p>
|
||||
Мы больше не нашли подходящих продуктов с текущими настройками.
|
||||
Можно изменить предпочтения или попробовать собрать корзину заново
|
||||
</p>
|
||||
</div>
|
||||
<ButtonBlock>
|
||||
<ActiveButton onClick={onRefresh}>Пересобрать корзину</ActiveButton>
|
||||
</ButtonBlock>
|
||||
</ModalContent>
|
||||
</Window>
|
||||
</Overlay>
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 4;
|
||||
overflow: auto;
|
||||
`;
|
||||
const Window = styled.div`
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
width: auto;
|
||||
max-width: 535px;
|
||||
padding: 20px 24px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
z-index: 9;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-weight: 300;
|
||||
line-height: 12px;
|
||||
color: #CEDFD0;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #ACBAAE;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModalContent = styled.div`
|
||||
padding: 20px;
|
||||
color: #0C3B2E;
|
||||
max-width: 335px;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
img {
|
||||
max-width: 335px;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
const ButtonBlock = styled.div`
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-direction: column;
|
||||
`;
|
||||
const Button = styled.div`
|
||||
height: 54px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
color: #5EA12D;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #F7FBF6;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: #EEF8EB;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
`;
|
||||
const ActiveButton = styled(Button)`
|
||||
background-color: #5EA12D;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: ${({theme}) => theme.colors.greenHover};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #4A8D19;
|
||||
}
|
||||
`;
|
||||
@ -1,4 +1,4 @@
|
||||
import styled from "styled-components";
|
||||
import styled, {css} from "styled-components";
|
||||
import {useState} from "react";
|
||||
import {FinishPopupContent} from "./FinishPopupContent";
|
||||
|
||||
@ -42,6 +42,8 @@ const Window = styled.div`
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
z-index: 9;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
@ -63,6 +65,9 @@ const CloseButton = styled.button`
|
||||
&:hover {
|
||||
color: #ACBAAE;
|
||||
}
|
||||
${({ theme }) => theme.media.mobile(css`
|
||||
top: 40px;
|
||||
`)};
|
||||
`;
|
||||
|
||||
const ModalContent = styled.div`
|
||||
|
||||
@ -71,6 +71,8 @@ const Window = styled.div`
|
||||
padding: 20px 24px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
|
||||
@ -141,7 +141,9 @@ const CategoryButton = styled.button<{ $active: boolean; $loading: boolean }>`
|
||||
cursor: pointer;
|
||||
transition: background-color 1.2s;
|
||||
background-color: ${({ $active }) => ($active ? '#0C3B2E' : '#F1FAF0')};
|
||||
span {
|
||||
color: ${({ $active }) => ($active ? '#F1FAF0' : '#0C3B2E')};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ $active }) => ($active ? '#0C3B2E' : '#e4f0de')};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export function CookiePopup() {
|
||||
@ -28,8 +28,9 @@ export function CookiePopup() {
|
||||
|
||||
return (
|
||||
<Popup $hiding={hiding}>
|
||||
<div>
|
||||
<IconWrapper>
|
||||
<img src="/images/cookie.png" alt="Cookie icon" />
|
||||
<img src="/images/cookie.png" alt="Cookie icon"/>
|
||||
</IconWrapper>
|
||||
|
||||
<Text>
|
||||
@ -43,6 +44,7 @@ export function CookiePopup() {
|
||||
</a>
|
||||
, это помогает улучшать сервис и запоминать ваши настройки
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<AcceptButton type="button" onClick={accept}>
|
||||
Хорошо
|
||||
@ -55,7 +57,7 @@ const Popup = styled.div<{ $hiding: boolean }>`
|
||||
position: fixed;
|
||||
bottom: 25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(${({ $hiding }) => ($hiding ? '30px' : '0')});
|
||||
transform: translateX(-50%) translateY(${({$hiding}) => ($hiding ? '30px' : '0')});
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
border: 2px solid #E7EDE7;
|
||||
@ -67,10 +69,16 @@ const Popup = styled.div<{ $hiding: boolean }>`
|
||||
width: 95%;
|
||||
max-width: 780px;
|
||||
z-index: 9999;
|
||||
opacity: ${({ $hiding }) => ($hiding ? 0 : 1)};
|
||||
visibility: ${({ $hiding }) => ($hiding ? 'hidden' : 'visible')};
|
||||
opacity: ${({$hiding}) => ($hiding ? 0 : 1)};
|
||||
visibility: ${({$hiding}) => ($hiding ? 'hidden' : 'visible')};
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
|
||||
& > div {
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
@ -108,9 +116,10 @@ const IconWrapper = styled.div`
|
||||
`;
|
||||
|
||||
const Text = styled.p`
|
||||
display: flex;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
color: ${({ theme }) => theme.colors.darkGreen};
|
||||
color: ${({theme}) => theme.colors.darkGreen};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.01em;
|
||||
@ -118,6 +127,7 @@ const Text = styled.p`
|
||||
a {
|
||||
color: #69a342;
|
||||
font-weight: 600;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import styled from 'styled-components';
|
||||
import styled, {css} from 'styled-components';
|
||||
|
||||
interface DaysSectionProps {
|
||||
value: number;
|
||||
@ -27,6 +27,9 @@ export function DaysSection({ value, onChange }: DaysSectionProps) {
|
||||
const DaysOptions = styled.div`
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
${props => props.theme.media.mobile(css`
|
||||
gap: 16px;
|
||||
`)}
|
||||
`;
|
||||
|
||||
const DayButton = styled.button<{ $active: boolean }>`
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled, {css} from 'styled-components';
|
||||
import { declension } from '../../utils/declension';
|
||||
import type { User } from '../../types/user';
|
||||
import { PersonModal } from './PersonModal';
|
||||
@ -39,7 +39,6 @@ export function FamilySection({
|
||||
{users.map((user, index) => (
|
||||
<FamilyMember
|
||||
key={`${user.name}-${index}`}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditingIndex(index);
|
||||
setIsModalOpen(true);
|
||||
@ -65,7 +64,10 @@ export function FamilySection({
|
||||
handleDelete(index);
|
||||
}}
|
||||
>
|
||||
×
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.167 14.1641L5.83366 5.83073M14.167 5.83073L5.83366 14.1641" stroke="#CEDFD0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
</DeleteButton>
|
||||
</FamilyMember>
|
||||
))}
|
||||
@ -115,17 +117,18 @@ const FamilyList = styled.div`
|
||||
gap: 12px;
|
||||
`;
|
||||
|
||||
const FamilyMember = styled.button`
|
||||
const FamilyMember = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
align-items: stretch;
|
||||
padding: 12px 20px;
|
||||
border: 1px solid ${({ theme }) => theme.colors.border};
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
transition: 0.2s;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: #c0c0c0;
|
||||
@ -169,28 +172,42 @@ const Info = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const Name = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${({ theme }) => theme.colors.darkGreen};
|
||||
${props => props.theme.media.mobile(css`
|
||||
padding-top: 1.5px;
|
||||
font-size: 14px;
|
||||
`)}
|
||||
`;
|
||||
|
||||
const Age = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 350;
|
||||
color: ${({ theme }) => theme.colors.gray};
|
||||
* {
|
||||
color: inherit;
|
||||
}
|
||||
${props => props.theme.media.mobile(css`
|
||||
font-size: 14px;
|
||||
padding-bottom: 4.5px;
|
||||
`)}
|
||||
`;
|
||||
|
||||
const DeleteButton = styled.button`
|
||||
color: #d1d5db;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
padding: 0 8px;
|
||||
transition: 0.2s;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
align-self: center;
|
||||
|
||||
&:hover {
|
||||
color: #e53935;
|
||||
@ -220,6 +237,10 @@ const AddButton = styled.button`
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
span {
|
||||
color:inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #F7FBF6;
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ export function PersonModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<Overlay onClick={onClose}>
|
||||
<Overlay onClick={() => showExpanded ? setShowExpanded('') : onClose()}>
|
||||
{showExpanded ? (
|
||||
<Content onClick={event => event.stopPropagation()}>
|
||||
<Header>
|
||||
@ -245,6 +245,7 @@ const Overlay = styled.div`
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
@ -321,10 +322,6 @@ const FormRow = styled.div`
|
||||
${FormGroup} {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const Hint = styled.span`
|
||||
@ -365,10 +362,6 @@ const PrefsGrid = styled.div`
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 16px;
|
||||
min-height: 200px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const SaveButton = styled.button`
|
||||
|
||||
@ -90,42 +90,6 @@ export function TagsInput({
|
||||
);
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(2px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
background: #ffffff;
|
||||
border-radius: 24px;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
||||
max-height: 95vh;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: ${({ theme }) => theme.colors.darkGreen};
|
||||
}
|
||||
`;
|
||||
|
||||
const Column = styled.div`
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
@ -4,6 +4,8 @@ import { ThemeProvider } from 'styled-components';
|
||||
import { App } from './App';
|
||||
import { GlobalStyle } from './styles/GlobalStyle';
|
||||
import { theme } from './styles/theme';
|
||||
import { Provider } from 'react-redux'
|
||||
import {store} from "./store";
|
||||
|
||||
const root = document.getElementById('root');
|
||||
|
||||
@ -11,11 +13,14 @@ if (!root) {
|
||||
throw new Error('Root element #root was not found');
|
||||
}
|
||||
|
||||
|
||||
createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyle />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
28
src/store/family.ts
Normal file
28
src/store/family.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type {User} from "../types/user";
|
||||
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
|
||||
|
||||
interface FamilyState {
|
||||
users: User[];
|
||||
}
|
||||
|
||||
const initialState: FamilyState = {
|
||||
users: [],
|
||||
}
|
||||
|
||||
export const familySlice = createSlice({
|
||||
name: 'family',
|
||||
initialState,
|
||||
reducers: {
|
||||
addUser: (state, action: PayloadAction<User>) => {
|
||||
state.users.push(action.payload);
|
||||
},
|
||||
removeUser: (state, action: PayloadAction<User>) => {
|
||||
state.users.splice(state.users.indexOf(action.payload), 1);
|
||||
},
|
||||
setUsers: (state, action: PayloadAction<User[]>) => {
|
||||
state.users = action.payload;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default familySlice.reducer;
|
||||
23
src/store/index.ts
Normal file
23
src/store/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import {useDispatch, useSelector, useStore} from "react-redux";
|
||||
import {familySlice} from "./family";
|
||||
// ...
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
family: familySlice.reducer,
|
||||
}
|
||||
})
|
||||
|
||||
// Get the type of our store variable
|
||||
export type AppStore = typeof store
|
||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||
export type RootState = ReturnType<AppStore['getState']>
|
||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||
export type AppDispatch = AppStore['dispatch']
|
||||
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||
export const useAppStore = useStore.withTypes<AppStore>()
|
||||
|
||||
export const {addUser, removeUser, setUsers} = familySlice.actions;
|
||||
2
src/styled-components.d.ts
vendored
2
src/styled-components.d.ts
vendored
@ -7,6 +7,8 @@ declare module "styled-components" {
|
||||
export interface DefaultTheme extends ThemeType {
|
||||
colors: ThemeType['colors'];
|
||||
radius: ThemeType['radius'];
|
||||
breakpoints: ThemeType['breakpoints'];
|
||||
media: ThemeType['media'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
import {createGlobalStyle, css} from 'styled-components';
|
||||
|
||||
export const GlobalStyle = createGlobalStyle`
|
||||
@font-face {
|
||||
@ -12,13 +12,16 @@ export const GlobalStyle = createGlobalStyle`
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Roboto', 'FallbackFont', system-ui, -apple-system, 'Segoe UI', Helvetica, sans-serif;
|
||||
color:#0C3B2E;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
background-color: ${({ theme }) => theme.colors.pageBg};
|
||||
background-image: url('/images/background-raspberry.png');
|
||||
background-repeat: no-repeat;
|
||||
@ -26,6 +29,17 @@ export const GlobalStyle = createGlobalStyle`
|
||||
background-size: contain;
|
||||
background-position: left top;
|
||||
min-height: 100vh;
|
||||
|
||||
${({ theme }) => theme.media.mobile(css`
|
||||
background-position: center 72%;
|
||||
`)};
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
${({ theme }) => theme.media.mobile(css`
|
||||
overflow: auto;
|
||||
`)};
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import {css} from "styled-components";
|
||||
import type { RuleSet } from 'styled-components';
|
||||
|
||||
export const theme = {
|
||||
colors: {
|
||||
darkGreen: '#0C3B2E',
|
||||
@ -16,4 +19,19 @@ export const theme = {
|
||||
button: '12px',
|
||||
small: '8px',
|
||||
},
|
||||
breakpoints: {
|
||||
xs: '576px'
|
||||
},
|
||||
media: {
|
||||
mobile: (inner: RuleSet) => css`
|
||||
@media (max-width: 576px) {
|
||||
${inner};
|
||||
}
|
||||
`,
|
||||
tablet: (inner: RuleSet) => css`
|
||||
@media (max-width: 768px) {
|
||||
${inner};
|
||||
}
|
||||
`
|
||||
} as const
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user