mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(curriculum): add otp generator (#59121)
Co-authored-by: Naomi <accounts+github@nhcarrigan.com> Co-authored-by: Tom <20648924+moT01@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -3872,7 +3872,13 @@
|
||||
"In this workshop, you will continue to learn about the <code>useEffect()</code> hook by building an application that fetches fruit data from an API based on user input and displays the results dynamically."
|
||||
]
|
||||
},
|
||||
"vjgg": { "title": "272", "intro": [] },
|
||||
"lab-one-time-password-generator": {
|
||||
"title": "Build a One-Time Password Generator",
|
||||
"intro": [
|
||||
"In this lab you'll build a one-time password generator.",
|
||||
"You'll practice using the useEffect hooks to create a timer and generate a random OTP."
|
||||
]
|
||||
},
|
||||
"review-react-state-and-hooks": {
|
||||
"title": "React State and Hooks Review",
|
||||
"intro": [
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Introduction to the Build a One-Time Password Generator
|
||||
block: lab-one-time-password-generator
|
||||
superBlock: full-stack-developer
|
||||
---
|
||||
|
||||
## Introduction to the Build a One-Time Password Generator
|
||||
|
||||
This is a test for the new project-based curriculum.
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Build a One-Time Password Generator",
|
||||
"isUpcomingChange": false,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "lab-one-time-password-generator",
|
||||
"superBlock": "full-stack-developer",
|
||||
"challengeOrder": [{ "id": "67c562286b29447da020d407", "title": "Build a One-Time Password Generator" }],
|
||||
"blockType": "lab",
|
||||
"blockLayout": "link",
|
||||
"helpCategory": "JavaScript"
|
||||
}
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
---
|
||||
id: 67c562286b29447da020d407
|
||||
title: Build a One-Time Password Generator
|
||||
challengeType: 25
|
||||
dashedName: build-a-one-time-password-generator
|
||||
demoType: onClick
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
In this lab, you will generate a 6-digit OTP (One-Time Password) and display it to the user. The OTP will expire after 5 seconds, and a new OTP will be generated when the user clicks the `"Generate New OTP"` button.
|
||||
|
||||
**Objective:** Fulfill the user stories below and get all the tests to pass to complete the lab.
|
||||
|
||||
**User Stories:**
|
||||
|
||||
1. You should use the `useEffect` hook to manage the countdown timer.
|
||||
2. Your `OTPGenerator` component should return a `div` element with the class name `container`.
|
||||
3. The `div` having the class `container` should include the following elements:
|
||||
|
||||
- An `h1` element with the ID `otp-title` and text `"OTP Generator"`.
|
||||
- An `h2` element with the ID `otp-display` that either displays the message `"Click 'Generate OTP' to get a code"` or shows the generated OTP if one is available.
|
||||
- A `p` element with the ID `otp-timer` that:
|
||||
- Starts off empty.
|
||||
- Displays `"Expires in: X seconds"` after the button is clicked, where `X` represents the remaining time before the OTP expires.
|
||||
- Shows the message `"OTP expired. Click the button to generate a new OTP."` once the countdown reaches `0`.
|
||||
- A `button` element with the ID `generate-otp-button` labeled `"Generate OTP"`. When clicked, it should generate a new OTP and start a 5-second countdown.
|
||||
- The `"Generate OTP"` button must be disabled while the countdown is active.
|
||||
|
||||
4. You should ensure the countdown timer stops automatically once it reaches `0` seconds to prevent unnecessary updates.
|
||||
5. The generated OTP should be 6 digits long.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
let clock = __FakeTimers.install();
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should not remove the existing `const { useState, useEffect, useRef } = React;` assignment from the code.
|
||||
|
||||
```js
|
||||
assert.match(code, /{\s*(useState\s*,\s*useEffect\s*,\s*useRef|useState\s*,\s*useRef\s*,\s*useEffect|useEffect\s*,\s*useState\s*,\s*useRef|useEffect\s*,\s*useRef\s*,\s*useState|useRef\s*,\s*useState\s*,\s*useEffect|useRef\s*,\s*useEffect\s*,\s*useState|useState|useEffect|useRef)\s*}\s*=\s*React;/);
|
||||
```
|
||||
|
||||
You should render a `div` with the class `.container`.
|
||||
|
||||
```js
|
||||
const containerDiv = document.querySelector('.container');
|
||||
assert.exists(containerDiv);
|
||||
```
|
||||
|
||||
The `.container` should contain an `h1` element with the ID `otp-title` the text `"OTP Generator"`.
|
||||
|
||||
```js
|
||||
const h1 = document.querySelector(".container h1#otp-title");
|
||||
assert.exists(h1);
|
||||
assert.equal(h1.textContent, "OTP Generator");
|
||||
```
|
||||
|
||||
The `.container` should contain an `h2` element with the ID `otp-display` that displays the OTP when generated.
|
||||
|
||||
```js
|
||||
const h2 = document.querySelector(".container h2#otp-display");
|
||||
assert.exists(h2);
|
||||
```
|
||||
|
||||
Initially, the `h2` inside `.container` should display the message `"Click 'Generate OTP' to get a code"`.
|
||||
|
||||
```js
|
||||
assert.strictEqual(document.querySelector('.container h2#otp-display').textContent.trim(), "Click 'Generate OTP' to get a code");
|
||||
```
|
||||
|
||||
Initially, there should be a `.container` element that does not contain any `<p>` elements.
|
||||
|
||||
```js
|
||||
const container = document.querySelector('.container');
|
||||
assert.exists(container, "The container element should exist");
|
||||
const pElements = document.querySelectorAll('.container p');
|
||||
assert.strictEqual(pElements.length, 0, "Initially, there should be no <p> elements inside '.container'");
|
||||
```
|
||||
|
||||
The `.container` should contain a `button` element with the ID `generate-otp-button` and text `"Generate OTP"`.
|
||||
|
||||
```js
|
||||
const button = document.querySelector('.container button#generate-otp-button').textContent;
|
||||
assert.exists(button);
|
||||
assert.strictEqual(button, 'Generate OTP');
|
||||
```
|
||||
|
||||
When the button is clicked, it should display a new OTP in the `h2` element with id `otp-display`.
|
||||
|
||||
```js
|
||||
async() => {
|
||||
try {
|
||||
// allow the page to render
|
||||
clock.tick(1)
|
||||
const button = document.querySelector(".container button#generate-otp-button");
|
||||
const h2 = document.querySelector(".container h2#otp-display");
|
||||
|
||||
button.click();
|
||||
|
||||
clock.tick(300)
|
||||
|
||||
assert.match(h2.textContent?.trim(), /[0-9]/, "OTP should be a number");
|
||||
} finally {
|
||||
// we need to wait for the countdown to finish for the next test to work otherwise the button will be disabled
|
||||
await clock.runAllAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The generated OTP should be 6 digits long.
|
||||
|
||||
```js
|
||||
async() => {
|
||||
try {
|
||||
const button = document.querySelector(".container button#generate-otp-button");
|
||||
button.click();
|
||||
clock.tick(1);
|
||||
const h2 = document.querySelector(".container h2#otp-display");
|
||||
|
||||
assert.match(h2.textContent.trim(), /^[0-9]{6}$/, "OTP should be a 6-digit number");
|
||||
|
||||
} finally {
|
||||
// we need to wait for the countdown to finish for the next test to work otherwise the button will be disabled
|
||||
await clock.runAllAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
When the button is clicked, the `p` element with id of `otp-timer` should show a 5-second countdown.
|
||||
|
||||
```js
|
||||
async() => {
|
||||
try {
|
||||
// allow the page to render
|
||||
clock.tick(1)
|
||||
const button = document.querySelector(".container button#generate-otp-button");
|
||||
const h2 = document.querySelector(".container h2#otp-display");
|
||||
|
||||
button.click();
|
||||
|
||||
clock.tick(300)
|
||||
const p = document.querySelector(".container p#otp-timer");
|
||||
assert.match(p.textContent, /Expires in: 5 seconds/, "Countdown should start at 5 seconds");
|
||||
|
||||
} finally {
|
||||
// we need to wait for the countdown to finish for the next test to work otherwise the button will be disabled
|
||||
await clock.runAllAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The message in the `p` element with id `otp-timer` should update every second to show the remaining time.
|
||||
|
||||
```js
|
||||
async() => {
|
||||
const button = document.querySelector(".container button#generate-otp-button");
|
||||
button.click();
|
||||
clock.tick(1)
|
||||
let p = document.querySelector(".container p#otp-timer");
|
||||
assert.match(p.textContent, /Expires in: 5 seconds/);
|
||||
await clock.tickAsync(1000)
|
||||
p = document.querySelector(".container p#otp-timer");
|
||||
assert.match(p.textContent, /Expires in: 4 seconds/, "Countdown should update to 4 seconds after 1 second");
|
||||
}
|
||||
```
|
||||
|
||||
The `"Generate OTP"` button should be disabled while the countdown timer is running.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
const button = document.querySelector(".container button#generate-otp-button");
|
||||
button.click();
|
||||
clock.tick(1);
|
||||
|
||||
assert.isTrue(button.disabled);
|
||||
} finally {
|
||||
await clock.runAllAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `"Generate OTP"` button should be enabled once the countdown timer reaches 0.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
|
||||
const button = document.querySelector(".container button#generate-otp-button");
|
||||
button.click();
|
||||
await clock.tickAsync(6000);
|
||||
|
||||
assert.isFalse(button.disabled);
|
||||
}
|
||||
```
|
||||
|
||||
When the countdown timer reaches `0`, you should display the message `OTP expired. Click the button to generate a new OTP.`.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
const button = document.querySelector(".container button#generate-otp-button");
|
||||
button.click();
|
||||
|
||||
await clock.tickAsync(6000);
|
||||
|
||||
const p = document.querySelector(".container p#otp-timer");
|
||||
assert.equal(p.textContent.trim(), "OTP expired. Click the button to generate a new OTP.");
|
||||
}
|
||||
```
|
||||
|
||||
You should export the `OTPGenerator` component.
|
||||
|
||||
```js
|
||||
assert.isFunction(window.index.OTPGenerator);
|
||||
```
|
||||
|
||||
Uninstall the clock, ignore this test
|
||||
|
||||
```js
|
||||
async () => {
|
||||
clock.uninstall()
|
||||
}
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Color Picker</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
|
||||
<script
|
||||
data-plugins="transform-modules-umd"
|
||||
type="text/babel"
|
||||
src="index.jsx"
|
||||
></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
data-plugins="transform-modules-umd"
|
||||
type="text/babel"
|
||||
data-presets="react"
|
||||
data-type="module"
|
||||
>
|
||||
import { OTPGenerator } from './index.jsx';
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<OTPGenerator />);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
```
|
||||
|
||||
```css
|
||||
|
||||
```
|
||||
|
||||
```jsx
|
||||
const { useState, useEffect, useRef } = React;
|
||||
|
||||
export const OTPGenerator = () => {};
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>OTP Generator</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
|
||||
<script
|
||||
data-plugins="transform-modules-umd"
|
||||
type="text/babel"
|
||||
src="index.jsx"
|
||||
></script>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
data-plugins="transform-modules-umd"
|
||||
type="text/babel"
|
||||
data-presets="react"
|
||||
data-type="module"
|
||||
>
|
||||
import { OTPGenerator } from './index.jsx';
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<OTPGenerator />);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
```
|
||||
|
||||
```css
|
||||
body {
|
||||
font-family: 'Castoro Titling', cursive;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 25px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin-top: 20px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
background-color: #383f59;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color:#57585c;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
```
|
||||
|
||||
```jsx
|
||||
const { useState, useEffect, useRef } = React;
|
||||
|
||||
export const OTPGenerator = () => {
|
||||
const intervalId = useRef(null);
|
||||
const [otp, setOtp] = useState(null);
|
||||
const [secondsLeft, setSecondsLeft] = useState(5);
|
||||
const [isCounting, setIsCounting] = useState(false);
|
||||
const [hasGeneratedOtp, setHasGeneratedOtp] = useState(false);
|
||||
|
||||
const generateOTP = () => Math.floor(100000 + Math.random() * 900000);
|
||||
|
||||
const handleGenerateOtp = () => {
|
||||
setOtp(generateOTP());
|
||||
setSecondsLeft(5);
|
||||
setIsCounting(true);
|
||||
setHasGeneratedOtp(true);
|
||||
intervalId.current = setInterval(() => {
|
||||
setSecondsLeft(prevSeconds => prevSeconds - 1);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (secondsLeft === 0) {
|
||||
setIsCounting(false);
|
||||
clearInterval(intervalId.current);
|
||||
}
|
||||
}, [secondsLeft]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
clearInterval(intervalId.current);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='container'>
|
||||
<h1 id='otp-title'>OTP Generator</h1>
|
||||
<h2 id='otp-display'>
|
||||
{otp ? otp : "Click 'Generate OTP' to get a code"}
|
||||
</h2>
|
||||
{isCounting ? (
|
||||
<p id='otp-timer'>Expires in: {secondsLeft} seconds</p>
|
||||
) : (
|
||||
hasGeneratedOtp && (
|
||||
<p id='otp-timer'>
|
||||
OTP expired. Click the button to generate a new OTP.
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
<button
|
||||
id='generate-otp-button'
|
||||
onClick={handleGenerateOtp}
|
||||
disabled={isCounting}
|
||||
>
|
||||
Generate OTP
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -1098,6 +1098,9 @@
|
||||
"dashedName": "lecture-understanding-effects-and-referencing-values-in-react"
|
||||
},
|
||||
{ "dashedName": "workshop-fruit-search-app" },
|
||||
{
|
||||
"dashedName": "lab-one-time-password-generator"
|
||||
},
|
||||
{
|
||||
"dashedName": "review-react-state-and-hooks"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user