diff --git a/backend/package-lock.json b/backend/package-lock.json index ecdc3e8..8a4085f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -8,6 +8,7 @@ "name": "coffee-shop-backend", "version": "1.0.0", "dependencies": { + "@emailjs/browser": "^4.4.1", "bcrypt": "^6.0.0", "connect-mongo": "^5.1.0", "cors": "^2.8.5", @@ -559,6 +560,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@emailjs/browser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@emailjs/browser/-/browser-4.4.1.tgz", + "integrity": "sha512-DGSlP9sPvyFba3to2A50kDtZ+pXVp/0rhmqs2LmbMS3I5J8FSOgLwzY2Xb4qfKlOVHh29EAutLYwe5yuEZmEFg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index 95876b0..a479a36 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "deploy": "npm ci --only=production && pm2 restart ecosystem.config.js" }, "dependencies": { + "@emailjs/browser": "^4.4.1", "bcrypt": "^6.0.0", "connect-mongo": "^5.1.0", "cors": "^2.8.5", @@ -33,15 +34,19 @@ "passport-jwt": "^4.0.1" }, "devDependencies": { - "nodemon": "^3.1.10", + "@jest/globals": "^29.7.0", "jest": "^29.7.0", - "supertest": "^6.3.4", - "@jest/globals": "^29.7.0" + "nodemon": "^3.1.10", + "supertest": "^6.3.4" }, "jest": { "testEnvironment": "node", - "setupFilesAfterEnv": ["/__tests__/setup.js"], - "testMatch": ["**/__tests__/**/*.test.js"], + "setupFilesAfterEnv": [ + "/__tests__/setup.js" + ], + "testMatch": [ + "**/__tests__/**/*.test.js" + ], "collectCoverageFrom": [ "routes/**/*.js", "models/**/*.js", diff --git a/package-lock.json b/package-lock.json index 1b434b0..c7de1d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@babel/preset-env": "^7.26.0", "@babel/preset-react": "^7.26.3", "@dotlottie/react-player": "^1.6.19", + "@emailjs/browser": "^4.4.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@eslint/config-array": "^0.19.1", @@ -59,7 +60,7 @@ "react": "^18.3.1", "react-animated-cursor": "^2.11.2", "react-dom": "^18.3.1", - "react-icons": "^5.4.0", + "react-icons": "^5.5.0", "react-locomotive-scroll": "^0.2.2", "react-redux": "^9.1.2", "react-router-dom": "^6.26.2", @@ -2398,6 +2399,15 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@emailjs/browser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@emailjs/browser/-/browser-4.4.1.tgz", + "integrity": "sha512-DGSlP9sPvyFba3to2A50kDtZ+pXVp/0rhmqs2LmbMS3I5J8FSOgLwzY2Xb4qfKlOVHh29EAutLYwe5yuEZmEFg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", diff --git a/package.json b/package.json index b0e39d4..0cf1e33 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@babel/preset-env": "^7.26.0", "@babel/preset-react": "^7.26.3", "@dotlottie/react-player": "^1.6.19", + "@emailjs/browser": "^4.4.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@eslint/config-array": "^0.19.1", @@ -54,7 +55,7 @@ "react": "^18.3.1", "react-animated-cursor": "^2.11.2", "react-dom": "^18.3.1", - "react-icons": "^5.4.0", + "react-icons": "^5.5.0", "react-locomotive-scroll": "^0.2.2", "react-redux": "^9.1.2", "react-router-dom": "^6.26.2", diff --git a/src/Pages/PasswordChecklist.js b/src/Pages/PasswordChecklist.js new file mode 100644 index 0000000..ab3ff28 --- /dev/null +++ b/src/Pages/PasswordChecklist.js @@ -0,0 +1,46 @@ +import React from "react"; +import { FaCheckCircle, FaTimesCircle } from "react-icons/fa"; // install react-icons if not already + +// Validation rules +const passwordRules = { + length: (pw) => pw.length >= 6, + uppercase: (pw) => /[A-Z]/.test(pw), + lowercase: (pw) => /[a-z]/.test(pw), + number: (pw) => /\d/.test(pw), + specialChar: (pw) => /[@$!%*?&]/.test(pw), +}; + +const PasswordChecklist = ({ password }) => { + const rules = [ + { label: "At least 6 characters", valid: passwordRules.length(password) }, + { label: "At least one uppercase letter", valid: passwordRules.uppercase(password) }, + { label: "At least one lowercase letter", valid: passwordRules.lowercase(password) }, + { label: "At least one number", valid: passwordRules.number(password) }, + { label: "At least one special character (@$!%*?&)", valid: passwordRules.specialChar(password) }, + ]; + + return ( +
+
    + {rules.map((rule, i) => ( +
  • + {rule.valid ? ( + + ) : ( + + )} + {rule.label} +
  • + ))} +
+
+ ); +}; + +export default PasswordChecklist; diff --git a/src/Pages/Register.js b/src/Pages/Register.js index fea3591..e006b3c 100644 --- a/src/Pages/Register.js +++ b/src/Pages/Register.js @@ -2,11 +2,12 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; -import { GoogleLogin } from '@react-oauth/google'; +import { GoogleLogin } from "@react-oauth/google"; import { login, googleLogin, clearError } from "../Store/authSlice"; -import { toast } from 'react-toastify'; +import { toast } from "react-toastify"; +import PasswordChecklist from "./PasswordChecklist"; -const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5001/api'; +const API_URL = process.env.REACT_APP_API_URL || "http://localhost:5001/api"; function SignupPage() { const [name, setName] = useState(""); @@ -15,12 +16,14 @@ function SignupPage() { const [confirmPassword, setConfirmPassword] = useState(""); const [acceptTerms, setAcceptTerms] = useState(false); const [isLoading, setIsLoading] = useState(false); - + const navigate = useNavigate(); const dispatch = useDispatch(); - + // Get auth state from Redux - const { loading, error, isAuthenticated } = useSelector((state) => state.auth); + const { loading, error, isAuthenticated } = useSelector( + (state) => state.auth + ); // Clear any existing errors when component mounts React.useEffect(() => { @@ -33,82 +36,85 @@ function SignupPage() { navigate("/home"); } }, [isAuthenticated, navigate]); -// Password validation function + // Password validation function -const validatePassword = (password) => { - const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{6,}$/; - return regex.test(password); -}; + const validatePassword = (password) => { + const regex = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{6,}$/; + return regex.test(password); + }; // Handle traditional form signup const handleSubmit = async (e) => { - e.preventDefault(); - - if (!name || !email || !password || !confirmPassword) { - toast.error("Please fill in all fields."); - return; - } - - if (password !== confirmPassword) { - toast.error("Passwords do not match."); - return; - } - - if (!validatePassword(password)) { - toast.error( - "Password must be at least 6 characters and include uppercase, lowercase, number, and special character." - ); - return; - } - - if (!acceptTerms) { - toast.error("Please accept the Terms and Conditions."); - return; - } - - setIsLoading(true); - - try { - const response = await fetch(`${API_URL}/auth/register`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name, email, password }), - }); - - const data = await response.json(); - - if (data.success) { - localStorage.setItem("token", data.token); - localStorage.setItem("user", JSON.stringify(data.user)); - dispatch(login(data.user)); - toast.success("Account created successfully! Welcome to MsCafe!"); - navigate("/home"); - } else { - toast.error(data.message || "Registration failed. Please try again."); + e.preventDefault(); + + if (!name || !email || !password || !confirmPassword) { + toast.error("Please fill in all fields."); + return; + } + + if (password !== confirmPassword) { + toast.error("Passwords do not match."); + return; + } + + if (!validatePassword(password)) { + toast.error( + "Password must be at least 6 characters and include uppercase, lowercase, number, and special character." + ); + return; + } + + if (!acceptTerms) { + toast.error("Please accept the Terms and Conditions."); + return; + } + + setIsLoading(true); + + try { + const response = await fetch(`${API_URL}/auth/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, email, password }), + }); + + const data = await response.json(); + + if (data.success) { + localStorage.setItem("token", data.token); + localStorage.setItem("user", JSON.stringify(data.user)); + dispatch(login(data.user)); + toast.success("Account created successfully! Welcome to MsCafe!"); + navigate("/home"); + } else { + toast.error(data.message || "Registration failed. Please try again."); + } + } catch (error) { + toast.error("Registration failed. Please check your connection."); + console.error("Registration error:", error); + } finally { + setIsLoading(false); } - } catch (error) { - toast.error("Registration failed. Please check your connection."); - console.error("Registration error:", error); - } finally { - setIsLoading(false); - } -}; + }; // Handle Google Authentication const handleGoogleSuccess = async (credentialResponse) => { try { - const result = await dispatch(googleLogin(credentialResponse.credential)).unwrap(); + const result = await dispatch( + googleLogin(credentialResponse.credential) + ).unwrap(); toast.success(`Welcome to MsCafe, ${result.user.name}! 🎉`); navigate("/home"); } catch (error) { toast.error("Google sign-up failed. Please try again."); - console.error('Google sign-up failed:', error); + console.error("Google sign-up failed:", error); } }; const handleGoogleError = () => { toast.error("Google sign-up failed. Please try again."); - console.error('Google sign-up failed'); + console.error("Google sign-up failed"); }; return ( @@ -122,20 +128,20 @@ const validatePassword = (password) => { className="w-full h-full object-cover" /> - + {/* Right Side: Signup Form */}

Join MsCafe Family

- + {/* Display error if any */} {error && (
{error}
)} - + {/* Google Sign-up Section */}
{ disabled={loading || isLoading} />
- + {/* Divider */}
- Or create account with email + + Or create account with email +
- + {/* Email/Password Form */}
@@ -179,7 +187,7 @@ const validatePassword = (password) => { disabled={loading || isLoading} />
- +
- +
- +
- + {/* Terms and Conditions */}
- + - + {/* Login Link */}

Already have an account?{" "} @@ -296,10 +322,12 @@ const validatePassword = (password) => { Log In

- + {/* Welcome Benefits */}
-

🎉 Welcome Benefits

+

+ 🎉 Welcome Benefits +

  • • Get 100 loyalty points for signing up
  • • Free welcome drink on your first visit
  • @@ -314,4 +342,4 @@ const validatePassword = (password) => { ); } -export default SignupPage; \ No newline at end of file +export default SignupPage; diff --git a/src/componets/footer.js b/src/componets/footer.js index 7a5e5a4..06333fe 100644 --- a/src/componets/footer.js +++ b/src/componets/footer.js @@ -336,55 +336,7 @@ function Footer() { {/* Social Icons */} - - - - - - - - - - - - - - - - - - - - - + {/* Divider */} @@ -479,6 +431,55 @@ function Footer() {
+ + + + + + + + + + + + + + + + + + + + + {/* Footer Text */}