Creating personalized underlines with gradient functionality in React using an API

I am eager to replicate the Underline Effect demonstrated in this Codepen using React and Typescript

The Codepen: https://codepen.io/krakruhahah/pen/jOzwXww

It seems like my problem lies within the interface declaration below. I've defined my types, but they are still being recognized as 'any'. Despite declaring 'max' as a number, it is still showing up as 'any'. The functions have been described in the comments.

tsx:

import React from 'react';
import Typography from '@mui/material/Typography';
import { Box } from '@mui/material';

interface Props {
   max: number;
}

const styles = {
   body: {
       width: "80%",
       margin: "10vw auto",
     },

     heading: {
       fontFamily: "Playfair Display, serif",
       fontSize: "10vw",
     },
     
     "subheading": {
       fontFamily: "Open Sans, sans-serif",
       fontSize: "1em",
       lineHeight: "1.5",
     },
     
     "@media screen and (min-width: 40em)": {
       body: {
         width: "50%",
       },
       heading:{
         fontSize: "6vw",
       },
     
       subheading: {
         fontSize: "1.75vw",
       }
     },
     
     "underline--magical": {
       backgroundImage: "linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)",
       backgroundRepeat: "no-repeat",
       backgroundSize: "100% 0.2em",
       backgroundPosition: "0 88%",
       transition: "backgroundSize 0.25s ease-in",
       "&:hover": {
         backgroundSize: "100% 88%",
       },
     },
};

function Effect(props: Props) {

   // VARIABLES
const magicalUnderlines = Array.from(document.querySelectorAll('.underline--magical'));

const gradientAPI = 'https://gist.githubusercontent.com/wking-io/3e116c0e5675c8bcad8b5a6dc6ca5344/raw/4e783ce3ad0bcd98811c6531e40256b8feeb8fc8/gradient.json';

// HELPER FUNCTIONS

// 1. Get random number in range. Used to get random index from array.
const randNumInRange = max => Math.floor(Math.random() * (max - 1));

// 2. Merge two separate array values at the same index to 
// be the same value in new array.
const mergeArrays = (arrOne, arrTwo) => arrOne
 .map((item, i) => `${item} ${arrTwo[i]}`)
 .join(', ');

// 3. Curried function to add a background to array of elms
const addBackground = (elms) => (color) => {
 elms.forEach(el => {
   el.style.backgroundImage = color;
 });
}
// 4. Function to get data from API
const getData = async(url): Promise<XMLHttpRequest> => {
 const response = await fetch(url);
 const data = await response.json();
 return data.data;
}

// 5. Partial Application of addBackground to always apply 
// background to the magicalUnderlines constant
const addBackgroundToUnderlines = addBackground(magicalUnderlines);

// GRADIENT FUNCTIONS

// 1. Build CSS formatted linear-gradient from API data
const buildGradient = (obj) => `linear-gradient(${obj.direction}, ${mergeArrays(obj.colors, obj.positions)})`;

// 2. Get single gradient from data pulled in array and
// apply single gradient to a callback function
const applyGradient = async(url, callback): Promise<XMLHttpRequest> => {
 const data = await getData(url);
 const gradient = buildGradient(data[randNumInRange(data.length)]);
 callback(gradient);
}

// RESULT
applyGradient(gradientAPI, addBackgroundToUnderlines);
   return (
       <Box>
           <Typography sx={styles.heading}>
               Look At This <span style={styles['underline--magical']}>Pretty</span> Underline
           </Typography>
           <Typography sx={styles.subheading}>
               Wow this one is super incredibly cool, and this{' '}
               <span style={styles['underline--magical']}>one is on Multiple Lines!</span> I wish I had found this like thirty
               projects ago when I was representing the lollipop guild.
           </Typography>
       </Box>
   );
}
export { Effect };

Answer №1

  1. Creating a random gradient involves using two arrays, colors and position, but for the linear-gradient property, we need a string of tuples. To simplify this process, I developed a helper function called generateGradientRangeArray.

  2. When working with React, we can implement a custom hook (useGradient) to fetch external data and manipulate it within our component.

  3. By utilizing components from @emotion and Material UI, we can enhance the styling of Box and Typography.

  4. Lastly, we define the Underline component, validate the gradient property, and apply the necessary styles through props.

App.tsx

import styled from "@emotion/styled";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";

import { Underline } from "./Underline";
import useGradient from "./useGradient";

const Heading = styled(Typography)`
  font-family: "Playfair Display", serif;
  font-size: 10vw;
  @media screen and (min-width: 40em) {
    font-size: 6vw;
  }
`;
const Subheading = styled(Typography)`
  font-family: "Open Sans", sans-serif;
  font-size: 1em;
  @media screen and (min-width: 40em) {
    font-size: 1.75vw;
  }
`;

export default function App() {
  const gradient = useGradient();
  return (
    <div className="App">
      <Box>
        <Heading>
          Look At This <Underline gradient={gradient}>Pretty</Underline>{" "}
          Underline
        </Heading>
        <Subheading>
          Wow this one is super incredibly cool, and this{" "}
          <Underline gradient={gradient}>one is on Multiple Lines!</Underline> I
          wish I had found this like thirty projects ago when I was representing
          the lollipop guild.
        </Subheading>
      </Box>
    </div>
  );
}

useGradient.ts

import { useEffect, useState } from "react";
import { generateGradientRangeArray, randNumInRange } from "./Helpers";
import { GradientType, IResult } from "./types";

const GRADIENT_API =
  "https://gist.githubusercontent.com/wking-io/3e116c0e5675c8bcad8b5a6dc6ca5344/raw/4e783ce3ad0bcd98811c6531e40256b8feeb8fc8/gradient.json";

const useGradient = () => {
  const [gradients, setGradients] = useState<GradientType>();

  useEffect(() => {
    const getData = async (url: string): Promise<void> => {
      // Fetching data from the API
      const response = await fetch(url);
      const result = (await response.json()) as IResult;
      // Selecting a single gradient randomly from the array of data received
      const gradient = result.data[randNumInRange(result.data.length)];
      const transform = generateGradientRangeArray(gradient);
      // Updating state with the transformed result
      setGradients(transform);
    };
    // Error handling
    getData(GRADIENT_API).catch(console.error);
  }, []);

  return gradients;
};

export default useGradient;

Underline.tsx

import styled from "@emotion/styled";
import { Props, GradientType } from "./types";

const UnderlineStyle = styled.span<{ gradient: GradientType }>`
  background-image: linear-gradient(
    ${(props) => props.gradient.direction},
    ${(props) => props.gradient.range}
  );
  background-repeat: no-repeat;
  background-size: 100% 0.2em;
  background-position: 0 88%;
  transition: background-size 0.25s ease-in;
  &:hover {
    background-size: 100% 88%;
  }
`;

export const Underline = ({ children, gradient }: Props) => {
  if (!gradient) return null;
  return <UnderlineStyle gradient={gradient}>{children}</UnderlineStyle>;
};

Helpers.ts

import { IGradient, GradientType } from "./types";
/// HELPER FUNCTIONS

// Generating a random number within a specified range to select a random index from an array.
export const randNumInRange = (max: number) =>
  Math.floor(Math.random() * (max - 1));

// Creating a range of colors for the gradient
export const generateGradientRangeArray = (data: IGradient): GradientType => {
  const { colors, direction, name, positions } = data;
  const rangeColorsArray: string[] = [];

  for (const clr in colors) {
    for (const pos in positions) {
      if (clr === pos) {
        rangeColorsArray.push(`${colors[clr]} ${positions[pos]},`);
      }
    }
  }
  const createGradientString = rangeColorsArray.join(" ").slice(0, -1);

  return { name, direction, range: createGradientString };
};

types.ts

import { ReactNode } from "react";

export interface IGradient {
  name: string;
  direction: string;
  colors: string[];
  positions: string[];
}

export type GradientType = { range: string } & Pick<
  IGradient,
  "name" | "direction"
>;

export interface IResult {
  data: IGradient[];
}

export interface Props {
  children: ReactNode;
  gradient?: GradientType;
}

https://codesandbox.io/s/customized-underline-with-gradient-api-in-react-pmr1sf?file=/src/App.tsx

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

A guide on verifying the response status and showing the appropriate message in React JS

The following code snippet is used to call the API. The API is functioning correctly as I am able to receive the response data. However, I need to verify if the status code is either 200 or 204 in order to display the content accordingly. // Service File ...

What could be the reason for the email not being displayed in the form?

I am attempting to automatically populate the user's email in a form when a button is clicked, preferably when the page is loaded. However, I am encountering issues with this process. This project is being developed using Google Apps Script. Code.gs ...

Caution - Unable to execute a function on a type that does not have a callable signature

Below is the code I am working with: export interface IStartCreate1 { (desc?: string, opts?: IDescribeOpts, arr?: Array<string | IDescribeOpts | TCreateHook>, fn?: TCreateHook): void; tooLate?: boolean; } export interface IStartCreate2 { (opt ...

Is it necessary to create an index.js file for exporting { default }?

Recently, I've been honing my skills with Material UI by taking a look at various example projects provided on the MUI docs page. Some of these project examples, such as React Most Wanted or React, Material UI, Firebase, involve creating a designated ...

Invoking a modal popup within an if-else statement

I have a script that currently triggers a modal window with a faded background on button click. Now, I am trying to implement this functionality within a conditional statement. Here's the latter part of my script: if (result != $version) { alert(&apo ...

I encountered a roadblock with my Npm start process when it got stuck at 70% completion after incorporating the "lazy

I have encountered a problem that has previously been discussed here, but none of the solutions seem to work for me. I recently incorporated this module into an existing project: import { NgModule } from '@angular/core'; import { CommonModule } ...

Scope challenges with making multiple rest calls in Angular.js

As a beginner in Angular.js, I am facing an issue with $scope not receiving additional value from one of two $resource rest calls. Below is the code snippet: controller: function ($scope, $modalInstance, $route) { $scope.server = {} ...

Is there a way to extract the HTML source code of a website using jQuery or JavaScript similar to PHP's file_get_contents function?

Can this be achieved without a server? $.get("http://xxxxx.com", function (data) { alert(data); }); I have tried the above code but it seems to not display any output. ...

Is verifying email and password with jquery possible?

I am currently working on a jQuery form validation project: While the password and username validation are working fine, I am facing issues with email and password confirmation validations. Surprisingly, I have used the same technique for both. If you wa ...

An error occurs when trying to access the 'map' property of an undefined variable

Is there a way for me to retrieve the return value for this situation? I attempted to use forEach, but each time I try to loop through the images variable, I encounter the error ("Cannot read property 'map/forEach' of undefined") // Conso ...

Can you help me create a CSS Toggle similar to this design?

I am currently working on a website and I need to create a FAQs page with a specific layout. When a user clicks on a question (Q), the answer (A) should open smoothly using jQuery animation. Only one question should be visible at a time. If a user clicks ...

I am attempting to create a footer with a set size, but encountering an issue

I'm in the process of developing a responsive website using Vue.js. One aspect I am working on is the footer container, which looks fine on the full screen but shrinks when the screen resolution is reduced below 1100 pixels. As shown in the images li ...

Every time I navigate to a new page in NextJs, the useEffect hook

I am working on developing a new blog app with Next.js. In the current layout of the blog, I have successfully fetched data for my sidebar (to display "recent posts") using the useEffect/fetch method, as getInitialProps only works on Pages. However, this ...

Obtaining the contents of a request's body

const app = express() app.use(bodyParser()) router.get('/edaman', (req, res) => { console.log(req.body) axios.get(edamanUrl) .then(function (response) { const recipes = response.data.hits return res.status(200).jso ...

I am facing unresolved peer dependency problems that seem insurmountable

Currently, I am in the process of revamping an application that is built using expo sdk version 45. Since this version has been deprecated, I decided to upgrade the sdk. Upon running npx expo-doctor, I received the following output: ✔ Validating global p ...

The function will not be triggered when the form is submitted

I've set up this form to send data to the server-side. It's built in HTML/CSS with AngularJS as well. I made sure that the button is placed inside the form, a common mistake that can cause issues. However, despite my efforts, the function "onAddE ...

Navigation bar featuring various distinct webpages

Apologies for the novice question, but as a beginner, I'm seeking help. I've created a navigation bar with text such as About and Projects, but no links attached (i.e., no pages for users to navigate to). How can I add these missing pages so that ...

Preserve progress even after reloading the page

I am facing an issue with my React login/logout authentication implemented using Node and a database. After successfully logging in, when I refresh the page, the cookie retains the login state but the navbar menu still shows me as logged in instead of show ...

Having trouble retrieving data from an HTML table populated with JSON using Jquery

Currently, I am utilizing MVC 5 to develop an HTML5 database integrated with Bootstrap 3. After loading a table with JSON data, the information is visible. However, upon clicking a link within a <td>, I am unable to retrieve the spouseID (5056). It s ...

Disable the scroll bar on a bootstrap modal

<span class="education"style="font-size:170%;line-height:150%;">&nbsp;Education <br> <small style=" color:gray;font-size:60%;">&nbsp; Blue Ridge University,2012-2014 </small> <br> <sma ...