Skip to content

Commit

Permalink
Add error handling and loading animation
Browse files Browse the repository at this point in the history
  • Loading branch information
yitong241 committed Oct 3, 2024
1 parent e7a0eb6 commit 7862aaf
Show file tree
Hide file tree
Showing 3 changed files with 352 additions and 78 deletions.
98 changes: 88 additions & 10 deletions frontend/src/pages/AccountSettings/AccountSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { ReactElement, useState } from "react";
import { Autocomplete, Box, Button, TextField, IconButton, InputAdornment, Typography } from "@mui/material";
import {
Autocomplete,
Box,
Button,
TextField,
IconButton,
InputAdornment,
Typography,
Alert,
CircularProgress,
} from "@mui/material";
import Navbar from "../../components/Navbar/Navbar";
import Footer from "../../components/Footer/Footer";
import Visibility from "@mui/icons-material/Visibility";
Expand All @@ -9,9 +19,18 @@ import "./AccountSettings.scss";
const AccountSettings = (): ReactElement => {
const [showPassword, setShowPassword] = useState(false);
const [showEmail, setShowEmail] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [confirmationMessage, setConfirmationMessage] = useState<string | null>(null);
const [displayedName, setDisplayedName] = useState("Echomo");
const [profilePhotoUrl, setProfilePhotoUrl] = useState("");
const [preferredLanguage, setPreferredLanguage] = useState("");
const [email, setEmail] = useState("[email protected]");
const [password, setPassword] = useState("password");

const handleClickShowPassword = () => setShowPassword(!showPassword);
const handleClickShowEmail = () => setShowEmail(!showEmail);

const programmingLanguages = [
"JavaScript",
"Python2",
Expand All @@ -36,34 +55,69 @@ const AccountSettings = (): ReactElement => {
"MATLAB",
];

const handleSaveChanges = async () => {
setLoading(true);
setError(null);
setConfirmationMessage(null);

try {
if (!displayedName || !email || !password) {
throw new Error("Displayed name, email, and password are required.");
}
// Simulate API call
await new Promise((resolve, reject) => {
setTimeout(() => {
if (email === "[email protected]") {
reject(new Error("Email already in use."));
} else {
resolve("Account details updated successfully!");
}
}, 3000);
});

setConfirmationMessage("Account details saved successfully!");
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};

return (
<Box className="AccountSettings">
<Navbar />
<Box className="AccountSettings-container">
<Typography variant="body2" className="Home-welcome-title">
Login
Account Settings
</Typography>
<Typography className="AccountSettings-subtitle" sx={{ mb: 3 }}>
Customize your information and experience here!{" "}
Customize your information and experience here!
</Typography>

<form className="AccountSettings-form">
<form
className="AccountSettings-form"
onSubmit={(e) => {
e.preventDefault();
handleSaveChanges();
}}
>
<Box className="AccountSettings-row">
<Typography variant="body1">Displayed Name (to others)</Typography>
<TextField
variant="outlined"
placeholder="Enter your name"
className="AccountSettings-input"
fullWidth
defaultValue="Echomo"
value={displayedName}
onChange={(e) => setDisplayedName(e.target.value)}
/>
</Box>

<Box className="AccountSettings-row">
<Typography variant="body1">Profile Photo</Typography>
<Box className="AccountSettings-photo-section">
<img
src="https://randomuser.me/api/portraits/men/75.jpg"
src={profilePhotoUrl || "https://randomuser.me/api/portraits/men/75.jpg"}
alt="Profile"
className="AccountSettings-profile-photo"
/>
Expand All @@ -72,6 +126,8 @@ const AccountSettings = (): ReactElement => {
placeholder="Enter new URL to change"
className="AccountSettings-url-input"
fullWidth
value={profilePhotoUrl}
onChange={(e) => setProfilePhotoUrl(e.target.value)}
/>
</Box>
</Box>
Expand All @@ -88,6 +144,8 @@ const AccountSettings = (): ReactElement => {
placeholder="Enter your preferred programming language"
className="AccountSettings-input"
fullWidth
value={preferredLanguage}
onChange={(e) => setPreferredLanguage(e.target.value)}
/>
)}
filterOptions={(options, { inputValue }) =>
Expand All @@ -103,7 +161,8 @@ const AccountSettings = (): ReactElement => {
variant="outlined"
className="AccountSettings-input"
fullWidth
defaultValue="[email protected]"
value={email}
onChange={(e) => setEmail(e.target.value)}
type={showEmail ? "text" : "email"}
/>
</Box>
Expand All @@ -114,8 +173,9 @@ const AccountSettings = (): ReactElement => {
variant="outlined"
className="AccountSettings-input"
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
type={showPassword ? "text" : "password"}
defaultValue="password"
InputProps={{
endAdornment: (
<InputAdornment position="end">
Expand All @@ -128,8 +188,26 @@ const AccountSettings = (): ReactElement => {
/>
</Box>

<Button color="primary" variant="contained" className="AccountSettings-save-button">
Save Changes
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}

{confirmationMessage && (
<Alert severity="success" sx={{ mb: 2 }}>
{confirmationMessage}
</Alert>
)}

<Button
color="primary"
variant="contained"
className="AccountSettings-save-button"
onClick={handleSaveChanges}
disabled={loading}
>
{loading ? <CircularProgress size={24} /> : "Save Changes"}
</Button>
</form>
</Box>
Expand Down
150 changes: 113 additions & 37 deletions frontend/src/pages/Login/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ReactElement, useState } from "react";
import { Box, Button, Typography, TextField, IconButton, InputAdornment } from "@mui/material";
import { useNavigate } from "react-router-dom";
import { Box, Button, Typography, TextField, IconButton, InputAdornment, Alert, CircularProgress } from "@mui/material";
import Navbar from "../../components/Navbar/Navbar";
import Footer from "../../components/Footer/Footer";
import Visibility from "@mui/icons-material/Visibility";
Expand All @@ -8,11 +9,62 @@ import "./Login.scss";

const Login = (): ReactElement => {
const [showPassword, setShowPassword] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
const [loginError, setLoginError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();

const togglePasswordVisibility = () => {
setShowPassword((prev) => !prev);
};

const validateEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};

const handleLogin = () => {
setErrors({});
setLoginError(null);

let formValid = true;
const newErrors: { email?: string; password?: string } = {};

if (!email) {
formValid = false;
newErrors.email = "Email is required.";
} else if (!validateEmail(email)) {
formValid = false;
newErrors.email = "Please enter a valid email.";
}

if (!password) {
formValid = false;
newErrors.password = "Password is required.";
}

if (!formValid) {
setErrors(newErrors);
return;
}

// Simulate login process
try {
if (email === "[email protected]" && password === "password") {
setLoading(true);
setTimeout(() => {
navigate("/");
}, 3000);
} else {
setLoginError("Invalid email or password.");
}
} catch (error) {
setLoginError("An unexpected error occurred. Please try again.");
}
};

return (
<Box className="Login">
<Navbar />
Expand All @@ -26,42 +78,66 @@ const Login = (): ReactElement => {
</Typography>

<Box className="Login-form" sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<Box sx={{ display: "flex", width: "100%", justifyContent: "space-between" }}>
<TextField label="Enter your Email" variant="outlined" fullWidth sx={{ mr: 1 }} />
<TextField
label="Enter your Password"
type={showPassword ? "text" : "password"}
variant="outlined"
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={togglePasswordVisibility} className="password-eye-icon" edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
</Box>
<Typography
component="a"
href="/forgot-password"
className="forgot-password"
sx={{ textDecoration: "underline", textAlign: "center" }}
>
Forgot password?
</Typography>

<Button color="primary" variant="contained" className="LoginUp-button">
Login
</Button>

<Typography className="Login-signup">
Not registered yet? <a href="/signup">Sign Up</a>
</Typography>

{/* <Typography className="SignUp-or">Or Continue with</Typography> */}
{loading ? (
<CircularProgress sx={{ mb: 3 }} />
) : (
<>
<Box sx={{ display: "flex", width: "100%", justifyContent: "space-between" }}>
<TextField
label="Enter your Email"
variant="outlined"
fullWidth
sx={{ mr: 1 }}
value={email}
onChange={(e) => setEmail(e.target.value)}
error={!!errors.email}
helperText={errors.email}
/>
<TextField
label="Enter your Password"
type={showPassword ? "text" : "password"}
variant="outlined"
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
error={!!errors.password}
helperText={errors.password}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={togglePasswordVisibility} className="password-eye-icon" edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
</Box>

<Typography
component="a"
href="/forgot-password"
className="forgot-password"
sx={{ textDecoration: "underline", textAlign: "center" }}
>
Forgot password?
</Typography>

{loginError && (
<Alert severity="error" sx={{ mt: 2 }}>
{loginError}
</Alert>
)}

<Button color="primary" variant="contained" className="LoginUp-button" onClick={handleLogin}>
Login
</Button>

<Typography className="Login-signup">
Not registered yet? <a href="/signup">Sign Up</a>
</Typography>
</>
)}
</Box>
</Box>

Expand Down
Loading

0 comments on commit 7862aaf

Please sign in to comment.