Skip to content

Commit 3e7ba87

Browse files
committed
Add form field validations
1 parent 07cf4b9 commit 3e7ba87

File tree

5 files changed

+237
-81
lines changed

5 files changed

+237
-81
lines changed

backend/user-service/utils/validators.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function validateUsername(username: string): {
6868
return {
6969
isValid: false,
7070
message:
71-
"Username must only contain alphanumeric characters, underscores, and full stops",
71+
"Username must only contain alphanumeric characters, underscores and full stops",
7272
};
7373
}
7474

@@ -86,10 +86,10 @@ export function validateName(
8686
};
8787
}
8888

89-
if (!/^[a-zA-Z0-9\s]+$/.test(name)) {
89+
if (!/^[a-zA-Z\s-]*$/.test(name)) {
9090
return {
9191
isValid: false,
92-
message: `${type} must only contain alphanumeric characters and white spaces`,
92+
message: `${type} must only contain alphabetical, hypen and white space characters`,
9393
};
9494
}
9595

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { TextField, TextFieldPropsSizeOverrides, TextFieldVariants } from "@mui/material";
2+
import { OverridableStringUnion } from '@mui/types';
3+
import { useState } from "react";
4+
5+
// Adapted from https://muhimasri.com/blogs/mui-validation/
6+
type CustomTextFieldProps = {
7+
label: string;
8+
variant?: TextFieldVariants;
9+
size?: OverridableStringUnion<"small" | "medium", TextFieldPropsSizeOverrides>;
10+
required?: boolean;
11+
emptyField?: boolean;
12+
validator?: (value: string) => string;
13+
onChange: (value: string, isValid: boolean) => void;
14+
};
15+
16+
const CustomTextField: React.FC<CustomTextFieldProps> = ({
17+
label,
18+
variant = "outlined",
19+
size = "medium",
20+
required = false,
21+
emptyField = false,
22+
validator,
23+
onChange,
24+
}) => {
25+
const [error, setError] = useState<string>("");
26+
27+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
28+
const input = event.target.value;
29+
30+
let errorMessage = "";
31+
if (validator) {
32+
errorMessage = validator(input);
33+
setError(errorMessage);
34+
}
35+
36+
onChange(input, !errorMessage);
37+
};
38+
39+
return (
40+
<TextField
41+
label={label}
42+
variant={variant}
43+
size={size}
44+
required={required}
45+
onChange={handleChange}
46+
error={(required && emptyField) || !!error}
47+
helperText={error}
48+
/>
49+
);
50+
};
51+
52+
export default CustomTextField;

frontend/src/pages/LogIn/index.tsx

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,47 @@
1-
import { Box, Button, Stack, TextField, Typography } from "@mui/material";
1+
import { Box, Button, Stack, Typography } from "@mui/material";
22
import LogInSvg from "../../assets/login.svg?react";
3-
import { useState } from "react";
3+
import { useRef, useState } from "react";
44
import { useNavigate } from "react-router-dom";
55
import { useAuth } from "../../contexts/AuthContext";
6+
import CustomTextField from "../../components/CustomTextField";
7+
import { emailValidator } from "../../utils/validators";
68

79
const LogIn: React.FC = () => {
8-
const [email, setEmail] = useState<string>("");
9-
const [password, setPassword] = useState<string>("");
1010
const navigate = useNavigate();
11-
1211
const auth = useAuth();
1312
if (!auth) {
1413
throw new Error("useAuth() must be used within AuthProvider");
1514
}
1615
const { login } = auth;
1716

17+
const formValues = useRef({ email: "", password: "" });
18+
const formValidity = useRef({ email: false, password: false });
19+
const [emptyFields, setEmptyFields] = useState<{ [key: string]: boolean }>({
20+
email: false,
21+
password: false,
22+
});
23+
24+
const handleInputChange = (field: keyof typeof formValues.current, value: string, isValid: boolean) => {
25+
formValues.current[field] = value;
26+
formValidity.current[field] = isValid;
27+
setEmptyFields((prevState) => ({ ...prevState, [field]: !value }));
28+
};
29+
30+
const handleLogIn = (event: React.FormEvent<HTMLFormElement>) => {
31+
event.preventDefault();
32+
33+
if (!Object.values(formValidity.current).every((isValid) => isValid)) {
34+
// Mark untouched required fields red
35+
Object.entries(formValues.current).forEach(([field, value]) => {
36+
setEmptyFields((prevState) => ({ ...prevState, [field]: !value }));
37+
});
38+
return;
39+
}
40+
41+
const { email, password } = formValues.current;
42+
login(email, password);
43+
};
44+
1845
return (
1946
<Box
2047
sx={{
@@ -42,43 +69,39 @@ const LogIn: React.FC = () => {
4269
PeerPrep
4370
</Typography>
4471
<Stack
72+
component="form"
4573
direction="column"
4674
spacing={1.5}
4775
sx={(theme) => ({
4876
marginTop: theme.spacing(2),
4977
marginBottom: theme.spacing(2),
5078
})}
79+
onSubmit={handleLogIn}
80+
noValidate
5181
>
52-
<TextField
82+
<CustomTextField
5383
label="Email"
54-
variant="outlined"
5584
size="small"
56-
onChange={(input) => setEmail(input.target.value)}
57-
slotProps={{
58-
59-
}}
85+
required
86+
emptyField={emptyFields.email}
87+
validator={emailValidator}
88+
onChange={(value, isValid) => handleInputChange("email", value, isValid)}
6089
/>
61-
<TextField
90+
<CustomTextField
6291
label="Password"
63-
variant="outlined"
6492
size="small"
65-
onChange={(input) => setPassword(input.target.value)}
66-
slotProps={{
67-
68-
}}
93+
required
94+
emptyField={emptyFields.password}
95+
onChange={(value, isValid) => handleInputChange("password", value, isValid)}
6996
/>
97+
<Button
98+
type="submit"
99+
variant="contained"
100+
sx={(theme) => ({ height: theme.spacing(5) })}
101+
>
102+
Log in
103+
</Button>
70104
</Stack>
71-
<Button
72-
variant="contained"
73-
sx={(theme) => ({
74-
marginTop: theme.spacing(1),
75-
marginBottom: theme.spacing(1),
76-
height: theme.spacing(5),
77-
})}
78-
onClick={() => login(email, password)}
79-
>
80-
Log in
81-
</Button>
82105
<Stack
83106
direction="row"
84107
spacing={0.5}

frontend/src/pages/SignUp/index.tsx

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,50 @@
1-
import { Box, Button, Stack, TextField, Typography } from "@mui/material";
1+
import { Box, Button, Stack, Typography } from "@mui/material";
22
import SignUpSvg from "../../assets/signup.svg?react";
3-
import { useState } from "react";
43
import { useNavigate } from "react-router-dom";
54
import { useAuth } from "../../contexts/AuthContext";
5+
import CustomTextField from "../../components/CustomTextField";
6+
import { emailValidator, nameValidator, passwordValidator, usernameValidator } from "../../utils/validators";
7+
import { useRef, useState } from "react";
68

79
const SignUp: React.FC = () => {
8-
const [firstName, setFirstName] = useState<string>("");
9-
const [lastName, setLastName] = useState<string>("");
10-
const [username, setUsername] = useState<string>("");
11-
const [email, setEmail] = useState<string>("");
12-
const [password, setPassword] = useState<string>("");
1310
const navigate = useNavigate();
14-
1511
const auth = useAuth();
1612
if (!auth) {
1713
throw new Error("useAuth() must be used within AuthProvider");
1814
}
1915
const { signup } = auth;
2016

17+
const formValues = useRef({ firstName: "", lastName: "", username: "", email: "", password: "" });
18+
const formValidity = useRef({ firstName: false, lastName: false, username: false, email: false, password: false });
19+
const [emptyFields, setEmptyFields] = useState<{ [key: string]: boolean }>({
20+
firstName: false,
21+
lastName: false,
22+
username: false,
23+
email: false,
24+
password: false,
25+
});
26+
27+
const handleInputChange = (field: keyof typeof formValues.current, value: string, isValid: boolean) => {
28+
formValues.current[field] = value;
29+
formValidity.current[field] = isValid;
30+
setEmptyFields((prevState) => ({ ...prevState, [field]: !value }));
31+
};
32+
33+
const handleSignUp = (event: React.FormEvent<HTMLFormElement>) => {
34+
event.preventDefault();
35+
36+
if (!Object.values(formValidity.current).every((isValid) => isValid)) {
37+
// Mark untouched required fields red
38+
Object.entries(formValues.current).forEach(([field, value]) => {
39+
setEmptyFields((prevState) => ({ ...prevState, [field]: !value }));
40+
});
41+
return;
42+
}
43+
44+
const { firstName, lastName, username, email, password } = formValues.current;
45+
signup(firstName, lastName, username, email, password);
46+
};
47+
2148
return (
2249
<Box
2350
sx={{
@@ -44,71 +71,65 @@ const SignUp: React.FC = () => {
4471
>
4572
PeerPrep
4673
</Typography>
47-
<Stack
74+
<Stack
75+
component="form"
4876
direction="column"
4977
spacing={1.5}
5078
sx={(theme) => ({
5179
marginTop: theme.spacing(2),
5280
marginBottom: theme.spacing(2),
5381
})}
82+
onSubmit={handleSignUp}
83+
noValidate
5484
>
55-
<TextField
85+
<CustomTextField
5686
label="First Name"
57-
variant="outlined"
5887
size="small"
59-
onChange={(input) => setFirstName(input.target.value)}
60-
slotProps={{
61-
62-
}}
88+
required
89+
emptyField={emptyFields.firstName}
90+
validator={nameValidator}
91+
onChange={(value, isValid) => handleInputChange("firstName", value, isValid)}
6392
/>
64-
<TextField
93+
<CustomTextField
6594
label="Last Name"
66-
variant="outlined"
6795
size="small"
68-
onChange={(input) => setLastName(input.target.value)}
69-
slotProps={{
70-
71-
}}
96+
required
97+
emptyField={emptyFields.lastName}
98+
validator={nameValidator}
99+
onChange={(value, isValid) => handleInputChange("lastName", value, isValid)}
72100
/>
73-
<TextField
101+
<CustomTextField
74102
label="Username"
75-
variant="outlined"
76103
size="small"
77-
onChange={(input) => setUsername(input.target.value)}
78-
slotProps={{
79-
80-
}}
104+
required
105+
emptyField={emptyFields.username}
106+
validator={usernameValidator}
107+
onChange={(value, isValid) => handleInputChange("username", value, isValid)}
81108
/>
82-
<TextField
109+
<CustomTextField
83110
label="Email"
84-
variant="outlined"
85111
size="small"
86-
onChange={(input) => setEmail(input.target.value)}
87-
slotProps={{
88-
89-
}}
112+
required
113+
emptyField={emptyFields.email}
114+
validator={emailValidator}
115+
onChange={(value, isValid) => handleInputChange("email", value, isValid)}
90116
/>
91-
<TextField
117+
<CustomTextField
92118
label="Password"
93-
variant="outlined"
94119
size="small"
95-
onChange={(input) => setPassword(input.target.value)}
96-
slotProps={{
97-
98-
}}
120+
required
121+
emptyField={emptyFields.password}
122+
validator={passwordValidator}
123+
onChange={(value, isValid) => handleInputChange("password", value, isValid)}
99124
/>
125+
<Button
126+
type="submit"
127+
variant="contained"
128+
sx={(theme) => ({ height: theme.spacing(5) })}
129+
>
130+
Sign up
131+
</Button>
100132
</Stack>
101-
<Button
102-
variant="contained"
103-
sx={(theme) => ({
104-
marginTop: theme.spacing(1),
105-
marginBottom: theme.spacing(1),
106-
height: theme.spacing(5),
107-
})}
108-
onClick={() => signup(firstName, lastName, username, email, password)}
109-
>
110-
Sign up
111-
</Button>
112133
<Stack
113134
direction="row"
114135
spacing={0.5}

0 commit comments

Comments
 (0)