0
(0)

Let’s see what we got to do here:

The file structure

What can I say, I like to segment the parts into smaller components and then put the pieces together in a main file, in this case the main file is TwoFactorAuthentication.tsx. TwoFactorAuthCountDown is the component on the left side that indicates the remaining time for the active code and the TwoFactorAuthPassowrdCode is the right component that actually display the random code. The TwoFactorAuthCircle only holds the SVG code, because I don’t like to paste it directly into my component, it looks better in the form of a component.

In the main file we return the following JSX:

We process the calculations in the main file and pass down the results to the child components(percent, remainingSeconds and code).

The code

TwoFactorAuthentication.tsx (main component)

import { useState, useEffect } from 'react';
import styled from 'styled-components';
import TwoFactorAuthCountDown from './TwoFactorAuthCountDown';
import TwoFactorAuthPasswordCode from './TwoFactorAuthPasswordCode';

const TwoFactorAuthContainer = styled.div`
    display: flex;
    align-items: center;
    column-gap: 30px;
`

const TwoFactorAuthentication = () => {
    const [code, setCode] = useState('');
    const [percent, setPercent] = useState(0);
    const [remainingSeconds, setRemainingSeconds] = useState<number | undefined>();
    const generatePasswordCode = () => {
        const min = 100000;
        const max = 999999;
        const randomCode = Math.floor(Math.random() * (max - min + 1)) + min;
        const formattedRandomCode = randomCode.toString().replace(/(\d{3})(\d{3})/, '$1 $2');
        return formattedRandomCode;
    }
    const halfMinuteCountDown = () => {
        const seconds = Math.floor((new Date().getTime() % 30000) / 1000);
        const halfMinuteRemainingSeconds = 30 - seconds;
        return halfMinuteRemainingSeconds;
    }    
   
    
    useEffect(() => {
        setRemainingSeconds(halfMinuteCountDown);
        setCode(generatePasswordCode);
        setPercent(halfMinuteCountDown);
        setInterval(
            () => {
                if(halfMinuteCountDown() === 30) setCode(generatePasswordCode);
                setRemainingSeconds(halfMinuteCountDown);
                setPercent(halfMinuteCountDown);
            }
        , 1000);
    }, []);

    return (
        <TwoFactorAuthContainer>
            <TwoFactorAuthCountDown percent={percent} remainingSeconds={remainingSeconds} />
            <TwoFactorAuthPasswordCode code={code} />
        </TwoFactorAuthContainer>
    )
}
export default TwoFactorAuthentication;

TwoFactorAuthPasswordCode.tsx

import styled from 'styled-components';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

type PasswordCodeProps = {
    code : string
}

const Title = styled.h2`
    font-size: 16px;
    margin: 0
`
const PasswordCode = styled.div`
    color: #c6f;
    font-size: 30px;
    cursor: pointer;
`

const TwoFactorAuthPasswordCode = ({code}: PasswordCodeProps) => {
    
    const copyToClipBoard = (ev: React.MouseEvent<HTMLElement>) => {
        const target = ev.target as HTMLElement;
        navigator.clipboard.writeText(target.innerText);
        toast("Password code copied to clipboard");
    }

    return (
        <div>
            <Title>One-time password code</Title>
            <PasswordCode onClick={copyToClipBoard}>{code}</PasswordCode>
            <ToastContainer />
        </div>
    )
}

export default TwoFactorAuthPasswordCode;

TwoFactorAuthCountDown.tsx

import styled from 'styled-components';
import SVG from './TwoFactorAuthCircle';

type CountdownProps = {
    percent: number,
    remainingSeconds: number | undefined
}

const CountdownDisplay = styled.div`
    display: flex;
    justify-content: center;
    align-items: center;
    width: 60px;  
    height: 60px;  
    position: relative;
`
const SvgHolder = styled.div<{percent: number}>`
    svg {
        transform: rotate(-90deg);
        display: block;
    }
    .percent {
        transition: stroke-dashoffset 0.3s;
        stroke-dasharray: 30;
        stroke-dashoffset: ${props => props.percent};
    } 
`
const RemainingTime = styled.span`
    position: absolute;
    top: 50%;
    line-height: 1;
    transform: translateY(-50%);
`

const TwoFactorAuthCountDown = ({percent, remainingSeconds}: CountdownProps) => {
    return (
        <CountdownDisplay>
            <SvgHolder percent={percent}>
                <SVG />
            </SvgHolder>
            <RemainingTime>{remainingSeconds}</RemainingTime>
        </CountdownDisplay>
    );
}

export default TwoFactorAuthCountDown;

By code splitting into 3 components plus one for the SVG, the code is more readable and each child component gets only a task that it is supposed to do, nothing more, nothing less. For example the TwoFactorAuthPasswordCode gets the task to display a text and if that text is clicked then memorize the task in the clipboard and notify the user that the text is memorized. Generating the code is done by the parent element. Also that code in real-life project could come from the backend, so it’s better for the child component to only receive the already generated code and do something with it.

The TwoFactorAuthCountDown component receives the percent of the circle indicator and the remaining time as arguments and then this child component takes these 2 information and display it as a circle with a stroke and a number inside.

Let’s generate a random 6 digit code separated by space in the middle

We need to generate a number between 100 000 and 999 999. By a formula we generate that code in the randomCode constant. After that we convert the number into a string and after the 3rd digit we add a space with regular expression and the replace() method.

Connect a countdown from 30 seconds but based on the local time(server clock).

  1. First we get the time in milliseconds: new Date().getTime() % 30000
  2. Then we Math.floor() it to remove decimals and divide it with 1000 to get the number in seconds.
  3. Last step is just subtract the current time seconds from 30 seconds, that way we get the remaining seconds from every 30 seconds in real time.

Setting up the 3 states that will be sent down to child components as arguments

The code will hold the generated password code as a string, percentage of the circular indicator will start from 0 and the remainingSeconds will be number but initially it is not defined because I want to avoid a glitch of displaying a 0 and then changing it to the current remaining seconds. I rather display the remaining seconds directly.

The setup at the component load

What I did here is set a value to the states on component load, the value that was calculated in the above mentioned functions( halfMinuteCountDown() and generatePasswordCode() ). Also on every second I want to update the states and therefore to rerender the layout(the child components too).

The password code’s compoments

After defining that the props that the compoment gets it is a string, we render a title, a password code element that has a click event handler that is linking to the copyToClipBoard function. That function copies the clicked element’s text and puts into clipboard. After that it calls the toastify plugin’s toast() function to notify the user that the clipboard has a new content.

Let’s have a look how this looks like:

The countdown’s component

After defining the components prop types as 2 numbers but the second can be also undefined since the state starts with no value, we render the RemainingTime styled component and also add the SVG before it. This child component is rerendered in each second in order to update the remaining seconds and also the visual remaining time indicator in the SVG.

In the styled-components definitions we have the changing variable that affects the SVG to render a slightly different stroke on the circle:

The logic is simple. We render 2 circles on top of each other. The first one has a gray stroke that is always 100% length and the other one on top of it has this changing length stroke. When the stroke-dasharray is 30 and the stroke-dashoffset is 0 then all the circle is covered by the stroke but when dasharray is 30 and dashoffset is 30 then you don’t see any stroke since the distance between multiple dashes is so big that that space covers all the circle’s stroke area. It’s very important to have the dasharray as big as the circle’s entire pathlength attribute or if not specified then the circle’s entire path length(laying out the circle into a straight line) otherwise one stroke won’t cover the entire circle and that way we have a different effect.

You can play with this on some of these great websites:

Shreshth Mohan’s article

Tympanus

We applied a -90 degrees rotation because the stroke does not start on the top, more like on 3 a clock. So we rotated the SVG a quarter counter-clockwise.

Thank you for your attention and as usual like, comment and subscribe if you like the article.

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.