Repository: trevorproject/rainbow-relax
Pull request #3: MPI-44: Integrate quick escape pop up
Author: login: MiguelCarreraE
Created: 2025-02-20T17:41:41Z | Merged: 2025-03-18T15:32:19Z

## Description

This Pull Request adds a Quick Escape modal feature that allows users to quickly exit the website by pressing the "Escape" key three times.

- If pressed three times within the time window, the user is redirected to https://google.com.
- If 1 seconds pass without pressing "Escape" three times, the counter resets.
- The modal can also be closed manually with the close button.

Jira Link: https://trevorproject.atlassian.net/browse/MPI-44

## Testing Steps (run|navigate|verify)

1. Run: git clone https://github.com/trevorproject/rainbow-relax.git
2. Run: npm run dev
3. Navigate: http://localhost:8080/
4. Verify: 
- Press "Escape" 3 times → Redirects to Google.
- Close the modal using the close button.

## Screenshots
### Web
<img width="775" alt="Captura de pantalla 2025-03-04 a la(s) 11 56 34 a m" src="https://github.com/user-attachments/assets/8d6e1214-a4df-4031-b2f2-3b872bbf11b4" />


### Mobile


![IMG_4790](https://github.com/user-attachments/assets/9208e223-2ac0-44bf-8d66-ddd10707ca80)



## Verify That You Have:

- [ ]  Ensured that pressing "Escape" three times correctly redirects to Google.
- [ ]  Verified that the modal closes when clicking the close button.
- [ ]  Added necessary screenshots.
- [ ]  Good pull request message.

## Before Merging

1. All status checks pass.
2. You have the necessary number of approved reviews.
3. Copy and paste the above Description into the merge Description.
4. Be sure to `Squash and Merge`.


Diff:
diff --git a/package-lock.json b/package-lock.json
index a4c1c16..a66e966 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
         "@tailwindcss/vite": "^4.0.1",
         "i18next": "^24.2.2",
         "i18next-browser-languagedetector": "^8.0.4",
+        "lucide-react": "^0.475.0",
         "react": "^18.3.1",
         "react-dom": "^18.3.1",
         "react-i18next": "^15.4.0",
@@ -3056,6 +3057,15 @@
         "yallist": "^3.0.2"
       }
     },
+    "node_modules/lucide-react": {
+      "version": "0.475.0",
+      "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz",
+      "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==",
+      "license": "ISC",
+      "peerDependencies": {
+        "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
diff --git a/package.json b/package.json
index 3b43b85..fddb023 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
   },
   "dependencies": {
     "@tailwindcss/vite": "^4.0.1",
+    "lucide-react": "^0.475.0",
     "i18next": "^24.2.2",
     "i18next-browser-languagedetector": "^8.0.4",
     "react": "^18.3.1",
diff --git a/src/App.tsx b/src/App.tsx
index e5216ee..9071894 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,16 +1,17 @@
+import QuickEscape from "./components/QuickEscape";
 
 function App() {
   return (
-    <div className="flex flex-col items-center justify-center min-h-screen bg-[var(--background-global)] text-[var(--color-text)]">
-      <h1 className="text-4xl font-bold mb-4" style={{ fontFamily: 'var(--font-global)' }}>
+    <div className="flex flex-col items-center justify-center min-h-screen bg-[var(--background-global)] text-[var(--color-text)]" style={{ fontFamily: 'var(--font-global)' }}>
+      <QuickEscape /> 
+      
+      <h1 className="text-4xl font-bold mb-4">
         Breathing Exercise
       </h1>
       <button className="px-6 py-2 text-[var(--color-button-text)] bg-[var(--color-button)] rounded-md shadow-md hover:opacity-80">
         Botón
       </button>
-
       <div className="mt-10 flex space-x-4">
-        {/* Círculos de niveles */}
         <div className="w-30 h-30 rounded-full" style={{ backgroundColor: 'var(--circle-level-1)' }}></div>
         <div className="w-30 h-30 rounded-full" style={{ backgroundColor: 'var(--circle-level-2)' }}></div>
         <div className="w-30 h-30 rounded-full" style={{ backgroundColor: 'var(--circle-level-3)' }}></div>
@@ -18,13 +19,11 @@ function App() {
       </div>
 
       <div className="mt-10 flex space-x-4">
-        {/* Círculos de posiciones */}
         <div className="w-30 h-30 rounded-full" style={{ backgroundColor: 'var(--circle-top)' }}></div>
         <div className="w-30 h-30 rounded-full" style={{ backgroundColor: 'var(--circle-mid-top)' }}></div>
         <div className="w-30 h-30 rounded-full" style={{ backgroundColor: 'var(--circle-mid-bottom)' }}></div>
         <div className="w-30 h-30 rounded-full" style={{ backgroundColor: 'var(--circle-bottom)' }}></div>
       </div>
-
     </div>
   );
 }
diff --git a/src/components/QuickEscape.tsx b/src/components/QuickEscape.tsx
new file mode 100644
index 0000000..63ace0d
--- /dev/null
+++ b/src/components/QuickEscape.tsx
@@ -0,0 +1,93 @@
+import { useState, useEffect, useRef} from "react";
+import { X } from "lucide-react";
+
+
+export default function QuickEscape() {
+  const [isOpen, setIsOpen] = useState<boolean>(true);
+  const [counter, setCounter] = useState<number>(0);
+  const resetTimeoutRef = useRef<number | null>(null);
+  const isMobile = "ontouchstart" in window;
+
+  const incrementCounter = () => {
+    setCounter((prevCounter) => prevCounter + 1);
+    if (resetTimeoutRef.current !== null) {
+      clearTimeout(resetTimeoutRef.current);
+    }
+    resetTimeoutRef.current = window.setTimeout(() => {
+      setCounter(0);
+    }, 1000);
+  };
+
+  useEffect(() => {
+    if (counter >= 3) {
+      setTimeout(() => {
+        window.location.href = "https://google.com"; 
+      }, 0);
+    }
+  }, [counter]);
+
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (!isMobile && event.key === "Escape") {
+        incrementCounter();
+      }
+    };
+
+    const handleTouch = () => {
+      if (isMobile) {
+        incrementCounter();
+      }
+    };
+
+    if (isMobile) {
+      window.addEventListener("touchstart", handleTouch);
+    } else {
+      window.addEventListener("keydown", handleKeyDown);
+    }
+
+    return () => {
+      if (isMobile) {
+        window.removeEventListener("touchstart", handleTouch);
+      } else {
+        window.removeEventListener("keydown", handleKeyDown);
+      }
+      if (resetTimeoutRef.current !== null) clearTimeout(resetTimeoutRef.current);
+    };
+  }, [isMobile]);
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 flex items-center justify-center">
+      <div
+        className="w-[450px] h-[250px] p-10 rounded-[25px] relative flex flex-col justify-center shadow-lg"
+        style={{
+          backgroundColor: "var(--color-button)",
+          color: "var(--color-button-text)",
+          fontFamily: "var(--font-global)", 
+        }}
+      >
+        <button
+          onClick={() => setIsOpen(false)}
+          className="absolute top-4 right-6 hover:opacity-70 cursor-pointer"
+        >
+          <X className="w-6 h-6" />
+        </button>
+        <h2 className="text-2xl font-bold mb-7">Quick Exit</h2>
+        <p className="text-base mb-7">
+          {isMobile
+            ? "Tap the screen three times to quickly leave our site."
+            : "Press the escape key three times to quickly leave our site."}
+        </p>
+        <span
+          onClick={() => setIsOpen(false)}
+          className="text-base underline cursor-pointer hover:opacity-70"
+        >
+          Got it
+        </span>
+      </div>
+    </div>
+  );
+}
+
diff --git a/src/index.css b/src/index.css
index df7c004..e17a50e 100644
--- a/src/index.css
+++ b/src/index.css
@@ -21,4 +21,6 @@
             --circle-mid-bottom: rgba(99, 133, 90, 0.3);
             --circle-bottom: rgba(99, 133, 90, 0.2);
         }
+        
+        
 }


Reviews:
user: login: alfredopizana
body: Added just minor comments and questions.
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-20T18:09:04Z
user: login: agnimoloc
body: I can not see the expected behavior, this is a video with the test

https://github.com/user-attachments/assets/bfc73903-5a62-4549-910a-ad8e2d69b65a


state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-20T18:12:58Z
user: login: MiguelCarreraE
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T19:42:24Z
user: login: MiguelCarreraE
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T19:42:49Z
user: login: agnimoloc
body: @MiguelCarreraE Now the Escape button is working !! 🎉
I think the pending part is about the tap: the first aceptance criteria says:
```
GIVEN a user is viewing our Breathing Exercise page content on mobile
WHEN they triple-tap anywhere on the screen
THEN they are immediately redirected to [Google](http://www.google.com/)  without confirmation.
```
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-25T17:27:52Z
user: login: agnimoloc
body: I still not seeing the 3 tap exit feature, could you check it @MiguelCarreraE ?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T16:39:41Z
user: login: MiguelCarreraE
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T22:06:29Z
user: login: agnimoloc
body: LGTM, @MiguelCarreraE only check my final comment, thank you!!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-05T22:26:12Z
user: login: MiguelCarreraE
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T23:35:42Z
user: login: agnimoloc
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-07T16:33:54Z
user: login: MirandaJaramillo
body: Overall amazing work @MiguelCarreraE !! 🚀 I added a couple of quick comments and changes!
state: DISMISSED
author_association: NONE
submitted_at: 2025-03-07T16:35:39Z
user: login: alfredopizana
body: LGTM
state: APPROVED
author_association: NONE
submitted_at: 2025-03-18T15:26:49Z

Review comments:
diff_hunk: @@ -0,0 +1,59 @@
+import { useState, useEffect } from "react";
+import { X } from "lucide-react";
+
+export default function QuickEscape() {
+  const [isOpen, setIsOpen] = useState<boolean>(true);
+  const [escCount, setEscCount] = useState<number>(0);
+  const [resetTimeout, setResetTimeout] = useState<number | null>(null);
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === "Escape") {
+        if (escCount === 0) {
+          const timeout = window.setTimeout(() => {
+            setEscCount(0);
+          }, 1000);
+          setResetTimeout(timeout);
+        }
+        setEscCount((prev) => prev + 1);
+      }
+    };
+    window.addEventListener("keydown", handleKeyDown);
+
+    if (escCount === 3) {
+
+      window.location.href = "https://google.com";
+    }
+    return () => {
+      window.removeEventListener("keydown", handleKeyDown);
+      if (resetTimeout !== null) clearTimeout(resetTimeout);
+    };
+  }, [escCount]); 
+  if (!isOpen) return null;
+  return (
+    <div className="fixed inset-0 flex items-center justify-center">
+      <div className="w-[450px] h-[250px] bg-[#FFFFFF] text-black p-10 rounded-[25px] relative border border-black flex flex-col justify-center">
path: src/components/QuickEscape.tsx
user: login: alfredopizana
body: what about having ust bg-white instead of `bg-[#FFFFFF]`
created_at: 2025-02-20T17:58:53Z
author_association: NONE
diff_hunk: @@ -0,0 +1,59 @@
+import { useState, useEffect } from "react";
+import { X } from "lucide-react";
+
+export default function QuickEscape() {
+  const [isOpen, setIsOpen] = useState<boolean>(true);
+  const [escCount, setEscCount] = useState<number>(0);
+  const [resetTimeout, setResetTimeout] = useState<number | null>(null);
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === "Escape") {
+        if (escCount === 0) {
+          const timeout = window.setTimeout(() => {
+            setEscCount(0);
+          }, 1000);
+          setResetTimeout(timeout);
+        }
+        setEscCount((prev) => prev + 1);
+      }
+    };
+    window.addEventListener("keydown", handleKeyDown);
+
+    if (escCount === 3) {
+
+      window.location.href = "https://google.com";
+    }
+    return () => {
+      window.removeEventListener("keydown", handleKeyDown);
+      if (resetTimeout !== null) clearTimeout(resetTimeout);
path: src/components/QuickEscape.tsx
user: login: agnimoloc
body: i love the cleanup after unload component, thank you!!!
created_at: 2025-02-20T18:00:54Z
author_association: COLLABORATOR
diff_hunk: @@ -11,6 +11,7 @@
   },
   "dependencies": {
     "@tailwindcss/vite": "^4.0.1",
+    "lucide-react": "^0.475.0",
path: package.json
user: login: alfredopizana
body: Nice!! thanks for adding the icon library!
created_at: 2025-02-20T18:00:57Z
author_association: NONE
diff_hunk: @@ -1,35 +1,11 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
+import QuickEscape from "./components/QuickEscape";
 
 function App() {
-  const [count, setCount] = useState(0)
-
   return (
-    <>
-      <div>
-        <a href="https://vite.dev" target="_blank">
-          <img src={viteLogo} className="logo" alt="Vite logo" />
-        </a>
-        <a href="https://react.dev" target="_blank">
-          <img src={reactLogo} className="logo react" alt="React logo" />
-        </a>
-      </div>
-      <h1>Vite + React</h1>
-      <div className="card">
-        <button onClick={() => setCount((count) => count + 1)}>
-          count is {count}
-        </button>
-        <p>
-          Edit <code>src/App.tsx</code> and save to test HMR
-        </p>
-      </div>
-      <p className="read-the-docs">
-        Click on the Vite and React logos to learn more
-      </p>
-    </>
-  )
+    <div className="App">
path: src/App.tsx
user: login: alfredopizana
body: Do we need this classname?
created_at: 2025-02-20T18:01:44Z
author_association: NONE
diff_hunk: @@ -1,70 +1,2 @@
 @import "tailwindcss";
 
path: src/index.css
user: login: alfredopizana
body: Thanks for removing the unnecessary CSS 🚀
created_at: 2025-02-20T18:08:45Z
author_association: NONE
diff_hunk: @@ -0,0 +1,59 @@
+import { useState, useEffect } from "react";
+import { X } from "lucide-react";
+
+export default function QuickEscape() {
+  const [isOpen, setIsOpen] = useState<boolean>(true);
+  const [escCount, setEscCount] = useState<number>(0);
+  const [resetTimeout, setResetTimeout] = useState<number | null>(null);
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === "Escape") {
+        if (escCount === 0) {
+          const timeout = window.setTimeout(() => {
+            setEscCount(0);
+          }, 1000);
+          setResetTimeout(timeout);
+        }
+        setEscCount((prev) => prev + 1);
+      }
+    };
+    window.addEventListener("keydown", handleKeyDown);
+
+    if (escCount === 3) {
+
+      window.location.href = "https://google.com";
+    }
+    return () => {
+      window.removeEventListener("keydown", handleKeyDown);
+      if (resetTimeout !== null) clearTimeout(resetTimeout);
+    };
+  }, [escCount]); 
+  if (!isOpen) return null;
+  return (
+    <div className="fixed inset-0 flex items-center justify-center">
+      <div className="w-[450px] h-[250px] bg-[#FFFFFF] text-black p-10 rounded-[25px] relative border border-black flex flex-col justify-center">
path: src/components/QuickEscape.tsx
user: login: MiguelCarreraE
body: I agree
created_at: 2025-02-24T19:42:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,35 +1,11 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
+import QuickEscape from "./components/QuickEscape";
 
 function App() {
-  const [count, setCount] = useState(0)
-
   return (
-    <>
-      <div>
-        <a href="https://vite.dev" target="_blank">
-          <img src={viteLogo} className="logo" alt="Vite logo" />
-        </a>
-        <a href="https://react.dev" target="_blank">
-          <img src={reactLogo} className="logo react" alt="React logo" />
-        </a>
-      </div>
-      <h1>Vite + React</h1>
-      <div className="card">
-        <button onClick={() => setCount((count) => count + 1)}>
-          count is {count}
-        </button>
-        <p>
-          Edit <code>src/App.tsx</code> and save to test HMR
-        </p>
-      </div>
-      <p className="read-the-docs">
-        Click on the Vite and React logos to learn more
-      </p>
-    </>
-  )
+    <div className="App">
path: src/App.tsx
user: login: MiguelCarreraE
body: Not really
created_at: 2025-02-24T19:42:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,30 +1,9 @@
+import QuickEscape from "./components/QuickEscape";
 
 function App() {
   return (
-    <div className="flex flex-col items-center justify-center min-h-screen bg-[var(--background-global)] text-[var(--color-text)]">
path: src/App.tsx
user: login: agnimoloc
body: do is required to remove this? how the theme could be tested if the buttons and background is not shown?
created_at: 2025-03-05T16:39:05Z
author_association: COLLABORATOR
diff_hunk: @@ -1,30 +1,9 @@
+import QuickEscape from "./components/QuickEscape";
 
 function App() {
   return (
-    <div className="flex flex-col items-center justify-center min-h-screen bg-[var(--background-global)] text-[var(--color-text)]">
path: src/App.tsx
user: login: MiguelCarreraE
body: I thought the theme would be tested in another branch
created_at: 2025-03-05T22:06:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,30 +1,9 @@
+import QuickEscape from "./components/QuickEscape";
 
 function App() {
   return (
-    <div className="flex flex-col items-center justify-center min-h-screen bg-[var(--background-global)] text-[var(--color-text)]">
path: src/App.tsx
user: login: agnimoloc
body: all tests are tested in main branch, if this will be override the main branch the testing is not possible to be made.
created_at: 2025-03-05T22:24:37Z
author_association: COLLABORATOR
diff_hunk: @@ -1,30 +1,9 @@
+import QuickEscape from "./components/QuickEscape";
 
 function App() {
   return (
-    <div className="flex flex-col items-center justify-center min-h-screen bg-[var(--background-global)] text-[var(--color-text)]">
path: src/App.tsx
user: login: MiguelCarreraE
body: Okay, it's resolved
created_at: 2025-03-06T23:35:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: alfredopizana
created_at: 2025-03-07T17:39:17Z
body: > Overall amazing work @MiguelCarreraE !! 🚀 I added a couple of quick comments and changes, but mainly this:
> 
> On the acceptance criteria we have that on desktop the user only needs to press the **esc key once** and they'll get redirected, and in your implementation we ask them to do it 3 times, could we change that behavior so they only need to press it once? @MiguelCarreraE

@MirandaJaramillo @agnimoloc I think pressing the Esc button three times would be a better approach to prevent accidentally closing the app with a misclick. What do you all think?


author_association: NONE
user: login: MirandaJaramillo
created_at: 2025-03-07T19:20:48Z
body: > > Overall amazing work @MiguelCarreraE !! 🚀 I added a couple of quick comments and changes, but mainly this:
> > On the acceptance criteria we have that on desktop the user only needs to press the **esc key once** and they'll get redirected, and in your implementation we ask them to do it 3 times, could we change that behavior so they only need to press it once? @MiguelCarreraE
> 
> @MirandaJaramillo @agnimoloc I think pressing the Esc button three times would be a better approach to prevent accidentally closing the app with a misclick. What do you all think?

Yes I agree! Thanks for bringing this up -- we're sticking with pressing the ESC button 3 times. 
author_association: NONE