add custom tariffs value inputs
This commit is contained in:
parent
c1894398f2
commit
912d16c22d
87
src/components/NumberInputWithUnitAdornment.tsx
Normal file
87
src/components/NumberInputWithUnitAdornment.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { InputAdornment, TextField, Typography, useTheme } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
adornmentText: string;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NumberInputWithUnitAdornment({ id, adornmentText, onChange }: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [valueField, setValueField] = useState<string>("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
size="small"
|
||||||
|
placeholder="Введите вручную"
|
||||||
|
id={id}
|
||||||
|
value={valueField}
|
||||||
|
onChange={e => {
|
||||||
|
let n = parseInt(e.target.value);
|
||||||
|
|
||||||
|
if (!isFinite(n)) n = 0;
|
||||||
|
|
||||||
|
onChange(n);
|
||||||
|
setValueField(n.toString());
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
maxWidth: "200px",
|
||||||
|
minWidth: "200px",
|
||||||
|
".MuiInputBase-root": {
|
||||||
|
pr: 0,
|
||||||
|
height: "48px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
backgroundColor: "#F2F3F7",
|
||||||
|
"fieldset": {
|
||||||
|
border: "1px solid" + theme.palette.grey2.main,
|
||||||
|
},
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: theme.palette.brightPurple.main,
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
height: "31px",
|
||||||
|
borderRight: !valueField ? "none" : "1px solid #9A9AAF",
|
||||||
|
},
|
||||||
|
"&.Mui-focused input": {
|
||||||
|
borderRight: "1px solid #9A9AAF",
|
||||||
|
},
|
||||||
|
"&:not(.Mui-focused) .MuiInputAdornment-root": {
|
||||||
|
display: !valueField ? "none" : undefined,
|
||||||
|
},
|
||||||
|
"&.Mui-focused ::-webkit-input-placeholder": {
|
||||||
|
color: "transparent",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hiding arrows
|
||||||
|
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": {
|
||||||
|
"WebkitAppearance": "none",
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
"input[type = number]": {
|
||||||
|
"MozAppearance": "textfield",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment
|
||||||
|
position="end"
|
||||||
|
sx={{
|
||||||
|
userSelect: "none",
|
||||||
|
pointerEvents: "none",
|
||||||
|
pl: "2px",
|
||||||
|
pr: "13px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" color="#4D4D4D">
|
||||||
|
{adornmentText}
|
||||||
|
</Typography>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -11,10 +11,10 @@ import { enqueueSnackbar } from "notistack";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serviceKey: string;
|
serviceKey: string;
|
||||||
tariffs: Privilege[];
|
privileges: Privilege[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomTariffCard({ serviceKey, tariffs }: Props) {
|
export default function CustomTariffCard({ serviceKey, privileges }: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const summaryPriceBeforeDiscounts = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
|
const summaryPriceBeforeDiscounts = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
|
||||||
@ -43,29 +43,31 @@ export default function CustomTariffCard({ serviceKey, tariffs }: Props) {
|
|||||||
boxShadow: cardShadow,
|
boxShadow: cardShadow,
|
||||||
}}>
|
}}>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
width: upMd ? "68.5%" : undefined,
|
|
||||||
p: "20px",
|
p: "20px",
|
||||||
pr: upMd ? "35px" : undefined,
|
pr: upMd ? "35px" : undefined,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
flexBasis: 0,
|
||||||
|
flexGrow: 2.37,
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "25px",
|
gap: "25px",
|
||||||
}}>
|
}}>
|
||||||
{tariffs.map(tariff =>
|
{privileges.map(privilege =>
|
||||||
<TariffPrivilegeSlider
|
<TariffPrivilegeSlider
|
||||||
key={tariff._id}
|
key={privilege._id}
|
||||||
tariff={tariff}
|
privilege={privilege}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{!upMd && <Divider sx={{ mx: "20px", my: "10px", borderColor: theme.palette.grey2.main }} />}
|
{!upMd && <Divider sx={{ mx: "20px", my: "10px", borderColor: theme.palette.grey2.main }} />}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
flexBasis: 0,
|
||||||
|
flexGrow: 1,
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "start",
|
alignItems: "start",
|
||||||
color: theme.palette.grey3.main,
|
color: theme.palette.grey3.main,
|
||||||
width: upMd ? "31.5%" : undefined,
|
|
||||||
p: "20px",
|
p: "20px",
|
||||||
pl: upMd ? "33px" : undefined,
|
pl: upMd ? "33px" : undefined,
|
||||||
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
|
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
|
||||||
|
@ -7,11 +7,8 @@ import ComplexHeader from "@root/components/ComplexHeader";
|
|||||||
import CustomTariffCard from "./CustomTariffCard";
|
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";
|
||||||
|
|
||||||
interface ServiceMap {
|
|
||||||
[key: string]: string;
|
|
||||||
}
|
|
||||||
const servicemap: ServiceMap = { templategen: "Шаблонизатор" };
|
|
||||||
|
|
||||||
export default function TariffConstructor() {
|
export default function TariffConstructor() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -23,72 +20,71 @@ export default function TariffConstructor() {
|
|||||||
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
|
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
|
||||||
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
|
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
return (
|
<SectionWrapper
|
||||||
<SectionWrapper
|
maxWidth="lg"
|
||||||
maxWidth="lg"
|
sx={{
|
||||||
sx={{
|
mt: upMd ? "25px" : "20px",
|
||||||
mt: upMd ? "25px" : "20px",
|
mb: upMd ? "93px" : "48px",
|
||||||
mb: upMd ? "93px" : "48px",
|
}}
|
||||||
}}
|
|
||||||
>
|
|
||||||
{upMd && <ComplexNavText text1="Все тарифы — " text2="Кастомный тариф" />}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mt: "20px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "80px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.entries(customTariffs).map(([serviceKey, tariffs], index) => (
|
|
||||||
<Box key={serviceKey}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mb: "40px",
|
|
||||||
display: "flex",
|
|
||||||
gap: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!upMd && index === 0 && (
|
|
||||||
<IconButton
|
|
||||||
sx={{
|
|
||||||
p: 0,
|
|
||||||
height: "28px",
|
|
||||||
width: "28px",
|
|
||||||
color: "black",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowBackIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
<ComplexHeader
|
|
||||||
text1="Кастомный тариф "
|
|
||||||
text2={servicemap[serviceKey]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<CustomTariffCard serviceKey={serviceKey} tariffs={tariffs} />
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
{upMd && (
|
|
||||||
<Link
|
|
||||||
to="/tariffconstructor/savedtariffs"
|
|
||||||
style={{
|
|
||||||
display: "block",
|
|
||||||
marginTop: "60px",
|
|
||||||
textUnderlinePosition: "under",
|
|
||||||
color: theme.palette.brightPurple.main,
|
|
||||||
textDecorationColor: theme.palette.brightPurple.main,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Ваши сохраненные тарифы
|
{upMd && <ComplexNavText text1="Все тарифы — " text2="Кастомный тариф" />}
|
||||||
</Link>
|
<Box
|
||||||
)}
|
sx={{
|
||||||
<TotalPrice
|
mt: "20px",
|
||||||
priceBeforeDiscounts={basePrice}
|
display: "flex",
|
||||||
priceAfterDiscounts={discountedPrice}
|
flexDirection: "column",
|
||||||
/>
|
gap: "80px",
|
||||||
</SectionWrapper>
|
}}
|
||||||
);
|
>
|
||||||
|
{Object.entries(customTariffs).map(([serviceKey, privileges], index) => (
|
||||||
|
<Box key={serviceKey}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: "40px",
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!upMd && index === 0 && (
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
height: "28px",
|
||||||
|
width: "28px",
|
||||||
|
color: "black",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<ComplexHeader
|
||||||
|
text1="Кастомный тариф "
|
||||||
|
text2={serviceNameByKey[serviceKey]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<CustomTariffCard serviceKey={serviceKey} privileges={privileges} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
{upMd && (
|
||||||
|
<Link
|
||||||
|
to="/tariffconstructor/savedtariffs"
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
marginTop: "60px",
|
||||||
|
textUnderlinePosition: "under",
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
textDecorationColor: theme.palette.brightPurple.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ваши сохраненные тарифы
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<TotalPrice
|
||||||
|
priceBeforeDiscounts={basePrice}
|
||||||
|
priceAfterDiscounts={discountedPrice}
|
||||||
|
/>
|
||||||
|
</SectionWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useThrottle } from "@frontend/kitui";
|
import { useThrottle } from "@frontend/kitui";
|
||||||
import { Box, SliderProps, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, SliderProps, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import CustomSlider from "@root/components/CustomSlider";
|
import CustomSlider from "@root/components/CustomSlider";
|
||||||
|
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 { Privilege, PrivilegeValueType } from "@root/model/privilege";
|
||||||
@ -8,14 +9,13 @@ import { useCartStore } from "@root/stores/cart";
|
|||||||
import { setCustomTariffsUserValue, useCustomTariffsStore } from "@root/stores/customTariffs";
|
import { setCustomTariffsUserValue, useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||||
import { useDiscountStore } from "@root/stores/discounts";
|
import { useDiscountStore } from "@root/stores/discounts";
|
||||||
import { useUserStore } from "@root/stores/user";
|
import { useUserStore } from "@root/stores/user";
|
||||||
import { formatDateWithDeclention } from "@root/utils/date";
|
|
||||||
import { getDeclension } from "@root/utils/declension";
|
import { getDeclension } from "@root/utils/declension";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
const sliderSettingsByType: Record<PrivilegeValueType, Partial<SliderProps>> = {
|
const sliderSettingsByType: Record<PrivilegeValueType, Partial<SliderProps>> = {
|
||||||
"день": {
|
"день": {
|
||||||
max: 366,
|
max: 365,
|
||||||
step: 1,
|
step: 1,
|
||||||
},
|
},
|
||||||
"шаблон": {
|
"шаблон": {
|
||||||
@ -29,30 +29,29 @@ const sliderSettingsByType: Record<PrivilegeValueType, Partial<SliderProps>> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tariff: Privilege;
|
privilege: Privilege;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TariffPrivilegeSlider({ tariff }: Props) {
|
export default function TariffPrivilegeSlider({ privilege }: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const userValue = useCustomTariffsStore(state => state.userValuesMap[tariff.serviceKey]?.[tariff._id]) ?? 0;
|
const userValue = useCustomTariffsStore(state => state.userValuesMap[privilege.serviceKey]?.[privilege._id]) ?? 0;
|
||||||
const discounts = useDiscountStore(state => state.discounts);
|
const discounts = useDiscountStore(state => state.discounts);
|
||||||
const currentCartTotal = useCartStore(state => state.cart.priceAfterDiscounts);
|
const currentCartTotal = useCartStore(state => state.cart.priceAfterDiscounts);
|
||||||
const purchasesAmount = useUserStore(state => state.userAccount?.wallet.purchasesAmount) ?? 0;
|
const purchasesAmount = useUserStore(state => state.userAccount?.wallet.purchasesAmount) ?? 0;
|
||||||
const [value, setValue] = useState<number>(userValue);
|
const [value, setValue] = useState<number>(userValue);
|
||||||
const throttledValue = useThrottle(value, 200);
|
const throttledValue = useThrottle(value, 200);
|
||||||
|
|
||||||
const quantityText = `${value} ${getDeclension(value, tariff.value)}`;
|
useEffect(function setStoreValue() {
|
||||||
|
setCustomTariffsUserValue(
|
||||||
const quantityElement = (
|
privilege.serviceKey,
|
||||||
<Typography variant="p1" color={theme.palette.brightPurple.main} mt={upMd ? undefined : "12px"}>
|
privilege._id,
|
||||||
{quantityText}
|
throttledValue,
|
||||||
</Typography>
|
discounts,
|
||||||
);
|
currentCartTotal,
|
||||||
|
purchasesAmount
|
||||||
const icon = tariff.type === "day"
|
);
|
||||||
? <CalendarIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />
|
}, [currentCartTotal, discounts, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue]);
|
||||||
: <PieChartIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />;
|
|
||||||
|
|
||||||
function handleSliderChange(event: Event, value: number | number[]) {
|
function handleSliderChange(event: Event, value: number | number[]) {
|
||||||
if (Array.isArray(value)) throw new Error("Slider uses multiple values instead of one");
|
if (Array.isArray(value)) throw new Error("Slider uses multiple values instead of one");
|
||||||
@ -60,22 +59,44 @@ export default function TariffPrivilegeSlider({ tariff }: Props) {
|
|||||||
setValue(value);
|
setValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(function setStoreValue() {
|
const quantityText = `${value} ${getDeclension(value, privilege.value)}`;
|
||||||
console.log(currentCartTotal)
|
|
||||||
setCustomTariffsUserValue(
|
const quantityElement = (
|
||||||
tariff.serviceKey,
|
<Box sx={{
|
||||||
tariff._id,
|
display: "flex",
|
||||||
throttledValue,
|
gap: "15px",
|
||||||
discounts,
|
alignItems: "center",
|
||||||
currentCartTotal,
|
justifyContent: upMd ? "end" : undefined,
|
||||||
purchasesAmount
|
flexWrap: "wrap",
|
||||||
);
|
mt: upMd ? undefined : "12px",
|
||||||
}, [currentCartTotal, discounts, purchasesAmount, tariff._id, tariff.serviceKey, throttledValue]);
|
}}>
|
||||||
|
<Typography variant="p1" color={theme.palette.brightPurple.main} textAlign="end">
|
||||||
|
{quantityText}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "15px",
|
||||||
|
alignItems: "center",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}>
|
||||||
|
<Typography sx={{ fontSize: "16px", lineHeight: "19px", mt: "1px" }}>или</Typography>
|
||||||
|
<NumberInputWithUnitAdornment
|
||||||
|
id={"privilege_input_" + privilege._id}
|
||||||
|
adornmentText={getDeclension(0, privilege.value)}
|
||||||
|
onChange={value => setValue(value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const icon = privilege.type === "day"
|
||||||
|
? <CalendarIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />
|
||||||
|
: <PieChartIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography sx={{ color: theme.palette.grey3.main, mb: "auto" }}>
|
<Typography sx={{ color: theme.palette.grey3.main, mb: "auto" }}>
|
||||||
{tariff.description}
|
{privilege.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -84,16 +105,19 @@ export default function TariffPrivilegeSlider({ tariff }: Props) {
|
|||||||
}}>
|
}}>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
// flexWrap: "wrap",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
mb: "8px",
|
mb: "8px",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
gap: "10px",
|
||||||
}}>
|
}}>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
gap: "22px",
|
gap: "22px",
|
||||||
}}>
|
}}>
|
||||||
{icon}
|
{icon}
|
||||||
<Typography variant="h5">{tariff.name}</Typography>
|
<Typography variant="h5">{privilege.name}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{upMd && quantityElement}
|
{upMd && quantityElement}
|
||||||
</Box>
|
</Box>
|
||||||
@ -102,7 +126,7 @@ export default function TariffPrivilegeSlider({ tariff }: Props) {
|
|||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={handleSliderChange}
|
onChange={handleSliderChange}
|
||||||
{...sliderSettingsByType[tariff.value]}
|
{...sliderSettingsByType[privilege.value]}
|
||||||
/>
|
/>
|
||||||
{!upMd && quantityElement}
|
{!upMd && quantityElement}
|
||||||
</Box>
|
</Box>
|
||||||
|
5
src/utils/serviceKeys.ts
Normal file
5
src/utils/serviceKeys.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const serviceNameByKey: Record<string, string | undefined> = {
|
||||||
|
templategen: "Шаблонизатор",
|
||||||
|
squiz: "Опросник",
|
||||||
|
reducer: "Сокращатель ссылок",
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user