Merge pull request #229 from bluewave-labs/feat/create-monitor-refactor

Refactored create monitor page styling using @mui components, resolves #218
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-06-28 19:39:58 -07:00
commit 61832570c3
14 files changed
+302 -243

No files matched your search

+8 -17
View File
@@ -1,24 +1,15 @@
.config-box {
display: flex;
padding: 30px 40px;
padding: var(--env-var-spacing-4) 50px;
padding-bottom: 60px;
border: 1px solid var(--env-var-color-16);
border-radius: var(--env-var-radius-1);
justify-content: space-between;
padding-bottom: 50px;
}
.config-box-desc {
width: 35%;
}
.config-box-desc-title {
color: var(--env-var-color-5);
font-size: var(--env-var-font-size-large);
font-weight: 700;
margin-bottom: var(--env-var-spacing-1);
}
.config-box-desc-text {
color: var(--env-var-color-5);
font-size: var(--env-var-font-size-medium);
.config-box>.MuiStack-root{
flex-direction: column;
flex: 1;
}
.config-box>.MuiStack-root:first-of-type{
margin-right: var(--env-var-spacing-3);
}
+15 -2
View File
@@ -1,3 +1,16 @@
.MuiButtonBase-root.MuiRadio-root.MuiRadio-colorPrimary.PrivateSwitchBase-root.MuiRadio-root.MuiRadio-colorPrimary.MuiRadio-root.MuiRadio-colorPrimary.css-w5ovia-MuiButtonBase-root-MuiRadio-root {
padding-top: 0;
.create-monitor-form .custom-radio-button.MuiFormControlLabel-root {
padding: 5px;
margin: -5px;
border-radius: var(--env-var-radius-1);
}
.create-monitor-form .custom-radio-button.MuiFormControlLabel-root:hover {
background-color: var(--env-var-color-13);
}
.create-monitor-form .custom-radio-button.MuiFormControlLabel-root svg{
stroke-width: 0;
}
.custom-radio-button.MuiFormControlLabel-root .MuiButtonBase-root {
margin-right: var(--env-var-spacing-1);
padding: 0;
}
+27 -18
View File
@@ -18,28 +18,37 @@
* @returns {JSX.Element} - The rendered RadioButton component.
*/
import { FormControlLabel, Radio } from "@mui/material";
import React from "react";
import "./index.css";
import PropTypes from "prop-types";
import { FormControlLabel, Radio, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import "./index.css";
function RadioButton(props) {
const theme = useTheme();
return (
<div className="custom-radio-button">
<FormControlLabel
checked={props.checked}
value={props.value}
control={<Radio id={props.id} size={props.size} />}
onChange={props.onChange}
label={
<div>
<div className="service-check-list-title">{props.title}</div>
<div className="service-check-list-desc">{props.desc}</div>
</div>
}
labelPlacement="end"
/>
</div>
<FormControlLabel
className="custom-radio-button"
checked={props.checked}
value={props.value}
control={
<Radio
id={props.id}
size={props.size}
sx={{ color: theme.palette.secondary.main }}
/>
}
onChange={props.onChange}
label={
<>
<Typography component="p">{props.title}</Typography>
<Typography component="h6" mt="2px">
{props.desc}
</Typography>
</>
}
labelPlacement="end"
sx={{ margin: 0, alignItems: "flex-start" }}
/>
);
}
@@ -107,9 +107,7 @@ const PasswordPanel = () => {
</div>
<div className="edit-password-form__wrapper">
<Stack>
<Typography variant="h4" component="h1">
Current password
</Typography>
<Typography component="h1">Current password</Typography>
</Stack>
<Stack>
<PasswordTextField
@@ -123,7 +121,7 @@ const PasswordPanel = () => {
error={errors[idToName["edit-current-password"]] ? true : false}
/>
{errors[idToName["edit-current-password"]] ? (
<Typography variant="h5" component="p" className="input-error">
<Typography component="p" className="input-error">
{errors[idToName["edit-current-password"]]}
</Typography>
) : (
@@ -133,9 +131,7 @@ const PasswordPanel = () => {
</div>
<div className="edit-password-form__wrapper">
<Stack>
<Typography variant="h4" component="h1">
New password
</Typography>
<Typography component="h1">New password</Typography>
</Stack>
<Stack>
<PasswordTextField
@@ -149,7 +145,7 @@ const PasswordPanel = () => {
error={errors[idToName["edit-new-password"]] ? true : false}
/>
{errors[idToName["edit-new-password"]] ? (
<Typography variant="h5" component="p" className="input-error">
<Typography component="p" className="input-error">
{errors[idToName["edit-new-password"]]}
</Typography>
) : (
@@ -159,9 +155,7 @@ const PasswordPanel = () => {
</div>
<div className="edit-password-form__wrapper">
<Stack>
<Typography variant="h4" component="h1">
Confirm new password
</Typography>
<Typography component="h1">Confirm new password</Typography>
</Stack>
<Stack>
<PasswordTextField
@@ -175,7 +169,7 @@ const PasswordPanel = () => {
error={errors[idToName["edit-confirm-password"]] ? true : false}
/>
{errors[idToName["edit-confirm-password"]] ? (
<Typography variant="h5" component="p" className="input-error">
<Typography component="p" className="input-error">
{errors[idToName["edit-confirm-password"]]}
</Typography>
) : (
@@ -152,9 +152,7 @@ const ProfilePanel = () => {
<form className="edit-profile-form" noValidate spellCheck="false">
<div className="edit-profile-form__wrapper">
<Stack>
<Typography variant="h4" component="h1">
First name
</Typography>
<Typography component="h1">First name</Typography>
</Stack>
<Stack>
<StringTextField
@@ -167,7 +165,7 @@ const ProfilePanel = () => {
error={errors[idToName["edit-first-name"]] ? true : false}
/>
{errors[idToName["edit-first-name"]] ? (
<Typography variant="h5" component="p" className="input-error">
<Typography component="p" className="input-error">
{errors[idToName["edit-first-name"]]}
</Typography>
) : (
@@ -177,9 +175,7 @@ const ProfilePanel = () => {
</div>
<div className="edit-profile-form__wrapper">
<Stack>
<Typography variant="h4" component="h1">
Last name
</Typography>
<Typography component="h1">Last name</Typography>
</Stack>
<Stack>
<StringTextField
@@ -192,7 +188,7 @@ const ProfilePanel = () => {
error={errors[idToName["edit-last-name"]] ? true : false}
/>
{errors[idToName["edit-last-name"]] ? (
<Typography variant="h5" component="p" className="input-error">
<Typography component="p" className="input-error">
{errors[idToName["edit-last-name"]]}
</Typography>
) : (
@@ -202,10 +198,8 @@ const ProfilePanel = () => {
</div>
<div className="edit-profile-form__wrapper">
<Stack>
<Typography variant="h4" component="h1">
Email
</Typography>
<Typography variant="h5" component="p">
<Typography component="h1">Email</Typography>
<Typography component="p">
This is your current email address — it cannot be changed.
</Typography>
</Stack>
@@ -221,7 +215,7 @@ const ProfilePanel = () => {
disabled={true}
/>
{errors[idToName["edit-email"]] ? (
<Typography variant="h5" component="p" className="input-error">
<Typography component="p" className="input-error">
{errors[idToName["edit-email"]]}
</Typography>
) : (
@@ -231,10 +225,8 @@ const ProfilePanel = () => {
</div>
<div className="edit-profile-form__wrapper">
<Stack>
<Typography variant="h4" component="h1">
Your photo
</Typography>
<Typography variant="h5" component="p">
<Typography component="h1">Your photo</Typography>
<Typography component="p">
This photo will be displayed in your profile page.
</Typography>
</Stack>
@@ -292,10 +284,8 @@ const ProfilePanel = () => {
<form className="delete-profile-form" noValidate spellCheck="false">
<div className="delete-profile-form__wrapper">
<Stack direction="column" gap="15px">
<Typography variant="h4" component="h1">
Delete account
</Typography>
<Typography variant="h5" component="p">
<Typography component="h1">Delete account</Typography>
<Typography component="p">
Note that deleting your account will remove all data from our
system. This is permanent and non-recoverable.
</Typography>
@@ -335,14 +325,10 @@ const ProfilePanel = () => {
},
}}
>
<Typography id="modal-delete-account" variant="h4" component="h1">
<Typography id="modal-delete-account" component="h1">
Really delete this account?
</Typography>
<Typography
id="delete-account-confirmation"
variant="h5"
component="p"
>
<Typography id="delete-account-confirmation" component="p">
If you delete your account, you will no longer be able to sign in,
and all of your data will be deleted. Deleting your account is
permanent and non-recoverable action.
@@ -386,7 +372,7 @@ const ProfilePanel = () => {
},
}}
>
<Typography id="modal-update-picture" variant="h4" component="h1">
<Typography id="modal-update-picture" component="h1">
Upload Image
</Typography>
<ImageField
@@ -186,13 +186,9 @@ const TeamPanel = () => {
return (
<TabPanel value="team">
<form className="edit-organization-form">
<div
className="edit-organization-form__wrapper"
>
<div className="edit-organization-form__wrapper">
<Stack>
<Typography variant="h4" component="h1">
Organization name
</Typography>
<Typography component="h1">Organization name</Typography>
</Stack>
<Stack
id="org-name-flex-container"
@@ -247,7 +243,7 @@ const TeamPanel = () => {
},
}}
>
<Typography id="modal-edit-org-name" variant="h4" component="h1">
<Typography id="modal-edit-org-name" component="h1">
Rename this organization?
</Typography>
<TextField
@@ -280,9 +276,7 @@ const TeamPanel = () => {
</Modal>
<form className="edit-team-form" noValidate spellCheck="false">
<div className="edit-team-form__wrapper">
<Typography variant="h4" component="h1">
Team members
</Typography>
<Typography component="h1">Team members</Typography>
</div>
<div className="edit-team-form__wrapper compact">
<Stack
@@ -488,10 +482,10 @@ const TeamPanel = () => {
},
}}
>
<Typography id="modal-invite-member" variant="h4" component="h1">
<Typography id="modal-invite-member" component="h1">
Invite new team member
</Typography>
<Typography id="invite-member-to-team" variant="h5" component="p">
<Typography id="invite-member-to-team" component="p">
When you add a new team member, they will get access to all
monitors.
</Typography>
@@ -1,5 +1,19 @@
import PropTypes from "prop-types";
import { Stack, Typography } from "@mui/material";
import "./flexibileTextField.css";
import React from "react";
import { useTheme } from "@emotion/react";
/**
* @param {Object} props - The component props.
* @param {string} props.id - The ID of the input field.
* @param {string} props.title - The title or label for the input field.
* @param {string} props.type - The type of the input field (e.g., 'text', 'password').
* @param {string} props.placeholder - The placeholder text for the input field.
* @param {string} props.value - The current value of the input field.
* @param {function} props.onChange - The function to call when the input field value changes.
* @param {string} [props.error] - Optional. Error message to display if there's an error with the input.
* @returns {JSX.Element} JSX Element representing the flexible text input field.
*/
const FlexibileTextField = ({
id,
@@ -8,20 +22,38 @@ const FlexibileTextField = ({
placeholder,
value,
onChange,
error,
}) => {
const theme = useTheme();
return (
<div className="flexibile-textfield">
<div className="flexibile-textfield-title">{title}</div>
<Stack className="flexible-textfield" gap={theme.gap.small}>
<Typography component="h2">{title}</Typography>
<input
id={id}
type={type}
className="flexibile-textfield-input"
placeholder={placeholder}
value={value}
onChange={onChange}
/>
</div>
{error ? (
<Typography component="p" className="input-error">
{error}
</Typography>
) : (
""
)}
</Stack>
);
};
FlexibileTextField.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
placeholder: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
onChange: PropTypes.func.isRequired,
error: PropTypes.string,
};
export default FlexibileTextField;
@@ -1,22 +1,30 @@
.flexibile-textfield-title {
.flexible-textfield{
position: relative;
}
.flexible-textfield h2.MuiTypography-root{
color: var(--env-var-color-5);
font-size: var(--env-var-font-size-medium);
font-weight: 400;
}
.flexibile-textfield-input {
width: 292.8px;
height: 6.8px;
padding: var(--env-var-spacing-1);
margin-top: 10px;
.flexible-textfield h2.MuiTypography-root span{
opacity: 0.6;
}
.flexible-textfield input {
min-width: 300px;
height: 34px;
padding: 0 var(--env-var-spacing-1);
border: 1px solid var(--env-var-color-4);
border-radius: var(--env-var-radius-1);
}
.flexibile-textfield-input:focus {
.flexible-textfield p.MuiTypography-root.input-error {
color: var(--env-var-color-24);
opacity: 0.8;
position: absolute;
top: calc(100% + 5px);
}
.flexible-textfield:has(p.MuiTypography-root.input-error) input{
border-color: var(--env-var-color-24);
}
.flexible-textfield input:focus {
outline: none;
}
.flexibile-textfield-input::placeholder {
color: var(--env-var-color-25);
font-size: var(--env-var-font-size-medium);
}
}
+2 -2
View File
@@ -30,8 +30,8 @@ const Account = ({ open = "profile" }) => {
//TODO - breakpoints for responsive design
minWidth={theme.spacing(55)}
maxWidth="1200px"
py={theme.spacing(5)}
px={theme.spacing(10)}
py={theme.content.pY}
px={theme.content.pX}
className="account"
>
<TabContext value={tab}>
+50 -65
View File
@@ -1,82 +1,67 @@
.monitors,
.new-monitor {
width: 70vw;
padding: 4vw;
.create-monitor-form h1.MuiTypography-root {
font-size: var(--env-var-font-size-large-plus);
color: var(--env-var-color-1);
}
.create-monitor-form h1.MuiTypography-root,
.create-monitor-form h2.MuiTypography-root {
font-weight: 600;
}
.create-monitor-form h2.MuiTypography-root {
font-size: var(--env-var-font-size-large);
}
.create-monitor-form p.MuiTypography-root,
#monitor-frequencies {
font-size: var(--env-var-font-size-medium);
}
.create-monitor-form h2.MuiTypography-root,
.create-monitor-form h6.MuiTypography-root,
.create-monitor-form p.MuiTypography-root,
#monitor-frequencies {
color: var(--env-var-color-2);
}
.create-monitor-form h6.MuiTypography-root {
font-size: var(--env-var-font-size-small-plus);
opacity: 0.8;
}
.monitors-bar {
display: flex;
justify-content: space-between;
align-items: center;
.create-monitor-form .error-container {
position: relative;
}
.create-monitor-form .error-container p.MuiTypography-root.input-error {
color: var(--env-var-color-24);
opacity: 0.8;
position: absolute;
top: 0;
}
.monitors-bar-title {
font-size: 24px;
font-weight: 700;
}
/* for testing, will remove later */
/* .config-box:has(.error-container):has(.input-error) {
border-color: var(--env-var-color-24);
} */
.monitors-stats {
display: flex;
gap: 24px;
.create-monitor-form .MuiSelect-select.MuiSelect-outlined.MuiInputBase-input {
padding: var(--env-var-spacing-1);
}
.create-monitor-form .MuiInputBase-root:has(#monitor-frequencies) {
min-width: 300px;
width: 100%;
height: 34px;
border: 1px solid var(--env-var-color-4);
border-radius: var(--env-var-radius-1);
overflow: hidden;
}
.create-monitor-form .MuiInputBase-root:has(#monitor-frequencies) fieldset {
outline: none;
border: none;
}
.monitors-gaps-medium {
height: var(--env-var-spacing-2);
}
.monitors-gaps-small-plus {
height: var(--env-var-spacing-1-plus);
}
.new-monitor-header {
color: var(--env-var-color-1);
font-size: var(--env-var-font-size-large-plus);
font-weight: 600;
}
.service-check-list-title {
color: var(--env-var-color-5);
font-size: var(--env-var-font-size-medium);
}
.service-check-list-desc {
color: var(--env-var-color-25);
font-size: var(--env-var-font-size-small);
}
.host-status-box-text {
font-size: var(--env-var-font-size-medium);
}
.monitors-dropdown-holder {
padding-left: 30px;
}
#ports-list {
width: 172.92px;
height: 12px;
font-size: var(--env-var-font-size-small);
align-items: center;
}
.incident-notif-config-title,
.adv-setting-title {
color: var(--env-var-color-5);
font-size: var(--env-var-font-size-medium);
}
.adv-setting-title {
font-weight: 500;
}
.config-box-inputs,
.service-check-list,
.incident-notif-config,
.advanced-setting-config,
.proxy-setting-config {
padding-right: 40px;
}
.monitors-create-button-holder {
display: flex;
justify-content: end;
+88 -64
View File
@@ -5,17 +5,19 @@ import React, { useState } from "react";
import RadioButton from "../../Components/RadioButton";
import CustomizableCheckBox from "../../Components/Checkbox/CustomizableCheckbox";
import Button from "../../Components/Button";
import { MenuItem, Select, Typography } from "@mui/material";
import { Box, MenuItem, Select, Stack, Typography } from "@mui/material";
import { useSelector, useDispatch } from "react-redux";
import { createMonitorValidation } from "../../Validation/validation";
import { createMonitor } from "../../Features/Monitors/monitorsSlice";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
const CreateNewMonitor = () => {
const MS_PER_MINUTE = 60000;
const { user, authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const [errors, setErrors] = useState({});
@@ -120,21 +122,31 @@ const CreateNewMonitor = () => {
const frequencies = [1, 2, 3, 4, 5];
return (
<form onSubmit={handleCreateNewMonitor} noValidate spellCheck="false">
<div className="new-monitor-header">Create new monitor</div>
<form
className="create-monitor-form"
onSubmit={handleCreateNewMonitor}
noValidate
spellCheck="false"
style={{
maxWidth: "1200px",
flex: 1,
padding: `${theme.content.pY} ${theme.content.pX}`,
}}
>
<Typography component="h1">Create new monitor</Typography>
<div className="monitors-gaps-medium"></div>
<ConfigBox
leftLayout={
<div className="config-box-desc">
<div className="config-box-desc-title">General settings</div>
<div className="config-box-desc-text">
<Stack gap={theme.gap.small}>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the type of
monitor.
</div>
</div>
</Typography>
</Stack>
}
rightLayout={
<div className="config-box-inputs">
<Stack gap={theme.gap.xl}>
<FlexibileTextField
id="monitor-url"
title="URL to monitor"
@@ -144,66 +156,57 @@ const CreateNewMonitor = () => {
onChange={(event) =>
handleChange(event, "url", setGeneralSettings)
}
error={errors["url"]}
/>
{errors["url"] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["url"]}
</Typography>
) : (
""
)}
<div className="monitors-gaps-medium"></div>
<FlexibileTextField
id="monitor-name"
title="Friendly name (optional)"
title={
<>
Friendly name <span>(optional)</span>
</>
}
type="text"
placeholder="Google"
value={generalSettings.name}
onChange={(event) =>
handleChange(event, "name", setGeneralSettings)
}
error={errors["name"]}
/>
{errors["name"] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["name"]}
</Typography>
) : (
""
)}
</div>
</Stack>
}
/>
<div className="monitors-gaps-medium"></div>
<div className="monitors-gaps-medium"></div>
<ConfigBox
leftLayout={
<div className="config-box-desc">
<div className="config-box-desc-title">Checks to perform</div>
<div className="config-box-desc-text">
<Stack gap={theme.gap.small}>
<Typography component="h2">Checks to perform</Typography>
<Typography component="p">
You can always add or remove checks after adding your site.
</div>
</div>
</Typography>
</Stack>
}
rightLayout={
<div className="service-check-list">
<Stack gap={theme.gap.large}>
<RadioButton
id="monitor-checks-http"
title="HTTP/website monitoring"
desc="Use HTTP(s) to monitor your website or API endpoint."
size="small"
value="http"
checked={checks.type === "http"}
onChange={(event) => handleChange(event, "type", setChecks)}
/>
<div className="monitors-gaps-medium"></div>
<RadioButton
id="monitor-checks-ping"
title="Ping monitoring"
desc="Check whether your server is available or not."
size="small"
value="ping"
checked={checks.type === "ping"}
onChange={(event) => handleChange(event, "type", setChecks)}
/>
<div className="monitors-gaps-medium"></div>
{/* TODO */}
{/* <RadioButton
id="monitor-checks-port"
@@ -231,14 +234,16 @@ const CreateNewMonitor = () => {
))}
</Select>
</div> */}
{errors["type"] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["type"]}
</Typography>
) : (
""
)}
</div>
<Box className="error-container">
{errors["type"] ? (
<Typography component="p" className="input-error">
{errors["type"]}
</Typography>
) : (
""
)}
</Box>
</Stack>
}
/>
{/* TODO */}
@@ -300,29 +305,47 @@ const CreateNewMonitor = () => {
<div className="monitors-gaps-medium"></div>
<ConfigBox
leftLayout={
<div className="config-box-desc">
<div className="config-box-desc-title">Advanced settings</div>
</div>
<Stack gap={theme.gap.small}>
<Typography component="h2">Advanced settings</Typography>
</Stack>
}
rightLayout={
<div className="advanced-setting-config">
<div className="adv-setting-title">Check frequency</div>
<div className="monitors-gaps-small"></div>
<Select
id="monitor-frequencies"
value={advancedSettings.frequency || 1}
inputProps={{ id: "monitor-frequencies-select" }}
onChange={(event) =>
handleChange(event, "frequency", setAdvancedSettings)
}
>
{frequencies.map((freq) => (
<MenuItem key={`port-${freq}`} value={freq}>
{freq} {freq === 1 ? "minute" : "minutes"}
</MenuItem>
))}
</Select>
<div className="monitors-gaps-medium"></div>
<Stack gap={theme.gap.large}>
{/* TODO - refactor select component */}
<Box>
<Typography component="p" mb={theme.gap.small}>
Check frequency
</Typography>
<Select
id="monitor-frequencies"
value={advancedSettings.frequency || 1}
inputProps={{ id: "monitor-frequencies-select" }}
onChange={(event) =>
handleChange(event, "frequency", setAdvancedSettings)
}
MenuProps={{
PaperProps: {
style: {
marginTop: "10px"
},
},
}}
>
{frequencies.map((freq) => (
<MenuItem
key={`port-${freq}`}
value={freq}
sx={{
fontSize: "13px",
borderRadius: `${theme.shape.borderRadius}px`,
margin: "5px",
}}
>
{freq} {freq === 1 ? "minute" : "minutes"}
</MenuItem>
))}
</Select>
</Box>
{/* TODO */}
{/* <FlexibileTextField
id="monitor-settings-retries"
@@ -353,7 +376,7 @@ const CreateNewMonitor = () => {
handleChange(event, "redirects", setAdvancedSettings)
}
/> */}
</div>
</Stack>
}
/>
{/* TODO */}
@@ -406,6 +429,7 @@ const CreateNewMonitor = () => {
label="Create new monitor"
sx={{ width: "210px", fontSize: "var(--env-var-font-size-medium)" }}
onClick={handleCreateNewMonitor}
disabled={Object.keys(errors).length !== 0 && true}
/>
</div>
</form>
+10
View File
@@ -86,6 +86,16 @@ const theme = createTheme({
borderThick: 2,
boxShadow: shadow,
},
content: {
pX: "80px",
pY: "40px"
},
gap: {
small: "8px",
medium: "12px",
large: "24px",
xl: "40px"
}
});
export default theme;
+15 -3
View File
@@ -133,9 +133,21 @@ const editPasswordValidation = joi.object({
});
const createMonitorValidation = joi.object({
url: joi.string().trim().messages({ "string.empty": "*Url is required." }),
name: joi.string().trim().messages({ "string.empty": "*Name is required." }),
type: joi.string().trim().messages({ "string.empty": "*Type is required." }),
url: joi
.string()
.trim()
.messages({ "string.empty": "*This field is required." }),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "*This field should not exceed the 50 characters limit.",
}),
type: joi
.string()
.trim()
.messages({ "string.empty": "*This field is required." }),
frequency: joi.number().messages({
"number.base": "*Frequency must be a number.",
"any.required": "*Frequency is required.",
}),
});
export {
+1
View File
@@ -64,6 +64,7 @@
--env-var-spacing-4: 40px;
--env-var-font-size-small: 11px;
--env-var-font-size-small-plus: 12px;
--env-var-font-size-medium: 13px;
--env-var-font-size-medium-plus: 14px;
--env-var-font-size-large: 16px;