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": {
"@emotion/react": "^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/material": "^5.10.14",
"@popperjs/core": "^2.11.8",

@ -1,10 +1,12 @@
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) {
return makeRequest<CreateTariffBody, CustomTariff>({
url: `https://admin.pena.digital/strator/tariff`,
return makeRequest<CreateTariffBody, Tariff>({
url: `${apiUrl}/tariff`,
method: "post",
useToken: true,
body: tariff,
@ -13,7 +15,7 @@ export function createTariff(tariff: CreateTariffBody) {
export function getTariffById(tariffId:string){
return makeRequest<never, Tariff>({
url: `https://admin.pena.digital/strator/tariff/${tariffId}`,
url: `${apiUrl}/tariff/${tariffId}`,
method: "get",
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 { 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 { currencyFormatter } from "@root/utils/currencyFormatter";
import { removeTariffFromCart } from "@root/stores/user";
import { enqueueSnackbar } from "notistack";
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 {
serviceData: ServiceCartData;
@ -21,19 +37,36 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
const [isExpanded, setIsExpanded] = useState<boolean>(false);
function handleItemDeleteClick(tariffId: string) {
removeTariffFromCart(tariffId).then(() => {
enqueueSnackbar("Тариф удален");
}).catch(error => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
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",
width: "100%",
}}
>
<Box
@ -56,101 +89,132 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
display: "flex",
gap: "10px",
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>
<ExpandIcon isExpanded={isExpanded} />
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography
sx={{ pr: "11px", color: theme.palette.grey3.main, fontSize: upSm ? "20px" : "16px", fontWeight: 500 }}
sx={{
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.text.secondary,
px: 0,
}}
>
{currencyFormatter.format(serviceData.price / 100)}
{name[serviceData.serviceKey]}
</Typography>
<Box
sx={{
paddingLeft: upSm ? "24px" : 0,
display: "flex",
justifyContent: "flex-end",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
></Box>
</Box>
</Box>
{isExpanded &&
serviceData.privileges.map(privilege => (
<Box
key={privilege.tariffId + privilege.privilegeId}
sx={{
py: upMd ? "10px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "20px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
width: "200px",
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.grey3.main,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{privilege.description}
{currencyFormatter.format(serviceData.price / 100)}
</Typography>
</Box>
</Box>
<IconButton
onClick={deleteService}
sx={{
padding: "3px",
height: "30px",
width: "30px",
}}
>
<CrossIcon
style={{
height: "24px",
width: "24px",
stroke: "#7E2AEA",
}}
/>
</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={{
py: upMd ? "10px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "20px",
display: "flex",
justifyContent: "space-between",
gap: "10px",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
width: "200px",
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.grey3.main,
fontSize: "20px",
fontWeight: 500,
}}
>
{currencyFormatter.format(privilege.price / 100)}
{privilege.description}
</Typography>
<SvgIcon
sx={{ cursor: "pointer", color: "#7E2AEA" }}
onClick={() => handleItemDeleteClick(privilege.tariffId)}
component={ClearIcon}
/>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: "10px",
alignItems: "center",
}}
>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: "20px",
fontWeight: 500,
}}
>
{currencyFormatter.format(privilege.price / 100)}
</Typography>
<SvgIcon
sx={{
cursor: "pointer",
width: "30px",
color: "#7E2AEA",
}}
onClick={() => handleItemDeleteClick(privilege.tariffId)}
component={ClearIcon}
/>
</Box>
</Box>
</Box>
))}
);
})}
</Box>
</Box>
);

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

@ -14,7 +14,6 @@ export default function Menu() {
const location = useLocation();
const color = location.pathname === "/" ? "white" : "black";
console.log(location)
const arrayMenu: MenuItem[] = [
{
@ -42,49 +41,50 @@ export default function Menu() {
overflow: "hidden",
}}
>
{location.pathname !== "/" ? arrayMenu.map(({ name, url, subMenu = [] }) => (
<Link
key={name}
style={{
textDecoration: "none",
height: "100%",
display: "flex",
alignItems: "center",
}}
to={url}
onMouseEnter={() => setActiveSubMenu(subMenu)}
>
<Typography
color={
location.pathname === url
? theme.palette.brightPurple.main
: color
}
variant="body2"
sx={{
whiteSpace: "nowrap",
}}
>
{name}
</Typography>
</Link>
))
:arrayMenu.map(({ name, url, subMenu = [] }) => (
<Typography
color={
location.pathname === url
? theme.palette.brightPurple.main
: color
}
variant="body2"
sx={{
whiteSpace: "nowrap",
}}
>
{name}
</Typography>
))
}
{location.pathname !== "/"
? arrayMenu.map(({ name, url, subMenu = [] }) => (
<Link
key={name}
style={{
textDecoration: "none",
height: "100%",
display: "flex",
alignItems: "center",
}}
to={url}
onMouseEnter={() => setActiveSubMenu(subMenu)}
state={{ previousUrl: location.pathname }}
>
<Typography
color={
location.pathname === url
? theme.palette.brightPurple.main
: color
}
variant="body2"
sx={{
whiteSpace: "nowrap",
}}
>
{name}
</Typography>
</Link>
))
: arrayMenu.map(({ name, url, subMenu = [] }) => (
<Typography
color={
location.pathname === url
? theme.palette.brightPurple.main
: color
}
variant="body2"
sx={{
whiteSpace: "nowrap",
}}
>
{name}
</Typography>
))}
<Box
sx={{
zIndex: "10",
@ -96,29 +96,30 @@ export default function Menu() {
}}
onMouseLeave={() => setActiveSubMenu([])}
>
{location.pathname !== "/" && activeSubMenu.map(({ name, url }) => (
<Link key={name} style={{ textDecoration: "none" }} to={url}>
<Typography
color={
location.pathname === url
? theme.palette.brightPurple.main
: color
}
variant="body2"
sx={{
padding: "15px",
whiteSpace: "nowrap",
paddingLeft: "185px",
"&:hover": {
color: theme.palette.brightPurple.main,
background: theme.palette.background.default,
},
}}
>
{name}
</Typography>
</Link>
))}
{location.pathname !== "/" &&
activeSubMenu.map(({ name, url }) => (
<Link key={name} style={{ textDecoration: "none" }} to={url}>
<Typography
color={
location.pathname === url
? theme.palette.brightPurple.main
: color
}
variant="body2"
sx={{
padding: "15px",
whiteSpace: "nowrap",
paddingLeft: "185px",
"&:hover": {
color: theme.palette.brightPurple.main,
background: theme.palette.background.default,
},
}}
>
{name}
</Typography>
</Link>
))}
</Box>
</Box>
);

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

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

@ -1,16 +1,26 @@
import { useTheme } from "@mui/material";
import { useTheme , Box} from "@mui/material";
interface Props {
isExpanded: boolean;
}
export default function ExpandIcon({ isExpanded }: Props) {
const theme = useTheme();
return (
<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">
<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">
<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" />
</svg>
);
}
</Box>
)
}

@ -12,7 +12,7 @@ import Tariffs from "./pages/Tariffs/Tariffs";
import SigninDialog from "./pages/auth/Signin";
import SignupDialog from "./pages/auth/Signup";
import History from "./pages/History";
import Basket from "./pages/Basket/Basket";
import Cart from "./pages/Cart/Cart";
import TariffPage from "./pages/Tariffs/TariffsPage";
import SavedTariffs from "./pages/SavedTariffs";
import lightTheme from "@utils/themes/light";
@ -24,13 +24,14 @@ import Layout from "./components/Layout";
import { clearUserData, setUser, setUserAccount, useUserStore } from "./stores/user";
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
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 { setCustomTariffs } from "@root/stores/customTariffs";
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
import { useDiscounts } from "./utils/hooks/useDiscounts";
import { setDiscounts } from "./stores/discounts";
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();
@ -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 }} />;
return (
@ -108,7 +116,7 @@ const App = () => {
<Route path="/support" element={<Support />} />
<Route path="/support/:ticketId" element={<Support />} />
<Route path="/tariffconstructor" element={<TariffConstructor />} />
<Route path="/cart" element={<Basket />} />
<Route path="/cart" element={<Cart />} />
<Route path="/wallet" element={<Wallet />} />
<Route path="/payment" element={<Payment />} />
<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;
@ -9,14 +10,9 @@ export type CustomTariffUserValuesMap = Record<ServiceKey, CustomTariffUserValue
export type ServiceKeyToPriceMap = Record<ServiceKey, number>;
export interface CustomTariff {
export interface CreateTariffBody {
name: string;
price?: number;
isCustom: boolean;
privilegies: PrivilegeWithAmount[];
updatedAt?: string;
isDeleted?: boolean;
createdAt?: string;
privilegies: PrivilegeWithoutPrice[];
}
export type CreateTariffBody = Omit<CustomTariff, "privilegies"> & { privilegies: PrivilegeWithoutPrice[]; };

@ -1,21 +1,6 @@
export interface Privilege {
_id: string;
name: string;
privilegeId: string;
serviceKey: string;
description: string;
type: "day" | "count";
value: PrivilegeValueType;
price: number;
updatedAt?: string;
isDeleted?: boolean;
createdAt?: string;
};
import { Privilege, PrivilegeWithAmount } from "@frontend/kitui";
export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>;
export type PrivilegeValueType = "шаблон" | "день" | "МБ";
export type PrivilegeWithAmount = Omit<Privilege, "_id"> & { amount: number; };
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,
} from "@mui/material";
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 SupportChat from "./SupportChat";
import CreateTicket from "./CreateTicket";
import TicketList from "./TicketList/TicketList";
import { useCallback } from "react";
import { useState, useCallback, useEffect } from "react";
import { Ticket, getMessageFromFetchError, useToken } from "@frontend/kitui";
import {
updateTickets,
@ -23,12 +23,14 @@ import { enqueueSnackbar } from "notistack";
import { useSSESubscription, useTickets } from "@frontend/kitui";
export default function Support() {
const [previousPage, setPreviousPage] = useState<string>("");
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const ticketId = useParams().ticketId;
const ticketApiPage = useTicketStore((state) => state.apiPage);
const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage);
const token = useToken();
const location = useLocation();
const fetchState = useTickets({
url: "https://hub.pena.digital/heruvym/getTickets",
@ -44,6 +46,10 @@ export default function Support() {
}, []),
});
useEffect(() => {
setPreviousPage(location.state?.previousUrl || "/");
}, []);
useSSESubscription<Ticket>({
enabled: Boolean(token),
url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`,
@ -73,7 +79,7 @@ export default function Support() {
}}
>
<Link
to="/support"
to={ticketId ? "/support" : previousPage}
style={{
textDecoration: "none",
display: "flex",

@ -1,144 +1,145 @@
import {
Box,
Divider,
Typography,
useMediaQuery,
useTheme,
Box,
Divider,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import CustomButton from "../../components/CustomButton";
import { Privilege } from "@root/model/privilege";
import TariffPrivilegeSlider from "./TariffItem";
import {
createAndSendTariff,
useCustomTariffsStore,
createAndSendTariff,
useCustomTariffsStore,
} from "@root/stores/customTariffs";
import { cardShadow } from "@root/utils/themes/shadow";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { devlog, getMessageFromFetchError } from "@frontend/kitui";
import { Privilege, getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack";
import { updateTariffs } from "@root/stores/tariffs";
import { addTariffToCart } from "@root/stores/user";
interface Props {
serviceKey: string;
privileges: Privilege[];
serviceKey: string;
privileges: Privilege[];
}
export default function CustomTariffCard({ serviceKey, privileges }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const summaryPriceBeforeDiscounts = useCustomTariffsStore(
(state) => state.summaryPriceBeforeDiscountsMap
);
const summaryPriceAfterDiscounts = useCustomTariffsStore(
(state) => state.summaryPriceAfterDiscountsMap
);
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const summaryPriceBeforeDiscounts = useCustomTariffsStore(
(state) => state.summaryPriceBeforeDiscountsMap
);
const summaryPriceAfterDiscounts = useCustomTariffsStore(
(state) => state.summaryPriceAfterDiscountsMap
);
const priceBeforeDiscounts = summaryPriceBeforeDiscounts[serviceKey] ?? 0;
const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0;
const priceBeforeDiscounts = summaryPriceBeforeDiscounts[serviceKey] ?? 0;
const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0;
async function handleConfirmClick() {
createAndSendTariff(serviceKey)
.then((result) => {
devlog(result);
enqueueSnackbar("Тариф создан");
})
.catch((error) => {
const message = getMessageFromFetchError(
error,
"Не удалось создать тариф"
);
if (message) enqueueSnackbar(message);
});
}
async function handleConfirmClick() {
try {
const tariff = await createAndSendTariff(serviceKey);
updateTariffs([tariff]);
await addTariffToCart(tariff._id);
enqueueSnackbar("Тариф добавлен в корзину");
} catch (error) {
const message = getMessageFromFetchError(
error,
"Не удалось создать тариф"
);
if (message) enqueueSnackbar(message);
}
}
return (
<Box
sx={{
backgroundColor: "white",
width: "100%",
display: "flex",
flexDirection: upMd ? "row" : "column",
borderRadius: "12px",
boxShadow: cardShadow,
}}
>
<Box
sx={{
p: "20px",
pr: upMd ? "35px" : undefined,
display: "flex",
flexBasis: 0,
flexGrow: 2.37,
flexWrap: "wrap",
flexDirection: "column",
gap: "25px",
}}
>
{privileges.map((privilege) => (
<TariffPrivilegeSlider key={privilege._id} privilege={privilege} />
))}
</Box>
{!upMd && (
<Divider
sx={{ mx: "20px", my: "10px", borderColor: theme.palette.grey2.main }}
/>
)}
<Box
sx={{
display: "flex",
flexBasis: 0,
flexGrow: 1,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.grey3.main,
p: "20px",
pl: upMd ? "33px" : undefined,
borderLeft: upMd
? `1px solid ${theme.palette.grey2.main}`
: undefined,
}}
>
return (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: "15%",
mb: "auto",
width: "100%",
}}
sx={{
backgroundColor: "white",
width: "100%",
display: "flex",
flexDirection: upMd ? "row" : "column",
borderRadius: "12px",
boxShadow: cardShadow,
}}
>
<Typography>
Чем больше пакеты, тем дешевле подписки и опции{" "}
</Typography>
<Box
sx={{
p: "20px",
pr: upMd ? "35px" : undefined,
display: "flex",
flexBasis: 0,
flexGrow: 2.37,
flexWrap: "wrap",
flexDirection: "column",
gap: "25px",
}}
>
{privileges.map((privilege) => (
<TariffPrivilegeSlider key={privilege._id} privilege={privilege} />
))}
</Box>
{!upMd && (
<Divider
sx={{ mx: "20px", my: "10px", borderColor: theme.palette.grey2.main }}
/>
)}
<Box
sx={{
display: "flex",
flexBasis: 0,
flexGrow: 1,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.grey3.main,
p: "20px",
pl: upMd ? "33px" : undefined,
borderLeft: upMd
? `1px solid ${theme.palette.grey2.main}`
: undefined,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: "15%",
mb: "auto",
width: "100%",
}}
>
<Typography>
Чем больше пакеты, тем дешевле подписки и опции{" "}
</Typography>
</Box>
<Typography mb="20px" mt="18px">
Сумма с учетом скидки
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
mb: "30px",
}}
>
<Typography variant="price">
{currencyFormatter.format(priceAfterDiscounts / 100)}
</Typography>
<Typography variant="oldPrice" pt="3px">
{currencyFormatter.format(priceBeforeDiscounts / 100)}
</Typography>
</Box>
<CustomButton
variant="contained"
onClick={handleConfirmClick}
sx={{
backgroundColor: theme.palette.brightPurple.main,
}}
>
Выбрать
</CustomButton>
</Box>
</Box>
<Typography mb="20px" mt="18px">
Сумма с учетом скидки
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
mb: "30px",
}}
>
<Typography variant="price">
{currencyFormatter.format(priceAfterDiscounts / 100)}
</Typography>
<Typography variant="oldPrice" pt="3px">
{currencyFormatter.format(priceBeforeDiscounts / 100)}
</Typography>
</Box>
<CustomButton
variant="contained"
onClick={handleConfirmClick}
sx={{
backgroundColor: theme.palette.brightPurple.main,
}}
>
Выбрать
</CustomButton>
</Box>
</Box>
);
);
}

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

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

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

@ -1,148 +1,124 @@
import {
Box,
Typography,
Tooltip,
SxProps,
Theme,
useTheme,
Box,
Typography,
Tooltip,
SxProps,
Theme,
useTheme,
} from "@mui/material";
import CustomButton from "@components/CustomButton";
import { MouseEventHandler, ReactNode } from "react";
import { cardShadow } from "@root/utils/themes/shadow";
interface Props {
icon: ReactNode;
headerText: string;
text: string | string[];
sx?: SxProps<Theme>;
buttonProps?: {
icon: ReactNode;
headerText: string;
text: string | string[];
sx?: SxProps<Theme>;
onClick?: MouseEventHandler<HTMLButtonElement>;
text?: string;
};
price?: ReactNode;
buttonProps?: {
sx?: SxProps<Theme>;
onClick?: MouseEventHandler<HTMLButtonElement>;
text?: string;
};
price?: ReactNode;
}
export default function TariffCard({
icon,
headerText,
text,
sx,
price,
buttonProps,
icon,
headerText,
text,
sx,
price,
buttonProps,
}: Props) {
const theme = useTheme();
const theme = useTheme();
return (
<Box
sx={{
width: "100%",
bgcolor: "white",
borderRadius: "12px",
display: "flex",
flexDirection: "column",
alignItems: "start",
p: "20px",
boxShadow: cardShadow,
...sx,
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "16px",
}}
>
{icon}
{price && (
<Box
sx={{
display: "flex",
alignItems: "baseline",
flexWrap: "wrap",
columnGap: "10px",
rowGap: 0,
}}
>
{price}
</Box>
)}
</Box>
<Tooltip title={headerText} placement="top">
<Typography
variant="h5"
sx={{
mt: "14px",
mb: "10px",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
width: "100%",
}}
>
{headerText}
</Typography>
</Tooltip>
{Array.isArray(text) ? (
text = Array.isArray(text) ? text : [text];
return (
<Box
sx={{
marginBottom: "auto",
}}
>
{text.map((line, index) => (
<Tooltip key={index} title={line} placement="top">
<Typography
sx={{
height: "65px",
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitBoxOrient: "vertical",
MozBoxOrient: "vertical",
WebkitLineClamp: 3,
}}
>
{line}
</Typography>
</Tooltip>
))}
</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,
width: "100%",
minHeight: "250px",
bgcolor: "white",
borderRadius: "12px",
display: "flex",
flexDirection: "column",
alignItems: "start",
p: "20px",
boxShadow: cardShadow,
...sx,
}}
>
{text}
</Typography>
</Tooltip>
)}
{buttonProps && (
<CustomButton
onClick={buttonProps.onClick}
variant="outlined"
sx={{
color: theme.palette.brightPurple.main,
borderColor: theme.palette.brightPurple.main,
mt: "10px",
...buttonProps.sx,
}}
>
{buttonProps.text}
</CustomButton>
)}
</Box>
);
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "16px",
}}
>
{icon}
{price && (
<Box
sx={{
display: "flex",
alignItems: "baseline",
flexWrap: "wrap",
columnGap: "10px",
rowGap: 0,
}}
>
{price}
</Box>
)}
</Box>
<Tooltip title={<Typography>{headerText}</Typography>} placement="top">
<Typography
variant="h5"
sx={{
mt: "14px",
mb: "10px",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
width: "100%",
}}
>
{headerText}
</Typography>
</Tooltip>
<Tooltip
title={text.map((line, index) => (
<Typography key={index}>{line}</Typography>
))}
placement="top"
>
<Box sx={{
overflow: "hidden",
textOverflow: "clip",
mb: "auto",
}}>
{text.map((line, index) => (
<Typography key={index}>{line}</Typography>
))}
</Box>
</Tooltip>
{buttonProps && (
<CustomButton
onClick={buttonProps.onClick}
variant="outlined"
sx={{
color: theme.palette.brightPurple.main,
borderColor: theme.palette.brightPurple.main,
mt: "10px",
...buttonProps.sx,
}}
>
{buttonProps.text}
</CustomButton>
)}
</Box>
);
}

@ -2,7 +2,6 @@ import { useState } from "react";
import { useLocation } from "react-router-dom";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import SectionWrapper from "@components/SectionWrapper";
import { useTariffs } from "@root/utils/hooks/useTariffs";
import { updateTariffs, useTariffStore } from "@root/stores/tariffs";
import { enqueueSnackbar } from "notistack";
import { Select } from "@root/components/Select";
@ -11,154 +10,147 @@ import TariffCard from "./TariffCard";
import NumberIcon from "@root/components/NumberIcon";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices";
import { getMessageFromFetchError } from "@frontend/kitui";
import { Tariff, getMessageFromFetchError } from "@frontend/kitui";
import FreeTariffCard from "./FreeTariffCard";
import { addTariffToCart, useUserStore } from "@root/stores/user";
import { useDiscountStore } from "@root/stores/discounts";
import { useCustomTariffsStore } from "@root/stores/customTariffs";
import { Slider } from "./slider";
import { useCartStore } from "@root/stores/cart";
import { useAllTariffsFetcher } from "@root/utils/hooks/useAllTariffsFetcher";
const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"];
export default function TariffPage() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const location = useLocation();
const tariffs = useTariffStore((state) => state.tariffs);
const [selectedItem, setSelectedItem] = useState<number>(0);
const discounts = useDiscountStore((state) => state.discounts);
const customTariffs = useCustomTariffsStore(
(state) => state.customTariffsMap
);
const purchasesAmount =
useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
const cart = useCartStore((state) => state.cart);
const unit: string = String(location.pathname).slice(9);
const StepperText: Record<string, string> = {
const StepperText: Record<string, string> = {
volume: "Тарифы на объём",
time: "Тарифы на время",
};
};
useTariffs({
apiPage: 0,
tariffsPerPage: 100,
onNewTariffs: updateTariffs,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
},
});
export default function TariffPage() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const location = useLocation();
const tariffs = useTariffStore((state) => state.tariffs);
const [selectedItem, setSelectedItem] = useState<number>(0);
const discounts = useDiscountStore((state) => state.discounts);
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
const cartTariffMap = useCartStore((state) => state.cartTariffMap);
function handleTariffItemClick(tariffId: string) {
addTariffToCart(tariffId)
.then(() => {
enqueueSnackbar("Тариф добавлен в корзину");
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
}
const unit: string = String(location.pathname).slice(9);
const currentTariffs = Object.values(cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
const filteredTariffs = tariffs.filter((tariff) => {
return (
tariff.privilegies.map((p) => p.type).includes("day") ===
(unit === "time") && !tariff.isDeleted
);
});
useAllTariffsFetcher({
onSuccess: updateTariffs,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
},
});
const tariffElements = filteredTariffs.map((tariff, index) => {
const { price, tariffPriceAfterDiscounts } = calcIndividualTariffPrices(
tariff,
discounts,
customTariffs,
purchasesAmount,
cart
);
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") && !tariff.isDeleted
);
});
const tariffElements = filteredTariffs.filter((tariff)=>tariff.privilegies.length > 0).map((tariff, index) => {
const { priceBeforeDiscounts, priceAfterDiscounts } = calcIndividualTariffPrices(
tariff,
discounts,
purchasesAmount,
currentTariffs,
);
return (
<TariffCard
key={tariff._id}
icon={
<NumberIcon
number={index + 1}
color={unit === "time" ? "#7E2AEA" : "#FB5607"}
backgroundColor={unit === "time" ? "#EEE4FC" : "#FEDFD0"}
/>
}
buttonProps={{
text: "Выбрать",
onClick: () => handleTariffItemClick(tariff._id),
}}
headerText={tariff.name}
text={tariff.privilegies.map((p) => `${p.name} - ${p.amount}`)}
price={
<>
{priceBeforeDiscounts !== priceAfterDiscounts && (
<Typography variant="oldPrice">
{currencyFormatter.format(priceBeforeDiscounts / 100)}
</Typography>
)}
<Typography variant="price">
{currencyFormatter.format(priceAfterDiscounts / 100)}
</Typography>
</>
}
/>
);
});
if (tariffElements.length < 6)
tariffElements.push(<FreeTariffCard key="free_tariff_card" />);
else tariffElements.splice(5, 0, <FreeTariffCard key="free_tariff_card" />);
return (
<TariffCard
key={tariff._id}
icon={
<NumberIcon
number={index + 1}
color={unit === "time" ? "#7E2AEA" : "#FB5607"}
backgroundColor={unit === "time" ? "#EEE4FC" : "#FEDFD0"}
/>
}
buttonProps={{
text: "Выбрать",
onClick: () => handleTariffItemClick(tariff._id),
}}
headerText={tariff.name}
text={tariff.privilegies.map((p) => `${p.name} - ${p.amount}`)}
price={
<>
{price !== undefined && price !== tariffPriceAfterDiscounts && (
<Typography variant="oldPrice">
{currencyFormatter.format(price / 100)}
</Typography>
<SectionWrapper
maxWidth="lg"
sx={{
mt: "20px",
mb: upMd ? "90px" : "63px",
display: "flex",
flexDirection: "column",
}}
>
<Typography variant="h4" sx={{ marginBottom: "23px", mt: "20px" }}>
{StepperText[unit]}
</Typography>
{isMobile ? (
<Select
items={subPages}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
) : (
<Tabs
items={subPages}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
)}
{tariffPriceAfterDiscounts !== undefined && (
<Typography variant="price">
{currencyFormatter.format(tariffPriceAfterDiscounts / 100)}
</Typography>
)}
</>
}
/>
<Box
sx={{
justifyContent: "center",
mt: "40px",
mb: "30px",
display: "grid",
gap: "40px",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 360px))",
}}
>
{tariffElements}
</Box>
<Typography variant="h4" sx={{ mt: "40px" }}>
Ранее вы покупали
</Typography>
<Slider items={tariffElements} />
</SectionWrapper>
);
});
if (tariffElements.length < 6)
tariffElements.push(<FreeTariffCard key="free_tariff_card" />);
else tariffElements.splice(5, 0, <FreeTariffCard key="free_tariff_card" />);
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: "20px",
mb: upMd ? "90px" : "63px",
display: "flex",
flexDirection: "column",
}}
>
<Typography variant="h4" sx={{ marginBottom: "23px", mt: "20px" }}>
{StepperText[unit]}
</Typography>
{isMobile ? (
<Select
items={subPages}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
) : (
<Tabs
items={subPages}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
)}
<Box
sx={{
justifyContent: "center",
mt: "40px",
mb: "30px",
display: "grid",
gap: "40px",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 360px))",
}}
>
{tariffElements}
</Box>
<Typography variant="h4" sx={{ mt: "40px" }}>
Ранее вы покупали
</Typography>
<Slider items={tariffElements} />
</SectionWrapper>
);
}

@ -1,21 +1,21 @@
import { createTariff } from "@root/api/tariff";
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 { create } from "zustand";
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 {
customTariffsMap: ServiceKeyToPrivilegesMap;
privilegeByService: ServiceKeyToPrivilegesMap;
userValuesMap: CustomTariffUserValuesMap;
summaryPriceBeforeDiscountsMap: ServiceKeyToPriceMap;
summaryPriceAfterDiscountsMap: ServiceKeyToPriceMap;
}
const initialState: CustomTariffsStore = {
customTariffsMap: {},
privilegeByService: {},
userValuesMap: {},
summaryPriceBeforeDiscountsMap: {},
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 = (
serviceKey: string,
@ -58,22 +58,22 @@ export const setCustomTariffsUserValue = (
let priceWithoutDiscounts = 0;
let priceAfterDiscounts = 0;
state.customTariffsMap[serviceKey].forEach(tariff => {
const amount = state.userValuesMap[serviceKey]?.[tariff._id] ?? 0;
priceWithoutDiscounts += tariff.price * amount;
state.privilegeByService[serviceKey].forEach(privilege => {
const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0;
priceWithoutDiscounts += privilege.price * amount;
const discount = findPrivilegeDiscount(tariff.privilegeId, tariff.price * amount, discounts);
priceAfterDiscounts += tariff.price * amount * discount.factor;
const discount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * amount, discounts);
priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount);
});
const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts);
priceAfterDiscounts *= serviceDiscount.factor;
priceAfterDiscounts *= findDiscountFactor(serviceDiscount);
const cartDiscount = findCartDiscount(currentCartTotal, discounts);
priceAfterDiscounts *= cartDiscount.factor;
priceAfterDiscounts *= findDiscountFactor(cartDiscount);
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
priceAfterDiscounts *= loyaltyDiscount.factor;
priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount);
state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts;
state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts;
@ -83,22 +83,21 @@ export const setCustomTariffsUserValue = (
export const createAndSendTariff = (serviceKey: string) => {
const state = useCustomTariffsStore.getState();
const privilegies: PrivilegeWithoutPrice[] = [];
const privilegies: PrivilegeWithAmount[] = [];
Object.entries(state.userValuesMap[serviceKey]).forEach(([privilegeId, userValue]) => {
if (userValue === 0) return;
const privilege = state.customTariffsMap[serviceKey].find(p => p._id === privilegeId);
if (!privilege) throw new Error(`Privilege not found: ${privilegeId}`);
const privilegeWithoutAmount = state.privilegeByService[serviceKey].find(p => p._id === privilegeId);
if (!privilegeWithoutAmount) throw new Error(`Privilege not found: ${privilegeId}`);
const p2 = {
...privilege,
privilegeId: privilege._id,
const privilege: PrivilegeWithAmount = {
...privilegeWithoutAmount,
privilegeId: privilegeWithoutAmount._id,
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(", ");

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,
{
type: "updateTariffs",
tariffsLength: tariffs.length,
tariffs: tariffs,
}
);

@ -1,3 +1,4 @@
// @ts-nocheck TODO fix tests
import { CartData, Discount, Tariff } from "@frontend/kitui";
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 {
@ -13,25 +13,31 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
};
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);
if (!serviceData) {
serviceData = {
serviceKey: tariff.privilegies[0].serviceKey,
tariffs: [],
price: 0,
appliedServiceDiscount: null,
};
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 => {
let serviceData = cartData.services.find(service => service.serviceKey === privilege.serviceKey);
if (!serviceData) {
serviceData = {
serviceKey: privilege.serviceKey,
privileges: [],
price: 0,
appliedServiceDiscount: null,
};
cartData.services.push(serviceData);
}
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,
serviceKey: privilege.serviceKey,
privilegeId: privilege.privilegeId,
@ -41,11 +47,13 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
tariffName: tariff.name,
};
serviceData.privileges.push(privilegeData);
serviceData.price += privilegePrice;
tariffCartData.privileges.push(privilegeCartData);
cartData.priceAfterDiscounts += privilegePrice;
cartData.itemCount++;
});
cartData.priceBeforeDiscounts += tariffCartData.price;
serviceData.price += tariffCartData.price;
});
applyPrivilegeDiscounts(cartData, discounts);

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

@ -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]) {
@ -22,4 +22,4 @@ export function getDeclension(number: number, word: PrivilegeValueType | "мес
case "МБ":
return "МБ";
}
};
};

@ -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"
strip-json-comments "^3.1.1"
"@frontend/kitui@^1.0.17":
version "1.0.17"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.17.tgz#a5bddaaa18b168be0e1814d5cfbd86e4030d15af"
integrity sha1-pb3aqhixaL4OGBTVz72G5AMNFa8=
"@frontend/kitui@1.0.21":
version "1.0.21"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.21.tgz#572b3e774ba42b65c4b946704aeb68237b4b3cd5"
integrity sha1-Vys+d0ukK2XEuUZwSutoI3tLPNU=
dependencies:
immer "^10.0.2"
reconnecting-eventsource "^1.6.2"