Repository: TeamShiksha/openlogo
Pull request #634: Signup Integration
Author: login: Severin84
Created: 2025-04-18T23:03:15Z | Merged: 2025-04-21T16:55:50Z

## Description
Closes #615
SignUp:
Integrates Signup api. Covers the functionalities and test cases both.The signup route is implemented , the user can signup.
Code change also includes changes in .env.example file:
.env.example(ui) : BASE_URL is changed to VITE_BASE_URL , reason being url was not accessible.
.env.example(app): Included ADMINSEMAILS as it was created issue and was not includes by me in Add functionality to signup Admins.

CSS update:
Moved the error-related CSS classes (.error-container, .input-error, .has-error) to the global CSS file(index.css).
Updated components to use regular class names instead of CSS module styles (imported index.css in App.jsx).
Changes done in SignUp and CustomInput component.

## What type of PR is this? (Check all applicable)

- [ ] 🍕 Feature
- [ ] 🐛 Bug Fix
- [ ] 📄 Documentation Update
- [x] 👨‍💻 Code Refactor
- [ ] 🔥 Performance Improvements
- [x] ✅ Test
- [ ] 🛠️ CI/CD

## Screenshots (if applicable)
![Screenshot 2025-04-19 043015](https://github.com/user-attachments/assets/77076607-b9c0-4772-8086-68e241f7b75b)
![Screenshot 2025-04-19 043125](https://github.com/user-attachments/assets/5dbf7d04-0ba5-4315-bc60-763fdd981f90)

## Checklist
- [x] I have performed a self-review of my code  
- [x] I have commented my code, particularly in hard-to-understand areas  
- [x] I have added tests that prove my fix is effective or that my feature works  
- [x] New and existing unit tests pass locally with my changes


Linked issue #615: Integrating APIs
### Describe the feature

Integrating following backend APIs with frontend screens-
1. gettingUserData
2. signup
3. signin
4. signout

task is carried out by @shafat730 @farhan294sha @Severin84 

### It should be implemented because ? Additional context

_No response_

### Confirmation

- [x] I have searched for similar issues before creating this one.
- [x] I have provided all the necessary information to understand this documentation issue.

Diff:
diff --git a/packages/app/.env.example b/packages/app/.env.example
index 7bc4d375..2aa6cb9a 100644
--- a/packages/app/.env.example
+++ b/packages/app/.env.example
@@ -22,5 +22,5 @@ DISTRIBUTION_DOMAIN=https://hmzsbpcw4ex33jxeiage.cloudfront.net
 CLOUD_FRONT_KEYPAIR_ID=Q8A4T7ZMUYRW3Z
 CLOUD_FRONT_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----RSA KEY FOR CLOUDFRONT=-----END RSA PRIVATE KEY-----
 KEY=s3_bucket_folder_name_can_customise_as_per_your_likings
-
+ADMINSEMAILS=admin_Email@gmail.com
 
diff --git a/packages/ui/.env.example b/packages/ui/.env.example
index 04d349b8..ca3c7ffd 100644
--- a/packages/ui/.env.example
+++ b/packages/ui/.env.example
@@ -1 +1 @@
-BASE_URL=http://localhost:5000
\ No newline at end of file
+VITE_BASE_URL=http://localhost:5000
\ No newline at end of file
diff --git a/packages/ui/__tests__/components/auth/Signup.test.jsx b/packages/ui/__tests__/components/auth/Signup.test.jsx
index 05eaf75f..5c201e76 100644
--- a/packages/ui/__tests__/components/auth/Signup.test.jsx
+++ b/packages/ui/__tests__/components/auth/Signup.test.jsx
@@ -6,6 +6,13 @@ import {
   PASSWORD_VALIDATION_MESSAGES,
 } from "../../../src/utils/Constants";
 
+const mockedMakeRequest = vi.fn();
+vi.mock("../../../src/hooks/useApi", () => ({
+  useApi: () => ({
+    makeRequest: mockedMakeRequest,
+  }),
+}));
+
 describe("SignUpForm UI and Functionality Tests", () => {
   it("renders all form elements correctly", () => {
     render(<SignUpForm toggleForm={vi.fn()} />);
@@ -76,11 +83,11 @@ describe("SignUpForm UI and Functionality Tests", () => {
     const emailInput = screen.getByLabelText("Email");
     const passwordInput = screen.getByLabelText("Password");
     const confirmPasswordInput = screen.getByLabelText("Confirm Password");
-    const signInButton = screen.getByRole("button", {
+    const signUpButton = screen.getByRole("button", {
       name: SIGNUP.submitButton,
     });
 
-    expect(signInButton).toBeDisabled();
+    expect(signUpButton).toBeDisabled();
 
     fireEvent.change(nameInput, { target: { value: "John Doe" } });
     fireEvent.change(emailInput, { target: { value: "xyz@example.com" } });
@@ -89,7 +96,7 @@ describe("SignUpForm UI and Functionality Tests", () => {
       target: { value: "Password@123" },
     });
 
-    fireEvent.click(signInButton);
+    fireEvent.click(signUpButton);
 
     await waitFor(() => {
       expect(nameInput.value).toBe(SIGNUP.initialValues.name);
@@ -113,7 +120,79 @@ describe("SignUpForm UI and Functionality Tests", () => {
     );
     expect(confirmPasswordError).not.toBeInTheDocument();
 
-    expect(signInButton).toBeDisabled();
+    expect(signUpButton).toBeDisabled();
     expect(document.activeElement).toBe(document.body);
   });
+  it("connectivity test passed", async () => {
+    mockedMakeRequest.mockResolvedValue(true);
+    render(<SignUpForm toggleForm={vi.fn()} />);
+
+    const nameInput = screen.getByLabelText("Name");
+    const emailInput = screen.getByLabelText("Email");
+    const passwordInput = screen.getByLabelText("Password");
+    const confirmPasswordInput = screen.getByLabelText("Confirm Password");
+    const signUpButton = screen.getByRole("button", {
+      name: SIGNUP.submitButton,
+    });
+
+    expect(signUpButton).toBeDisabled();
+
+    fireEvent.change(nameInput, { target: { value: "Test" } });
+    fireEvent.change(emailInput, { target: { value: "test@gmail.com" } });
+    fireEvent.change(passwordInput, { target: { value: "Test@1234" } });
+    fireEvent.change(confirmPasswordInput, { target: { value: "Test@1234" } });
+
+    expect(signUpButton).not.toBeDisabled();
+    fireEvent.click(signUpButton);
+
+    await waitFor(() => {
+      expect(nameInput.value).toBe(SIGNUP.initialValues.name);
+      expect(emailInput.value).toBe(SIGNUP.initialValues.email);
+      expect(passwordInput.value).toBe(SIGNUP.initialValues.password);
+      expect(confirmPasswordInput.value).toBe(
+        SIGNUP.initialValues.confirmPassword
+      );
+    });
+
+    await waitFor(() => {
+      expect(mockedMakeRequest).toHaveBeenCalled();
+    });
+    expect(signUpButton).toBeDisabled();
+  });
+  it("connectivity test failed", async () => {
+    mockedMakeRequest.mockResolvedValue(false);
+    render(<SignUpForm toggleForm={vi.fn()} />);
+
+    const nameInput = screen.getByLabelText("Name");
+    const emailInput = screen.getByLabelText("Email");
+    const passwordInput = screen.getByLabelText("Password");
+    const confirmPasswordInput = screen.getByLabelText("Confirm Password");
+    const signUpButton = screen.getByRole("button", {
+      name: SIGNUP.submitButton,
+    });
+
+    expect(signUpButton).toBeDisabled();
+
+    fireEvent.change(nameInput, { target: { value: "Test" } });
+    fireEvent.change(emailInput, { target: { value: "test@gmail.com" } });
+    fireEvent.change(passwordInput, { target: { value: "Test@1234" } });
+    fireEvent.change(confirmPasswordInput, { target: { value: "Test@1234" } });
+
+    expect(signUpButton).not.toBeDisabled();
+    fireEvent.click(signUpButton);
+
+    await waitFor(() => {
+      expect(nameInput.value).toBe(SIGNUP.initialValues.name);
+      expect(emailInput.value).toBe(SIGNUP.initialValues.email);
+      expect(passwordInput.value).toBe(SIGNUP.initialValues.password);
+      expect(confirmPasswordInput.value).toBe(
+        SIGNUP.initialValues.confirmPassword
+      );
+    });
+
+    await waitFor(() => {
+      expect(mockedMakeRequest).toHaveBeenCalled();
+    });
+    expect(signUpButton).toBeDisabled();
+  });
 });
diff --git a/packages/ui/src/App.jsx b/packages/ui/src/App.jsx
index 0e28d811..a328bdf8 100644
--- a/packages/ui/src/App.jsx
+++ b/packages/ui/src/App.jsx
@@ -1,4 +1,5 @@
 import { useState } from "react";
+import "./index.css";
 import { Route, Routes } from "react-router-dom";
 import Header from "./components/header/Header";
 import AdminDashboard from "./page/admin/Admin";
diff --git a/packages/ui/src/api/api_instance.js b/packages/ui/src/api/api_instance.js
index 5b341750..4fdb2781 100644
--- a/packages/ui/src/api/api_instance.js
+++ b/packages/ui/src/api/api_instance.js
@@ -1,6 +1,7 @@
 import axios from "axios";
 
 export const instance = axios.create({
+  baseURL: `${import.meta.env.VITE_BASE_URL}/api`,
   headers: {
     "Content-Type": "application/json",
   },
diff --git a/packages/ui/src/components/auth/Signup.jsx b/packages/ui/src/components/auth/Signup.jsx
index e9a80371..c3f2ef29 100644
--- a/packages/ui/src/components/auth/Signup.jsx
+++ b/packages/ui/src/components/auth/Signup.jsx
@@ -5,6 +5,7 @@ import PropTypes from "prop-types";
 import { SIGNUP, BUTTON_TEXT } from "../../utils/Constants";
 import styles from "./SignForm.module.css";
 import { validate } from "../../utils/Helpers";
+import { useApi } from "../../hooks/useApi";
 
 function SignUp({ toggleForm }) {
   const [formValues, setFormValues] = useState(SIGNUP.initialValues);
@@ -12,6 +13,11 @@ function SignUp({ toggleForm }) {
   const [isSubmit, setIsSubmit] = useState(false);
   const [isFormValid, setIsFormValid] = useState(false);
   const [focusedField, setFocusedField] = useState(null);
+  const { makeRequest, errorMsg } = useApi({
+    url: `/auth/signup`,
+    method: "post",
+    data: formValues,
+  });
 
   useEffect(() => {
     if (!focusedField) {
@@ -47,6 +53,12 @@ function SignUp({ toggleForm }) {
     setFormErrors({});
     setIsSubmit(false);
     setFocusedField(null);
+
+    const success = await makeRequest();
+    if (success) {
+      setFormValues(SIGNUP.initialValues);
+      setIsSubmit(true);
+    }
   };
 
   return (
@@ -58,6 +70,9 @@ function SignUp({ toggleForm }) {
         onSubmit={handleSubmit}
       >
         <h2 className={styles.title}>{SIGNUP.title}</h2>
+        <div className={`"error-container" ${errorMsg ? "has-error" : ""}`}>
+          <p className="input-error">{errorMsg}</p>
+        </div>
         <div className={styles["form-width"]}>
           {SIGNUP["fields"].map((field) => (
             <CustomInput
diff --git a/packages/ui/src/components/common/input/CustomInput.jsx b/packages/ui/src/components/common/input/CustomInput.jsx
index 01e1bbad..d5524e70 100644
--- a/packages/ui/src/components/common/input/CustomInput.jsx
+++ b/packages/ui/src/components/common/input/CustomInput.jsx
@@ -34,10 +34,8 @@ function CustomInput({
       >
         {label}
       </label>
-      <div
-        className={`${styles["error-container"]} ${error ? styles["has-error"] : ""}`}
-      >
-        <p className={styles["input-error"]}>{error}</p>
+      <div className={`"error-container" ${error ? "has-error" : ""}`}>
+        <p className="input-error">{error}</p>
       </div>
     </div>
   );
diff --git a/packages/ui/src/components/common/input/CustomInput.module.css b/packages/ui/src/components/common/input/CustomInput.module.css
index 96bf2bf4..6e2be10b 100644
--- a/packages/ui/src/components/common/input/CustomInput.module.css
+++ b/packages/ui/src/components/common/input/CustomInput.module.css
@@ -48,29 +48,3 @@
 .group:not(:focus-within) .input-label {
   color: var(--label-color);
 }
-
-.error-container {
-  overflow: hidden;
-  max-height: 0;
-  transition: max-height 0.3s ease-out;
-}
-
-.has-error {
-  max-height: 50px;
-}
-
-.input-error {
-  color: var(--error);
-  font-size: 0.8rem;
-  opacity: 0;
-  padding: 0 4px;
-  transition:
-    opacity 0.2s ease-out,
-    padding 0.2s ease-out;
-  transition-delay: 0.1s;
-}
-
-.has-error .input-error {
-  opacity: 1;
-  padding: 4px;
-}
diff --git a/packages/ui/src/hooks/useApi.js b/packages/ui/src/hooks/useApi.js
new file mode 100644
index 00000000..9ca59163
--- /dev/null
+++ b/packages/ui/src/hooks/useApi.js
@@ -0,0 +1,51 @@
+import { useState } from "react";
+import { instance } from "../api/api_instance";
+
+/**
+ * Custom React hook to make API requests using axios.
+ *
+ * @param {Object} config - The axios request configuration.
+ *
+ * @returns {Object} - Contains the response data, a function to set the data, the error message if any, the loading state, and a function to make the request.
+ */
+
+export const useApi = (config) => {
+  const [data, setData] = useState(null);
+  const [errorMsg, setErrorMsg] = useState("");
+  const [isSuccess, setIsSuccess] = useState(false);
+  const [loading, setLoading] = useState(false);
+
+  const makeRequest = async () => {
+    setErrorMsg(null);
+    setIsSuccess(false);
+    setLoading(true);
+    let success = false;
+    try {
+      const response = await instance(config);
+      setData(response.data);
+      setIsSuccess(true);
+      success = true;
+    } catch (err) {
+      setIsSuccess(false);
+      if (err?.response?.data?.message) {
+        setErrorMsg(err?.response?.data?.message);
+      } else {
+        setErrorMsg(err?.message);
+      }
+    } finally {
+      setLoading(false);
+    }
+    return success;
+  };
+  return {
+    data,
+    setData,
+    errorMsg,
+    loading,
+    setLoading,
+    makeRequest,
+    isSuccess,
+    setIsSuccess,
+    setErrorMsg,
+  };
+};
diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css
index 2153d55f..6041eb56 100644
--- a/packages/ui/src/index.css
+++ b/packages/ui/src/index.css
@@ -66,6 +66,32 @@ body {
   flex-grow: 1;
 }
 
+.error-container {
+  overflow: hidden;
+  max-height: 0;
+  transition: max-height 0.3s ease-out;
+}
+
+.has-error {
+  max-height: 50px;
+}
+
+.input-error {
+  color: var(--error);
+  font-size: 0.8rem;
+  opacity: 0;
+  padding: 0 4px;
+  transition:
+    opacity 0.2s ease-out,
+    padding 0.2s ease-out;
+  transition-delay: 0.1s;
+}
+
+.has-error .input-error {
+  opacity: 1;
+  padding: 4px;
+}
+
 footer {
   margin-top: auto;
 }


Reviews:
user: login: aps08
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-20T13:15:44Z
user: login: aps08
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-20T13:17:35Z
user: login: Severin84
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-20T16:33:36Z
user: login: aps08
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T16:01:27Z
user: login: aps08
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T16:02:33Z
user: login: aps08
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-21T16:54:45Z

Review comments:
diff_hunk: @@ -5,6 +5,7 @@ import {
   SIGNUP,
   PASSWORD_VALIDATION_MESSAGES,
 } from "../../../src/utils/Constants";
+import * as apimodule from "../../../src/hooks/useApi";
path: packages/ui/__tests__/components/auth/Signup.test.jsx
user: login: aps08
body: universal import not a good idea
created_at: 2025-04-20T13:15:44Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,7 @@
 import axios from "axios";
 
 export const instance = axios.create({
+  baseURL: `${import.meta.env.VITE_BASE_URL}/api`,
path: packages/ui/src/api/api_instance.js
user: login: aps08
body: if whole url is needed then why are we not using process.env and why this ?
created_at: 2025-04-20T13:17:34Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,7 @@
 import axios from "axios";
 
 export const instance = axios.create({
+  baseURL: `${import.meta.env.VITE_BASE_URL}/api`,
path: packages/ui/src/api/api_instance.js
user: login: Severin84
body: Because as we are  VITE then access environmental variable we have to use import.meta.env and have to start environmental variable with a prefix of VITE_ . And if we use process.env then it gives this error : Uncaught ReferenceError: process is not defined
created_at: 2025-04-20T16:33:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,7 @@
 import axios from "axios";
 
 export const instance = axios.create({
+  baseURL: `${import.meta.env.VITE_BASE_URL}/api`,
path: packages/ui/src/api/api_instance.js
user: login: aps08
body: can't you change the env name ?
created_at: 2025-04-21T16:01:27Z
author_association: MEMBER
diff_hunk: @@ -1 +1 @@
-BASE_URL=http://localhost:5000
\ No newline at end of file
+VITE_BASE_URL=http://localhost:5000
path: packages/ui/.env.example
user: login: aps08
body: you only changed it from BASE_URL to VITE_BASE_URL why is this for ?
created_at: 2025-04-21T16:02:32Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-21T16:02:03Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=TeamShiksha_openlogo&pullRequest=634) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=TeamShiksha_openlogo&pullRequest=634&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=TeamShiksha_openlogo&pullRequest=634&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=TeamShiksha_openlogo&pullRequest=634&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=TeamShiksha_openlogo&pullRequest=634&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=TeamShiksha_openlogo&pullRequest=634&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=TeamShiksha_openlogo&pullRequest=634)


author_association: NONE