This commit is contained in:
Zakhar 2026-09-02 09:41:12 +03:00
parent 099755d516
commit 820f12d33a
24 changed files with 567 additions and 106 deletions

109
package-lock.json generated
View File

@ -5,9 +5,11 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.12.0",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"react-redux": "^9.3.0",
"styled-components": "^6.5.3", "styled-components": "^6.5.3",
"zustand": "^5.0.15" "zustand": "^5.0.15"
}, },
@ -225,6 +227,32 @@
"url": "https://github.com/sponsors/Boshen" "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": { "node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz",
@ -487,6 +515,18 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/minimatch": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@ -524,6 +564,12 @@
"@types/react": "^19.2.0" "@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": { "node_modules/@types/validator": {
"version": "13.15.10", "version": "13.15.10",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
@ -1894,6 +1940,16 @@
"node": ">=0.10.0" "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": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -2844,6 +2900,44 @@
"react": "^19.2.8" "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": { "node_modules/reflect-metadata": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
@ -2857,6 +2951,12 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT" "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": { "node_modules/retry": {
"version": "0.13.1", "version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@ -3301,6 +3401,15 @@
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
"license": "MIT" "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": { "node_modules/util": {
"version": "0.12.5", "version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",

View File

@ -4,9 +4,11 @@
"build": "vite build" "build": "vite build"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.12.0",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"react-redux": "^9.3.0",
"styled-components": "^6.5.3", "styled-components": "^6.5.3",
"zustand": "^5.0.15" "zustand": "^5.0.15"
}, },

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components'; import styled, {css} from 'styled-components';
import { loadInitialData, setOption } from './api/promoApi'; import {clearCart, loadInitialData, setOption} from './api/promoApi';
import { Cart } from './components/Cart/Cart'; import { Cart } from './components/Cart/Cart';
import { CookiePopup } from './components/CookiePopup/CookiePopup'; import { CookiePopup } from './components/CookiePopup/CookiePopup';
import { FamilySection } from './components/Family/FamilySection'; import { FamilySection } from './components/Family/FamilySection';
@ -13,27 +13,40 @@ import type { Product } from './types/cart';
import type { User } from './types/user'; import type { User } from './types/user';
import CustomSlider from "./components/Fullness/FullnessSlider"; import CustomSlider from "./components/Fullness/FullnessSlider";
import {useGlobalZustandState} from "./hooks/useGlobalZustandState"; import {useGlobalZustandState} from "./hooks/useGlobalZustandState";
import {setUsers, useAppDispatch, useAppSelector} from "./store";
import {FinitaLaCommedia} from "./components/Cart/FinitaLaCommedia";
export function App() { export function App() {
const [initialData, setInitialData] = useState<InitialData | null>(null); const [initialData, setInitialData] = useState<InitialData | null>(null);
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
const [isCartComplete, setIsCartComplete] = useState(false); const [isCartComplete, setIsCartComplete] = useState(false);
const [showEmptyPopup, setShowEmptyPopup] = useState(false);
const [isCartLoading, setIsCartLoading] = useState(true); const [isCartLoading, setIsCartLoading] = useState(true);
const [openSection, setOpenSection] = useState<string>('family'); const [openSection, setOpenSection] = useState<string>('family');
const [needRefresh, setNeedRefresh] = useState<boolean>(true); const [needRefresh, setNeedRefresh] = useState<boolean>(true);
const [isFullnessHighlighted, setIsFullnessHighlighted] = useState<boolean>(false); 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 () => { const loadInitialState = async () => {
loadInitialData().then(data => { loadInitialData().then(data => {
setInitialData(data); setInitialData(data);
dispatch(setUsers(data.users));
setProducts(data.cart.flat().map(product => normalizeProductImage(product))); setProducts(data.cart.flat().map(product => normalizeProductImage(product)));
setInitial(false, data.excess || 0, data.precision || 0); setInitial(false, data.excess || 0, data.precision || 0);
setIsCartComplete(false) setIsCartComplete(false)
}); });
}; };
const emptyCart = async () => {
return clearCart().finally(() => {
return loadInitialState();
});
};
useEffect(() => { useEffect(() => {
if (needRefresh) { if (needRefresh) {
setNeedRefresh(false); setNeedRefresh(false);
@ -41,7 +54,6 @@ export function App() {
} }
}, [needRefresh, setNeedRefresh, loadInitialState]); }, [needRefresh, setNeedRefresh, loadInitialState]);
const users = initialData?.users ?? [];
const categories = initialData?.available_groups ?? []; const categories = initialData?.available_groups ?? [];
const selectedCategories = initialData?.initial_groups ?? []; const selectedCategories = initialData?.initial_groups ?? [];
const days = initialData?.days ?? 7; const days = initialData?.days ?? 7;
@ -66,16 +78,32 @@ export function App() {
loadInitialState(); loadInitialState();
}, [loadInitialState]); }, [loadInitialState]);
useEffect(() => {
if (isCartComplete && !isCartLoading && precision < 80) {
setShowEmptyPopup(true);
} else {
setShowEmptyPopup(false);
}
}, [isCartComplete, isCartLoading, precision, setShowEmptyPopup]);
const pageContent = useMemo(() => { const pageContent = useMemo(() => {
if (!initialData) { if (!initialData) {
return <LoadingPage>Загрузка...</LoadingPage>; return <LoadingPage>Загрузка...</LoadingPage>;
} }
return ( return (
<Container> <Container $fullHeight={mobileShowFilters}>
<LeftColumn> <LeftColumn $dataShow={!mobileShowFilters}>
<TitleBlock> <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> <p>Заботимся о балансе нутриентов</p>
</TitleBlock> </TitleBlock>
@ -105,9 +133,14 @@ export function App() {
/> />
</LeftColumn> </LeftColumn>
<RightColumn> <RightColumn $dataShow={mobileShowFilters}>
<FilterTitle> <FilterTitle>
<h2>Настройте корзину под себя</h2> <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> <p>Чем больше вы расскажете, тем точнее будет подбор</p>
</FilterTitle> </FilterTitle>
@ -157,12 +190,12 @@ export function App() {
categories={categories} categories={categories}
selectedCategories={selectedCategories} selectedCategories={selectedCategories}
onLocalChange={handleCategoriesChange} onLocalChange={handleCategoriesChange}
onSaved={() => undefined} onSaved={() => setNeedRefresh(true)}
/> />
</Accordion> </Accordion>
<Accordion <Accordion
id="fullness" id="fullness"
title="Заполненность корзины" title="Наполненность корзины"
icon={<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 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"/> <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>} </svg>}
@ -214,6 +247,7 @@ export function App() {
<> <>
{pageContent} {pageContent}
<CookiePopup /> <CookiePopup />
{showEmptyPopup && <FinitaLaCommedia onClose={() => setShowEmptyPopup(false)} onRefresh={emptyCart} />}
</> </>
); );
} }
@ -227,7 +261,7 @@ const LoadingPage = styled.div`
font-size: 20px; font-size: 20px;
`; `;
const Container = styled.main` const Container = styled.main<{$fullHeight: boolean}>`
position: relative; position: relative;
display: flex; display: flex;
width: calc(100% - 40px); width: calc(100% - 40px);
@ -241,9 +275,18 @@ const Container = styled.main`
@media (max-width: 900px) { @media (max-width: 900px) {
flex-direction: column; flex-direction: column;
height: auto; height: ${$fullHeight => $fullHeight ? 'auto' : '90vh'};
min-height: 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` const Column = styled.section`
@ -262,22 +305,34 @@ const Column = styled.section`
& + & { & + & {
border-left: none; border-left: none;
border-top: 1px solid #f0f0f0;
} }
} }
`; `;
const LeftColumn = styled(Column)` const LeftColumn = styled(Column)<{$dataShow: boolean}>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
max-width: 490px; 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` const TitleBlock = styled.div`
margin-bottom: 32px; margin-bottom: 32px;
position: relative;
h1 { h1 {
font-size: 32px; font-size: 32px;
@ -296,6 +351,7 @@ const TitleBlock = styled.div`
`; `;
const FilterTitle = styled.div` const FilterTitle = styled.div`
position: relative;
margin-bottom: 40px; margin-bottom: 40px;
h2 { h2 {
@ -311,6 +367,23 @@ const FilterTitle = styled.div`
color: ${({ theme }) => theme.colors.gray}; color: ${({ theme }) => theme.colors.gray};
font-weight: 350; 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` const SettingsPanel = styled.div`
@ -333,4 +406,31 @@ const Alert = styled.div`
font-weight: 450; font-weight: 450;
color: #5EA12D; 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;
`)}
` `

View File

@ -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> { export function setOption<TValue>(key: string, value: TValue): Promise<unknown> {
return requestWithRefresh(apiUrl('/api/promo/set_option'), { return requestWithRefresh(apiUrl('/api/promo/set_option'), {
method: 'POST', method: 'POST',

View File

@ -1,5 +1,5 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import styled from 'styled-components'; import styled, {css} from 'styled-components';
import { ArrowIcon } from '../Icons/ArrowIcon'; import { ArrowIcon } from '../Icons/ArrowIcon';
import { HeartIcon } from '../Icons/HeartIcon'; import { HeartIcon } from '../Icons/HeartIcon';
@ -15,7 +15,6 @@ interface AccordionProps {
} }
export function Accordion({ export function Accordion({
id,
title, title,
description, description,
icon, icon,
@ -70,6 +69,9 @@ const Header = styled.div`
background: transparent; background: transparent;
border: 0; border: 0;
text-align: left; text-align: left;
${props => props.theme.media.mobile(css`
padding: 10px 20px 16px 20px;
`)};
`; `;
const HeaderLeft = styled.div` const HeaderLeft = styled.div`
@ -78,8 +80,8 @@ const HeaderLeft = styled.div`
`; `;
const IconCircle = styled.div<{ $highlighted: boolean }>` const IconCircle = styled.div<{ $highlighted: boolean }>`
width: 44px; width: 40px;
height: 44px; height: 40px;
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
@ -89,8 +91,8 @@ const IconCircle = styled.div<{ $highlighted: boolean }>`
background: ${({ $highlighted }) => ($highlighted ? '#E6F696' : '#E7EDE7')}; background: ${({ $highlighted }) => ($highlighted ? '#E6F696' : '#E7EDE7')};
img { img {
width: 44px; width: 40px;
height: 44px; height: 40px;
} }
svg rect { svg rect {
@ -119,10 +121,13 @@ const ArrowWrap = styled.div<{ $open: boolean }>`
color: ${({ theme }) => theme.colors.gray}; color: ${({ theme }) => theme.colors.gray};
transition: transform 0.3s ease; transition: transform 0.3s ease;
transform: ${({ $open }) => ($open ? 'rotate(180deg)' : 'rotate(0deg)')}; transform: ${({ $open }) => ($open ? 'rotate(180deg)' : 'rotate(0deg)')};
width: 24px;
height: 24px;
margin-left:8px;
svg { svg {
width: 16px; width: 24px;
height: 16px; height: 24px;
} }
`; `;
@ -134,4 +139,7 @@ const Content = styled.div<{ $open: boolean }>`
const ContentInner = styled.div` const ContentInner = styled.div`
padding: 0 24px 20px; padding: 0 24px 20px;
${props => props.theme.media.mobile(css`
padding: 0 20px 20px;
`)};
`; `;

View File

@ -323,6 +323,10 @@ const ShowMore = styled.button`
opacity: 0.8; opacity: 0.8;
} }
span {
color: ${({ theme }) => theme.colors.green};
}
svg { svg {
width: 24px; width: 24px;
height: 24px; height: 24px;

View File

@ -2,6 +2,7 @@ import styled from 'styled-components';
import {BagIcon} from "../Icons/BagIcon"; import {BagIcon} from "../Icons/BagIcon";
import {useState} from "react"; import {useState} from "react";
import {OrderPopup} from "./OrderPopup"; import {OrderPopup} from "./OrderPopup";
import {useAppSelector} from "../../store";
interface CartFooterProps { interface CartFooterProps {
@ -11,6 +12,7 @@ interface CartFooterProps {
export function CartFooter({ isVisible, totalPrice }: CartFooterProps) { export function CartFooter({ isVisible, totalPrice }: CartFooterProps) {
const [showCompletePopup, setShowCompletePopup] = useState(false); const [showCompletePopup, setShowCompletePopup] = useState(false);
const users = useAppSelector(state => state.family.users);
return ( return (
<Footer $visible={isVisible}> <Footer $visible={isVisible}>
@ -28,7 +30,7 @@ export function CartFooter({ isVisible, totalPrice }: CartFooterProps) {
</TotalWrap> </TotalWrap>
<CheckoutButton type="button" onClick={() => setShowCompletePopup(true)}> <CheckoutButton type="button" onClick={() => setShowCompletePopup(true)}>
Оформить заказ {users.length > 1 ? 'Нам' : 'Мне'} хватило
</CheckoutButton> </CheckoutButton>
{showCompletePopup && <> {showCompletePopup && <>
<OrderPopup setShowCompletePopup={setShowCompletePopup} /> <OrderPopup setShowCompletePopup={setShowCompletePopup} />

View File

@ -151,6 +151,7 @@ const Overlay = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 9; z-index: 9;
overflow: auto;
`; `;
const Dropdown = styled.div` const Dropdown = styled.div`

View File

@ -13,14 +13,17 @@ export const FinishPopupContent = ({setShowCompletePopup}: any) => {
<img src="/images/broc-gifts.png" alt=""/> <img src="/images/broc-gifts.png" alt=""/>
<h2>Спасибо!</h2> <h2>Спасибо!</h2>
<p> <p>
Вы помогли нам протестировать автокорзину. Сейчас сервис ещё разрабатывается, поэтому оформить заказ пока нельзя. Вы помогли нам протестировать автокорзину. Сейчас сервис ещё разрабатывается, поэтому оформить заказ пока не получится.
Оставьте адрес электронной почты, и после запуска мы подарим вам <strong>3 бесплатные корзины</strong>. </p>
<p>
Если вы оставите адрес электронной почты - после запуска мы подарим вам <strong>3 бесплатные корзины</strong>.
</p> </p>
<TextInput placeholder={'Почта'} value={email} onChange={setEmail} /> <TextInput placeholder={'Почта'} value={email} onChange={setEmail} />
<FinishButton onClick={() => { <FinishButton onClick={() => {
if (buttonContent === 'Получить подарок') { if (buttonContent === 'Получить подарок') {
setButtonContent('...') setButtonContent('...')
subscribeOnFinish(email).then((data) => { subscribeOnFinish(email).then((data) => {
// @ts-ignore
if (data && data.status === 200) { if (data && data.status === 200) {
setButtonContent('Письмо отправлено') setButtonContent('Письмо отправлено')
} else { } else {
@ -37,9 +40,12 @@ export const FinishPopupContent = ({setShowCompletePopup}: any) => {
const FinishPopupContentWrap = styled.div` const FinishPopupContentWrap = styled.div`
padding: 20px; padding: 20px;
display: flex;
flex-direction: column;
img { img {
max-width: 100%; max-width: 100%;
display: flex;
} }
h2 { h2 {
margin-top: 24px; margin-top: 24px;

View 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}>
&times;
</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;
}
`;

View File

@ -1,4 +1,4 @@
import styled from "styled-components"; import styled, {css} from "styled-components";
import {useState} from "react"; import {useState} from "react";
import {FinishPopupContent} from "./FinishPopupContent"; import {FinishPopupContent} from "./FinishPopupContent";
@ -42,6 +42,8 @@ const Window = styled.div`
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
position: relative; position: relative;
z-index: 9; z-index: 9;
max-height: 90vh;
overflow: auto;
`; `;
const CloseButton = styled.button` const CloseButton = styled.button`
@ -63,6 +65,9 @@ const CloseButton = styled.button`
&:hover { &:hover {
color: #ACBAAE; color: #ACBAAE;
} }
${({ theme }) => theme.media.mobile(css`
top: 40px;
`)};
`; `;
const ModalContent = styled.div` const ModalContent = styled.div`

View File

@ -71,6 +71,8 @@ const Window = styled.div`
padding: 20px 24px; padding: 20px 24px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
position: relative; position: relative;
max-height: 90vh;
overflow: auto;
`; `;
const CloseButton = styled.button` const CloseButton = styled.button`

View File

@ -141,7 +141,9 @@ const CategoryButton = styled.button<{ $active: boolean; $loading: boolean }>`
cursor: pointer; cursor: pointer;
transition: background-color 1.2s; transition: background-color 1.2s;
background-color: ${({ $active }) => ($active ? '#0C3B2E' : '#F1FAF0')}; background-color: ${({ $active }) => ($active ? '#0C3B2E' : '#F1FAF0')};
color: ${({ $active }) => ($active ? '#F1FAF0' : '#0C3B2E')}; span {
color: ${({ $active }) => ($active ? '#F1FAF0' : '#0C3B2E')};
}
&:hover { &:hover {
background-color: ${({ $active }) => ($active ? '#0C3B2E' : '#e4f0de')}; background-color: ${({ $active }) => ($active ? '#0C3B2E' : '#e4f0de')};

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import {useEffect, useState} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
export function CookiePopup() { export function CookiePopup() {
@ -28,21 +28,23 @@ export function CookiePopup() {
return ( return (
<Popup $hiding={hiding}> <Popup $hiding={hiding}>
<IconWrapper> <div>
<img src="/images/cookie.png" alt="Cookie icon" /> <IconWrapper>
</IconWrapper> <img src="/images/cookie.png" alt="Cookie icon"/>
</IconWrapper>
<Text> <Text>
Мы используем{' '} Мы используем{' '}
<a href="/cookies_and_recommendations.html#c" target="_blank" rel="noreferrer"> <a href="/cookies_and_recommendations.html#c" target="_blank" rel="noreferrer">
куки куки
</a>{' '} </a>{' '}
и{' '} и{' '}
<a href="/cookies_and_recommendations.html#r" target="_blank" rel="noreferrer"> <a href="/cookies_and_recommendations.html#r" target="_blank" rel="noreferrer">
рекомендательные технологии рекомендательные технологии
</a> </a>
, это помогает улучшать сервис и запоминать ваши настройки , это помогает улучшать сервис и запоминать ваши настройки
</Text> </Text>
</div>
<AcceptButton type="button" onClick={accept}> <AcceptButton type="button" onClick={accept}>
Хорошо Хорошо
@ -55,7 +57,7 @@ const Popup = styled.div<{ $hiding: boolean }>`
position: fixed; position: fixed;
bottom: 25px; bottom: 25px;
left: 50%; left: 50%;
transform: translateX(-50%) translateY(${({ $hiding }) => ($hiding ? '30px' : '0')}); transform: translateX(-50%) translateY(${({$hiding}) => ($hiding ? '30px' : '0')});
background: #ffffff; background: #ffffff;
border-radius: 20px; border-radius: 20px;
border: 2px solid #E7EDE7; border: 2px solid #E7EDE7;
@ -67,10 +69,16 @@ const Popup = styled.div<{ $hiding: boolean }>`
width: 95%; width: 95%;
max-width: 780px; max-width: 780px;
z-index: 9999; z-index: 9999;
opacity: ${({ $hiding }) => ($hiding ? 0 : 1)}; opacity: ${({$hiding}) => ($hiding ? 0 : 1)};
visibility: ${({ $hiding }) => ($hiding ? 'hidden' : 'visible')}; visibility: ${({$hiding}) => ($hiding ? 'hidden' : 'visible')};
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); 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) { @media (max-width: 600px) {
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
@ -108,9 +116,10 @@ const IconWrapper = styled.div`
`; `;
const Text = styled.p` const Text = styled.p`
display: flex;
margin: 0; margin: 0;
flex-grow: 1; flex-grow: 1;
color: ${({ theme }) => theme.colors.darkGreen}; color: ${({theme}) => theme.colors.darkGreen};
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
letter-spacing: 0.01em; letter-spacing: 0.01em;
@ -118,6 +127,7 @@ const Text = styled.p`
a { a {
color: #69a342; color: #69a342;
font-weight: 600; font-weight: 600;
display: contents;
} }
@media (max-width: 600px) { @media (max-width: 600px) {

View File

@ -1,4 +1,4 @@
import styled from 'styled-components'; import styled, {css} from 'styled-components';
interface DaysSectionProps { interface DaysSectionProps {
value: number; value: number;
@ -27,6 +27,9 @@ export function DaysSection({ value, onChange }: DaysSectionProps) {
const DaysOptions = styled.div` const DaysOptions = styled.div`
display: flex; display: flex;
gap: 12px; gap: 12px;
${props => props.theme.media.mobile(css`
gap: 16px;
`)}
`; `;
const DayButton = styled.button<{ $active: boolean }>` const DayButton = styled.button<{ $active: boolean }>`

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import styled from 'styled-components'; import styled, {css} from 'styled-components';
import { declension } from '../../utils/declension'; import { declension } from '../../utils/declension';
import type { User } from '../../types/user'; import type { User } from '../../types/user';
import { PersonModal } from './PersonModal'; import { PersonModal } from './PersonModal';
@ -39,7 +39,6 @@ export function FamilySection({
{users.map((user, index) => ( {users.map((user, index) => (
<FamilyMember <FamilyMember
key={`${user.name}-${index}`} key={`${user.name}-${index}`}
type="button"
onClick={() => { onClick={() => {
setEditingIndex(index); setEditingIndex(index);
setIsModalOpen(true); setIsModalOpen(true);
@ -65,7 +64,10 @@ export function FamilySection({
handleDelete(index); 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> </DeleteButton>
</FamilyMember> </FamilyMember>
))} ))}
@ -115,17 +117,18 @@ const FamilyList = styled.div`
gap: 12px; gap: 12px;
`; `;
const FamilyMember = styled.button` const FamilyMember = styled.div`
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: stretch;
padding: 12px 16px; padding: 12px 20px;
border: 1px solid ${({ theme }) => theme.colors.border}; border: 1px solid ${({ theme }) => theme.colors.border};
border-radius: 12px; border-radius: 12px;
background: #fff; background: #fff;
transition: 0.2s; transition: 0.2s;
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
height: 100%;
&:hover { &:hover {
border-color: #c0c0c0; border-color: #c0c0c0;
@ -169,28 +172,42 @@ const Info = styled.div`
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between;
`; `;
const Name = styled.div` const Name = styled.div`
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: ${({ theme }) => theme.colors.darkGreen}; color: ${({ theme }) => theme.colors.darkGreen};
${props => props.theme.media.mobile(css`
padding-top: 1.5px;
font-size: 14px;
`)}
`; `;
const Age = styled.div` const Age = styled.div`
font-size: 16px; font-size: 16px;
font-weight: 350; font-weight: 350;
color: ${({ theme }) => theme.colors.gray}; color: ${({ theme }) => theme.colors.gray};
* {
color: inherit;
}
${props => props.theme.media.mobile(css`
font-size: 14px;
padding-bottom: 4.5px;
`)}
`; `;
const DeleteButton = styled.button` const DeleteButton = styled.button`
color: #d1d5db; color: #d1d5db;
cursor: pointer; cursor: pointer;
font-size: 18px; font-size: 18px;
padding: 0 8px;
transition: 0.2s; transition: 0.2s;
background: transparent; background: transparent;
border: 0; border: 0;
width: 20px;
height: 20px;
align-self: center;
&:hover { &:hover {
color: #e53935; color: #e53935;
@ -220,6 +237,10 @@ const AddButton = styled.button`
background-color: transparent; background-color: transparent;
border: 0; border: 0;
span {
color:inherit;
}
&:hover { &:hover {
background-color: #F7FBF6; background-color: #F7FBF6;
} }

View File

@ -110,7 +110,7 @@ export function PersonModal({
} }
return ( return (
<Overlay onClick={onClose}> <Overlay onClick={() => showExpanded ? setShowExpanded('') : onClose()}>
{showExpanded ? ( {showExpanded ? (
<Content onClick={event => event.stopPropagation()}> <Content onClick={event => event.stopPropagation()}>
<Header> <Header>
@ -245,6 +245,7 @@ const Overlay = styled.div`
justify-content: center; justify-content: center;
z-index: 1000; z-index: 1000;
padding: 20px; padding: 20px;
overflow: auto;
`; `;
const Content = styled.div` const Content = styled.div`
@ -321,10 +322,6 @@ const FormRow = styled.div`
${FormGroup} { ${FormGroup} {
margin-bottom: 0; margin-bottom: 0;
} }
@media (max-width: 600px) {
flex-direction: column;
}
`; `;
const Hint = styled.span` const Hint = styled.span`
@ -365,10 +362,6 @@ const PrefsGrid = styled.div`
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
padding-top: 16px; padding-top: 16px;
min-height: 200px; min-height: 200px;
@media (max-width: 600px) {
flex-direction: column;
}
`; `;
const SaveButton = styled.button` const SaveButton = styled.button`

View File

@ -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` const Column = styled.div`
flex: 1; flex: 1;
position: relative; position: relative;

View File

@ -4,6 +4,8 @@ import { ThemeProvider } from 'styled-components';
import { App } from './App'; import { App } from './App';
import { GlobalStyle } from './styles/GlobalStyle'; import { GlobalStyle } from './styles/GlobalStyle';
import { theme } from './styles/theme'; import { theme } from './styles/theme';
import { Provider } from 'react-redux'
import {store} from "./store";
const root = document.getElementById('root'); const root = document.getElementById('root');
@ -11,11 +13,14 @@ if (!root) {
throw new Error('Root element #root was not found'); throw new Error('Root element #root was not found');
} }
createRoot(root).render( createRoot(root).render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider theme={theme}> <Provider store={store}>
<GlobalStyle /> <ThemeProvider theme={theme}>
<App /> <GlobalStyle />
</ThemeProvider> <App />
</ThemeProvider>
</Provider>
</React.StrictMode>, </React.StrictMode>,
); );

28
src/store/family.ts Normal file
View 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
View 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;

View File

@ -7,6 +7,8 @@ declare module "styled-components" {
export interface DefaultTheme extends ThemeType { export interface DefaultTheme extends ThemeType {
colors: ThemeType['colors']; colors: ThemeType['colors'];
radius: ThemeType['radius']; radius: ThemeType['radius'];
breakpoints: ThemeType['breakpoints'];
media: ThemeType['media'];
} }
} }

View File

@ -1,4 +1,4 @@
import { createGlobalStyle } from 'styled-components'; import {createGlobalStyle, css} from 'styled-components';
export const GlobalStyle = createGlobalStyle` export const GlobalStyle = createGlobalStyle`
@font-face { @font-face {
@ -12,13 +12,16 @@ export const GlobalStyle = createGlobalStyle`
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
font-family: 'Roboto', 'FallbackFont', system-ui, -apple-system, 'Segoe UI', Helvetica, sans-serif; font-family: 'Roboto', 'FallbackFont', system-ui, -apple-system, 'Segoe UI', Helvetica, sans-serif;
color:#0C3B2E;
} }
html { html {
overflow: hidden; overflow: hidden;
height: 100%;
} }
body { body {
height: 100%;
background-color: ${({ theme }) => theme.colors.pageBg}; background-color: ${({ theme }) => theme.colors.pageBg};
background-image: url('/images/background-raspberry.png'); background-image: url('/images/background-raspberry.png');
background-repeat: no-repeat; background-repeat: no-repeat;
@ -26,6 +29,17 @@ export const GlobalStyle = createGlobalStyle`
background-size: contain; background-size: contain;
background-position: left top; background-position: left top;
min-height: 100vh; min-height: 100vh;
${({ theme }) => theme.media.mobile(css`
background-position: center 72%;
`)};
}
#root {
height: 100%;
${({ theme }) => theme.media.mobile(css`
overflow: auto;
`)};
} }
button { button {

View File

@ -1,3 +1,6 @@
import {css} from "styled-components";
import type { RuleSet } from 'styled-components';
export const theme = { export const theme = {
colors: { colors: {
darkGreen: '#0C3B2E', darkGreen: '#0C3B2E',
@ -16,4 +19,19 @@ export const theme = {
button: '12px', button: '12px',
small: '8px', 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
}; };