From 9ea104e9bfd4c602dc741e29a51691f1af081d88 Mon Sep 17 00:00:00 2001 From: sahilshahane Date: Mon, 19 May 2025 23:00:57 +0530 Subject: [PATCH 1/3] [fix] login page --- src/App.js | 46 +++++++++---------- src/components/LoginForm.css | 14 +++++- .../{LoginForm.js => LoginForm.jsx} | 37 +++++++++++---- src/components/Welcome.js | 18 -------- src/components/Welcome.jsx | 24 ++++++++++ src/constants/auth.js | 1 + src/hooks/useUserLogin.js | 38 +++++++++++++++ src/hooks/useUserLogout.js | 30 ++++++++++++ src/index.css | 10 ++-- src/providers/AuthProvider/AuthProvider.jsx | 40 ++++++++++++++++ src/providers/AuthProvider/types.tsx | 9 ++++ 11 files changed, 213 insertions(+), 54 deletions(-) rename src/components/{LoginForm.js => LoginForm.jsx} (51%) delete mode 100644 src/components/Welcome.js create mode 100644 src/components/Welcome.jsx create mode 100644 src/constants/auth.js create mode 100644 src/hooks/useUserLogin.js create mode 100644 src/hooks/useUserLogout.js create mode 100644 src/providers/AuthProvider/AuthProvider.jsx create mode 100644 src/providers/AuthProvider/types.tsx diff --git a/src/App.js b/src/App.js index c383c91..378dc98 100644 --- a/src/App.js +++ b/src/App.js @@ -1,32 +1,32 @@ -import React, { useState } from 'react'; -import './App.css'; -import LoginForm from './components/LoginForm'; -import Welcome from './components/Welcome'; +import React, { useState } from "react"; +import "./App.css"; +import LoginForm from "./components/LoginForm"; +import Welcome from "./components/Welcome"; +import { + AuthContext, + AuthProvider, +} from "./providers/AuthProvider/AuthProvider"; +import { useUserLogin } from "./hooks/useUserLogin"; +import { useContext } from "react"; +import { useUserLogout } from "./hooks/useUserLogout"; -function App() { - const [isLoggedIn, setIsLoggedIn] = useState(false); - const [userName, setUserName] = useState(''); - - const handleLogin = (formData) => { - // In a real app, you would validate credentials here - setIsLoggedIn(true); - setUserName(formData.name); - }; - - const handleLogout = () => { - setIsLoggedIn(false); - setUserName(''); - }; +const HTMLBody = () => { + const authInfo = useContext(AuthContext); + if (!authInfo.init) return <>; return (
- {isLoggedIn ? ( - - ) : ( - - )} + {authInfo?.user?.name ? : }
); +}; + +function App() { + return ( + + + + ); } export default App; diff --git a/src/components/LoginForm.css b/src/components/LoginForm.css index eeae4c2..6cc917a 100644 --- a/src/components/LoginForm.css +++ b/src/components/LoginForm.css @@ -13,6 +13,7 @@ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); width: 100%; max-width: 400px; + margin: 0 auto; } .login-form h2 { @@ -55,6 +56,7 @@ font-size: 1rem; cursor: pointer; transition: background-color 0.2s; + margin-bottom: 1rem; } .login-button:hover { @@ -64,4 +66,14 @@ .login-button:focus { outline: none; box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2); -} \ No newline at end of file +} + +#login-error { + width: 100%; + padding: 0.75rem 0; + background-color: #dc3545; + color: white; + border: none; + border-radius: 4px; + font-size: 1rem; +} diff --git a/src/components/LoginForm.js b/src/components/LoginForm.jsx similarity index 51% rename from src/components/LoginForm.js rename to src/components/LoginForm.jsx index 26c8cc3..966d006 100644 --- a/src/components/LoginForm.js +++ b/src/components/LoginForm.jsx @@ -1,23 +1,37 @@ -import React, { useState } from 'react'; -import './LoginForm.css'; +import { useState } from "react"; +import "./LoginForm.css"; +import { useUserLogin, LOGIN_STATUS } from "../hooks/useUserLogin"; + +function LoginForm() { + const { Login: onLogin, loginStatus, errorMessage } = useUserLogin(); -function LoginForm({ onLogin }) { const [formData, setFormData] = useState({ - name: '', - password: '' + name: "", + password: "", }); const handleChange = (e) => { const { name, value } = e.target; - setFormData(prevState => ({ + + setFormData((prevState) => ({ ...prevState, - [name]: value + [name]: value, })); }; + const handleFormSubmit = (e) => { + e.preventDefault(); + + // Make Login API call + onLogin(formData.name, formData.password).catch((error) => { + console.error("Something went wrong while logging in"); + console.error(error); + }); + }; + return (
-
+

Login

@@ -28,6 +42,7 @@ function LoginForm({ onLogin }) { value={formData.name} onChange={handleChange} required + disabled={loginStatus === LOGIN_STATUS.LOADING} />
@@ -38,15 +53,19 @@ function LoginForm({ onLogin }) { name="password" value={formData.password} onChange={handleChange} + disabled={loginStatus === LOGIN_STATUS.LOADING} required />
+ {loginStatus === LOGIN_STATUS.ERROR && ( +
{errorMessage || "Something went wrong"}
+ )}
); } -export default LoginForm; \ No newline at end of file +export default LoginForm; diff --git a/src/components/Welcome.js b/src/components/Welcome.js deleted file mode 100644 index 3c721b3..0000000 --- a/src/components/Welcome.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import './Welcome.css'; - -function Welcome({ userName, onLogout }) { - return ( -
-
-

Welcome, {userName}!

-

You have successfully logged in.

- -
-
- ); -} - -export default Welcome; \ No newline at end of file diff --git a/src/components/Welcome.jsx b/src/components/Welcome.jsx new file mode 100644 index 0000000..5c3e8bf --- /dev/null +++ b/src/components/Welcome.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import "./Welcome.css"; +import { AuthContext } from "../providers/AuthProvider/AuthProvider"; +import { useContext } from "react"; +import { useUserLogout } from "../hooks/useUserLogout"; + +function Welcome() { + const authInfo = useContext(AuthContext); + const { Logout: onLogout } = useUserLogout(); + + return ( +
+
+

Welcome, {authInfo?.user?.name}!

+

You have successfully logged in.

+ +
+
+ ); +} + +export default Welcome; diff --git a/src/constants/auth.js b/src/constants/auth.js new file mode 100644 index 0000000..911c15e --- /dev/null +++ b/src/constants/auth.js @@ -0,0 +1 @@ +export const LOCALSTORAGE_USERNAME_KEY = "username"; diff --git a/src/hooks/useUserLogin.js b/src/hooks/useUserLogin.js new file mode 100644 index 0000000..969e592 --- /dev/null +++ b/src/hooks/useUserLogin.js @@ -0,0 +1,38 @@ +import { useContext } from "react"; +import { AuthContext } from "../providers/AuthProvider/AuthProvider"; +import { useState } from "react"; +import { LOCALSTORAGE_USERNAME_KEY } from "../constants/auth"; + +export const LOGIN_STATUS = { + LOADING: 1, + ERROR: 2, + SUCCESS: 3, +}; + +export const useUserLogin = () => { + const authContext = useContext(AuthContext); + const [loginStatus, setLoginStatus] = useState(0); + const [errorMessage, setErrorMessage] = useState(""); + + /** + * @param name {string} + * @param password {string} + */ + const Login = async (name, password) => { + if (loginStatus === LOGIN_STATUS.LOADING) return; + setLoginStatus(LOGIN_STATUS.LOADING); + + // Make API call to Backend server, but since this is a small frontend only assignment. + // i'll just hardcode username & password + if (name !== "sahil" || password !== "12345678") { + setErrorMessage("Invalid username or password"); + setLoginStatus(LOGIN_STATUS.ERROR); + } else { + authContext.setUser({ name }); + localStorage.setItem(LOCALSTORAGE_USERNAME_KEY, name); + setLoginStatus(LOGIN_STATUS.SUCCESS); + } + }; + + return { Login, loginStatus, errorMessage }; +}; diff --git a/src/hooks/useUserLogout.js b/src/hooks/useUserLogout.js new file mode 100644 index 0000000..33cc1ef --- /dev/null +++ b/src/hooks/useUserLogout.js @@ -0,0 +1,30 @@ +import { useContext } from "react"; +import { AuthContext } from "../providers/AuthProvider/AuthProvider"; +import { useState } from "react"; +import { LOCALSTORAGE_USERNAME_KEY } from "../constants/auth"; + +export const LOGOUT_STATUS = { + LOADING: 1, + ERROR: 2, + SUCCESS: 3, +}; + +export const useUserLogout = () => { + const authContext = useContext(AuthContext); + const [logoutStatus, setLogoutStatus] = useState(0); + + const Logout = async () => { + if (logoutStatus === LOGOUT_STATUS.LOADING) return; + setLogoutStatus(LOGOUT_STATUS.LOADING); + + // Make API call to Backend server to logout and invalidate the current credentials, + // but since this is a small frontend only assignment. + // i'll just remove the user from Browser's memory + authContext.setUser(undefined); + localStorage.removeItem(LOCALSTORAGE_USERNAME_KEY); + + setLogoutStatus(LOGOUT_STATUS.SUCCESS); + }; + + return { Logout, logoutStatus }; +}; diff --git a/src/index.css b/src/index.css index ec2585e..3a3a203 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,17 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } + +input { + box-sizing: border-box; +} diff --git a/src/providers/AuthProvider/AuthProvider.jsx b/src/providers/AuthProvider/AuthProvider.jsx new file mode 100644 index 0000000..b65bb63 --- /dev/null +++ b/src/providers/AuthProvider/AuthProvider.jsx @@ -0,0 +1,40 @@ +import { useEffect } from "react"; +import { useState } from "react"; +import { createContext } from "react"; +import { LOCALSTORAGE_USERNAME_KEY } from "../../constants/auth"; + +/** + * @type {React.Context} + */ +export const AuthContext = createContext({ + setUser: (info) => {}, + init: false, +}); + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState({}); + const [init, setState] = useState(false); + + useEffect(() => { + const username = localStorage.getItem(LOCALSTORAGE_USERNAME_KEY); + + if (username) + setUser(() => ({ + name: username, + })); + + setState(true); + }, []); + + return ( + + {children} + + ); +}; diff --git a/src/providers/AuthProvider/types.tsx b/src/providers/AuthProvider/types.tsx new file mode 100644 index 0000000..edb3621 --- /dev/null +++ b/src/providers/AuthProvider/types.tsx @@ -0,0 +1,9 @@ +export interface UserInfo { + name: string; +} + +export interface AuthProviderInformation { + setUser: (info: UserInfo) => void; + user?: UserInfo; + init: boolean; +} From 4ccae8cc4b4789d3d91bbb767e4b963d684e5993 Mon Sep 17 00:00:00 2001 From: sahilshahane Date: Mon, 19 May 2025 23:03:04 +0530 Subject: [PATCH 2/3] [chore] added react types --- package-lock.json | 33 +++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 34 insertions(+) diff --git a/package-lock.json b/package-lock.json index 623a7e0..8227832 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@testing-library/cypress": "^10.0.3", + "@types/react": "^19.1.4", "cypress": "^14.3.3", "start-server-and-test": "^2.0.12" } @@ -3710,6 +3711,16 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, + "node_modules/@types/react": { + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", + "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -6327,6 +6338,13 @@ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/cypress": { "version": "14.3.3", "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.3.3.tgz", @@ -20054,6 +20072,15 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, + "@types/react": { + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", + "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", + "devOptional": true, + "requires": { + "csstype": "^3.0.2" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -21923,6 +21950,12 @@ } } }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true + }, "cypress": { "version": "14.3.3", "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.3.3.tgz", diff --git a/package.json b/package.json index f12a85a..64b0de7 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ }, "devDependencies": { "@testing-library/cypress": "^10.0.3", + "@types/react": "^19.1.4", "cypress": "^14.3.3", "start-server-and-test": "^2.0.12" } From aa00ec6a3f7c1a516070de6032615eaf2fe5d023 Mon Sep 17 00:00:00 2001 From: sahilshahane Date: Mon, 19 May 2025 23:05:13 +0530 Subject: [PATCH 3/3] [test] created cypress tests --- cypress/e2e/login.cy.js | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/login.cy.js b/cypress/e2e/login.cy.js index c2fbb3a..b4fd139 100644 --- a/cypress/e2e/login.cy.js +++ b/cypress/e2e/login.cy.js @@ -1,2 +1,38 @@ -describe('Login Component', () => { -}) \ No newline at end of file +describe("Login Component", () => { + beforeEach(() => { + cy.visit("/LoginForm"); // Change this route according to your setup + }); + + it("should render the login form with inputs and button", () => { + cy.get("form.login-form").should("exist"); + cy.get('input[name="name"]').should("exist"); + cy.get('input[name="password"]').should("exist"); + cy.get('button[type="submit"]').should("contain", "Login"); + }); + + it("should allow typing into name and password fields", () => { + cy.get('input[name="name"]').type("john"); + cy.get('input[name="name"]').should("have.value", "john"); + + cy.get('input[name="password"]').type("secret123"); + cy.get('input[name="password"]').should("have.value", "secret123"); + }); + + it("should submit the form and log-in successfully", () => { + cy.get('input[name="name"]').type("sahil"); + cy.get('input[name="password"]').type("12345678"); + cy.get('button[type="submit"]').click(); + + cy.get("#welcome-screen", { timeout: 10000 }).should("be.visible"); + }); + + it("should handle login error gracefully", () => { + cy.get('input[name="name"]').type("wronguser"); + cy.get('input[name="password"]').type("wrongpass"); + cy.get('button[type="submit"]').click(); + + cy.get("#login-error", { timeout: 10000 }).should("be.visible"); + + cy.contains("Invalid username or password").should("be.visible"); + }); +});