diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4b581e1c..7f8a7222 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,7 +24,7 @@ "react-error-boundary": "^4.0.13", "react-markdown": "^9.0.1", "react-router-dom": "^6.22.0", - "react-scripts": "5.0.1", + "react-scripts": "^5.0.1", "react-simple-code-editor": "^0.13.1", "react-use": "^17.5.0", "reagraph": "^4.15.10", @@ -17148,6 +17148,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", + "license": "MIT", "dependencies": { "@babel/core": "^7.16.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", diff --git a/frontend/package.json b/frontend/package.json index f49d891b..f8c8c4f4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,7 @@ "react-error-boundary": "^4.0.13", "react-markdown": "^9.0.1", "react-router-dom": "^6.22.0", - "react-scripts": "5.0.1", + "react-scripts": "^5.0.1", "react-simple-code-editor": "^0.13.1", "react-use": "^17.5.0", "reagraph": "^4.15.10", diff --git a/frontend/src/components/BookImage.tsx b/frontend/src/components/BookImage.tsx index 73462b73..a05d1773 100644 --- a/frontend/src/components/BookImage.tsx +++ b/frontend/src/components/BookImage.tsx @@ -32,7 +32,9 @@ import { ClothingActivity } from "./ClothingActivity"; import BookRushHour from "./BookRushHour"; import { ImageQuestion } from "./ImageQuestion"; import { DragMultiChoice } from "./DragMultiChoice"; - +import { PasswordChecker } from "./PasswordChecker"; +import { DragDrop } from "./DragDrop"; +import FixPassword from "./FixPassword"; export function BookImage({ image, page, @@ -141,6 +143,15 @@ export function BookImage({ {image === "DragMultiChoice" && ( )} + {image === "PasswordChecker" && ( + + )} + {image === "DragDrop" && ( + + )} + {image === "FixPassword" && ( + + )} ); } diff --git a/frontend/src/components/DragDrop.tsx b/frontend/src/components/DragDrop.tsx new file mode 100644 index 00000000..a80fad91 --- /dev/null +++ b/frontend/src/components/DragDrop.tsx @@ -0,0 +1,143 @@ +import { Dispatch, SetStateAction, useEffect, useState } from "react"; + +interface WordItem { + id: number; + text: string; + correctBox: string; +} + +interface DropAreaProps { + title: string; + words: WordItem[]; + onDrop: (item: WordItem) => void; + errorMessage: string | null; +} + +const DropArea = ({ title, words, onDrop, errorMessage }: DropAreaProps) => { + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + const word = JSON.parse(event.dataTransfer.getData("word")); + onDrop(word); + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + }; + + return ( +
+

{title}

+ {errorMessage &&
{errorMessage}
} +
+ {words.map((word) => ( +
+ {word.text} +
+ ))} +
+
+ ); +}; + +const DraggableWord = ({ word }: { word: WordItem }) => { + const handleDragStart = (event: React.DragEvent) => { + event.dataTransfer.setData("word", JSON.stringify(word)); + }; + + return ( +
+ {word.text} +
+ ); +}; + +export function DragDrop({ + props, + setAllowNext, +}: { + props: any; + setAllowNext: Dispatch>; +}) { + const [strongWords, setStrongWords] = useState([]); + const [weakWords, setWeakWords] = useState([]); + const [wordBank, setWordBank] = useState(props.words); + const [strongErrorMessage, setStrongErrorMessage] = useState( + null, + ); + const [weakErrorMessage, setWeakErrorMessage] = useState(null); + + const boxNames = props.boxNames || { strong: "Strong", weak: "Weak" }; + + useEffect(() => { + const allWordsValid = + strongWords.every((word) => word.correctBox === boxNames.strong) && + weakWords.every((word) => word.correctBox === boxNames.weak) && + strongWords.length + weakWords.length === props.words.length; + setAllowNext(allWordsValid); + }, [ + strongWords, + weakWords, + boxNames.strong, + boxNames.weak, + props.words.length, + setAllowNext, + ]); + + const handleDropStrong = (item: WordItem) => { + if (item.correctBox === boxNames.strong) { + setStrongWords((prev) => [...prev, item]); + setWordBank((prev) => prev.filter((word) => word.id !== item.id)); + setStrongErrorMessage(null); + } else { + setStrongErrorMessage(`Incorrect`); + } + }; + + const handleDropWeak = (item: WordItem) => { + if (item.correctBox === boxNames.weak) { + setWeakWords((prev) => [...prev, item]); + setWordBank((prev) => prev.filter((word) => word.id !== item.id)); + setWeakErrorMessage(null); + } else { + setWeakErrorMessage(`Incorrect`); + } + }; + + return ( +
+

+ Drag and Drop the words into the correct categories +

+
+ + +
+
+

Word Bank

+
+ {wordBank.map((word) => ( + + ))} +
+
+
+ ); +} diff --git a/frontend/src/components/FixPassword.tsx b/frontend/src/components/FixPassword.tsx new file mode 100644 index 00000000..46f4d13a --- /dev/null +++ b/frontend/src/components/FixPassword.tsx @@ -0,0 +1,86 @@ +import { useState, useEffect } from "react"; + +interface FixPasswordProps { + props: { + weakPasswords: string[]; + }; + setAllowNext: React.Dispatch>; +} + +const isStrongPassword = (password: string) => { + const minLength = 8; + const hasUpperCase = /[A-Z]/.test(password); + const hasLowerCase = /[a-z]/.test(password); + const hasNumber = /[0-9]/.test(password); + const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); + + return ( + password.length >= minLength && + hasUpperCase && + hasLowerCase && + hasNumber && + hasSpecialChar + ); +}; + +const FixPassword = ({ props, setAllowNext }: FixPasswordProps) => { + const { weakPasswords } = props; + const [passwords, setPasswords] = useState( + Array(weakPasswords.length).fill(""), + ); + const [isStrong, setIsStrong] = useState( + Array(weakPasswords.length).fill(false), + ); + const [showPasswords, setShowPasswords] = useState( + Array(weakPasswords.length).fill(false), + ); + + useEffect(() => { + setAllowNext(isStrong.every((strength) => strength)); + }, [isStrong, setAllowNext]); + + const handleChange = (index: number, value: string) => { + const updatedPasswords = [...passwords]; + updatedPasswords[index] = value; + setPasswords(updatedPasswords); + + const updatedIsStrong = [...isStrong]; + updatedIsStrong[index] = isStrongPassword(value); + setIsStrong(updatedIsStrong); + }; + + const toggleShowPassword = (index: number) => { + const updatedShowPasswords = [...showPasswords]; + updatedShowPasswords[index] = !updatedShowPasswords[index]; + setShowPasswords(updatedShowPasswords); + }; + + return ( +
+ {weakPasswords.map((weakPassword, index) => ( +
+ {weakPassword} +
+ handleChange(index, e.target.value)} + className="border p-2 mr-2" + /> + + {isStrong[index] && ( + Success! + )} +
+
+ ))} +
+ ); +}; + +export default FixPassword; diff --git a/frontend/src/components/PasswordChecker.tsx b/frontend/src/components/PasswordChecker.tsx new file mode 100644 index 00000000..63331224 --- /dev/null +++ b/frontend/src/components/PasswordChecker.tsx @@ -0,0 +1,111 @@ +import { Dispatch, SetStateAction, useEffect, useState } from "react"; + +export function PasswordChecker({ + props, + setAllowNext, +}: { + props: any; + setAllowNext: Dispatch>; +}) { + const [password, setPassword] = useState(""); + const [isValid, setIsValid] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [hasMinLength, setHasMinLength] = useState(false); + const [hasUpperCase, setHasUpperCase] = useState(false); + const [hasLowerCase, setHasLowerCase] = useState(false); + const [hasSpecialChar, setHasSpecialChar] = useState(false); + const [hasNumber, setHasNumber] = useState(false); + + useEffect(() => { + validatePassword(password, props); + }, [password, props]); + + const validatePassword = (password: string, requirements: any) => { + const minLength = password.length >= requirements.minLength; + const upperCase = requirements.requireUpperCase + ? /[A-Z]/.test(password) + : true; + const lowerCase = requirements.requireLowerCase + ? /[a-z]/.test(password) + : true; + const specialChar = requirements.requireSpecialChar + ? /[!@#$%^&*(),.?":{}|<>]/.test(password) + : true; + const number = requirements.requireNumber ? /\d/.test(password) : true; + + setHasMinLength(minLength); + setHasUpperCase(upperCase); + setHasLowerCase(lowerCase); + setHasSpecialChar(specialChar); + setHasNumber(number); + + setIsValid(minLength && upperCase && lowerCase && specialChar && number); + }; + + useEffect(() => { + setAllowNext(isValid); + }, [isValid, setAllowNext]); + + const toggleShowPassword = () => { + setShowPassword((prev) => !prev); + }; + + return ( +
+

Type your password below:

+
+ setPassword(e.target.value)} + className={`border p-2 ${isValid ? "border-green-500" : "border-red-500"} flex-grow`} + /> + +
+

+ Password must meet the following requirements: + {props.minLength && ( + +
- Minimum {props.minLength} characters long +
+ )} + {props.requireUpperCase && ( + +
- At least one uppercase letter +
+ )} + {props.requireLowerCase && ( + +
- At least one lowercase letter +
+ )} + {props.requireSpecialChar && ( + +
- At least one special character +
+ )} + {props.requireNumber && ( + +
- At least one number +
+ )} +

+ {isValid && ( +

Password meets all requirements!

+ )} +
+ ); +} diff --git a/frontend/src/util/componentEditorDefaults.ts b/frontend/src/util/componentEditorDefaults.ts index e627bf05..d21c02a4 100644 --- a/frontend/src/util/componentEditorDefaults.ts +++ b/frontend/src/util/componentEditorDefaults.ts @@ -211,4 +211,27 @@ export const editorDefaults: { [key: string]: any } = { }, ], }, + PasswordChecker: { + minLength: 8, + requireUpperCase: true, + requireLowerCase: true, + requireSpecialChar: true, + requireNumber: true, + }, + DragDrop: { + words: [ + { id: 1, text: "Secure", correctBox: "Strong" }, + { id: 2, text: "123456", correctBox: "Weak" }, + { id: 3, text: "Password", correctBox: "Weak" }, + { id: 4, text: "StrongPass1!", correctBox: "Strong" }, + { id: 5, text: "WeakPass", correctBox: "Weak" }, + ], + boxNames: { + strong: "Strong", + weak: "Weak", + }, + }, + FixPassword: { + weakPasswords: ["weakPass1", "123456", "password"], + }, };