From c5c86890829d60d6fed491a8148ebc935b7d476b Mon Sep 17 00:00:00 2001 From: nflnkr Date: Mon, 26 Jun 2023 19:13:29 +0300 Subject: [PATCH 1/4] fix free tariff card position --- src/pages/Tariffs/FreeTariffCard.tsx | 99 +++++++--------------------- src/pages/Tariffs/TariffsPage.tsx | 78 +++++++++------------- 2 files changed, 57 insertions(+), 120 deletions(-) diff --git a/src/pages/Tariffs/FreeTariffCard.tsx b/src/pages/Tariffs/FreeTariffCard.tsx index 384b409..a78865a 100644 --- a/src/pages/Tariffs/FreeTariffCard.tsx +++ b/src/pages/Tariffs/FreeTariffCard.tsx @@ -1,78 +1,29 @@ -import { Box, SxProps, Theme } from "@mui/material"; import Typography from "@mui/material/Typography"; -import { useNavigate } from "react-router-dom"; +import TariffCard from "./TariffCard"; +import NumberIcon from "@root/components/NumberIcon"; -import CustomButton from "@components/CustomButton"; -import { IconsCreate } from "../../lib/IconsCreate"; - -import ZeroIcons from "../../assets/Icons/ZeroIcons.svg"; - -interface Props { - headerText: string; - text: string; - money?: string; - sx: SxProps; - href: string; -} - -export default function FreeTariffCard({ headerText, text, sx, href, money = "0" }: Props) { - const navigate = useNavigate(); - const icon = ; - - return ( - - - {icon} - - {money} руб. - - - - - {headerText} - - {text} - navigate(href)} - variant="outlined" - sx={{ - color: "white", - borderColor: "white", - mt: "33px", - }} - > - Подробнее - - - ); +export default function FreeTariffCard() { + return ( + } + buttonText="Выбрать" + headerText="бесплатно" + text="Текст-заполнитель — это текст, который имеет " + onButtonClick={undefined} + price={0 руб.} + sx={{ + backgroundColor: "#7E2AEA", + color: "white", + }} + buttonSx={{ + color: "white", + borderColor: "white", + }} + /> + ); } diff --git a/src/pages/Tariffs/TariffsPage.tsx b/src/pages/Tariffs/TariffsPage.tsx index af6cfb4..51786c6 100644 --- a/src/pages/Tariffs/TariffsPage.tsx +++ b/src/pages/Tariffs/TariffsPage.tsx @@ -12,6 +12,7 @@ import NumberIcon from "@root/components/NumberIcon"; import { currencyFormatter } from "@root/utils/currencyFormatter"; import { calcTariffPrices } from "@root/utils/calcTariffPrices"; import { getMessageFromFetchError } from "@frontend/kitui"; +import FreeTariffCard from "./FreeTariffCard"; export default function TariffPage() { @@ -40,6 +41,36 @@ export default function TariffPage() { return tariff.privilegies.map(p => p.type).includes("day") === (unit === "time"); }); + const tariffElements = filteredTariffs.map((tariff, index) => { + const { price, priceWithDiscounts } = calcTariffPrices(tariff); + + return ( + } + buttonText="Выбрать" + headerText={tariff.name} + text={tariff.privilegies.map(p => `${p.name} - ${p.amount}`)} + onButtonClick={undefined} + price={<> + {price !== undefined && price !== priceWithDiscounts && + {currencyFormatter.format(price / 100)} + } + {priceWithDiscounts !== undefined && + {currencyFormatter.format(priceWithDiscounts / 100)} + } + } + /> + ); + }); + + if (tariffElements.length < 6) tariffElements.push(); + else tariffElements.splice(5, 0, ); + return ( - {filteredTariffs.map((tariff, index) => { - const { price, priceWithDiscounts } = calcTariffPrices(tariff); - - return ( - } - buttonText="Выбрать" - headerText={tariff.name} - text={tariff.privilegies.map(p => `${p.name} - ${p.amount}`)} - onButtonClick={undefined} - price={<> - {price !== undefined && price !== priceWithDiscounts && - {currencyFormatter.format(price / 100)} - } - {priceWithDiscounts !== undefined && - {currencyFormatter.format(priceWithDiscounts / 100)} - } - } - /> - ); - })} - } - buttonText="Выбрать" - headerText="бесплатно" - text="Текст-заполнитель — это текст, который имеет " - onButtonClick={undefined} - price={0 руб.} - sx={{ - backgroundColor: "#7E2AEA", - color: "white", - }} - buttonSx={{ - color: "white", - borderColor: "white", - }} - /> + {tariffElements} ); From fb552952a391b436761b10923d14a61d8a8f971d Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 30 Jun 2023 18:28:10 +0300 Subject: [PATCH 2/4] minor refactor --- src/model/tariff.ts | 1 + src/pages/AccountSettings/AccountSettings.tsx | 2 +- src/pages/Tariffs/FreeTariffCard.tsx | 2 ++ src/pages/Tariffs/TariffCard.tsx | 1 - src/pages/Tariffs/Tariffs.tsx | 3 +++ src/stores/customTariffs.ts | 4 ++-- src/stores/discounts.ts | 2 +- src/stores/messages.ts | 6 +++--- src/stores/tickets.ts | 2 +- src/stores/unauthTicket.ts | 2 +- 10 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/model/tariff.ts b/src/model/tariff.ts index 037434c..265595f 100644 --- a/src/model/tariff.ts +++ b/src/model/tariff.ts @@ -9,6 +9,7 @@ export interface GetTariffsResponse { export interface Tariff { _id: string; name: string; + /** Кастомная цена, undefined если isCustom === true */ price?: number; isCustom: boolean; privilegies: PrivilegeWithAmount[]; diff --git a/src/pages/AccountSettings/AccountSettings.tsx b/src/pages/AccountSettings/AccountSettings.tsx index b0dd641..d5c2edb 100644 --- a/src/pages/AccountSettings/AccountSettings.tsx +++ b/src/pages/AccountSettings/AccountSettings.tsx @@ -29,7 +29,7 @@ export default function AccountSettings() { }; function handleSendDataClick() { - sendUserData().then(result => { + sendUserData().then(() => { enqueueSnackbar("Информация обновлена"); }).catch(error => { const message = getMessageFromFetchError(error); diff --git a/src/pages/Tariffs/FreeTariffCard.tsx b/src/pages/Tariffs/FreeTariffCard.tsx index a78865a..93bffa3 100644 --- a/src/pages/Tariffs/FreeTariffCard.tsx +++ b/src/pages/Tariffs/FreeTariffCard.tsx @@ -4,6 +4,7 @@ import NumberIcon from "@root/components/NumberIcon"; export default function FreeTariffCard() { + return ( navigate("time")} + sx={{ maxWidth: "360px" }} /> } @@ -53,6 +54,7 @@ export default function Tariffs() { text="200 шаблонов, 1000 шаблонов, 5000 шаблонов, 10 000 шаблонов" buttonText="Подробнее" onButtonClick={() => navigate("volume")} + sx={{ maxWidth: "360px" }} /> } @@ -60,6 +62,7 @@ export default function Tariffs() { text="Текст-заполнитель — это текст, который имеет " buttonText="Подробнее" onButtonClick={() => navigate("/tariffconstructor")} + sx={{ maxWidth: "360px" }} /> diff --git a/src/stores/customTariffs.ts b/src/stores/customTariffs.ts index 69fba5b..8312e31 100644 --- a/src/stores/customTariffs.ts +++ b/src/stores/customTariffs.ts @@ -21,7 +21,7 @@ export const useCustomTariffsStore = create()( summaryPrice: {}, }), { - name: "Custom tariffs store", + name: "Custom tariffs", enabled: process.env.NODE_ENV === "development", }), { @@ -42,7 +42,7 @@ export const setCustomTariffsUserValue = ( value: number, ) => useCustomTariffsStore.setState( produce(state => { - if (!state.userValues[serviceKey]) state.userValues[serviceKey] = {}; + state.userValues[serviceKey] ??= {}; state.userValues[serviceKey][privilegeId] = value; const sum = state.customTariffs[serviceKey].reduce((acc, tariff) => { diff --git a/src/stores/discounts.ts b/src/stores/discounts.ts index 3f33ca0..1fd6cc2 100644 --- a/src/stores/discounts.ts +++ b/src/stores/discounts.ts @@ -14,7 +14,7 @@ export const useMessageStore = create()( discounts: mockDiscounts }), { - name: "Message store (marketplace)", + name: "Discounts", enabled: process.env.NODE_ENV === "development", } ) diff --git a/src/stores/messages.ts b/src/stores/messages.ts index 3776255..ae31944 100644 --- a/src/stores/messages.ts +++ b/src/stores/messages.ts @@ -21,16 +21,16 @@ export const useMessageStore = create()( isPreventAutoscroll: false, }), { - name: "Message store (marketplace)" + name: "Messages" } ) ); export const addOrUpdateMessages = (receivedMessages: TicketMessage[]) => { - const state = useMessageStore.getState(); + const messages = useMessageStore.getState().messages; const messageIdToMessageMap: { [messageId: string]: TicketMessage; } = {}; - [...state.messages, ...receivedMessages].forEach(message => messageIdToMessageMap[message.id] = message); + [...messages, ...receivedMessages].forEach(message => messageIdToMessageMap[message.id] = message); const sortedMessages = Object.values(messageIdToMessageMap).sort(sortMessagesByTime); diff --git a/src/stores/tickets.ts b/src/stores/tickets.ts index 4211229..dcbf795 100644 --- a/src/stores/tickets.ts +++ b/src/stores/tickets.ts @@ -20,7 +20,7 @@ export const useTicketStore = create()( ticketsPerPage: 10, }), { - name: "Tickets store (marketplace)" + name: "Tickets" } ) ); diff --git a/src/stores/unauthTicket.ts b/src/stores/unauthTicket.ts index 36f8263..c6a3b28 100644 --- a/src/stores/unauthTicket.ts +++ b/src/stores/unauthTicket.ts @@ -29,7 +29,7 @@ export const useUnauthTicketStore = create()( isPreventAutoscroll: false, }), { - name: "Unauth ticket store" + name: "Unauth tickets" } ), { From 9332a0f1151e07a0d11ad3cc7ebba2a0b3cce84b Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 30 Jun 2023 18:35:31 +0300 Subject: [PATCH 3/4] fix cart --- src/api/cart.ts | 20 ++ src/api/tariff.ts | 18 +- src/components/CustomWrapperDrawer.tsx | 279 +++++++++--------- src/components/Drawers.tsx | 377 +++++++++++-------------- src/components/Navbar/NavbarFull.tsx | 12 - src/components/TotalPrice.tsx | 27 +- src/model/cart.ts | 19 ++ src/model/customTariffs.ts | 6 +- src/pages/Basket/Basket.tsx | 104 +++---- src/pages/Basket/CustomWrapper.tsx | 311 ++++++++++---------- src/pages/Tariffs/TariffsPage.tsx | 27 +- src/stores/cart.ts | 80 ++++++ src/stores/tariffs.ts | 28 +- src/stores/user.ts | 44 ++- src/utils/calcCart.ts | 55 ++++ src/utils/calcTariffPrices.ts | 19 +- src/utils/hooks/useCart.ts | 46 +++ src/utils/hooks/useTariffs.ts | 25 +- 18 files changed, 848 insertions(+), 649 deletions(-) create mode 100644 src/api/cart.ts create mode 100644 src/model/cart.ts create mode 100644 src/stores/cart.ts create mode 100644 src/utils/calcCart.ts create mode 100644 src/utils/hooks/useCart.ts diff --git a/src/api/cart.ts b/src/api/cart.ts new file mode 100644 index 0000000..5007d5a --- /dev/null +++ b/src/api/cart.ts @@ -0,0 +1,20 @@ +import { makeRequest } from "@frontend/kitui"; + + +const apiUrl = process.env.NODE_ENV === "production" ? "/customer" : "https://hub.pena.digital/customer"; + +export function patchCart(tariffId: string) { + return makeRequest({ + url: apiUrl + `/cart?id=${tariffId}`, + method: "PATCH", + useToken: true, + }); +} + +export function deleteCart(tariffId: string) { + return makeRequest({ + url: apiUrl + `/cart?id=${tariffId}`, + method: "DELETE", + useToken: true, + }); +} \ No newline at end of file diff --git a/src/api/tariff.ts b/src/api/tariff.ts index 620493f..c5ddc7c 100644 --- a/src/api/tariff.ts +++ b/src/api/tariff.ts @@ -1,15 +1,21 @@ import { makeRequest } from "@frontend/kitui"; -import { CustomTariff } from "@root/model/customTariffs"; -import { PrivilegeWithoutPrice } from "@root/model/privilege"; +import { CreateTariffBody, CustomTariff } from "@root/model/customTariffs"; +import { Tariff } from "@root/model/tariff"; -export function createTariff< - T = Omit & { privilegies: PrivilegeWithoutPrice[]; } ->(tariff: T) { - return makeRequest({ +export function createTariff(tariff: CreateTariffBody) { + return makeRequest({ url: `https://admin.pena.digital/strator/tariff`, method: "post", useToken: true, body: tariff, }); +} + +export function getTariffById(tariffId:string){ + return makeRequest({ + url: `https://admin.pena.digital/strator/tariff/${tariffId}`, + method: "get", + useToken: true, + }); } \ No newline at end of file diff --git a/src/components/CustomWrapperDrawer.tsx b/src/components/CustomWrapperDrawer.tsx index a16ad96..46a002d 100644 --- a/src/components/CustomWrapperDrawer.tsx +++ b/src/components/CustomWrapperDrawer.tsx @@ -2,156 +2,159 @@ import { useState } from "react"; import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material"; import ClearIcon from "@mui/icons-material/Clear"; -import { basketStore } from "@root/stores/BasketStore"; import { cardShadow } from "@root/utils/themes/shadow"; +import { ServiceCartData } from "@root/model/cart"; +import { currencyFormatter } from "@root/utils/currencyFormatter"; +import { removeTariffFromCart } from "@root/stores/user"; +import { enqueueSnackbar } from "notistack"; +import { getMessageFromFetchError } from "@frontend/kitui"; + + +const name: Record = { templategen: "Шаблонизатор", squiz: "Опросник", reducer: "Скоращатель ссылок" }; interface Props { - type: "templ" | "squiz" | "reducer"; - content: { - name: string; - desc: string; - id: string; - privelegeid: string; - amount: number; - price: number; - }[]; + serviceData: ServiceCartData; } -export default function CustomWrapperDrawer({ type, content }: Props) { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const upSm = useMediaQuery(theme.breakpoints.up("sm")); - const [isExpanded, setIsExpanded] = useState(false); +export default function CustomWrapperDrawer({ serviceData }: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const upSm = useMediaQuery(theme.breakpoints.up("sm")); + const [isExpanded, setIsExpanded] = useState(false); - const { remove } = basketStore(); + function handleItemDeleteClick(tariffId: string) { + removeTariffFromCart(tariffId).then(() => { + enqueueSnackbar("Тариф удален"); + }).catch(error => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }); + } - const totalSum = Object.values(content).reduce((accamulator, { price }) => (accamulator += price), 0); - const name: Record = { templ: "Шаблонизатор", squiz: "Опросник", reducer: "Скоращатель ссылок" }; - - return ( - - + return ( setIsExpanded((prev) => !prev)} - sx={{ - height: "72px", - - display: "flex", - alignItems: "center", - justifyContent: "space-between", - cursor: "pointer", - userSelect: "none", - }} + sx={{ + overflow: "hidden", + borderRadius: "12px", + boxShadow: cardShadow, + }} > - - {name[type]} - - - - - {totalSum} руб. - - - - {isExpanded && - Object.values(content).map(({ desc, id, privelegeid, amount, price }, index) => ( - - - {desc} - - - setIsExpanded((prev) => !prev)} + sx={{ + height: "72px", + + display: "flex", + alignItems: "center", + justifyContent: "space-between", + cursor: "pointer", + userSelect: "none", + }} > - {price} руб. - + + {name[serviceData.serviceKey]} + - remove(type, id)} - component={ClearIcon} - /> - + + + {currencyFormatter.format(serviceData.price / 100)} + + + + + {isExpanded && + serviceData.privileges.map(privilege => ( + + + {privilege.name} + + + + {currencyFormatter.format(privilege.price / 100)} + + + handleItemDeleteClick(privilege.tariffId)} + component={ClearIcon} + /> + + + ))} - ))} - - - ); + + ); } diff --git a/src/components/Drawers.tsx b/src/components/Drawers.tsx index f4a6768..f10d6f5 100644 --- a/src/components/Drawers.tsx +++ b/src/components/Drawers.tsx @@ -1,228 +1,185 @@ -import React, { useEffect } from "react"; import { Typography, Drawer, useMediaQuery, useTheme, Box, IconButton, SvgIcon, Icon } from "@mui/material"; import { IconsCreate } from "@root/lib/IconsCreate"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ClearIcon from "@mui/icons-material/Clear"; - -import { basketStore } from "@root/stores/BasketStore"; -import { useState } from "react"; - import BasketIcon from "../assets/Icons/BasketIcon.svg"; import SectionWrapper from "./SectionWrapper"; import CustomWrapperDrawer from "./CustomWrapperDrawer"; import CustomButton from "./CustomButton"; import { useNavigate } from "react-router"; +import { useCart } from "@root/utils/hooks/useCart"; +import { currencyFormatter } from "@root/utils/currencyFormatter"; +import { closeCartDrawer, openCartDrawer, useCartStore } from "@root/stores/cart"; -interface TabPanelProps { - index: number; - value: number; - children?: React.ReactNode; - mt: string; -} - -type BasketItem = { - name: string; - desc: string; - id: string; - privelegeid: string; - amount: number; - price: number; -}[]; - -function TabPanel({ index, value, children, mt }: TabPanelProps) { - return ( - - ); -} export default function Drawers() { - const [tabIndex, setTabIndex] = useState(0); - const [basketQuantity, setBasketQuantity] = useState(); - const navigate = useNavigate(); - const { templ, squiz, reducer, open, openDrawer } = basketStore(); - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); + const navigate = useNavigate(); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const isDrawerOpen = useCartStore(state => state.isDrawerOpen); + const cart = useCart(); - const newArray: BasketItem = [...Object.values(templ), ...Object.values(squiz), ...Object.values(reducer)]; - const sum = newArray.reduce((accamulator, { price }) => (accamulator += price), 0); - - useEffect(() => { - setBasketQuantity(Object.keys(templ).length + Object.keys(squiz).length + Object.keys(reducer).length); - }, [templ, squiz, reducer]); - - return ( - - - - - {basketQuantity && ( - - - {basketQuantity} - - - )} - - - - - {!upMd && ( - - - - )} - - Корзина + return ( + + + - - - - - {Object.keys(templ).length > 0 ? ( - - ) : ( - <> - )} - {Object.keys(squiz).length > 0 ? ( - - ) : ( - <> - )} - {Object.keys(reducer).length > 0 ? ( - - ) : ( - <> - )} - - - - - Итоговая цена - - - Текст-заполнитель — это текст, который имеет Текст-заполнитель — это текст, который имеет - Текст-заполнитель — это текст, который имеет Текст-заполнитель — это текст, который имеет - Текст-заполнитель - - - - - - 20 190 руб. - - - {sum} руб. - - - navigate("/basket")} - sx={{ - mt: "25px", - backgroundColor: theme.palette.brightPurple.main, - }} > - Оплатить - - - - - - - - ); + + {cart.itemCount} + + + )} + + + + + {!upMd && ( + + + + )} + + Корзина + + + + + + + {cart.services.map(serviceData => + + )} + + + + Итоговая цена + + + Текст-заполнитель — это текст, который имеет Текст-заполнитель — это текст, который имеет + Текст-заполнитель — это текст, который имеет Текст-заполнитель — это текст, который имеет + Текст-заполнитель + + + + + + {currencyFormatter.format(cart.priceBeforeDiscounts / 100)} + + + {currencyFormatter.format(cart.priceAfterDiscounts / 100)} + + + navigate("/basket")} + sx={{ + mt: "25px", + backgroundColor: theme.palette.brightPurple.main, + }} + > + Оплатить + + + + + + + + ); } diff --git a/src/components/Navbar/NavbarFull.tsx b/src/components/Navbar/NavbarFull.tsx index 50de6a4..0fe0cfa 100644 --- a/src/components/Navbar/NavbarFull.tsx +++ b/src/components/Navbar/NavbarFull.tsx @@ -1,10 +1,6 @@ import { Link, useLocation, useNavigate } from "react-router-dom"; -import { useEffect } from "react"; import { Box, Button, Container, IconButton, Typography, useTheme } from "@mui/material"; - import SectionWrapper from "../SectionWrapper"; -import { basketStore } from "@stores/BasketStore"; - import LogoutIcon from "../icons/LogoutIcon"; import WalletIcon from "../icons/WalletIcon"; import CustomAvatar from "./Avatar"; @@ -26,14 +22,6 @@ export default function NavbarFull({ isLoggedIn }: Props) { const navigate = useNavigate(); const user = useUserStore((state) => state.user); - const { open } = basketStore(); - - useEffect(() => { - if (location.pathname === "/basket") { - open(false); - } - }, [location.pathname, open]); - async function handleLogoutClick() { try { await logout(); diff --git a/src/components/TotalPrice.tsx b/src/components/TotalPrice.tsx index 9726396..b6c0f55 100644 --- a/src/components/TotalPrice.tsx +++ b/src/components/TotalPrice.tsx @@ -1,25 +1,16 @@ import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; -import { basketStore } from "@root/stores/BasketStore"; import CustomButton from "./CustomButton"; +import { currencyFormatter } from "@root/utils/currencyFormatter"; -export default function TotalPrice() { +interface Props { + price: number; + priceWithDiscounts: number; +} + +export default function TotalPrice({price,priceWithDiscounts}:Props) { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); - const { templ, squiz, reducer } = basketStore(); - - type BasketItem = { - name: string; - desc: string; - id: string; - privelegeid: string; - amount: number; - price: number; - }[]; - - const newArray: BasketItem = [...Object.values(templ), ...Object.values(squiz), ...Object.values(reducer)]; - - const sum = newArray.reduce((accamulator, { price }) => (accamulator += price), 0); return ( - 20 190 руб. + {currencyFormatter.format(price / 100)} - {sum} руб. + {currencyFormatter.format(priceWithDiscounts / 100)} ; @@ -15,4 +15,6 @@ export interface CustomTariff { updatedAt?: string; isDeleted?: boolean; createdAt?: string; -} \ No newline at end of file +} + +export type CreateTariffBody = Omit & { privilegies: PrivilegeWithoutPrice[]; }; \ No newline at end of file diff --git a/src/pages/Basket/Basket.tsx b/src/pages/Basket/Basket.tsx index c45c7d7..6945fd1 100644 --- a/src/pages/Basket/Basket.tsx +++ b/src/pages/Basket/Basket.tsx @@ -1,72 +1,54 @@ import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"; import SectionWrapper from "@components/SectionWrapper"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import { useState } from "react"; import TotalPrice from "@components/TotalPrice"; -import { basketStore } from "@root/stores/BasketStore"; import CustomWrapper from "./CustomWrapper"; import ComplexNavText from "@root/components/ComplexNavText"; +import { useCart } from "@root/utils/hooks/useCart"; -interface TabPanelProps { - index: number; - value: number; - children?: React.ReactNode; - mt: string; -} - -function TabPanel({ index, value, children, mt }: TabPanelProps) { - return ( - - ); -} export default function Basket() { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const cart = useCart(); - const [tabIndex, setTabIndex] = useState(0); - const { templ, squiz, reducer, open } = basketStore(); - - const handleChange = (event: React.SyntheticEvent, newValue: number) => { - setTabIndex(newValue); - }; - - open(false); - - return ( - - {upMd && } - - {!upMd && ( - - - - )} - - Корзина - - - - {Object.keys(templ).length > 0 ? : <>} - {Object.keys(squiz).length > 0 ? : <>} - {Object.keys(reducer).length > 0 ? : <>} - - - - ); + return ( + + {upMd && } + + {!upMd && ( + + + + )} + + Корзина + + + + {cart.services.map(serviceData => + + )} + + + + ); } diff --git a/src/pages/Basket/CustomWrapper.tsx b/src/pages/Basket/CustomWrapper.tsx index b8617b7..6ae0f9e 100644 --- a/src/pages/Basket/CustomWrapper.tsx +++ b/src/pages/Basket/CustomWrapper.tsx @@ -1,177 +1,176 @@ import { useState } from "react"; import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material"; - import ExpandIcon from "@components/icons/ExpandIcon"; - import ClearIcon from "@mui/icons-material/Clear"; -import { basketStore } from "@root/stores/BasketStore"; import { cardShadow } from "@root/utils/themes/shadow"; +import { ServiceCartData } from "@root/model/cart"; +import { currencyFormatter } from "@root/utils/currencyFormatter"; +import { removeTariffFromCart } from "@root/stores/user"; +import { enqueueSnackbar } from "notistack"; +import { getMessageFromFetchError } from "@frontend/kitui"; -interface Templ { - name: string; - desc: string; - id: string; - privelegeid?: string; - amount: number; - price: number; -} + +const name: Record = { templategen: "Шаблонизатор", squiz: "Опросник", reducer: "Сокращатель ссылок" }; interface Props { - type: "templ" | "squiz" | "reducer"; - content: Record; + serviceData: ServiceCartData; } -export default function CustomWrapper({ type, content }: Props) { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const upSm = useMediaQuery(theme.breakpoints.up("sm")); - const [isExpanded, setIsExpanded] = useState(false); +export default function CustomWrapper({ serviceData }: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const upSm = useMediaQuery(theme.breakpoints.up("sm")); + const [isExpanded, setIsExpanded] = useState(false); - const { remove } = basketStore(); + function handleItemDeleteClick(tariffId: string) { + removeTariffFromCart(tariffId).then(() => { + enqueueSnackbar("Тариф удален"); + }).catch(error => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }); + } - const totalSum = Object.values(content).reduce((accamulator, { price }) => (accamulator += price), 0); - const name: Record = { templ: "Шаблонизатор", squiz: "Опросник", reducer: "Скоращатель ссылок" }; - - return ( - - + return ( setIsExpanded((prev) => !prev)} - sx={{ - height: "72px", - px: "20px", - - display: "flex", - alignItems: "center", - justifyContent: "space-between", - cursor: "pointer", - userSelect: "none", - }} + sx={{ + overflow: "hidden", + borderRadius: "12px", + boxShadow: cardShadow, + }} > - - {name[type]} - - - - - {totalSum} руб. - - - - - - {isExpanded && - Object.values(content).map(({ desc, id, privelegeid, amount, price }, index) => ( - - - {desc} - - - - {price} руб. - - {upSm ? ( - remove(type, id)} + > + setIsExpanded((prev) => !prev)} sx={{ - color: theme.palette.text.secondary, - borderBottom: `1px solid ${theme.palette.text.secondary}`, - width: "max-content", - lineHeight: "19px", - cursor: "pointer", + height: "72px", + px: "20px", + + display: "flex", + alignItems: "center", + justifyContent: "space-between", + cursor: "pointer", + userSelect: "none", }} - > - Удалить - - ) : ( - remove(type, id)} component={ClearIcon}> - )} - + > + + {name[serviceData.serviceKey]} + + + + + {currencyFormatter.format(serviceData.price / 100)} + + + + + + + {isExpanded && + serviceData.privileges.map(privilege => ( + + + {privilege.name} + + + + {currencyFormatter.format(privilege.price / 100)} + + {upSm ? ( + handleItemDeleteClick(privilege.tariffId)} + sx={{ + color: theme.palette.text.secondary, + borderBottom: `1px solid ${theme.palette.text.secondary}`, + width: "max-content", + lineHeight: "19px", + cursor: "pointer", + }} + > + Удалить + + ) : ( + handleItemDeleteClick(privilege.tariffId)} component={ClearIcon}> + )} + + + ))} - ))} - - - ); + + ); } diff --git a/src/pages/Tariffs/TariffsPage.tsx b/src/pages/Tariffs/TariffsPage.tsx index 51786c6..2cf6403 100644 --- a/src/pages/Tariffs/TariffsPage.tsx +++ b/src/pages/Tariffs/TariffsPage.tsx @@ -1,10 +1,10 @@ -import { useCallback, useState } from "react"; +import { useState } from "react"; import { useLocation } from "react-router-dom"; import { Box, Tabs, Typography, useMediaQuery, useTheme } from "@mui/material"; import SectionWrapper from "@components/SectionWrapper"; import ComplexNavText from "@root/components/ComplexNavText"; import { useTariffs } from "@root/utils/hooks/useTariffs"; -import { setTariffs, useTariffStore } from "@root/stores/tariffs"; +import { updateTariffs, useTariffStore } from "@root/stores/tariffs"; import { enqueueSnackbar } from "notistack"; import { CustomTab } from "@root/components/CustomTab"; import TariffCard from "./TariffCard"; @@ -13,6 +13,7 @@ import { currencyFormatter } from "@root/utils/currencyFormatter"; import { calcTariffPrices } from "@root/utils/calcTariffPrices"; import { getMessageFromFetchError } from "@frontend/kitui"; import FreeTariffCard from "./FreeTariffCard"; +import { addTariffToCart } from "@root/stores/user"; export default function TariffPage() { @@ -27,16 +28,24 @@ export default function TariffPage() { const StepperText: Record = { volume: "Тарифы на объём", time: "Тарифы на время" }; useTariffs({ - url: "https://admin.pena.digital/strator/tariff", apiPage: 0, tariffsPerPage: 100, - onNewTariffs: setTariffs, - onError: useCallback(error => { + onNewTariffs: updateTariffs, + onError: error => { const errorMessage = getMessageFromFetchError(error); if (errorMessage) enqueueSnackbar(errorMessage); - }, []) + } }); + function handleTariffItemClick(tariffId: string) { + addTariffToCart(tariffId).then(() => { + enqueueSnackbar("Тариф добавлен в корзину"); + }).catch(error => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }); + } + const filteredTariffs = tariffs.filter(tariff => { return tariff.privilegies.map(p => p.type).includes("day") === (unit === "time"); }); @@ -55,7 +64,7 @@ export default function TariffPage() { buttonText="Выбрать" headerText={tariff.name} text={tariff.privilegies.map(p => `${p.name} - ${p.amount}`)} - onButtonClick={undefined} + onButtonClick={() => handleTariffItemClick(tariff._id)} price={<> {price !== undefined && price !== priceWithDiscounts && {currencyFormatter.format(price / 100)} @@ -68,8 +77,8 @@ export default function TariffPage() { ); }); - if (tariffElements.length < 6) tariffElements.push(); - else tariffElements.splice(5, 0, ); + if (tariffElements.length < 6) tariffElements.push(); + else tariffElements.splice(5, 0, ); return ( ; + cart: CartData; + isDrawerOpen: boolean; +} + +export const useCartStore = create()( + devtools( + (get, set) => ({ + cartTariffMap: {}, + cart: { + services: [], + priceBeforeDiscounts: 0, + priceAfterDiscounts: 0, + itemCount: 0, + }, + isDrawerOpen: false, + }), + { + name: "Cart", + enabled: process.env.NODE_ENV === "development", + trace: true, + actionsBlacklist: "rejected", + } + ) +); + +export const setCartTariffStatus = (tariffId: string, status: "loading" | "not found") => useCartStore.setState( + produce(state => { + state.cartTariffMap[tariffId] = status; + }), + false, + { + type: "setCartTariffStatus", + tariffId, + status, + } +); + +export const addCartTariffs = (tariffs: Tariff[]) => useCartStore.setState( + produce(state => { + tariffs.forEach(tariff => { + state.cartTariffMap[tariff._id] = tariff; + }); + const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object"); + state.cart = calcCart(cartTariffs); + }), + false, + { + type: tariffs.length > 0 ? "addCartTariffs" : "rejected", + tariffIds: tariffs.map(tariff => tariff._id), + } +); + +export const removeMissingTariffs = (tariffIds: string[]) => useCartStore.setState( + produce(state => { + for (const key in state.cartTariffMap) { + if (!tariffIds.includes(key)) delete state.cartTariffMap[key]; + } + const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object"); + state.cart = calcCart(cartTariffs); + }), + false, + { + type: "removeMissingTariffs", + tariffIds, + } +); + +export const openCartDrawer = () => useCartStore.setState({ isDrawerOpen: true }); + +export const closeCartDrawer = () => useCartStore.setState({ isDrawerOpen: false }); diff --git a/src/stores/tariffs.ts b/src/stores/tariffs.ts index cf334bd..9df24ce 100644 --- a/src/stores/tariffs.ts +++ b/src/stores/tariffs.ts @@ -13,10 +13,34 @@ export const useTariffStore = create()( tariffs: [], }), { - name: "Tariff store", + name: "Tariffs", enabled: process.env.NODE_ENV === "development", + trace: true, } ) ); -export const setTariffs = (tariffs: Tariff[]) => useTariffStore.setState({tariffs}) \ No newline at end of file +export const updateTariffs = (tariffs: TariffStore["tariffs"]) => useTariffStore.setState( + state => { + const tariffMap: Record = {}; + + [...state.tariffs, ...tariffs].forEach(tariff => tariffMap[tariff._id] = tariff); + + const sortedTariffs = Object.values(tariffMap).sort(sortTariffsByCreatedAt); + + return { tariffs: sortedTariffs }; + }, + false, + { + type: "updateTariffs", + tariffsLength: tariffs.length, + } +); + +function sortTariffsByCreatedAt(tariff1: Tariff, tariff2: Tariff) { + if (!tariff1.createdAt || !tariff2.createdAt) throw new Error("Trying to sort tariffs without createdAt field"); + + const date1 = new Date(tariff1.createdAt).getTime(); + const date2 = new Date(tariff2.createdAt).getTime(); + return date1 - date2; +} diff --git a/src/stores/user.ts b/src/stores/user.ts index a7c5596..88b0fc6 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -6,6 +6,7 @@ import { StringSchema, string } from "yup"; import { patchUser } from "@root/api/user"; import { UserAccount, UserAccountSettingsFieldStatus, UserName, VerificationStatus } from "@root/model/account"; import { patchUserAccount } from "@root/api/account"; +import { deleteCart, patchCart } from "@root/api/cart"; interface UserStore { @@ -60,7 +61,7 @@ const initialState: UserStore = { "ИНН": { ...defaultDocument }, "Устав": { ...defaultDocument }, "Свидетельство о регистрации НКО": { ...defaultDocument }, - } + }, }; export const useUserStore = create()( @@ -68,8 +69,9 @@ export const useUserStore = create()( devtools( (set, get) => initialState, { - name: "User store", + name: "User", enabled: process.env.NODE_ENV === "development", + trace: true, } ), { @@ -108,6 +110,17 @@ export const setUserAccount = (user: UserAccount) => useUserStore.setState( state.settingsFields.secondname.value = user?.name.secondname ?? ""; state.settingsFields.middlename.value = user?.name.middlename ?? ""; state.settingsFields.orgname.value = user?.name.orgname ?? ""; + }), + false, + { + type: "setUserAccount", + payload: user, + } +); + +export const setCart = (cart: string[]) => useUserStore.setState( + produce(state => { + if (state.userAccount) state.userAccount.cart = cart; }) ); @@ -203,7 +216,7 @@ export const setSettingsField = ( state.settingsFields[fieldName].error = errorMessage; state.settingsFields.hasError = Object.values(state.settingsFields).reduce((acc: boolean, field) => { - if (typeof field == "boolean") return acc; + if (typeof field === "boolean") return acc; if (field.error !== null) return true; return acc; @@ -239,13 +252,20 @@ export const sendUserData = async () => { orgname: state.settingsFields.orgname.value, }; - const [user, userAccount] = await Promise.all([ + await Promise.all([ isPatchingUser && patchUser(userPayload), isPatchingUserAccount && patchUserAccount(userAccountPayload), ]); +}; - // if (user) setUser(user); - // if (userAccount) setUserAccount(userAccount); +export const addTariffToCart = async (tariffId: string) => { + const result = await patchCart(tariffId); + setCart(result); +}; + +export const removeTariffFromCart = async (tariffId: string) => { + const result = await deleteCart(tariffId); + setCart(result); }; const validators: Record = { @@ -256,18 +276,18 @@ const validators: Record = { skipAbsent: true, test(value, ctx) { if (value !== undefined) { - if (value.length == 0) return true + if (value.length === 0) return true; if (!/^[.,:;-_+\d\w]+$/.test(value)) { - return ctx.createError({ message: 'Некорректные символы в пароле' }) + return ctx.createError({ message: 'Некорректные символы в пароле' }); } if (value.length > 0 && value.length < 8) { - return ctx.createError({ message: 'Минимум 8 символов' }) + return ctx.createError({ message: 'Минимум 8 символов' }); } } - return true + return true; } - }), - // min(8, "Минимум 8 символов").matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы в пароле"), + }), + // min(8, "Минимум 8 символов").matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы в пароле"), firstname: string(), secondname: string(), middlename: string(), diff --git a/src/utils/calcCart.ts b/src/utils/calcCart.ts new file mode 100644 index 0000000..880ca23 --- /dev/null +++ b/src/utils/calcCart.ts @@ -0,0 +1,55 @@ +import { mockDiscounts } from "@root/__mocks__/discounts"; +import { CartData, PrivilegeCartData } from "@root/model/cart"; +import { AnyDiscount } from "@root/model/discount"; +import { Tariff } from "@root/model/tariff"; +import { findPrivilegeDiscount, findServiceDiscount } from "./calcTariffPrices"; + + +export function calcCart(tariffs: Tariff[], discounts: AnyDiscount[] = mockDiscounts): CartData { + const cartData: CartData = { + services: [], + priceBeforeDiscounts: 0, + priceAfterDiscounts: 0, + itemCount: 0, + }; + + tariffs.forEach(tariff => { + if (tariff.price && tariff.price > 0) cartData.priceBeforeDiscounts += tariff.price; + + tariff.privilegies.forEach(privilege => { + let serviceData = cartData.services.find(service => service.serviceKey === privilege.serviceKey); + if (!serviceData) { + serviceData = { + serviceKey: privilege.serviceKey, + privileges: [], + price: 0, + }; + cartData.services.push(serviceData); + } + + let privilegePrice = privilege.amount * privilege.price; + + if (!tariff.price) cartData.priceBeforeDiscounts += privilegePrice; + + const privilegeDiscount = findPrivilegeDiscount(privilege, discounts); + if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor; + + const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice, discounts); + if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor; + + const privilegeData: PrivilegeCartData = { + tariffId: tariff._id, + privilegeId: privilege.privilegeId, + name: privilege.description, + price: privilegePrice, + }; + + serviceData.privileges.push(privilegeData); + serviceData.price += privilegePrice; + cartData.priceAfterDiscounts += privilegePrice; + cartData.itemCount++; + }); + }); + + return cartData; +} \ No newline at end of file diff --git a/src/utils/calcTariffPrices.ts b/src/utils/calcTariffPrices.ts index 8ffc999..c9d0587 100644 --- a/src/utils/calcTariffPrices.ts +++ b/src/utils/calcTariffPrices.ts @@ -1,22 +1,22 @@ import { Tariff } from "@root/model/tariff"; import { mockDiscounts } from "../__mocks__/discounts"; import { PrivilegeWithAmount } from "@root/model/privilege"; -import { PrivilegeDiscount, ServiceDiscount } from "../model/discount"; +import { AnyDiscount, PrivilegeDiscount, ServiceDiscount } from "../model/discount"; -export function calcTariffPrices(tariff: Tariff): { +export function calcTariffPrices(tariff: Tariff, discounts: AnyDiscount[] = mockDiscounts): { price: number | undefined; priceWithDiscounts: number | undefined; } { - let price = tariff.price ?? tariff.privilegies.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0); + let price = tariff.price || tariff.privilegies.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0); const priceWithDiscounts = tariff.privilegies.reduce((sum, privilege) => { let privilegePrice = privilege.amount * privilege.price; - const privilegeDiscount = findPrivilegeDiscount(privilege); + const privilegeDiscount = findPrivilegeDiscount(privilege, discounts); if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor; - const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice); + const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice, discounts); if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor; return sum + privilegePrice; @@ -28,8 +28,8 @@ export function calcTariffPrices(tariff: Tariff): { }; } -function findPrivilegeDiscount(privilege: PrivilegeWithAmount): PrivilegeDiscount | null { - const applicableDiscounts = mockDiscounts.filter((discount): discount is PrivilegeDiscount => { +export function findPrivilegeDiscount(privilege: PrivilegeWithAmount, discounts: AnyDiscount[]): PrivilegeDiscount | null { + const applicableDiscounts = discounts.filter((discount): discount is PrivilegeDiscount => { return ( discount.conditionType === "privilege" && privilege.privilegeId === discount.condition.privilege.id && @@ -46,11 +46,12 @@ function findPrivilegeDiscount(privilege: PrivilegeWithAmount): PrivilegeDiscoun return maxValueDiscount; } -function findServiceDiscount( +export function findServiceDiscount( serviceKey: string, currentPrice: number, + discounts: AnyDiscount[], ): ServiceDiscount | null { - const discountsForTariffService = mockDiscounts.filter((discount): discount is ServiceDiscount => { + const discountsForTariffService = discounts.filter((discount): discount is ServiceDiscount => { return ( discount.conditionType === "service" && discount.condition.service.id === serviceKey && diff --git a/src/utils/hooks/useCart.ts b/src/utils/hooks/useCart.ts new file mode 100644 index 0000000..b5f16a6 --- /dev/null +++ b/src/utils/hooks/useCart.ts @@ -0,0 +1,46 @@ +import { devlog } from "@frontend/kitui"; +import { getTariffById } from "@root/api/tariff"; +import { useTariffStore } from "@root/stores/tariffs"; +import { useUserStore } from "@root/stores/user"; +import { useEffect } from "react"; +import { addCartTariffs, removeMissingTariffs, setCartTariffStatus, useCartStore } from "@root/stores/cart"; +import { Tariff } from "@root/model/tariff"; + + +export function useCart() { + const tariffs = useTariffStore(state => state.tariffs); + const cartTariffMap = useCartStore(state => state.cartTariffMap); + const cartTariffIds = useUserStore(state => state.userAccount?.cart); + const cart = useCartStore(state => state.cart); + + useEffect(function addTariffsToCart() { + const knownTariffs: Tariff[] = []; + + cartTariffIds?.forEach(tariffId => { + if (typeof cartTariffMap[tariffId] === "object") return; + + const tariff = tariffs.find(tariff => tariff._id === tariffId); + if (tariff) return knownTariffs.push(tariff); + + if (!cartTariffMap[tariffId]) { + setCartTariffStatus(tariffId, "loading"); + + getTariffById(tariffId).then(tariff => { + devlog("Unlisted tariff", tariff); + addCartTariffs([tariff]); + }).catch(error => { + devlog(`Error fetching unlisted tariff ${tariffId}`, error); + setCartTariffStatus(tariffId, "not found"); + }); + } + }); + + if (knownTariffs.length > 0) addCartTariffs(knownTariffs); + }, [cartTariffIds, cartTariffMap, tariffs]); + + useEffect(function cleanUpCart() { + if (cartTariffIds) removeMissingTariffs(cartTariffIds); + }, [cartTariffIds]); + + return cart; +} \ No newline at end of file diff --git a/src/utils/hooks/useTariffs.ts b/src/utils/hooks/useTariffs.ts index 4f42053..c1471b4 100644 --- a/src/utils/hooks/useTariffs.ts +++ b/src/utils/hooks/useTariffs.ts @@ -1,39 +1,36 @@ import { devlog, makeRequest } from "@frontend/kitui"; import { GetTariffsResponse, Tariff } from "@root/model/tariff"; -import { useEffect, useState } from "react"; +import { useEffect, useRef } from "react"; -export function useTariffs({ url, tariffsPerPage, apiPage, onNewTariffs, onError }: { - url: string; +export function useTariffs({ baseUrl = "https://admin.pena.digital/strator/tariff", tariffsPerPage, apiPage, onNewTariffs, onError }: { + baseUrl?: string; tariffsPerPage: number; apiPage: number; onNewTariffs: (response: Tariff[]) => void; onError: (error: Error) => void; }) { - const [fetchState, setFetchState] = useState<"fetching" | "idle" | "all fetched">("idle"); + const onNewTariffsRef = useRef<(response: Tariff[]) => void>(onNewTariffs); + const onErrorRef = useRef<(error: Error) => void>(onError); useEffect(() => { const controller = new AbortController(); - setFetchState("fetching"); makeRequest({ - url, + url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`, method: "get", useToken: true, signal: controller.signal, }).then((result) => { - devlog("GetTicketsResponse", result); + devlog("Tariffs", result); if (result.tariffs.length > 0) { - onNewTariffs(result.tariffs); - setFetchState("idle"); - } else setFetchState("all fetched"); + onNewTariffsRef.current(result.tariffs); + } }).catch(error => { devlog("Error fetching tariffs", error); - onError(error); + onErrorRef.current(error); }); return () => controller.abort(); - }, [onError, onNewTariffs, apiPage, tariffsPerPage, url]); - - return fetchState; + }, [apiPage, tariffsPerPage, baseUrl]); } \ No newline at end of file From 55d0e0746f453257fbd7893fe79f30fab830f96b Mon Sep 17 00:00:00 2001 From: Nastya Date: Fri, 30 Jun 2023 20:16:17 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=9B=D0=9A=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F.=20=D0=97=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D1=89=D0=B5=D0=BD=D0=B0=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D0=BF=D0=BE=D0=B4=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B2=20=D1=80=D0=B5=D0=B3=D1=80=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20=D0=B8=20=D0=BD=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=BA=D1=83=20=D0=BA=D0=B0=D0=B1=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D1=82=D0=B0.=20=D0=93=D0=BB=D0=B0=D0=B7=D0=B8?= =?UTF-8?q?=D0=BA=D0=B8-=D1=81=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D1=88=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D1=8F.=20+7=20=D0=B2=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=D0=B5=20=D1=82=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D1=84=D0=BE=D0=BD=D0=B0=20=D0=BF=D1=80=D0=B8=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/passwordInput.tsx | 120 ++++++++++++++++++ src/pages/AccountSettings/AccountSettings.tsx | 6 +- src/pages/auth/Signin.tsx | 3 +- src/pages/auth/Signup.tsx | 11 +- 4 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 src/components/passwordInput.tsx diff --git a/src/components/passwordInput.tsx b/src/components/passwordInput.tsx new file mode 100644 index 0000000..2f13e85 --- /dev/null +++ b/src/components/passwordInput.tsx @@ -0,0 +1,120 @@ +import { + FormControl, + IconButton, + InputLabel, + SxProps, + TextField, + TextFieldProps, + Theme, + useMediaQuery, + useTheme, + } from "@mui/material"; + import * as React from 'react'; + import InputAdornment from '@mui/material/InputAdornment'; + import Visibility from '@mui/icons-material/Visibility'; + import VisibilityOff from '@mui/icons-material/VisibilityOff'; + + interface Props { + id: string; + label?: string; + bold?: boolean; + gap?: string; + color?: string; + FormInputSx?: SxProps; + TextfieldProps: TextFieldProps; + onChange: (e: React.ChangeEvent) => void; + } + + export default function ({ + id, + label, + bold = false, + gap = "10px", + onChange, + TextfieldProps, + color, + FormInputSx, + }: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + + const labelFont = upMd + ? bold + ? theme.typography.p1 + : { ...theme.typography.body1, fontWeight: 500 } + : theme.typography.body2; + + const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" }; + + const [showPassword, setShowPassword] = React.useState(false); + + const handleClickShowPassword = () => setShowPassword((show) => !show); + + const handleMouseDownPassword = (event: React.MouseEvent) => { + event.preventDefault(); + }; + + return ( + + + + {label} + + + + {showPassword ? : } + + + ), + sx: { + backgroundColor: color, + borderRadius: "8px", + height: "48px", + py: 0, + color: "black", + ...placeholderFont, + }, + }} + onChange={onChange} + type={showPassword ? 'text' : 'password'} + /> + + + ); + } + \ No newline at end of file diff --git a/src/pages/AccountSettings/AccountSettings.tsx b/src/pages/AccountSettings/AccountSettings.tsx index d5c2edb..083edf6 100644 --- a/src/pages/AccountSettings/AccountSettings.tsx +++ b/src/pages/AccountSettings/AccountSettings.tsx @@ -1,6 +1,7 @@ import { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"; import CustomButton from "@components/CustomButton"; import InputTextfield from "@components/InputTextfield"; +import PasswordInput from "@components/passwordInput"; import SectionWrapper from "@components/SectionWrapper"; import ComplexNavText from "@root/components/ComplexNavText"; import { openDocumentsDialog, sendUserData, setSettingsField, useUserStore } from "@root/stores/user"; @@ -46,7 +47,6 @@ export default function AccountSettings() { }} > - Настройки аккаунта - setSettingsField("password", e.target.value)} id="password" diff --git a/src/pages/auth/Signin.tsx b/src/pages/auth/Signin.tsx index af4d289..6943167 100644 --- a/src/pages/auth/Signin.tsx +++ b/src/pages/auth/Signin.tsx @@ -14,6 +14,7 @@ import { setUserId, useUserStore } from "@root/stores/user"; import { getMessageFromFetchError } from "@frontend/kitui"; import { makeRequest } from "@frontend/kitui"; import { cardShadow } from "@root/utils/themes/shadow"; +import PasswordInput from "@root/components/passwordInput"; interface Values { login: string; @@ -147,7 +148,7 @@ export default function SigninDialog() { label="Логин" gap={upMd ? "10px" : "10px"} /> - - -