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");
+ });
+});
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"
}
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 (
);
}
-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;
+}