๐Ÿงช Challenges

Custom Captcha

Challenge 9 - Medium

Description

Create an in house Bot Detection Image Verification Application, a CAPTCHA.

User Flow:

A user loads the bot detection captcha and is instructed to select all dogs. The CAPTCHA shows a collection of images including cats and dogs. The user picks all the dogs, leaving only cat images, and clicks submit. The CAPTCHA validates successfully, notifies the user, and resets to a fresh CAPTCHA state.

Requirements:

  • Display a 3x3 grid filled with random generated images sized 200x200 in pixels.
  • The random generated images will be either of a flower or a kitten.
  • Use a service like LoremFlickr for requesting these images.
  • Randomly decide which type should be the target of the CAPTCHA: Flower or Kitten.
  • The target is how we will validate this captcha and it will direct the user's actions
  • Use this for the following prompt: โ€œSelect all images that contain ...โ€
  • Toggle the images when they are picked.
  • When a user clicks on an image, replace it with the other image.
  • Example: if I click a kitten image, replace it with a flower image. If I click a flower image, replace it with a kitten image.
  • Validate the CAPTCHA upon user submission
  • When a user clicks the submit button, determine if the CAPTCHA is valid.
  • All visible images should be the opposite of the CAPTCHA target.
  • For ex: If the target is kitten, the user should have clicked all the kittens, leaving only flower images.
  • If its valid, display a success message and generate a new grid.
  • If its not valid, display a failure message and generate a new grid.

1. Example

Captcha grid

Select all images that contain flower

The service sometimes returns a picture of a statue of a cat sitting in a human-like way on the curb of a street. This is the default image if the service is not working correctly. That is why the images have text on top just in case.

2. Cell - Implementation

Interface

The cell interface is going to get the cell initial value, the handle click function to change the state and the index of the cell on the grid. The setActive function is going to be used to set the active cell on the grid to highlight it.

0 1 2 3 4 5 6 interface CaptchaCellPropType { cell: CaptchaAnswer; handleCellClick: (index: number) => void; index: number; setActive: (index: number) => void; }

Events

The component uses three event handlers: handleClick for clicks, handleKeyDown for keyboard interaction, and handleOnFocus for focus changes. These are optimized using useCallback to ensure they maintain stable references and donโ€™t cause unnecessary re-renders. By wrapping the entire component in React.memo, it only re-renders when its props (cell, handleCellClick, index, setActive) change, further improving performance, especially when used within large grids.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const CaptchaCell: React.FC<CaptchaCellPropType> = ({cell, handleCellClick, index, setActive}) => { const handleClick = useCallback((): void => { handleCellClick(index); }, [index, handleCellClick]); const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleClick(); } },[handleClick]); const handleOnFocus = useCallback(() => { setActive(index); },[setActive, index]); // Render return (...); }; export default React.memo(CaptchaCell);

Render

The component renders a div with the role gridcell, making it accessible in a grid structure. It includes ARIA labels, a paragraph showing the cell's type (kitten or flower), and an image loaded dynamically from a URL based on the cell type and index. The div supports user interaction with mouse clicks and keyboard input (tabIndex={0}), and styling is applied through CaptchaCell.module.css to enhance its visual appearance.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const CaptchaCell: React.FC<CaptchaCellPropType> = ({cell, handleCellClick, index, setActive}) => { // Events {...} return ( <div aria-label={`captcha image of a ${cell}`} className={styles.cell} id={`captcha-cell-${index}`} onClick={handleClick} onFocus={handleOnFocus} onKeyDown={handleKeyDown} role="gridcell" tabIndex={0} > <p className={styles.name}> {cell} </p> <img className={styles.image} src={`https://loremflickr.com/200/200/${cell}?random=${index}`} alt={`A ${cell}`} /> </div> ); }; export default React.memo(CaptchaCell);

3. Grid - Implementation

Component Definition

The component is going to recieve the correct answer by parameter and not much more.

0 1 2 3 4 5 6 7 8 9 export type CaptchaAnswer = "kitten" | "flower"; interface CaptchaGridPropType { answer: CaptchaAnswer; } const CaptchaGrid: React.FC<CaptchaGridPropType> = ({answer}) => { {...} }

State

In terms of state, the component is going to have two states: cells and active. The first one is going to store the current state of the grid, basically the value of each cell. The second one is going to store the index of the active cell on the grid.

0 1 2 const [cells, setCells] = useState<CaptchaAnswer[]>([]); const [active, setActive] = useState<number>();

Initialization

We are using a useEffect to initialize the grid with random values. The getInitialCells function is going to be called only once when the component is mounted. This function is going to generate a random value for each cell on the grid and set the state with these values.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // Function to get random cats and dogs. const getInitialCells = useCallback( (): void => { let newCells: CaptchaAnswer[] = []; for(let i=0; i<9; i++) { const cellData = Math.floor(Math.random() * 2) === 0 ? "flower" : "kitten"; newCells.push(cellData) } setCells(newCells); },[]); // Populate the state with random answers. useEffect(() => { getInitialCells(); }, [getInitialCells]);

Events

The component has two events: handleSubmit and handleCellClick. The first one is going to be called when the user submits the captcha. It checks if the user selected the correct images and logs the result. The second one is going to be called when the user clicks on a cell. It toggles the value of the cell.

In the second case, we are using useCallback to ensure that the function is not recreated on each render. Additionally, we use the setCells function with its functional update form to work with the most up-to-date state, avoiding stale closures and ensuring that we can memoize.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // Submition of the captcha. const handleSubmit = () => { const different = cells.some(cell => cell === answer); if(different) console.log(`FAIL`); else console.log(`SUCCESS`); getInitialCells(); }; // Update the state of one cell const handleCellClick = useCallback((index: number) => { setCells(prevCells => { const newArray = [...prevCells]; newArray[index] = newArray[index] === "flower" ? "kitten" : "flower"; return newArray; }); }, []);

Render

In the render there is not much to say. We are using the cells state to render the grid and the answer to show the prompt. The grid is going to be a 3x3 grid with the cells being rendered by the CaptchaCell component. The active state is used to highlight the active cell on the grid.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 return ( <section className={styles.captcha}> <h1 id="captcha-grid"> Captcha grid </h1> <p> Select all images that contain {answer} </p> <p className={styles.disclaimer}> The service sometimes returns a picture of a statue of a cat sitting in a human-like way on the curb of a street. This is the default image if the service is not working correctly. That is why the images have text on top just in case. </p> <div role="grid" className={styles.grid} aria-labelledby='captcha-grid' aria-activedescendant={`captcha-cell-${active}`}> { cells.map((cell: CaptchaAnswer, index: number) => { return ( <CaptchaCell cell={cell} index={index} handleCellClick={handleCellClick} key={`captcha-cell-${index}`} setActive={setActive} /> ); }) } </div> <button onClick={handleSubmit} aria-label='captcha submit'> Submit </button> </section> );

4. Complete Code

Grid Component

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 'use client' import React, { useCallback, useEffect, useState } from 'react'; import styles from './CaptchaGrid.module.css'; import CaptchaCell from './CaptchaCell'; export type CaptchaAnswer = "kitten" | "flower"; interface CaptchaGridPropType { answer: CaptchaAnswer; } const CaptchaGrid: React.FC<CaptchaGridPropType> = ({answer}) => { const [cells, setCells] = useState<CaptchaAnswer[]>([]); const [active, setActive] = useState<number>(); // Function to get random cats and dogs. const getInitialCells = useCallback( (): void => { let newCells: CaptchaAnswer[] = []; for(let i=0; i<9; i++) { const cellData = Math.floor(Math.random() * 2) === 0 ? "flower" : "kitten"; newCells.push(cellData) } setCells(newCells); },[]); // Populate the state with random answers. useEffect(() => { getInitialCells(); }, [getInitialCells]); // Submition of the captcha. const handleSubmit = () => { const different = cells.some(cell => cell === answer); if(different) console.log(`FAIL`); else console.log(`SUCCESS`); getInitialCells(); }; // Update the state of one cell const handleCellClick = useCallback((index: number) => { setCells(prevCells => { const newArray = [...prevCells]; newArray[index] = newArray[index] === "flower" ? "kitten" : "flower"; return newArray; }); }, []); return ( <section className={styles.captcha}> <h1 id="captcha-grid"> Captcha grid </h1> <p> Select all images that contain {answer} </p> <p className={styles.disclaimer}> The service sometimes returns a picture of a statue of a cat sitting in a human-like way on the curb of a street. This is the default image if the service is not working correctly. That is why the images have text on top just in case. </p> <div role="grid" className={styles.grid} aria-labelledby='captcha-grid' aria-activedescendant={`captcha-cell-${active}`}> { cells.map((cell: CaptchaAnswer, index: number) => { return ( <CaptchaCell cell={cell} index={index} handleCellClick={handleCellClick} key={`captcha-cell-${index}`} setActive={setActive} /> ); }) } </div> <button onClick={handleSubmit} aria-label='captcha submit'> Submit </button> </section> ); }; export default CaptchaGrid;

Cell Component

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import React, { useCallback } from 'react'; import styles from './CaptchaCell.module.css'; import { CaptchaAnswer } from './CaptchaGrid'; interface CaptchaCellPropType { cell: CaptchaAnswer; handleCellClick: (index: number) => void; index: number; setActive: (index: number) => void; } const CaptchaCell: React.FC<CaptchaCellPropType> = ({cell, handleCellClick, index, setActive}) => { const handleClick = useCallback((): void => { handleCellClick(index); }, [index, handleCellClick]); const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleClick(); } },[handleClick]); const handleOnFocus = useCallback(() => { setActive(index); },[setActive, index]); return ( <div aria-label={`captcha image of a ${cell}`} className={styles.cell} id={`captcha-cell-${index}`} onClick={handleClick} onFocus={handleOnFocus} onKeyDown={handleKeyDown} role="gridcell" tabIndex={0} > <p className={styles.name}> {cell} </p> <img className={styles.image} src={`https://loremflickr.com/200/200/${cell}?random=${index}`} alt={`A ${cell}`} /> </div> ); }; export default React.memo(CaptchaCell);
๐Ÿ›๏ธ ๐Ÿงฎ ๐Ÿ“ โš›๏ธ ๐Ÿงช ๐Ÿ