Skip to content

Commit

Permalink
Merge pull request #106 from bokuanT/branch-handshakeRejectTimeout
Browse files Browse the repository at this point in the history
Improve UI UX
  • Loading branch information
bokuanT authored Nov 11, 2023
2 parents bf30576 + 066c693 commit b4687ed
Show file tree
Hide file tree
Showing 8 changed files with 423 additions and 170 deletions.
1 change: 0 additions & 1 deletion frontend/components/CollabPage/CollabPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,6 @@ const CollabPage = () => {

// Server tells clients this when a client in room has rejected next question prompt
socket.on("dontProceedWithNextQuestion", () => {
console.log("dontProceedWithNextQuestion");
setIsNextQnHandshakeOpen(false);
setIHaveAcceptedNextQn(false);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*Source: https://mui.com/material-ui/react-modal/*/
import * as React from "react";
import React, { useEffect, useState, useRef } from "react";
import {
CircularProgress,
Stack,
Expand Down Expand Up @@ -45,6 +45,44 @@ export default function EndSessionModal({
if (reason && reason == "escapeKeyDown") return; //prevent user from closing dialog using esacpe button
setIsEndSessionHandshakeOpen(false);
};

// Add timer that automatically rejects after 15s upon model opening
const [progress, setProgress] = useState(0);
const waitTime =10000;
const timerRef = useRef<number | null>(null);
useEffect(() => {
if (isEndSessionHandshakeOpen) {
if (timerRef.current) {
clearInterval(timerRef.current);
}
setProgress(0);
timerRef.current = window.setInterval(() => {
setProgress((prevProgress) =>
prevProgress < 100 ? prevProgress + 0.1 : 100
);
}, waitTime / 1000);

return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}
}, [isEndSessionHandshakeOpen]);

useEffect(() => {
if (isEndSessionHandshakeOpen && progress >= 100) {
handleIPressedRejectEndSession();
if (timerRef.current) {
clearInterval(timerRef.current);
}
}
}, [progress]);
const elapsedTime = (progress / 100) * waitTime;
const remainingTime = waitTime - elapsedTime; // Remaining time until the countdown is finished
const remainingTimeInSeconds = Math.floor(remainingTime / 1000);
const remainingMinutes = Math.floor(remainingTimeInSeconds / 60);
const remainingSeconds = remainingTimeInSeconds % 60;
return (
<div>
<Modal
Expand Down Expand Up @@ -76,9 +114,15 @@ export default function EndSessionModal({
Waiting for other members to accept/reject the
proposal.
</Typography>
<Typography variant="caption" display="block" gutterBottom>
Time Remaining: {remainingMinutes}:{remainingSeconds.toString().padStart(2, '0')}
</Typography>
</Stack>
) : (
<Box>
<Typography variant="caption" display="block" gutterBottom>
Time Remaining: {remainingMinutes}:{remainingSeconds.toString().padStart(2, '0')}
</Typography>
<Button
variant="outlined"
color="success"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*Source: https://mui.com/material-ui/react-modal/*/
import * as React from 'react';
import React, { useEffect, useState, useRef } from "react";
import {
CircularProgress,
Stack,
Expand Down Expand Up @@ -40,13 +40,53 @@ export default function BasicModal(
iHaveAcceptedNextQn,
isLastQuestion }: NextQnHandshakeModalProps) {
const handleClose = (event: any, reason: string) => {
if (reason && reason == "backdropClick")
if (reason && reason == "backdropClick") {
return; /* This prevents modal from closing on an external click */

if (reason && reason == "escapeKeyDown")
return; //prevent user from closing dialog using esacpe button
}
if (reason && reason == "escapeKeyDown") {
return; //prevent user from closing dialog using esacpe button
}
setIsNextQnHandshakeOpen(false);
};

// Add timer that automatically rejects after 15s upon model opening
const [progress, setProgress] = useState(0);
const waitTime =10000;
const timerRef = useRef<number | null>(null);
useEffect(() => {
if (isNextQnHandshakeOpen) {
if (timerRef.current) {
clearInterval(timerRef.current);
}
setProgress(0);
timerRef.current = window.setInterval(() => {
setProgress((prevProgress) =>
prevProgress < 100 ? prevProgress + 0.1 : 100
);
}, waitTime / 1000);

return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}
}, [isNextQnHandshakeOpen]);

useEffect(() => {
if (isNextQnHandshakeOpen && progress >= 100) {
handleIPressedReject();
if (timerRef.current) {
clearInterval(timerRef.current);
}
}
}, [progress]);
const elapsedTime = (progress / 100) * waitTime;
const remainingTime = waitTime - elapsedTime; // Remaining time until the countdown is finished
const remainingTimeInSeconds = Math.floor(remainingTime / 1000);
const remainingMinutes = Math.floor(remainingTimeInSeconds / 60);
const remainingSeconds = remainingTimeInSeconds % 60;

return (
<div>
<Modal
Expand Down Expand Up @@ -76,9 +116,15 @@ export default function BasicModal(
<Typography>
Waiting for other members to accept/reject the proposal.
</Typography>
<Typography variant="caption" display="block" gutterBottom>
Time Remaining: {remainingMinutes}:{remainingSeconds.toString().padStart(2, '0')}
</Typography>
</Stack>
) : (
<Box>
<Typography variant="caption" display="block" gutterBottom>
Time Remaining: {remainingMinutes}:{remainingSeconds.toString().padStart(2, '0')}
</Typography>
<Button
variant="outlined"
color="success"
Expand Down
12 changes: 10 additions & 2 deletions frontend/components/LoginPage/AdminSignup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
Typography,
Container,
} from "@mui/material";
import PasswordStrengthCheck, { testPasswordStrength, PasswordStrength } from "./PasswordStength";
import { messageHandler } from "@/utils/handlers";

function Copyright(props: any) {
return (
Expand All @@ -40,6 +42,11 @@ export default function AdminSignup() {

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// Check password is strong
if (testPasswordStrength(password) !== PasswordStrength.STRONG) {
messageHandler("Your password is not strong", "error");
return;
}
await fetchPost("/api/users", {
username: username,
email: router.query.email,
Expand All @@ -51,11 +58,11 @@ export default function AdminSignup() {
alert("Success! Added: " + res.data.email);
router.push("/login?mode=login");
} else {
alert(res.message);
messageHandler(res.message, "error");
}
})
.catch((err) => {
alert(err);
messageHandler(err.message, "error");
});
};

Expand Down Expand Up @@ -111,6 +118,7 @@ export default function AdminSignup() {
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<PasswordStrengthCheck password={password} />
</Grid>
<Grid item xs={12}>
<TextField
Expand Down
119 changes: 119 additions & 0 deletions frontend/components/LoginPage/PasswordStength.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Box, Typography } from "@mui/material";
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import CheckOutlinedIcon from '@mui/icons-material/CheckOutlined';

// Password strength requirements, source: https://www.linkedin.com/pulse/guide-how-check-password-strength-react-materialui-brito-bittencourt/
const atLeastMinLength = (password: string) => new RegExp("^(?=.{8,})").test(password);
const atLeastOneUppercaseLetter = (password: string) => new RegExp("^(?=.*[A-Z])").test(password);
const atLeastOneLowercaseLetter = (password: string) => new RegExp("^(?=.*[a-z])").test(password);
const atLeastOneNumber = (password: string) => new RegExp("^(?=.*[0-9])").test(password);
const atLeastOneSpecialCharacter = (password: string) => new RegExp("^(?=.*[ !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~])").test(password);

export enum PasswordStrength {
STRONG = "strong",
MEDIUM = "medium",
WEAK = "weak",
}

export function testPasswordStrength(password?: string) : PasswordStrength {
if (!password) {
return PasswordStrength.WEAK;
}
let points = 0;
if (atLeastMinLength(password)) {
points++;
}
if (atLeastOneUppercaseLetter(password)) {
points++;
}
if (atLeastOneLowercaseLetter(password)) {
points++;
}
if (atLeastOneNumber(password)) {
points++;
}
if (atLeastOneSpecialCharacter(password)) {
points++;
}
if (points >= 5) {
return PasswordStrength.STRONG;
} else if (points >= 3) {
return PasswordStrength.MEDIUM;
} else {
return PasswordStrength.WEAK;
}
}

function getIcon(strength: PasswordStrength) {
let icon = ErrorOutlineIcon;
switch (strength) {
case PasswordStrength.STRONG:
icon = CheckOutlinedIcon;
break;
case PasswordStrength.MEDIUM:
icon = ErrorOutlineIcon;
break;
case PasswordStrength.WEAK:
icon = ErrorOutlineIcon;
break;
}
return icon;
}

function generateColours(strength: PasswordStrength): string[] {
let result: string[] = [];
const colours = {
NEUTRAL: "#9e9e9e",
WEAK: "#ff0000",
MEDIUM: "#ffa500",
STRONG: "#00ff00",
};
switch (strength) {
case PasswordStrength.STRONG:
result = [colours.STRONG, colours.STRONG, colours.STRONG, colours.STRONG, colours.STRONG];
break;
case PasswordStrength.MEDIUM:
result = [colours.MEDIUM, colours.MEDIUM, colours.MEDIUM, colours.NEUTRAL, colours.NEUTRAL];
break;
case PasswordStrength.WEAK:
result = [colours.WEAK, colours.NEUTRAL, colours.NEUTRAL, colours.NEUTRAL, colours.NEUTRAL];
break;
}
return result;
}

interface PasswordStrengthProps {
password: string;
}

export default function PasswordStrengthCheck({ password }: PasswordStrengthProps) {
const passwordStrength = testPasswordStrength(password);
const Icon = getIcon(passwordStrength);
const colors = generateColours(passwordStrength);

return (
<>
<Box display="flex" alignItems="center" justifyContent="center" gap="5px" margin="10px">
{colors.map((color, index) => (
<Box key={index} flex={1} height="5px" borderRadius="5px" bgcolor={color}></Box>
))}
</Box>

<Box display="flex" alignItems="center" justifyContent="flex-start" gap="5px" margin="0 15px">
<Icon htmlColor={colors[0]} />
<Typography color={colors[0]}>{passwordStrength}</Typography>
</Box>

{passwordStrength !== PasswordStrength.STRONG && (
<>
<Typography variant="subtitle2" fontSize="1rem" color="text.secondary" margin="0 0 8px 0">
Your password is not strong
</Typography>
<Typography variant="subtitle2" fontSize="14px" fontWeight={500} color="text.secondary" margin="0 0 24px 0">
Include at least 8 characters, a number, a special character, an uppercase letter, and a lowercase letter
</Typography>
</>
)}
</>
);
}
27 changes: 16 additions & 11 deletions frontend/components/LoginPage/SignupBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,8 @@ import {
Typography,
Container,
} from "@mui/material";
import { enqueueSnackbar } from "notistack";

type User = {
username: string;
email: string;
};
import PasswordStrengthCheck, { testPasswordStrength, PasswordStrength } from "./PasswordStength";
import { messageHandler } from "@/utils/handlers";

function Copyright(props: any) {
return (
Expand Down Expand Up @@ -47,6 +43,16 @@ export default function SignUpBox() {

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// Check password is strong
if (testPasswordStrength(password) !== PasswordStrength.STRONG) {
messageHandler("Your password is not strong", "error");
return;
}
// Check email is valid
if (email.length < 1 || !email.includes("@")) {
messageHandler("Please enter a valid email", "error");
return;
}
console.log(
"sending username, email, password: {} {} {} ",
username,
Expand All @@ -61,16 +67,14 @@ export default function SignUpBox() {
})
.then((res) => {
if (res.status == 201) {
enqueueSnackbar("Success! Added: " + res.data.email, {
variant: "success",
});
messageHandler("Success! Added: " + res.data.email, "success");
router.push("/login?mode=login");
} else {
enqueueSnackbar(res.message, { variant: "error" });
messageHandler(res.message, "error");
}
})
.catch((err) => {
enqueueSnackbar(err);
messageHandler(err.message, "error");
});
};

Expand Down Expand Up @@ -137,6 +141,7 @@ export default function SignUpBox() {
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<PasswordStrengthCheck password={password} />
</Grid>
</Grid>
<Button
Expand Down
Loading

0 comments on commit b4687ed

Please sign in to comment.