Repository: geekelo/jjrsf-cbt
Pull request #5: Added a Confirmation Page & Added modern UI elements for a better user experience.
Author: login: nicojoy-gif
Created: 2025-03-17T07:54:09Z | Merged: 2025-03-17T11:29:50Z

### Improvements
**Added a Confirmation Page**
- [x] Displays a message thanking the candidate after completing the exam.
- [x] Informs them to check their email for results.

**Updated Modal Behavior**
- [x] When the exam is completed, the modal now redirects to the confirmation page instead of staying on the question page.
- [x] Styled the Confirmation Page

**Added modern UI elements for a better user experience.**

- [x] Improved spacing, fonts, and animations for a clean design.
- [x] Fixed Date-Time Format Issue

### Fixes
**Issue:**
- Backend timestamps (UTC) were causing mismatched times on the frontend.

**Resolve**
- Adjusted frontend formatting to correctly reflect local time zones.


Diff:
diff --git a/src/App.js b/src/App.js
index 17a1a65..307eb66 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,5 +1,10 @@
 import React from "react";
-import { BrowserRouter as Router, Route, Routes, Navigate } from "react-router-dom";
+import {
+  BrowserRouter as Router,
+  Route,
+  Routes,
+  Navigate,
+} from "react-router-dom";
 
 import "./App.css";
 import Home from "./components/Home";
@@ -7,6 +12,7 @@ import Instructions from "./components/Instructions";
 import QuestionPage from "./components/Question";
 import Layout from "./components/Layout";
 import HelpSection from "./components/Help";
+import ConfirmationPage from "./components/Confirmation";
 
 const App = () => {
   return (
@@ -17,7 +23,8 @@ const App = () => {
           <Route path="/instructions" element={<Instructions />} />
           <Route path="/questions" element={<QuestionPage />} />
           <Route path="/help" element={<HelpSection />} />
-          {/* Redirect all unknown routes to Home */}
+          <Route path="/:exam_code" element={<Home />} />
+          <Route path="/confirmation" element={<ConfirmationPage />} />
           <Route path="*" element={<Navigate to="/" />} />
         </Routes>
       </Layout>
diff --git a/src/components/Confirmation.js b/src/components/Confirmation.js
new file mode 100644
index 0000000..a092e72
--- /dev/null
+++ b/src/components/Confirmation.js
@@ -0,0 +1,18 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import "../styles/ConfirmationPage.css"; 
+
+const ConfirmationPage = () => {
+  const navigate = useNavigate();
+
+  return (
+    <div className="confirmation-container">
+      <h1>🎉 Congratulations! 🎉</h1>
+      <p>Thank you for completing the exam.</p>
+      <p>Check your email for further details.</p>
+     
+    </div>
+  );
+};
+
+export default ConfirmationPage;
diff --git a/src/components/Home.js b/src/components/Home.js
index 6873252..e28dd8f 100644
--- a/src/components/Home.js
+++ b/src/components/Home.js
@@ -1,6 +1,6 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
 import axios from "axios";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
 import { ToastContainer, toast } from "react-toastify";
 import "react-toastify/dist/ReactToastify.css";
 import "../styles/Home.css";
@@ -8,40 +8,50 @@ import "../styles/Home.css";
 const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
 
 const Home = () => {
+  const { exam_code } = useParams(); // Get exam code from URL
+  const navigate = useNavigate();
   const [email, setEmail] = useState("");
   const [loading, setLoading] = useState(false);
-  const navigate = useNavigate();
+
+  useEffect(() => {
+    if (exam_code) {
+      if (!/^[a-zA-Z0-9]{6}$/.test(exam_code)) {
+        toast.error(
+          "Invalid exam code! It must be exactly 6 letters or digits.",
+          { position: "top-right" }
+        );
+      }
+    }
+  }, [exam_code]);
 
   const validateAndCheckEmail = async () => {
-    if (!email) {
+    if (!email.trim()) {
       toast.error("Please enter your email!", { position: "top-right" });
       return;
     }
 
-    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+    if (!/^[^\s@]+@[^\s@]+$/.test(email)) {
       toast.error("Invalid email format", { position: "top-right" });
       return;
     }
 
+    if (!exam_code || !/^[a-zA-Z0-9]{6}$/.test(exam_code)) {
+      toast.error("Invalid exam code!", { position: "top-right" });
+      return;
+    }
     setLoading(true);
 
     try {
-      const apiUrl = `${API_BASE_URL}/clacbt_check_candidates/check?exam_code=123456&email=${email}`;
-      console.log("API URL:", apiUrl);
-
+      const apiUrl = `${API_BASE_URL}/clacbt_check_candidates/check?exam_code=${exam_code}&email=${email}`;
       const response = await axios.get(apiUrl);
-      console.log("API Response:", response.data);
-
       if (response.data.message === "Candidate authorized") {
-        const candidateData = response.data.candidate;
-
-        localStorage.removeItem("candidate");
-
-        const candidateWithExpiry = {
-          ...candidateData,
-          timestamp: new Date().getTime(),
-        };
-        localStorage.setItem("candidate", JSON.stringify(candidateWithExpiry));
+        localStorage.setItem(
+          "candidate",
+          JSON.stringify({
+            ...response.data.candidate,
+            timestamp: new Date().getTime(),
+          })
+        );
 
         toast.success("Email verified! Redirecting...", {
           position: "top-right",
@@ -67,22 +77,6 @@ const Home = () => {
     }
   };
 
-  const removeExpiredCandidate = () => {
-    const storedCandidate = localStorage.getItem("candidate");
-    if (storedCandidate) {
-      const { timestamp } = JSON.parse(storedCandidate);
-      const oneHour = 60 * 60 * 1000;
-      if (new Date().getTime() - timestamp > oneHour) {
-        localStorage.removeItem("candidate");
-        console.log("Candidate data expired and removed.");
-      }
-    }
-  };
-
-  React.useEffect(() => {
-    removeExpiredCandidate();
-  }, []);
-
   return (
     <div className="home-container">
       <ToastContainer />
diff --git a/src/components/Question.js b/src/components/Question.js
index 4c9969f..b6cb718 100644
--- a/src/components/Question.js
+++ b/src/components/Question.js
@@ -29,11 +29,12 @@ const QuestionPage = () => {
       return;
     }
     const currentTime = Date.now();
-    const examStartTime = new Date(examData.start_time).getTime();
-    const examEndTime = new Date(examData.end_time).getTime();
+    const examStartTime = new Date(examData.start_time).toISOString().replace("T", " ").split(".")[0];
+    const examEndTime = new Date(examData.end_time).toISOString().replace("T", " ").split(".")[0];
+  
 
     if (currentTime < examStartTime) {
-      setModalMessage(`The exam is scheduled for ${new Date(examData.start_time).toLocaleString()}`);
+      setModalMessage(`The exam is scheduled for ${new Date(examData.start_time)}`);
       setShowModal(true);
       return;
     }
@@ -108,7 +109,7 @@ const QuestionPage = () => {
       }
   
       const apiUrl = `${API_BASE_URL}/clacbt_exams/${examData.id}/clacbt_candidates/${candidate.id}`;
-  console.log(apiUrl)
+  
       const payload = {
         clacbt_candidate: {
           email: candidate.email,
@@ -122,19 +123,21 @@ const QuestionPage = () => {
           headers: { "Content-Type": "application/json" },
           body: JSON.stringify(payload),
         });
-  console.log(response)
+  
         if (!response.ok) {
           throw new Error(`Failed to submit score: ${response.statusText}`);
         }
   
-        const result = await response.json();
-        console.log("Score submitted successfully:", result);
+        console.log("Score submitted successfully:", await response.json());
       } catch (error) {
         console.error("Error updating score:", error);
       }
   
       setModalMessage(`Test Completed! Your Score: ${finalScore} / ${totalMarks}`);
       setShowModal(true);
+  
+      // Navigate to confirmation page after a delay
+      setTimeout(() => navigate("/confirmation"), 3000);
     }
   };
   
diff --git a/src/styles/ConfirmationPage.css b/src/styles/ConfirmationPage.css
new file mode 100644
index 0000000..750c5a8
--- /dev/null
+++ b/src/styles/ConfirmationPage.css
@@ -0,0 +1,43 @@
+.confirmation-container {
+  flex-direction: column;
+
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  background: #1a1f36;
+  background-image: radial-gradient(
+    rgba(255, 255, 255, 0.1) 1px,
+    transparent 0
+  );
+  background-size: 25px 25px;
+  background-position: -10px -10px;
+}
+
+.confirmation-container h1 {
+  font-size: 2rem;
+  color: #2d8cff;
+  margin-bottom: 10px;
+}
+
+.confirmation-container p {
+  font-size: 1.2rem;
+  color: #fff;
+  margin-bottom: 20px;
+}
+
+.confirmation-container button {
+  padding: 12px 24px;
+  font-size: 1rem;
+  color: #fff;
+  background-color: #442e79;
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  font-weight: 700;
+  transition: 0.3s;
+}
+
+.confirmation-container button:hover {
+  background-color: #1c6dcc;
+}
diff --git a/src/styles/QuestionPage.css b/src/styles/QuestionPage.css
index 39e4114..311a3ed 100644
--- a/src/styles/QuestionPage.css
+++ b/src/styles/QuestionPage.css
@@ -50,8 +50,11 @@ h2 {
 .options-list {
   list-style: none;
   padding: 0;
+  max-height: 350px; 
+  overflow-y: auto;
 }
 
+
 /* Option Item */
 .option {
   display: flex;
@@ -159,7 +162,7 @@ h2 {
 .timer {
   font-size: 20px;
   font-weight: bold;
-  color: #28a745; /* Green for plenty of time */
+  color: #28a745; 
   text-align: center;
   padding: 10px;
   border: 2px solid #28a745;
@@ -175,7 +178,7 @@ h2 {
 
 /* Timer Warning */
 .timer.low-time {
-  color: #d9534f; /* Red when time is low */
+  color: #d9534f; 
   border-color: #d9534f;
   background: #fbeaea;
   animation: blink 1s infinite alternate;
@@ -268,8 +271,13 @@ h2 {
   }
 
   .question-text {
-    font-size: 16px;
+    font-size: 18px;
+    margin: 15px 0;
+    max-height: 150px; 
+    overflow-y: auto;
+    padding-right: 10px;
   }
+  
 
   .option {
     font-size: 14px;


Reviews:
user: login: geekelo
body: Great improvements and well-implemented fixes.
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-17T10:54:28Z
user: login: nicojoy-gif
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T11:00:01Z
user: login: nicojoy-gif
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T11:00:04Z
user: login: geekelo
body: All requested changes have been looked into.
Great!
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-17T11:29:06Z

Review comments:
diff_hunk: @@ -1,65 +1,73 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
 import axios from "axios";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
 import { ToastContainer, toast } from "react-toastify";
 import "react-toastify/dist/ReactToastify.css";
 import "../styles/Home.css";
 
 const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
 
 const Home = () => {
+  const { exam_code } = useParams(); // Get exam code from URL
+  const navigate = useNavigate();
   const [email, setEmail] = useState("");
   const [loading, setLoading] = useState(false);
-  const navigate = useNavigate();
+
+  useEffect(() => {
+    console.log("Exam Code from URL:", exam_code);
+
+    if (exam_code) {
+      if (!/^[a-zA-Z0-9]{6}$/.test(exam_code)) {
+        toast.error("Invalid exam code! It must be exactly 6 letters or digits.", { position: "top-right" });
+      }
+    }
+    
+
+    removeExpiredCandidate();
+  }, [exam_code]); // Runs whenever exam_code changes
 
   const validateAndCheckEmail = async () => {
-    if (!email) {
+    if (!email.trim()) {
       toast.error("Please enter your email!", { position: "top-right" });
       return;
     }
 
-    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+    if (!/^[^\s@]+@[^\s@]+$/.test(email)) {
       toast.error("Invalid email format", { position: "top-right" });
       return;
     }
 
+    if (!exam_code || !/^[a-zA-Z0-9]{6}$/.test(exam_code)) {
+      toast.error("Invalid exam code!", { position: "top-right" });
+      return;
+    }
+
     setLoading(true);
 
     try {
-      const apiUrl = `${API_BASE_URL}/clacbt_check_candidates/check?exam_code=123456&email=${email}`;
+      const apiUrl = `${API_BASE_URL}/clacbt_check_candidates/check?exam_code=${exam_code}&email=${email}`;
       console.log("API URL:", apiUrl);
 
       const response = await axios.get(apiUrl);
       console.log("API Response:", response.data);
 
       if (response.data.message === "Candidate authorized") {
-        const candidateData = response.data.candidate;
-
-        localStorage.removeItem("candidate");
path: src/components/Home.js
user: login: geekelo
body: Hey, 
Nice, but there'd be no need to remove candidates since they are automatically logged-out after an exam.

created_at: 2025-03-17T10:49:13Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,43 @@
+.confirmation-container {
+   
+    flex-direction: column;
+    
+   
+    display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  background: #1a1f36; 
+  background-image: radial-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 0);
+  background-size: 25px 25px;
+  background-position: -10px -10px;
+  }
+  
+  .confirmation-container h1 {
+    font-size: 2rem;
+    color: #2d8cff;
+    margin-bottom: 10px;
+  }
+  
+  .confirmation-container p {
+    font-size: 1.2rem;
+    color: #fff;
+    margin-bottom: 20px;
+  }
+  
+  .confirmation-container button {
+    padding: 12px 24px;
+    font-size: 1rem;
+    color: #fff;
+    background-color: #442e79;
+    border: none;
+    border-radius: 8px;
+    cursor: pointer;
+    font-weight: 700;
+    transition: 0.3s;
+  }
+  
+  .confirmation-container button:hover {
+    background-color: #1c6dcc;
+  }
+  
path: src/styles/ConfirmationPage.css
user: login: geekelo
body: Please an oversight here. Remember to add an extra line.
created_at: 2025-03-17T10:52:48Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,43 @@
+.confirmation-container {
+   
+    flex-direction: column;
+    
+   
+    display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  background: #1a1f36; 
+  background-image: radial-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 0);
+  background-size: 25px 25px;
+  background-position: -10px -10px;
+  }
+  
+  .confirmation-container h1 {
+    font-size: 2rem;
+    color: #2d8cff;
+    margin-bottom: 10px;
+  }
+  
+  .confirmation-container p {
+    font-size: 1.2rem;
+    color: #fff;
+    margin-bottom: 20px;
+  }
+  
+  .confirmation-container button {
+    padding: 12px 24px;
+    font-size: 1rem;
+    color: #fff;
+    background-color: #442e79;
+    border: none;
+    border-radius: 8px;
+    cursor: pointer;
+    font-weight: 700;
+    transition: 0.3s;
+  }
+  
+  .confirmation-container button:hover {
+    background-color: #1c6dcc;
+  }
+  
path: src/styles/ConfirmationPage.css
user: login: nicojoy-gif
body: I have done that
created_at: 2025-03-17T11:00:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,65 +1,73 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
 import axios from "axios";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
 import { ToastContainer, toast } from "react-toastify";
 import "react-toastify/dist/ReactToastify.css";
 import "../styles/Home.css";
 
 const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
 
 const Home = () => {
+  const { exam_code } = useParams(); // Get exam code from URL
+  const navigate = useNavigate();
   const [email, setEmail] = useState("");
   const [loading, setLoading] = useState(false);
-  const navigate = useNavigate();
+
+  useEffect(() => {
+    console.log("Exam Code from URL:", exam_code);
+
+    if (exam_code) {
+      if (!/^[a-zA-Z0-9]{6}$/.test(exam_code)) {
+        toast.error("Invalid exam code! It must be exactly 6 letters or digits.", { position: "top-right" });
+      }
+    }
+    
+
+    removeExpiredCandidate();
+  }, [exam_code]); // Runs whenever exam_code changes
 
   const validateAndCheckEmail = async () => {
-    if (!email) {
+    if (!email.trim()) {
       toast.error("Please enter your email!", { position: "top-right" });
       return;
     }
 
-    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+    if (!/^[^\s@]+@[^\s@]+$/.test(email)) {
       toast.error("Invalid email format", { position: "top-right" });
       return;
     }
 
+    if (!exam_code || !/^[a-zA-Z0-9]{6}$/.test(exam_code)) {
+      toast.error("Invalid exam code!", { position: "top-right" });
+      return;
+    }
+
     setLoading(true);
 
     try {
-      const apiUrl = `${API_BASE_URL}/clacbt_check_candidates/check?exam_code=123456&email=${email}`;
+      const apiUrl = `${API_BASE_URL}/clacbt_check_candidates/check?exam_code=${exam_code}&email=${email}`;
       console.log("API URL:", apiUrl);
 
       const response = await axios.get(apiUrl);
       console.log("API Response:", response.data);
 
       if (response.data.message === "Candidate authorized") {
-        const candidateData = response.data.candidate;
-
-        localStorage.removeItem("candidate");
path: src/components/Home.js
user: login: nicojoy-gif
body: That makes sense since candidates are automatically logged out after the exam, so there's no need to remove them manually. Thanks for the clarification! I also removed the expired function since the exam automatically ends and logs them out afterward.
created_at: 2025-03-17T11:00:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-17T07:54:13Z
body: [vc]: #kAQi40Vcvg7Gfm8de09PorA2nbN2J/tXR0Bl4XwK6VI=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJqanJzZi1jYnQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9nZWVrZWxvcy1wcm9qZWN0cy9qanJzZi1jYnQvQVQyd25yNjVCSHVwNVZnZE5DTG9KU2RSRlBtRiIsInByZXZpZXdVcmwiOiIiLCJuZXh0Q29tbWl0U3RhdHVzIjoiRkFJTEVEIiwicm9vdERpcmVjdG9yeSI6bnVsbH1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **jjrsf-cbt** | ❌ Failed ([Inspect](https://vercel.com/geekelos-projects/jjrsf-cbt/AT2wnr65BHup5VgdNCLoJSdRFPmF)) |  |  | Mar 17, 2025 11:23am |


author_association: NONE
user: login: nicojoy-gif
created_at: 2025-03-17T11:03:53Z
body: I have resolved all the changes you requested. Kindly go through and verify.
author_association: CONTRIBUTOR