Merge branch 'dev' into 'main'

Dev

See merge request frontend/marketplace!35
This commit is contained in:
Nastya 2023-08-12 22:02:48 +00:00
commit de0f243e62
35 changed files with 1279 additions and 963 deletions

@ -14,7 +14,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.17", "@frontend/kitui": "1.0.21",
"@mui/icons-material": "^5.10.14", "@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14", "@mui/material": "^5.10.14",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",

@ -1,10 +1,12 @@
import { Tariff, makeRequest } from "@frontend/kitui"; import { Tariff, makeRequest } from "@frontend/kitui";
import { CreateTariffBody, CustomTariff } from "@root/model/customTariffs"; import { CreateTariffBody } from "@root/model/customTariffs";
const apiUrl = process.env.NODE_ENV === "production" ? "/strator" : "https://hub.pena.digital/strator";
export function createTariff(tariff: CreateTariffBody) { export function createTariff(tariff: CreateTariffBody) {
return makeRequest<CreateTariffBody, CustomTariff>({ return makeRequest<CreateTariffBody, Tariff>({
url: `https://admin.pena.digital/strator/tariff`, url: `${apiUrl}/tariff`,
method: "post", method: "post",
useToken: true, useToken: true,
body: tariff, body: tariff,
@ -13,7 +15,7 @@ export function createTariff(tariff: CreateTariffBody) {
export function getTariffById(tariffId:string){ export function getTariffById(tariffId:string){
return makeRequest<never, Tariff>({ return makeRequest<never, Tariff>({
url: `https://admin.pena.digital/strator/tariff/${tariffId}`, url: `${apiUrl}/tariff/${tariffId}`,
method: "get", method: "get",
useToken: true, useToken: true,
}); });

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="black" xmlns="http://www.w3.org/2000/svg">
<path d="M0.400391 0.399902L19.6004 19.5999M0.400391 19.5999L19.6004 0.399902" />
</svg>

After

Width:  |  Height:  |  Size: 200 B

@ -0,0 +1,159 @@
import { useState } from "react";
import {
Box, Typography,
IconButton,
useMediaQuery,
useTheme
} from "@mui/material";
import ExpandIcon from "@components/icons/ExpandIcon";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { removeTariffFromCart } from "@root/stores/user";
import { enqueueSnackbar } from "notistack";
import { TariffCartData, getMessageFromFetchError } from "@frontend/kitui";
import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg";
interface Props {
tariffCartData: TariffCartData;
}
export default function CustomTariffAccordion({ tariffCartData }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const [isExpanded, setIsExpanded] = useState<boolean>(false);
function handleDeleteClick() {
removeTariffFromCart(tariffCartData.tariffId)
.then(() => {
enqueueSnackbar("Тариф удален");
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
}
return (
<Box
sx={{
overflow: "hidden",
}}
>
<Box
sx={{
backgroundColor: "white",
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
pr: "20px",
pl: "30px",
display: "flex",
gap: "15px",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<Box
sx={{
width: "50px",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
<Typography
sx={{
width: "100%",
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 400,
color: theme.palette.text.secondary,
px: 0,
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
Кастомный тариф
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
}}
>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(tariffCartData.price / 100)}
</Typography>
</Box>
<IconButton
onClick={handleDeleteClick}
sx={{ padding: 0, height: "22px", width: "22px" }}
>
<CrossIcon style={{ height: "22 px", width: "22px" }} />
</IconButton>
</Box>
{isExpanded && tariffCartData.privileges.map((privilege) => (
<Box
key={privilege.privilegeId}
sx={{
px: "50px",
py: upMd ? "15px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.grey3.main,
width: "100%",
}}
>
{privilege.description}
</Typography>
<Box
sx={{
width: upSm ? "140px" : "123px",
marginRight: upSm ? "65px" : 0,
}}
>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(privilege.price / 100)}
</Typography>
</Box>
</Box>
))}
</Box>
</Box>
);
}

@ -1,14 +1,30 @@
import { useState } from "react"; import { useState } from "react";
import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material"; import {
Box,
SvgIcon,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { removeTariffFromCart } from "@root/stores/user"; import { removeTariffFromCart } from "@root/stores/user";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui"; import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui";
import ExpandIcon from "@components/icons/ExpandIcon";
import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg";
const name: Record<string, string> = { templategen: "Шаблонизатор", squiz: "Опросник", reducer: "Скоращатель ссылок" }; import type { MouseEvent } from "react";
import CustomTariffAccordion from "@root/components/CustomTariffAccordion";
const name: Record<string, string> = {
templategen: "Шаблонизатор",
squiz: "Опросник",
reducer: "Скоращатель ссылок",
};
interface Props { interface Props {
serviceData: ServiceCartData; serviceData: ServiceCartData;
@ -21,19 +37,36 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
const [isExpanded, setIsExpanded] = useState<boolean>(false); const [isExpanded, setIsExpanded] = useState<boolean>(false);
function handleItemDeleteClick(tariffId: string) { function handleItemDeleteClick(tariffId: string) {
removeTariffFromCart(tariffId).then(() => { removeTariffFromCart(tariffId)
.then(() => {
enqueueSnackbar("Тариф удален"); enqueueSnackbar("Тариф удален");
}).catch(error => { })
.catch((error) => {
const message = getMessageFromFetchError(error); const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message); if (message) enqueueSnackbar(message);
}); });
} }
const deleteService = async (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
setIsExpanded(false);
for (const { tariffId } of serviceData.tariffs) {
try {
await removeTariffFromCart(tariffId);
} catch { }
}
enqueueSnackbar("Тарифы удален");
};
return ( return (
<Box <Box
sx={{ sx={{
overflow: "hidden", overflow: "hidden",
borderRadius: "12px", borderRadius: "12px",
width: "100%",
}} }}
> >
<Box <Box
@ -56,13 +89,21 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
onClick={() => setIsExpanded((prev) => !prev)} onClick={() => setIsExpanded((prev) => !prev)}
sx={{ sx={{
height: "72px", height: "72px",
display: "flex", display: "flex",
gap: "10px",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
cursor: "pointer", cursor: "pointer",
userSelect: "none", userSelect: "none",
}} }}
>
<ExpandIcon isExpanded={isExpanded} />
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
}}
> >
<Typography <Typography
sx={{ sx={{
@ -75,7 +116,6 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
> >
{name[serviceData.serviceKey]} {name[serviceData.serviceKey]}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -85,23 +125,43 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
}} }}
> >
<Typography <Typography
sx={{ pr: "11px", color: theme.palette.grey3.main, fontSize: upSm ? "20px" : "16px", fontWeight: 500 }} sx={{
color: theme.palette.grey3.main,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
> >
{currencyFormatter.format(serviceData.price / 100)} {currencyFormatter.format(serviceData.price / 100)}
</Typography> </Typography>
<Box
sx={{
paddingLeft: upSm ? "24px" : 0,
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
></Box>
</Box> </Box>
</Box> </Box>
<IconButton
onClick={deleteService}
sx={{
padding: "3px",
height: "30px",
width: "30px",
}}
>
<CrossIcon
style={{
height: "24px",
width: "24px",
stroke: "#7E2AEA",
}}
/>
</IconButton>
</Box>
{isExpanded && {isExpanded &&
serviceData.privileges.map(privilege => ( serviceData.tariffs.map(tariff => {
const privilege = tariff.privileges[0];
return tariff.privileges.length > 1 ? (
<CustomTariffAccordion
key={privilege.tariffId + privilege.privilegeId}
tariffCartData={tariff}
/>
) : (
<Box <Box
key={privilege.tariffId + privilege.privilegeId} key={privilege.tariffId + privilege.privilegeId}
sx={{ sx={{
@ -142,15 +202,19 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
> >
{currencyFormatter.format(privilege.price / 100)} {currencyFormatter.format(privilege.price / 100)}
</Typography> </Typography>
<SvgIcon <SvgIcon
sx={{ cursor: "pointer", color: "#7E2AEA" }} sx={{
cursor: "pointer",
width: "30px",
color: "#7E2AEA",
}}
onClick={() => handleItemDeleteClick(privilege.tariffId)} onClick={() => handleItemDeleteClick(privilege.tariffId)}
component={ClearIcon} component={ClearIcon}
/> />
</Box> </Box>
</Box> </Box>
))} );
})}
</Box> </Box>
</Box> </Box>
); );

@ -6,11 +6,9 @@ import {
useTheme, useTheme,
Box, Box,
IconButton, IconButton,
SvgIcon,
Badge, Badge,
} from "@mui/material"; } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ClearIcon from "@mui/icons-material/Clear";
import { useTickets } from "@frontend/kitui"; import { useTickets } from "@frontend/kitui";
import SectionWrapper from "./SectionWrapper"; import SectionWrapper from "./SectionWrapper";
import CustomWrapperDrawer from "./CustomWrapperDrawer"; import CustomWrapperDrawer from "./CustomWrapperDrawer";
@ -24,7 +22,6 @@ import {
openCartDrawer, openCartDrawer,
useCartStore, useCartStore,
} from "@root/stores/cart"; } from "@root/stores/cart";
import { useCustomTariffsStore } from "@root/stores/customTariffs";
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
import { import {
updateTickets, updateTickets,
@ -34,6 +31,7 @@ import {
import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg"; import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg";
import { ReactComponent as CartIcon } from "@root/assets/Icons/cart.svg"; import { ReactComponent as CartIcon } from "@root/assets/Icons/cart.svg";
import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg";
export default function Drawers() { export default function Drawers() {
const [openNotificationsModal, setOpenNotificationsModal] = const [openNotificationsModal, setOpenNotificationsModal] =
@ -44,12 +42,6 @@ export default function Drawers() {
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isDrawerOpen = useCartStore((state) => state.isDrawerOpen); const isDrawerOpen = useCartStore((state) => state.isDrawerOpen);
const cart = useCart(); const cart = useCart();
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(
(state) => state.summaryPriceBeforeDiscountsMap
);
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(
(state) => state.summaryPriceAfterDiscountsMap
);
const userAccount = useUserStore((state) => state.userAccount); const userAccount = useUserStore((state) => state.userAccount);
const { tickets, apiPage, ticketsPerPage } = useTicketStore((state) => state); const { tickets, apiPage, ticketsPerPage } = useTicketStore((state) => state);
@ -64,21 +56,12 @@ export default function Drawers() {
onError: () => {}, onError: () => {},
}); });
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce(
(a, e) => a + e,
0
);
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce(
(a, e) => a + e,
0
);
const notificationsCount = tickets.filter( const notificationsCount = tickets.filter(
({ user, top_message }) => user !== top_message.user_id ({ user, top_message }) => user !== top_message.user_id
).length; ).length;
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts + basePrice; const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts;
const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice; const totalPriceAfterDiscounts = cart.priceAfterDiscounts;
return ( return (
<Box sx={{ display: "flex", gap: "20px" }}> <Box sx={{ display: "flex", gap: "20px" }}>
@ -227,15 +210,23 @@ export default function Drawers() {
Корзина Корзина
</Typography> </Typography>
<IconButton onClick={closeCartDrawer} sx={{ p: 0 }}> <IconButton onClick={closeCartDrawer} sx={{ p: 0 }}>
<SvgIcon component={ClearIcon} /> <CrossIcon />
</IconButton> </IconButton>
</Box> </Box>
<Box sx={{ pl: "20px", pr: "20px" }}> <Box sx={{ pl: "20px", pr: "20px" }}>
{cart.services.map((serviceData) => ( {cart.services.map((serviceData) => (
<Box
key={serviceData.serviceKey}
sx={{
display: "flex",
alignItems: "center",
}}
>
<CustomWrapperDrawer <CustomWrapperDrawer
key={serviceData.serviceKey} key={serviceData.serviceKey}
serviceData={serviceData} serviceData={serviceData}
/> />
</Box>
))} ))}
<Box <Box
sx={{ sx={{

@ -14,7 +14,6 @@ export default function Menu() {
const location = useLocation(); const location = useLocation();
const color = location.pathname === "/" ? "white" : "black"; const color = location.pathname === "/" ? "white" : "black";
console.log(location)
const arrayMenu: MenuItem[] = [ const arrayMenu: MenuItem[] = [
{ {
@ -42,7 +41,8 @@ export default function Menu() {
overflow: "hidden", overflow: "hidden",
}} }}
> >
{location.pathname !== "/" ? arrayMenu.map(({ name, url, subMenu = [] }) => ( {location.pathname !== "/"
? arrayMenu.map(({ name, url, subMenu = [] }) => (
<Link <Link
key={name} key={name}
style={{ style={{
@ -53,6 +53,7 @@ export default function Menu() {
}} }}
to={url} to={url}
onMouseEnter={() => setActiveSubMenu(subMenu)} onMouseEnter={() => setActiveSubMenu(subMenu)}
state={{ previousUrl: location.pathname }}
> >
<Typography <Typography
color={ color={
@ -83,8 +84,7 @@ export default function Menu() {
> >
{name} {name}
</Typography> </Typography>
)) ))}
}
<Box <Box
sx={{ sx={{
zIndex: "10", zIndex: "10",
@ -96,7 +96,8 @@ export default function Menu() {
}} }}
onMouseLeave={() => setActiveSubMenu([])} onMouseLeave={() => setActiveSubMenu([])}
> >
{location.pathname !== "/" && activeSubMenu.map(({ name, url }) => ( {location.pathname !== "/" &&
activeSubMenu.map(({ name, url }) => (
<Link key={name} style={{ textDecoration: "none" }} to={url}> <Link key={name} style={{ textDecoration: "none" }} to={url}>
<Typography <Typography
color={ color={

@ -85,7 +85,7 @@ export default function DialogMenu({ handleClose }: DialogMenuProps) {
key={index} key={index}
component={Link} component={Link}
to={url} to={url}
state={user ? undefined : { backgroundLocation: location }} state={{ previousUrl: location.pathname }}
disableRipple disableRipple
variant="text" variant="text"
onClick={() => onClick={() =>
@ -134,6 +134,7 @@ export default function DialogMenu({ handleClose }: DialogMenuProps) {
}} }}
to={url} to={url}
onClick={closeDialogMenu} onClick={closeDialogMenu}
state={{ previousUrl: location.pathname }}
> >
<Typography <Typography
variant="body2" variant="body2"

@ -58,6 +58,7 @@ export default function NavbarFull({ isLoggedIn, children }: Props) {
disableGutters disableGutters
maxWidth={false} maxWidth={false}
sx={{ sx={{
zIndex: 1,
position: "fixed", position: "fixed",
top: "0", top: "0",
px: "16px", px: "16px",

@ -1,16 +1,26 @@
import { useTheme } from "@mui/material"; import { useTheme , Box} from "@mui/material";
interface Props { interface Props {
isExpanded: boolean; isExpanded: boolean;
} }
export default function ExpandIcon({ isExpanded }: Props) { export default function ExpandIcon({ isExpanded }: Props) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Box sx={{
width: "33px",
height: "33px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg style={{ transform: isExpanded ? "rotate(180deg)" : undefined }} xmlns="http://www.w3.org/2000/svg" width="32" height="33" viewBox="0 0 32 33" fill="none"> <svg style={{ transform: isExpanded ? "rotate(180deg)" : undefined }} xmlns="http://www.w3.org/2000/svg" width="32" height="33" viewBox="0 0 32 33" fill="none">
<path stroke={isExpanded ? theme.palette.orange.main : theme.palette.brightPurple.main} d="M16 28.7949C22.6274 28.7949 28 23.4223 28 16.7949C28 10.1675 22.6274 4.79492 16 4.79492C9.37258 4.79492 4 10.1675 4 16.7949C4 23.4223 9.37258 28.7949 16 28.7949Z" strokeWidth="2" strokeMiterlimit="10" /> <path stroke={isExpanded ? theme.palette.orange.main : theme.palette.brightPurple.main} d="M16 28.7949C22.6274 28.7949 28 23.4223 28 16.7949C28 10.1675 22.6274 4.79492 16 4.79492C9.37258 4.79492 4 10.1675 4 16.7949C4 23.4223 9.37258 28.7949 16 28.7949Z" strokeWidth="2" strokeMiterlimit="10" />
<path stroke={isExpanded ? theme.palette.orange.main : theme.palette.brightPurple.main} d="M20.5 15.2949L16 20.2949L11.5 15.2949" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path stroke={isExpanded ? theme.palette.orange.main : theme.palette.brightPurple.main} d="M20.5 15.2949L16 20.2949L11.5 15.2949" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
); </Box>
)
} }

@ -12,7 +12,7 @@ import Tariffs from "./pages/Tariffs/Tariffs";
import SigninDialog from "./pages/auth/Signin"; import SigninDialog from "./pages/auth/Signin";
import SignupDialog from "./pages/auth/Signup"; import SignupDialog from "./pages/auth/Signup";
import History from "./pages/History"; import History from "./pages/History";
import Basket from "./pages/Basket/Basket"; import Cart from "./pages/Cart/Cart";
import TariffPage from "./pages/Tariffs/TariffsPage"; import TariffPage from "./pages/Tariffs/TariffsPage";
import SavedTariffs from "./pages/SavedTariffs"; import SavedTariffs from "./pages/SavedTariffs";
import lightTheme from "@utils/themes/light"; import lightTheme from "@utils/themes/light";
@ -24,13 +24,14 @@ import Layout from "./components/Layout";
import { clearUserData, setUser, setUserAccount, useUserStore } from "./stores/user"; import { clearUserData, setUser, setUserAccount, useUserStore } from "./stores/user";
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor"; import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
import { useUser } from "./utils/hooks/useUser"; import { useUser } from "./utils/hooks/useUser";
import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui"; import { clearAuthToken, getMessageFromFetchError, usePrivilegeFetcher } from "@frontend/kitui";
import { useUserAccount } from "./utils/hooks/useUserAccount"; import { useUserAccount } from "./utils/hooks/useUserAccount";
import { setCustomTariffs } from "@root/stores/customTariffs"; import { setCustomTariffs } from "@root/stores/customTariffs";
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs"; import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
import { useDiscounts } from "./utils/hooks/useDiscounts"; import { useDiscounts } from "./utils/hooks/useDiscounts";
import { setDiscounts } from "./stores/discounts"; import { setDiscounts } from "./stores/discounts";
import { pdfjs } from "react-pdf"; import { pdfjs } from "react-pdf";
import { setPrivileges } from "./stores/privileges";
pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.js", import.meta.url).toString(); pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.js", import.meta.url).toString();
@ -85,6 +86,13 @@ const App = () => {
} }
}); });
usePrivilegeFetcher({
onSuccess: setPrivileges,
onError: error => {
console.log("usePrivilegeFetcher error :>> ", error);
}
});
if (location.state?.redirectTo) return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />; if (location.state?.redirectTo) return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />;
return ( return (
@ -108,7 +116,7 @@ const App = () => {
<Route path="/support" element={<Support />} /> <Route path="/support" element={<Support />} />
<Route path="/support/:ticketId" element={<Support />} /> <Route path="/support/:ticketId" element={<Support />} />
<Route path="/tariffconstructor" element={<TariffConstructor />} /> <Route path="/tariffconstructor" element={<TariffConstructor />} />
<Route path="/cart" element={<Basket />} /> <Route path="/cart" element={<Cart />} />
<Route path="/wallet" element={<Wallet />} /> <Route path="/wallet" element={<Wallet />} />
<Route path="/payment" element={<Payment />} /> <Route path="/payment" element={<Payment />} />
<Route path="/settings" element={<AccountSettings />} /> <Route path="/settings" element={<AccountSettings />} />

@ -1,4 +1,5 @@
import { PrivilegeWithAmount, PrivilegeWithoutPrice } from "./privilege"; import { PrivilegeWithAmount } from "@frontend/kitui";
import { PrivilegeWithoutPrice } from "./privilege";
type ServiceKey = string; type ServiceKey = string;
@ -9,14 +10,9 @@ export type CustomTariffUserValuesMap = Record<ServiceKey, CustomTariffUserValue
export type ServiceKeyToPriceMap = Record<ServiceKey, number>; export type ServiceKeyToPriceMap = Record<ServiceKey, number>;
export interface CustomTariff { export interface CreateTariffBody {
name: string; name: string;
price?: number; price?: number;
isCustom: boolean; isCustom: boolean;
privilegies: PrivilegeWithAmount[]; privilegies: PrivilegeWithoutPrice[];
updatedAt?: string;
isDeleted?: boolean;
createdAt?: string;
} }
export type CreateTariffBody = Omit<CustomTariff, "privilegies"> & { privilegies: PrivilegeWithoutPrice[]; };

@ -1,21 +1,6 @@
export interface Privilege { import { Privilege, PrivilegeWithAmount } from "@frontend/kitui";
_id: string;
name: string;
privilegeId: string;
serviceKey: string;
description: string;
type: "day" | "count";
value: PrivilegeValueType;
price: number;
updatedAt?: string;
isDeleted?: boolean;
createdAt?: string;
};
export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>; export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>;
export type PrivilegeValueType = "шаблон" | "день" | "МБ";
export type PrivilegeWithAmount = Omit<Privilege, "_id"> & { amount: number; };
export type PrivilegeWithoutPrice = Omit<PrivilegeWithAmount, "price">; export type PrivilegeWithoutPrice = Omit<PrivilegeWithAmount, "price">;

@ -1,61 +0,0 @@
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import SectionWrapper from "@components/SectionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import TotalPrice from "@components/TotalPrice";
import CustomWrapper from "./CustomWrapper";
import { useCart } from "@root/utils/hooks/useCart";
import { useCustomTariffsStore } from "@root/stores/customTariffs";
export default function Basket() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const cart = useCart();
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts + basePrice;
const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice;
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
mb: upMd ? "70px" : "37px",
}}
>
<Box
sx={{
mt: "20px",
mb: upMd ? "40px" : "20px",
display: "flex",
gap: "10px",
}}
>
{!upMd && (
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography component="h4" variant="h4">
Корзина
</Typography>
</Box>
<Box sx={{
mt: upMd ? "27px" : "10px",
}}>
{cart.services.map(serviceData =>
<CustomWrapper
key={serviceData.serviceKey}
serviceData={serviceData}
/>
)}
</Box>
<TotalPrice priceBeforeDiscounts={totalPriceBeforeDiscounts} priceAfterDiscounts={totalPriceAfterDiscounts} />
</SectionWrapper>
);
}

@ -1,196 +0,0 @@
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 { cardShadow } from "@root/utils/themes/shadow";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { removeTariffFromCart } from "@root/stores/user";
import { enqueueSnackbar } from "notistack";
import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui";
const name: Record<string, string> = {
templategen: "Шаблонизатор",
squiz: "Опросник",
reducer: "Сокращатель ссылок",
};
interface Props {
serviceData: ServiceCartData;
}
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<boolean>(false);
function handleItemDeleteClick(tariffId: string) {
removeTariffFromCart(tariffId)
.then(() => {
enqueueSnackbar("Тариф удален");
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
}
return (
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
boxShadow: cardShadow,
}}
>
<Box
sx={{
backgroundColor: "white",
"&:first-of-type": {
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
},
"&:last-of-type": {
borderBottomLeftRadius: "12px",
borderBottomRightRadius: "12px",
},
"&:not(:last-of-type)": {
borderBottom: `1px solid ${theme.palette.grey2.main}`,
},
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
px: "20px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<Typography
sx={{
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.text.secondary,
px: 0,
}}
>
{name[serviceData.serviceKey]}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
}}
>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(serviceData.price / 100)}
</Typography>
<Box
sx={{
borderLeft: upSm ? "1px solid #9A9AAF" : "none",
paddingLeft: upSm ? "24px" : 0,
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
</Box>
</Box>
{isExpanded &&
serviceData.privileges.map((privilege) => (
<Box
key={privilege.tariffId + privilege.privilegeId}
sx={{
px: "20px",
py: upMd ? "25px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.grey3.main,
}}
>
{privilege.description}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: "10px",
alignItems: "center",
width: upSm ? "195px" : "123px",
marginRight: upSm ? "65px" : 0,
}}
>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(privilege.price / 100)}
</Typography>
{upSm ? (
<Typography
component="div"
onClick={() => handleItemDeleteClick(privilege.tariffId)}
sx={{
color: theme.palette.text.secondary,
borderBottom: `1px solid ${theme.palette.text.secondary}`,
width: "max-content",
lineHeight: "19px",
cursor: "pointer",
}}
>
Удалить
</Typography>
) : (
<SvgIcon
onClick={() => handleItemDeleteClick(privilege.tariffId)}
component={ClearIcon}
sx={{ fill: "#7E2AEA" }}
/>
)}
</Box>
</Box>
))}
</Box>
</Box>
);
}

67
src/pages/Cart/Cart.tsx Normal file

@ -0,0 +1,67 @@
import {
Box,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import SectionWrapper from "@components/SectionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import TotalPrice from "@components/TotalPrice";
import CustomWrapper from "./CustomWrapper";
import { useCart } from "@root/utils/hooks/useCart";
export default function Cart() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const cart = useCart();
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts;
const totalPriceAfterDiscounts = cart.priceAfterDiscounts;
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
mb: upMd ? "70px" : "37px",
}}
>
<Box
sx={{
mt: "20px",
mb: upMd ? "40px" : "20px",
display: "flex",
gap: "10px",
}}
>
{!upMd && (
<IconButton
sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
>
<ArrowBackIcon />
</IconButton>
)}
<Typography component="h4" variant="h4">
Корзина
</Typography>
</Box>
<Box
sx={{
mt: upMd ? "27px" : "10px",
}}
>
{cart.services.map((serviceData) => (
<CustomWrapper
key={serviceData.serviceKey}
serviceData={serviceData}
/>
))}
</Box>
<TotalPrice
priceBeforeDiscounts={totalPriceBeforeDiscounts}
priceAfterDiscounts={totalPriceAfterDiscounts}
/>
</SectionWrapper>
);
}

@ -0,0 +1,220 @@
import { useState } from "react";
import {
Box,
SvgIcon,
Typography,
IconButton,
useMediaQuery,
useTheme,
} from "@mui/material";
import ExpandIcon from "@components/icons/ExpandIcon";
import ClearIcon from "@mui/icons-material/Clear";
import { cardShadow } from "@root/utils/themes/shadow";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { removeTariffFromCart } from "@root/stores/user";
import { enqueueSnackbar } from "notistack";
import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui";
import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg";
import type { MouseEvent } from "react";
import CustomTariffAccordion from "@root/components/CustomTariffAccordion";
const name: Record<string, string> = {
templategen: "Шаблонизатор",
squiz: "Опросник",
reducer: "Сокращатель ссылок",
};
interface Props {
serviceData: ServiceCartData;
}
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<boolean>(false);
function handleItemDeleteClick(tariffId: string) {
removeTariffFromCart(tariffId)
.then(() => {
enqueueSnackbar("Тариф удален");
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
}
const deleteService = async (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
setIsExpanded(false);
for (const { tariffId } of serviceData.tariffs) {
try {
await removeTariffFromCart(tariffId);
} catch { }
}
enqueueSnackbar("Тарифы удален");
};
return (
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
boxShadow: cardShadow,
}}
>
<Box
sx={{
backgroundColor: "white",
"&:first-of-type": {
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
},
"&:last-of-type": {
borderBottomLeftRadius: "12px",
borderBottomRightRadius: "12px",
},
"&:not(:last-of-type)": {
borderBottom: `1px solid ${theme.palette.grey2.main}`,
},
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
px: "20px",
display: "flex",
gap: "15px",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<Box
sx={{
width: "50px",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
<Typography
sx={{
width: "100%",
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.text.secondary,
px: 0,
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{name[serviceData.serviceKey]}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
}}
>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(serviceData.price / 100)}
</Typography>
</Box>
<IconButton
onClick={deleteService}
sx={{ padding: 0, height: "22px", width: "22px" }}
>
<CrossIcon style={{ height: "22 px", width: "22px" }} />
</IconButton>
</Box>
{isExpanded &&
serviceData.tariffs.map(tariff => {
const privilege = tariff.privileges[0];
return tariff.privileges.length > 1 ? (
<CustomTariffAccordion
key={privilege.tariffId + privilege.privilegeId}
tariffCartData={tariff}
/>
) : (
<Box
key={privilege.tariffId + privilege.privilegeId}
sx={{
px: "20px",
py: upMd ? "25px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.grey3.main,
width: "100%",
}}
>
{privilege.description}
</Typography>
<Box
sx={{
width: upSm ? "140px" : "123px",
marginRight: upSm ? "65px" : 0,
}}
>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(tariff.price / 100)}
</Typography>
</Box>
<Box
sx={{
cursor: "pointer",
width: "35px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => handleItemDeleteClick(privilege.tariffId)}
>
<SvgIcon component={ClearIcon} sx={{ fill: "#7E2AEA" }} />
</Box>
</Box>
);
})}
</Box>
</Box>
);
}

@ -6,12 +6,12 @@ import {
IconButton, IconButton,
} from "@mui/material"; } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { Link, useParams } from "react-router-dom"; import { Link, useParams, useLocation } from "react-router-dom";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import SupportChat from "./SupportChat"; import SupportChat from "./SupportChat";
import CreateTicket from "./CreateTicket"; import CreateTicket from "./CreateTicket";
import TicketList from "./TicketList/TicketList"; import TicketList from "./TicketList/TicketList";
import { useCallback } from "react"; import { useState, useCallback, useEffect } from "react";
import { Ticket, getMessageFromFetchError, useToken } from "@frontend/kitui"; import { Ticket, getMessageFromFetchError, useToken } from "@frontend/kitui";
import { import {
updateTickets, updateTickets,
@ -23,12 +23,14 @@ import { enqueueSnackbar } from "notistack";
import { useSSESubscription, useTickets } from "@frontend/kitui"; import { useSSESubscription, useTickets } from "@frontend/kitui";
export default function Support() { export default function Support() {
const [previousPage, setPreviousPage] = useState<string>("");
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const ticketId = useParams().ticketId; const ticketId = useParams().ticketId;
const ticketApiPage = useTicketStore((state) => state.apiPage); const ticketApiPage = useTicketStore((state) => state.apiPage);
const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage); const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage);
const token = useToken(); const token = useToken();
const location = useLocation();
const fetchState = useTickets({ const fetchState = useTickets({
url: "https://hub.pena.digital/heruvym/getTickets", url: "https://hub.pena.digital/heruvym/getTickets",
@ -44,6 +46,10 @@ export default function Support() {
}, []), }, []),
}); });
useEffect(() => {
setPreviousPage(location.state?.previousUrl || "/");
}, []);
useSSESubscription<Ticket>({ useSSESubscription<Ticket>({
enabled: Boolean(token), enabled: Boolean(token),
url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`, url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`,
@ -73,7 +79,7 @@ export default function Support() {
}} }}
> >
<Link <Link
to="/support" to={ticketId ? "/support" : previousPage}
style={{ style={{
textDecoration: "none", textDecoration: "none",
display: "flex", display: "flex",

@ -6,7 +6,6 @@ import {
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import CustomButton from "../../components/CustomButton"; import CustomButton from "../../components/CustomButton";
import { Privilege } from "@root/model/privilege";
import TariffPrivilegeSlider from "./TariffItem"; import TariffPrivilegeSlider from "./TariffItem";
import { import {
createAndSendTariff, createAndSendTariff,
@ -14,8 +13,10 @@ import {
} from "@root/stores/customTariffs"; } from "@root/stores/customTariffs";
import { cardShadow } from "@root/utils/themes/shadow"; import { cardShadow } from "@root/utils/themes/shadow";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { devlog, getMessageFromFetchError } from "@frontend/kitui"; import { Privilege, getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { updateTariffs } from "@root/stores/tariffs";
import { addTariffToCart } from "@root/stores/user";
interface Props { interface Props {
serviceKey: string; serviceKey: string;
@ -36,18 +37,18 @@ export default function CustomTariffCard({ serviceKey, privileges }: Props) {
const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0; const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0;
async function handleConfirmClick() { async function handleConfirmClick() {
createAndSendTariff(serviceKey) try {
.then((result) => { const tariff = await createAndSendTariff(serviceKey);
devlog(result); updateTariffs([tariff]);
enqueueSnackbar("Тариф создан"); await addTariffToCart(tariff._id);
}) enqueueSnackbar("Тариф добавлен в корзину");
.catch((error) => { } catch (error) {
const message = getMessageFromFetchError( const message = getMessageFromFetchError(
error, error,
"Не удалось создать тариф" "Не удалось создать тариф"
); );
if (message) enqueueSnackbar(message); if (message) enqueueSnackbar(message);
}); }
} }
return ( return (

@ -7,12 +7,16 @@ import CustomTariffCard from "./CustomTariffCard";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import TotalPrice from "@root/components/TotalPrice"; import TotalPrice from "@root/components/TotalPrice";
import { serviceNameByKey } from "@root/utils/serviceKeys"; import { serviceNameByKey } from "@root/utils/serviceKeys";
import { useAllTariffsFetcher } from "@root/utils/hooks/useAllTariffsFetcher";
import { updateTariffs } from "@root/stores/tariffs";
import { getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack";
export default function TariffConstructor() { export default function TariffConstructor() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const customTariffs = useCustomTariffsStore( const customTariffs = useCustomTariffsStore(
(state) => state.customTariffsMap (state) => state.privilegeByService
); );
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore( const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(
(state) => state.summaryPriceBeforeDiscountsMap (state) => state.summaryPriceBeforeDiscountsMap
@ -30,6 +34,14 @@ export default function TariffConstructor() {
0 0
); );
useAllTariffsFetcher({
onSuccess: updateTariffs,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
},
});
return ( return (
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"

@ -1,4 +1,4 @@
import { useThrottle } from "@frontend/kitui"; import { Privilege, PrivilegeValueType, useThrottle } from "@frontend/kitui";
import { import {
Box, Box,
SliderProps, SliderProps,
@ -10,7 +10,6 @@ import { CustomSlider } from "@root/components/CustomSlider";
import NumberInputWithUnitAdornment from "@root/components/NumberInputWithUnitAdornment"; import NumberInputWithUnitAdornment from "@root/components/NumberInputWithUnitAdornment";
import CalendarIcon from "@root/components/icons/CalendarIcon"; import CalendarIcon from "@root/components/icons/CalendarIcon";
import PieChartIcon from "@root/components/icons/PieChartIcon"; import PieChartIcon from "@root/components/icons/PieChartIcon";
import { Privilege, PrivilegeValueType } from "@root/model/privilege";
import { useCartStore } from "@root/stores/cart"; import { useCartStore } from "@root/stores/cart";
import { import {
setCustomTariffsUserValue, setCustomTariffsUserValue,

@ -18,7 +18,13 @@ export default function FreeTariffCard() {
sx={{ sx={{
backgroundColor: "#7E2AEA", backgroundColor: "#7E2AEA",
color: "white", color: "white",
minHeight: "206px", }}
buttonProps={{
text: "Выбрать",
sx: {
color: "white",
borderColor: "white",
},
}} }}
/> />
); );

@ -33,10 +33,13 @@ export default function TariffCard({
}: Props) { }: Props) {
const theme = useTheme(); const theme = useTheme();
text = Array.isArray(text) ? text : [text];
return ( return (
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
minHeight: "250px",
bgcolor: "white", bgcolor: "white",
borderRadius: "12px", borderRadius: "12px",
display: "flex", display: "flex",
@ -71,7 +74,7 @@ export default function TariffCard({
</Box> </Box>
)} )}
</Box> </Box>
<Tooltip title={headerText} placement="top"> <Tooltip title={<Typography>{headerText}</Typography>} placement="top">
<Typography <Typography
variant="h5" variant="h5"
sx={{ sx={{
@ -86,49 +89,22 @@ export default function TariffCard({
{headerText} {headerText}
</Typography> </Typography>
</Tooltip> </Tooltip>
{Array.isArray(text) ? ( <Tooltip
<Box title={text.map((line, index) => (
sx={{ <Typography key={index}>{line}</Typography>
marginBottom: "auto", ))}
}} placement="top"
> >
{text.map((line, index) => ( <Box sx={{
<Tooltip key={index} title={line} placement="top">
<Typography
sx={{
height: "65px",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "clip",
display: "-webkit-box", mb: "auto",
WebkitBoxOrient: "vertical", }}>
MozBoxOrient: "vertical", {text.map((line, index) => (
WebkitLineClamp: 3, <Typography key={index}>{line}</Typography>
}}
>
{line}
</Typography>
</Tooltip>
))} ))}
</Box> </Box>
) : (
<Tooltip title={text} placement="top">
<Typography
sx={{
minHeight: "calc(1.185*2em)",
marginBottom: "auto",
height: "65px",
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitBoxOrient: "vertical",
MozBoxOrient: "vertical",
WebkitLineClamp: 3,
}}
>
{text}
</Typography>
</Tooltip> </Tooltip>
)}
{buttonProps && ( {buttonProps && (
<CustomButton <CustomButton
onClick={buttonProps.onClick} onClick={buttonProps.onClick}

@ -2,7 +2,6 @@ import { useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import { useTariffs } from "@root/utils/hooks/useTariffs";
import { updateTariffs, useTariffStore } from "@root/stores/tariffs"; import { updateTariffs, useTariffStore } from "@root/stores/tariffs";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { Select } from "@root/components/Select"; import { Select } from "@root/components/Select";
@ -11,16 +10,21 @@ import TariffCard from "./TariffCard";
import NumberIcon from "@root/components/NumberIcon"; import NumberIcon from "@root/components/NumberIcon";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices"; import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices";
import { getMessageFromFetchError } from "@frontend/kitui"; import { Tariff, getMessageFromFetchError } from "@frontend/kitui";
import FreeTariffCard from "./FreeTariffCard"; import FreeTariffCard from "./FreeTariffCard";
import { addTariffToCart, useUserStore } from "@root/stores/user"; import { addTariffToCart, useUserStore } from "@root/stores/user";
import { useDiscountStore } from "@root/stores/discounts"; import { useDiscountStore } from "@root/stores/discounts";
import { useCustomTariffsStore } from "@root/stores/customTariffs";
import { Slider } from "./slider"; import { Slider } from "./slider";
import { useCartStore } from "@root/stores/cart"; import { useCartStore } from "@root/stores/cart";
import { useAllTariffsFetcher } from "@root/utils/hooks/useAllTariffsFetcher";
const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"]; const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"];
const StepperText: Record<string, string> = {
volume: "Тарифы на объём",
time: "Тарифы на время",
};
export default function TariffPage() { export default function TariffPage() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
@ -29,23 +33,14 @@ export default function TariffPage() {
const tariffs = useTariffStore((state) => state.tariffs); const tariffs = useTariffStore((state) => state.tariffs);
const [selectedItem, setSelectedItem] = useState<number>(0); const [selectedItem, setSelectedItem] = useState<number>(0);
const discounts = useDiscountStore((state) => state.discounts); const discounts = useDiscountStore((state) => state.discounts);
const customTariffs = useCustomTariffsStore( const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
(state) => state.customTariffsMap const cartTariffMap = useCartStore((state) => state.cartTariffMap);
);
const purchasesAmount =
useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
const cart = useCartStore((state) => state.cart);
const unit: string = String(location.pathname).slice(9); const unit: string = String(location.pathname).slice(9);
const currentTariffs = Object.values(cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
const StepperText: Record<string, string> = { useAllTariffsFetcher({
volume: "Тарифы на объём", onSuccess: updateTariffs,
time: "Тарифы на время",
};
useTariffs({
apiPage: 0,
tariffsPerPage: 100,
onNewTariffs: updateTariffs,
onError: (error) => { onError: (error) => {
const errorMessage = getMessageFromFetchError(error); const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage); if (errorMessage) enqueueSnackbar(errorMessage);
@ -70,13 +65,12 @@ export default function TariffPage() {
); );
}); });
const tariffElements = filteredTariffs.map((tariff, index) => { const tariffElements = filteredTariffs.filter((tariff)=>tariff.privilegies.length > 0).map((tariff, index) => {
const { price, tariffPriceAfterDiscounts } = calcIndividualTariffPrices( const { priceBeforeDiscounts, priceAfterDiscounts } = calcIndividualTariffPrices(
tariff, tariff,
discounts, discounts,
customTariffs,
purchasesAmount, purchasesAmount,
cart currentTariffs,
); );
return ( return (
@ -97,16 +91,14 @@ export default function TariffPage() {
text={tariff.privilegies.map((p) => `${p.name} - ${p.amount}`)} text={tariff.privilegies.map((p) => `${p.name} - ${p.amount}`)}
price={ price={
<> <>
{price !== undefined && price !== tariffPriceAfterDiscounts && ( {priceBeforeDiscounts !== priceAfterDiscounts && (
<Typography variant="oldPrice"> <Typography variant="oldPrice">
{currencyFormatter.format(price / 100)} {currencyFormatter.format(priceBeforeDiscounts / 100)}
</Typography> </Typography>
)} )}
{tariffPriceAfterDiscounts !== undefined && (
<Typography variant="price"> <Typography variant="price">
{currencyFormatter.format(tariffPriceAfterDiscounts / 100)} {currencyFormatter.format(priceAfterDiscounts / 100)}
</Typography> </Typography>
)}
</> </>
} }
/> />

@ -1,21 +1,21 @@
import { createTariff } from "@root/api/tariff"; import { createTariff } from "@root/api/tariff";
import { CustomTariffUserValuesMap, ServiceKeyToPriceMap } from "@root/model/customTariffs"; import { CustomTariffUserValuesMap, ServiceKeyToPriceMap } from "@root/model/customTariffs";
import { ServiceKeyToPrivilegesMap, PrivilegeWithoutPrice } from "@root/model/privilege"; import { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
import { produce } from "immer"; import { produce } from "immer";
import { create } from "zustand"; import { create } from "zustand";
import { devtools, persist } from "zustand/middleware"; import { devtools, persist } from "zustand/middleware";
import { Discount, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui"; import { Discount, PrivilegeWithAmount, findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
interface CustomTariffsStore { interface CustomTariffsStore {
customTariffsMap: ServiceKeyToPrivilegesMap; privilegeByService: ServiceKeyToPrivilegesMap;
userValuesMap: CustomTariffUserValuesMap; userValuesMap: CustomTariffUserValuesMap;
summaryPriceBeforeDiscountsMap: ServiceKeyToPriceMap; summaryPriceBeforeDiscountsMap: ServiceKeyToPriceMap;
summaryPriceAfterDiscountsMap: ServiceKeyToPriceMap; summaryPriceAfterDiscountsMap: ServiceKeyToPriceMap;
} }
const initialState: CustomTariffsStore = { const initialState: CustomTariffsStore = {
customTariffsMap: {}, privilegeByService: {},
userValuesMap: {}, userValuesMap: {},
summaryPriceBeforeDiscountsMap: {}, summaryPriceBeforeDiscountsMap: {},
summaryPriceAfterDiscountsMap: {}, summaryPriceAfterDiscountsMap: {},
@ -41,7 +41,7 @@ export const useCustomTariffsStore = create<CustomTariffsStore>()(
) )
); );
export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => useCustomTariffsStore.setState({ customTariffsMap: customTariffs }); export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => useCustomTariffsStore.setState({ privilegeByService: customTariffs });
export const setCustomTariffsUserValue = ( export const setCustomTariffsUserValue = (
serviceKey: string, serviceKey: string,
@ -58,22 +58,22 @@ export const setCustomTariffsUserValue = (
let priceWithoutDiscounts = 0; let priceWithoutDiscounts = 0;
let priceAfterDiscounts = 0; let priceAfterDiscounts = 0;
state.customTariffsMap[serviceKey].forEach(tariff => { state.privilegeByService[serviceKey].forEach(privilege => {
const amount = state.userValuesMap[serviceKey]?.[tariff._id] ?? 0; const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0;
priceWithoutDiscounts += tariff.price * amount; priceWithoutDiscounts += privilege.price * amount;
const discount = findPrivilegeDiscount(tariff.privilegeId, tariff.price * amount, discounts); const discount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * amount, discounts);
priceAfterDiscounts += tariff.price * amount * discount.factor; priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount);
}); });
const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts); const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts);
priceAfterDiscounts *= serviceDiscount.factor; priceAfterDiscounts *= findDiscountFactor(serviceDiscount);
const cartDiscount = findCartDiscount(currentCartTotal, discounts); const cartDiscount = findCartDiscount(currentCartTotal, discounts);
priceAfterDiscounts *= cartDiscount.factor; priceAfterDiscounts *= findDiscountFactor(cartDiscount);
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts); const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
priceAfterDiscounts *= loyaltyDiscount.factor; priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount);
state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts; state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts;
state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts; state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts;
@ -83,22 +83,21 @@ export const setCustomTariffsUserValue = (
export const createAndSendTariff = (serviceKey: string) => { export const createAndSendTariff = (serviceKey: string) => {
const state = useCustomTariffsStore.getState(); const state = useCustomTariffsStore.getState();
const privilegies: PrivilegeWithoutPrice[] = []; const privilegies: PrivilegeWithAmount[] = [];
Object.entries(state.userValuesMap[serviceKey]).forEach(([privilegeId, userValue]) => { Object.entries(state.userValuesMap[serviceKey]).forEach(([privilegeId, userValue]) => {
if (userValue === 0) return; if (userValue === 0) return;
const privilege = state.customTariffsMap[serviceKey].find(p => p._id === privilegeId); const privilegeWithoutAmount = state.privilegeByService[serviceKey].find(p => p._id === privilegeId);
if (!privilege) throw new Error(`Privilege not found: ${privilegeId}`); if (!privilegeWithoutAmount) throw new Error(`Privilege not found: ${privilegeId}`);
const p2 = { const privilege: PrivilegeWithAmount = {
...privilege, ...privilegeWithoutAmount,
privilegeId: privilege._id, privilegeId: privilegeWithoutAmount._id,
amount: userValue, amount: userValue,
} as PrivilegeWithoutPrice; };
delete (p2 as any).price;
privilegies.push(p2); privilegies.push(privilege);
}); });
const name = [...privilegies.map(p => p.name), new Date().toISOString()].join(", "); const name = [...privilegies.map(p => p.name), new Date().toISOString()].join(", ");

24
src/stores/privileges.ts Normal file

@ -0,0 +1,24 @@
import { PrivilegeWithAmount } from "@frontend/kitui";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface PrivilegeStore {
privileges: PrivilegeWithAmount[];
}
const initialState: PrivilegeStore = {
privileges: [],
};
const usePrivilegeStore = create<PrivilegeStore>()(
devtools(
(get, set) => initialState,
{
name: "Privileges",
enabled: process.env.NODE_ENV === "development",
}
)
);
export const setPrivileges = (privileges: PrivilegeStore["privileges"]) => usePrivilegeStore.setState({ privileges });

@ -33,7 +33,7 @@ export const updateTariffs = (tariffs: TariffStore["tariffs"]) => useTariffStore
false, false,
{ {
type: "updateTariffs", type: "updateTariffs",
tariffsLength: tariffs.length, tariffs: tariffs,
} }
); );

@ -1,3 +1,4 @@
// @ts-nocheck TODO fix tests
import { CartData, Discount, Tariff } from "@frontend/kitui"; import { CartData, Discount, Tariff } from "@frontend/kitui";
import { calcCart } from "./calcCart"; import { calcCart } from "./calcCart";

@ -1,4 +1,4 @@
import { CartData, Discount, PrivilegeCartData, Tariff, applyCartDiscount, applyLoyaltyDiscount, applyPrivilegeDiscounts, applyServiceDiscounts } from "@frontend/kitui"; import { CartData, Discount, PrivilegeCartData, Tariff, TariffCartData, applyCartDiscount, applyLoyaltyDiscount, applyPrivilegeDiscounts, applyServiceDiscounts } from "@frontend/kitui";
export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmount: number): CartData { export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmount: number): CartData {
@ -13,25 +13,31 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
}; };
tariffs.forEach(tariff => { tariffs.forEach(tariff => {
if (tariff.price && tariff.price > 0) cartData.priceBeforeDiscounts += tariff.price; let serviceData = cartData.services.find(service => service.serviceKey === tariff.privilegies[0].serviceKey);
tariff.privilegies.forEach(privilege => {
let serviceData = cartData.services.find(service => service.serviceKey === privilege.serviceKey);
if (!serviceData) { if (!serviceData) {
serviceData = { serviceData = {
serviceKey: privilege.serviceKey, serviceKey: tariff.privilegies[0].serviceKey,
privileges: [], tariffs: [],
price: 0, price: 0,
appliedServiceDiscount: null, appliedServiceDiscount: null,
}; };
cartData.services.push(serviceData); cartData.services.push(serviceData);
} }
const tariffCartData: TariffCartData = {
price: tariff.price ?? 0,
isCustom: tariff.isCustom,
privileges: [],
tariffId: tariff._id,
};
serviceData.tariffs.push(tariffCartData);
tariff.privilegies.forEach(privilege => {
const privilegePrice = privilege.amount * privilege.price; const privilegePrice = privilege.amount * privilege.price;
if (!tariff.price) cartData.priceBeforeDiscounts += privilegePrice; if (!tariff.price) tariffCartData.price += privilegePrice;
const privilegeData: PrivilegeCartData = { const privilegeCartData: PrivilegeCartData = {
tariffId: tariff._id, tariffId: tariff._id,
serviceKey: privilege.serviceKey, serviceKey: privilege.serviceKey,
privilegeId: privilege.privilegeId, privilegeId: privilege.privilegeId,
@ -41,11 +47,13 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
tariffName: tariff.name, tariffName: tariff.name,
}; };
serviceData.privileges.push(privilegeData); tariffCartData.privileges.push(privilegeCartData);
serviceData.price += privilegePrice;
cartData.priceAfterDiscounts += privilegePrice; cartData.priceAfterDiscounts += privilegePrice;
cartData.itemCount++; cartData.itemCount++;
}); });
cartData.priceBeforeDiscounts += tariffCartData.price;
serviceData.price += tariffCartData.price;
}); });
applyPrivilegeDiscounts(cartData, discounts); applyPrivilegeDiscounts(cartData, discounts);

@ -1,47 +1,24 @@
import { ServiceKeyToPrivilegesMap } from "@root/model/privilege"; import { Discount, Tariff, findDiscountFactor } from "@frontend/kitui";
import { CartData, Discount, Tariff, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui"; import { calcCart } from "./calcCart/calcCart";
export function calcIndividualTariffPrices( export function calcIndividualTariffPrices(
tariff: Tariff, tariff: Tariff,
discounts: Discount[], discounts: Discount[],
privilegies: ServiceKeyToPrivilegesMap,
purchasesAmount: number, purchasesAmount: number,
cart: CartData, currentTariffs: Tariff[],
): { ): {
price: number | undefined; priceBeforeDiscounts: number;
tariffPriceAfterDiscounts: number | undefined; priceAfterDiscounts: number;
} { } {
let price = tariff.price || tariff.privilegies.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0); const priceBeforeDiscounts = tariff.price || tariff.privilegies.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0);
let priceAfterDiscounts = priceBeforeDiscounts;
let tariffPriceAfterDiscounts = tariff.privilegies.reduce((sum, privilege) => { const cart = calcCart([...currentTariffs, tariff], discounts, purchasesAmount);
let privilegePrice = privilege.amount * privilege.price;
let realprivilegie = privilegies[privilege.serviceKey]?.find(e => e._id === privilege.privilegeId); cart.allAppliedDiscounts.forEach(discount => {
if (realprivilegie) privilege.privilegeId = realprivilegie.privilegeId; priceAfterDiscounts *= findDiscountFactor(discount);
});
const privilegeDiscount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * privilege.amount, discounts); return { priceBeforeDiscounts, priceAfterDiscounts };
privilegePrice *= privilegeDiscount.factor;
const serviceCartData = cart.services.find(e => e.serviceKey === privilege.serviceKey);
let serviceprice = 0;
if (serviceCartData) serviceprice = serviceCartData.price;
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice + serviceprice, discounts);
if (serviceDiscount) privilegePrice *= serviceDiscount.factor;
return sum + privilegePrice;
}, 0);
const cartDiscount = findCartDiscount(tariffPriceAfterDiscounts + cart.priceAfterDiscounts, discounts);
tariffPriceAfterDiscounts *= cartDiscount.factor;
const loyalDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
tariffPriceAfterDiscounts *= loyalDiscount.factor;
return {
price,
tariffPriceAfterDiscounts: tariffPriceAfterDiscounts,
};
} }

@ -1,4 +1,4 @@
import { PrivilegeValueType } from "@root/model/privilege"; import { PrivilegeValueType } from "@frontend/kitui";
function declension(number: number, declensions: string[], cases = [2, 0, 1, 1, 1, 2]) { function declension(number: number, declensions: string[], cases = [2, 0, 1, 1, 1, 2]) {

@ -0,0 +1,55 @@
import { GetTariffsResponse, Tariff, makeRequest } from "@frontend/kitui";
import { useRef, useLayoutEffect, useEffect } from "react";
export function useAllTariffsFetcher({
baseUrl = process.env.NODE_ENV === "production" ? "/strator/tariff" : "https://hub.pena.digital/strator/tariff",
onSuccess,
onError,
}: {
baseUrl?: string;
onSuccess: (response: Tariff[]) => void;
onError?: (error: Error) => void;
}) {
const onNewTariffsRef = useRef(onSuccess);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onNewTariffsRef.current = onSuccess;
onErrorRef.current = onError;
}, [onError, onSuccess]);
useEffect(() => {
const controller = new AbortController();
async function getPaginatedTariffs() {
let apiPage = 1;
const tariffsPerPage = 100;
let isDone = false;
while (!isDone) {
try {
const result = await makeRequest<never, GetTariffsResponse>({
url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`,
method: "get",
useToken: true,
signal: controller.signal,
});
if (result.tariffs.length > 0) {
onNewTariffsRef.current(result.tariffs);
apiPage++;
} else {
isDone = true;
}
} catch (error) {
onErrorRef.current?.(error as Error);
isDone = true;
}
}
}
getPaginatedTariffs();
return () => controller.abort();
}, [baseUrl]);
}

@ -0,0 +1,50 @@
import { Tariff, makeRequest } from "@frontend/kitui";
import { GetTariffsResponse } from "@root/model/tariff";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
export function useTariffFetcher({
baseUrl = process.env.NODE_ENV === "production" ? "/strator/tariff" : "https://hub.pena.digital/strator/tariff",
tariffsPerPage,
apiPage,
onSuccess,
onError,
}: {
baseUrl?: string;
tariffsPerPage: number;
apiPage: number;
onSuccess: (response: Tariff[]) => void;
onError?: (error: Error) => void;
}) {
const [fetchState, setFetchState] = useState<"fetching" | "idle" | "all fetched">("idle");
const onSuccessRef = useRef(onSuccess);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onSuccessRef.current = onSuccess;
onErrorRef.current = onError;
}, [onError, onSuccess]);
useEffect(() => {
const controller = new AbortController();
setFetchState("fetching");
makeRequest<never, GetTariffsResponse>({
url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`,
method: "get",
useToken: true,
signal: controller.signal,
}).then((result) => {
if (result.tariffs.length > 0) {
onSuccessRef.current(result.tariffs);
setFetchState("idle");
} else setFetchState("all fetched");
}).catch(error => {
onErrorRef.current?.(error);
});
return () => controller.abort();
}, [apiPage, tariffsPerPage, baseUrl]);
return fetchState;
}

@ -1,41 +0,0 @@
import { Tariff, devlog, makeRequest } from "@frontend/kitui";
import { GetTariffsResponse } from "@root/model/tariff";
import { useEffect, useLayoutEffect, useRef } from "react";
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 onNewTariffsRef = useRef(onNewTariffs);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onNewTariffsRef.current = onNewTariffs;
onErrorRef.current = onError;
}, [onError, onNewTariffs]);
useEffect(() => {
const controller = new AbortController();
makeRequest<never, GetTariffsResponse>({
url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`,
method: "get",
useToken: true,
signal: controller.signal,
}).then((result) => {
devlog("Tariffs", result);
if (result.tariffs.length > 0) {
onNewTariffsRef.current(result.tariffs);
}
}).catch(error => {
devlog("Error fetching tariffs", error);
onErrorRef.current(error);
});
return () => controller.abort();
}, [apiPage, tariffsPerPage, baseUrl]);
}

@ -1532,10 +1532,10 @@
minimatch "^3.1.2" minimatch "^3.1.2"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@frontend/kitui@^1.0.17": "@frontend/kitui@1.0.21":
version "1.0.17" version "1.0.21"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.17.tgz#a5bddaaa18b168be0e1814d5cfbd86e4030d15af" resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.21.tgz#572b3e774ba42b65c4b946704aeb68237b4b3cd5"
integrity sha1-pb3aqhixaL4OGBTVz72G5AMNFa8= integrity sha1-Vys+d0ukK2XEuUZwSutoI3tLPNU=
dependencies: dependencies:
immer "^10.0.2" immer "^10.0.2"
reconnecting-eventsource "^1.6.2" reconnecting-eventsource "^1.6.2"