add custom tariffs value inputs

This commit is contained in:
nflnkr 2023-07-27 19:51:12 +03:00 committed by Nastya
parent c1894398f2
commit 912d16c22d
5 changed files with 222 additions and 108 deletions

@ -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

@ -0,0 +1,5 @@
export const serviceNameByKey: Record<string, string | undefined> = {
templategen: "Шаблонизатор",
squiz: "Опросник",
reducer: "Сокращатель ссылок",
};