<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joyce Prodipta Banerjee</title>
    <description>The latest articles on DEV Community by Joyce Prodipta Banerjee (@banerjeeprodipta).</description>
    <link>https://dev.to/banerjeeprodipta</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F653255%2F452f706b-d861-4f6f-9f3f-e3fca8ac7c50.jpg</url>
      <title>DEV Community: Joyce Prodipta Banerjee</title>
      <link>https://dev.to/banerjeeprodipta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/banerjeeprodipta"/>
    <language>en</language>
    <item>
      <title>Frontend Challenge v24.07.24</title>
      <dc:creator>Joyce Prodipta Banerjee</dc:creator>
      <pubDate>Tue, 30 Jul 2024 03:20:31 +0000</pubDate>
      <link>https://dev.to/banerjeeprodipta/frontend-challenge-v240724-glam-up-my-markup-recreation-what-i-built-4gb5</link>
      <guid>https://dev.to/banerjeeprodipta/frontend-challenge-v240724-glam-up-my-markup-recreation-what-i-built-4gb5</guid>
      <description>&lt;p&gt;&lt;em&gt;I created an interactive website for the New York Recreational Cricket League that features:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Horizontal Scrolling Sections:&lt;/strong&gt; &lt;br&gt;
Utilized GSAP and ScrollTrigger to create a smooth horizontal scrolling effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3D Scene:&lt;/strong&gt; &lt;br&gt;
Integrated a 3D cricket ball model using React Three Fiber to add dynamic visual appeal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive Design:&lt;/strong&gt; &lt;br&gt;
Implemented a responsive layout to ensure a seamless experience across different devices.&lt;br&gt;
The goal was to combine interactive animations and modern 3D graphics to create a visually engaging experience for users interested in the cricket league.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check out the live demo of the project here: &lt;a href="https://nyrcl.vercel.app/" rel="noopener noreferrer"&gt;Demo&lt;/a&gt;&lt;br&gt;
Alternatively, view the code on GitHub: &lt;a href="https://github.com/BanerjeeProdipta/nyrcl-landing-page" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design &amp;amp; Planning:&lt;/strong&gt;&lt;br&gt;
Defined the project requirements and sketched out the layout and interactions.&lt;br&gt;
Chose GSAP for horizontal scrolling animations and React Three Fiber for the 3D scene.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation:&lt;/strong&gt;&lt;br&gt;
Set up the project with Next.js and Tailwind CSS.&lt;br&gt;
Created horizontal scrolling sections using GSAP and ScrollTrigger.&lt;br&gt;
Added a 3D cricket ball model that rotates and scales based on scroll progress.&lt;br&gt;
Ensured responsive design and smooth performance across different devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge:&lt;/strong&gt; &lt;br&gt;
Synchronizing the 3D scene with the scroll progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; &lt;br&gt;
Calculated the ball's position and scale based on scroll position, using a curved path for smooth motion.&lt;br&gt;
Challenge: Ensuring smooth horizontal scrolling with different screen sizes.&lt;br&gt;
Used flexible sizing and scroll triggers to adapt to various viewport dimensions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learnings:&lt;/strong&gt;&lt;br&gt;
Improved my skills in integrating GSAP with React for animations. Gained hands-on experience with 3D rendering using React Three Fiber. Learned how to handle complex animations and interactions in a responsive design.&lt;/p&gt;

&lt;p&gt;This project was developed by: &lt;a href="https://dev.to/banerjeeprodipta"&gt;banerjeeprodipta&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>css</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Creating Lyrics Video using FFmpeg</title>
      <dc:creator>Joyce Prodipta Banerjee</dc:creator>
      <pubDate>Sun, 08 Oct 2023 11:56:56 +0000</pubDate>
      <link>https://dev.to/banerjeeprodipta/creating-a-video-generation-api-with-nodejs-and-ffmpeg-1ea5</link>
      <guid>https://dev.to/banerjeeprodipta/creating-a-video-generation-api-with-nodejs-and-ffmpeg-1ea5</guid>
      <description>&lt;p&gt;The Video Generation API we are building in this tutorial will allow users to convert audio files (e.g., MP3 songs) and lyric files (e.g., LRC files) into video content with subtitles. This can be particularly useful for creating lyric videos or promotional material for your music.&lt;/p&gt;

&lt;p&gt;We will cover the following key components in our API:&lt;/p&gt;

&lt;p&gt;File Upload: Users can upload their MP3 and LRC files to the API.&lt;/p&gt;

&lt;p&gt;LRC to SRT Conversion: We will convert LRC (Lyric) files into SRT (SubRip) format, which is commonly used for subtitles in videos.&lt;/p&gt;

&lt;p&gt;Video Generation: We will use FFmpeg to combine the background image, audio file, and subtitles to create the final video.&lt;/p&gt;

&lt;p&gt;API Endpoint: We will expose an API endpoint that users can call to trigger the video generation process.&lt;/p&gt;

&lt;p&gt;Prerequisites&lt;br&gt;
Before we dive into the code, make sure you have the following prerequisites installed:&lt;/p&gt;

&lt;p&gt;Node.js: You can download it here.&lt;/p&gt;

&lt;p&gt;FFmpeg: Install FFmpeg on your system or use the @ffmpeg-installer/ffmpeg package in Node.js.&lt;/p&gt;

&lt;p&gt;Basic knowledge of JavaScript and Node.js.&lt;/p&gt;

&lt;p&gt;Setting Up the Project&lt;br&gt;
Let's start by setting up our Node.js project and installing the necessary packages. We'll use multer for file uploads and child_process for running FFmpeg commands. The project structure should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project-root/
  ├─ public/
  │   ├─ uploads/
  │   │   ├─ audio/
  │   │   ├─ subtitle/
  │   │   ├─ thumbnail/
  │   │   └─ video/
  │   └─ bg.jpg
  ├─ api/
  │   └─ videoGeneration.js
  ├─ package.json
  └─ ...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting Up multer for File Uploads&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import multer from "multer";

// Define storage configuration for multer
const storage = multer.diskStorage({
  destination: (req, file, cb) =&amp;gt; {
    // Specify the destination directory for uploaded files
    cb(null, path.join(baseDir, uploadDir, "subtitle"));
  },
  filename: (req, file, cb) =&amp;gt; {
    // Define the filename for uploaded files
    cb(null, file.originalname);
  },
});

// Create a multer instance with the storage configuration
const upload = multer({ storage });

// Handle file uploads using upload.fields() in your route handler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LRC to SRT Conversion&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function lrcToSrt(lrcContent) {
  const lrcLines = lrcContent.split("\n");
  let srt = "";
  let counter = 1;

  const formatTime = (timeStr) =&amp;gt; {
    // Split the input time string into minutes and seconds
    const [minutes, seconds] = timeStr.split(":");

    // Extract seconds and milliseconds
    const [secondsPart, millisecondsPart] = seconds.split(".");

    // Format each part with leading zeros
    const formattedMinutes = minutes.padStart(2, "0");
    const formattedSeconds = secondsPart.padStart(2, "0");
    const formattedMilliseconds = millisecondsPart.padEnd(3, "0");

    // Combine the formatted parts into the desired format
    const formattedTime = `00:${formattedMinutes}:${formattedSeconds},${formattedMilliseconds}`;

    return formattedTime;
  };

  for (let i = 0; i &amp;lt; lrcLines.length; i++) {
    const line = lrcLines[i].trim();
    if (line === "") continue;

    // Extract timestamps and lyrics
    const match = line.match(/\[(\d+:\d+\.\d+)\](.*)/);
    if (match) {
      const startTime = formatTime(match[1]); // Convert to SRT format

      let endTime;

      if (i &amp;lt; lrcLines.length - 1) {
        const nextLineMatch = lrcLines[i + 1].match(/\[(\d+:\d+\.\d+)\]/);
        endTime = nextLineMatch ? formatTime(nextLineMatch[1]) : "99:59:59,999";
      } else {
        // Use start time + 5 seconds if it's the last line
        const timeSplits = startTime.split(":");
        const addSecondsInSecondSplit = parseInt(timeSplits[1]) + 5;

        endTime = [
          timeSplits[0],
          ":",
          addSecondsInSecondSplit,
          ":",
          timeSplits[2],
        ].join("");
      }

      const lyrics = match[2].trim();

      srt += `${counter}\n${startTime} --&amp;gt; ${endTime}\n${lyrics}\n\n`;
      counter++;
    }
  }

  return srt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Video Generation with FFmpeg&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { exec } from "child_process";
import ffmpegPath from "@ffmpeg-installer/ffmpeg";

async function generateVideo(mp3Path, lrcPath, songTitle) {
  return new Promise((resolve, reject) =&amp;gt; {
    const mp3FilePath = `"${mp3Path}"`;
    const srtPath = path.join(
      publicDir,
      uploadDir,
      "subtitle",
      `${songTitle}.srt`
    );
    const videoPath = path.join(
      publicDir,
      uploadDir,
      "video",
      `${songTitle}.mp4`
    );

    let isVideoFileExist = fs.existsSync(videoPath);

    const lrcContent = fs.readFileSync(lrcPath, "utf8");
    const cleanedLrcContent = lrcContent.replace(/[\r\n]+/gm, "\n");
    const srtContent = lrcToSrt(cleanedLrcContent);
    console.log("🚀 Generating LRC");

    fs.writeFileSync(srtPath, srtContent, "utf8");
    console.log("🎉 Done making LRC");

    function convertVideoToSubtitle(mp3FilePath) {
      let subtitlePath = mp3FilePath
        .replace(/\\/g, "\\\\")
        .replace(/audio/g, "subtitle")
        .replace(/:/g, "\\:")
        .replace(/-/g, "\\-")
        .replace(".mp3", ".srt");

      return subtitlePath;
    }

    const subtitlePath = convertVideoToSubtitle(mp3FilePath);

    if (isVideoFileExist) {
      return resolve({ videoUrl: videoPath });
    }

    const ffmpegCommand = [
      ffmpegPath.path,
      "-loop",
      "1",
      "-i",
      backgroundImage,
      "-i",
      mp3FilePath,
      "-c:v",
      "libx264",
      "-c:a",
      "aac",
      "-strict",
      "experimental",
      "-b:a",
      "192k",
      "-shortest",
      "-y",
      "-vf",
      `subtitles='${subtitlePath}:force_style=Alignment=10,Fontsize=48,Outline=2'`,
      `"${videoPath}"`,
    ].join(" ");

    console.log({ ffmpegCommand });
    exec(ffmpegCommand, (ffmpegError, stdout) =&amp;gt; {
      if (ffmpegError) {
        console.error("❌", ffmpegError);
        if (fs.existsSync(videoPath)) {
          fs.unlink(videoPath, (unlinkError) =&amp;gt; {
            if (unlinkError) {
              console.error("❌ Error deleting video file:", unlinkError);
            }
          });
        }
        console.error("❌ Video generation failed");
        return reject(new Error("Video generation failed"));
      } else {
        console.log("FFmpeg output:", stdout);
        console.log("🎉 Video generation completed!");
        return resolve({ videoUrl: videoPath });
      }
    });
  });
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These code snippets cover the file upload setup with multer, LRC to SRT conversion, and video generation using FFmpeg. You can integrate these snippets into your Node.js project as discussed in the blog post to create your Video Generation API.&lt;/p&gt;

&lt;p&gt;API Endpoint&lt;br&gt;
Now, let's create the API endpoint that users can call to trigger the video generation process. In your Node.js route handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import express from "express";

// Create an Express application
const app = express();

// Define a POST endpoint for video generation
app.post("/generate-video", async (req, res) =&amp;gt; {
  const { lrcFile } = await uploadFiles(req, res);

  const mp3FileName = req.body.mp3File;
  const lrcFileName = lrcFile[0].originalname;

  const mp3Path = path.join(baseDir, uploadDir, "audio", `${mp3FileName}.mp3`);
  const lrcPath = path.join(baseDir, uploadDir, "subtitle", lrcFileName);

  try {
    const response = await generateVideo(mp3Path, lrcPath, mp3FileName);
    const videoUrl = response.videoUrl;
    return res.status(200).json({ videoUrl });
  } catch (error) {
    console.error("Error generating video:", error);

    return res.status(500).json({ error: "Video generation failed" });
  }
});

// Start the Express server
app.listen(3000, () =&amp;gt; {
  console.log("Server is running on port 3000");
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is my whole code, PS. I used next.js API&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import multer from "multer";
import fs from "fs";
import path from "path";
import { exec } from "child_process";
import ffmpegPath from "@ffmpeg-installer/ffmpeg";

export const config = {
  api: {
    bodyParser: false, // Disables the default body parser as we will handle the form data ourselves
  },
};

const publicDir = path.join(process.cwd(), "public");
const uploadDir = "/uploads"; // Directory to store uploaded files
const backgroundImage = path.join(publicDir, "/bg.jpg"); // Replace with the correct path to your image file

const baseDir = publicDir; // Set the base directory to public/uploads

// Create multer storage configuration
const storage = multer.diskStorage({
  destination: (req, file, cb) =&amp;gt; {
    cb(null, path.join(baseDir, uploadDir, "subtitle"));
  },
  filename: (req, file, cb) =&amp;gt; {
    cb(null, file.originalname);
  },
});

const upload = multer({ storage });

function uploadFiles(req, res) {
  return new Promise((resolve, reject) =&amp;gt; {
    upload.fields([{ name: "lrcFile" }])(req, res, (err) =&amp;gt; {
      if (err) {
        console.error("Error uploading files:", err);
        return reject(new Error("Error uploading files"));
      }

      const { lrcFile } = req.files;
      resolve({ lrcFile });
    });
  });
}

function lrcToSrt(lrcContent) {
  const lrcLines = lrcContent.split("\n");
  let srt = "";
  let counter = 1;

  const formatTime = (timeStr) =&amp;gt; {
    // Split the input time string into minutes and seconds
    const [minutes, seconds] = timeStr.split(":");

    // Extract seconds and milliseconds
    const [secondsPart, millisecondsPart] = seconds.split(".");

    // Format each part with leading zeros
    const formattedMinutes = minutes.padStart(2, "0");
    const formattedSeconds = secondsPart.padStart(2, "0");
    const formattedMilliseconds = millisecondsPart.padEnd(3, "0");

    // Combine the formatted parts into the desired format
    const formattedTime = `00:${formattedMinutes}:${formattedSeconds},${formattedMilliseconds}`;

    return formattedTime;
  };

  for (let i = 0; i &amp;lt; lrcLines.length; i++) {
    const line = lrcLines[i].trim();
    if (line === "") continue;

    // Extract timestamps and lyrics
    const match = line.match(/\[(\d+:\d+\.\d+)\](.*)/);
    if (match) {
      const startTime = formatTime(match[1]); // Convert to SRT format

      let endTime;

      if (i &amp;lt; lrcLines.length - 1) {
        const nextLineMatch = lrcLines[i + 1].match(/\[(\d+:\d+\.\d+)\]/);
        endTime = nextLineMatch ? formatTime(nextLineMatch[1]) : "99:59:59,999";
      } else {
        // Use start time + 5 seconds if it's the last line
        const timeSplits = startTime.split(":");
        const addSecondsInSecondSplit = parseInt(timeSplits[1]) + 5;

        endTime = [
          timeSplits[0],
          ":",
          addSecondsInSecondSplit,
          ":",
          timeSplits[2],
        ].join("");
      }

      const lyrics = match[2].trim();

      srt += `${counter}\n${startTime} --&amp;gt; ${endTime}\n${lyrics}\n\n`;
      counter++;
    }
  }

  return srt;
}

async function generateVideo(mp3Path, lrcPath, songTitle) {
  return new Promise((resolve, reject) =&amp;gt; {
    const mp3FilePath = `"${mp3Path}"`;
    const srtPath = path.join(
      publicDir,
      uploadDir,
      "subtitle",
      `${songTitle}.srt`
    );
    const videoPath = path.join(
      publicDir,
      uploadDir,
      "video",
      `${songTitle}.mp4`
    );

    let isVideoFileExist = fs.existsSync(videoPath);

    const lrcContent = fs.readFileSync(lrcPath, "utf8");
    const cleanedLrcContent = lrcContent.replace(/[\r\n]+/gm, "\n");
    const srtContent = lrcToSrt(cleanedLrcContent);
    console.log("🚀 Generating LRC");

    fs.writeFileSync(srtPath, srtContent, "utf8");
    console.log("🎉 Done making LRC");

    function convertVideoToSubtitle(mp3FilePath) {
      let subtitlePath = mp3FilePath
        .replace(/\\/g, "\\\\")
        .replace(/audio/g, "subtitle")
        .replace(/:/g, "\\:")
        .replace(/-/g, "\\-")
        .replace(".mp3", ".srt");

      return subtitlePath;
    }

    const subtitlePath = convertVideoToSubtitle(mp3FilePath);

    if (isVideoFileExist) {
      return resolve({ videoUrl: videoPath });
    }

    const ffmpegCommand = [
      ffmpegPath.path,
      "-loop",
      "1",
      "-i",
      backgroundImage,
      "-i",
      mp3FilePath,
      "-c:v",
      "libx264",
      "-c:a",
      "aac",
      "-strict",
      "experimental",
      "-b:a",
      "192k",
      "-shortest",
      "-y",
      "-vf",
      `subtitles='${subtitlePath}:force_style=Alignment=10,Fontsize=48,Outline=2'`,
      `"${videoPath}"`,
    ].join(" ");

    console.log({ ffmpegCommand });
    exec(ffmpegCommand, (ffmpegError, stdout) =&amp;gt; {
      if (ffmpegError) {
        console.error("❌", ffmpegError);
        if (fs.existsSync(videoPath)) {
          fs.unlink(videoPath, (unlinkError) =&amp;gt; {
            if (unlinkError) {
              console.error("❌ Error deleting video file:", unlinkError);
            }
          });
        }
        console.error("❌ Video generation failed");
        return reject(new Error("Video generation failed"));
      } else {
        console.log("FFmpeg output:", stdout);
        console.log("🎉 Video generation completed!");
        return resolve({ videoUrl: videoPath });
      }
    });
  });
}

async function handleVideoGeneration(req, res) {
  const { lrcFile } = await uploadFiles(req, res);

  const mp3FileName = req.body.mp3File;
  const lrcFileName = lrcFile[0].originalname;

  const mp3Path = path.join(baseDir, uploadDir, "audio", `${mp3FileName}.mp3`);
  const lrcPath = path.join(baseDir, uploadDir, "subtitle", lrcFileName);

  try {
    const response = await generateVideo(mp3Path, lrcPath, mp3FileName);
    const videoUrl = response.videoUrl;
    return res.status(200).json({ videoUrl });
  } catch (error) {
    console.error("Error generating video:", error);

    return res.status(500).json({ error: "Video generation failed" });
  }
}

export default async function handler(req, res) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "Method Not Allowed" });
  }
  try {
    await handleVideoGeneration(req, res);
  } catch (error) {
    console.error("Error handling video generation:", error);
    return res.status(500).json({ error: "Video generation failed" });
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conclusion&lt;br&gt;
In this blog post, we've explored how to create a Video Generation API using Node.js and FFmpeg. This API can be a valuable addition to your multimedia content creation toolkit, allowing you to automate the process of turning audio and lyric files into engaging videos.&lt;/p&gt;

&lt;p&gt;Feel free to customize and extend this API to suit your specific needs. You can enhance it by adding more features, such as custom styling for subtitles or support for different video formats.&lt;/p&gt;

&lt;p&gt;Now that you have a basic understanding of how to create a Video Generation API, you can explore more advanced options and integrate it into your projects for creative video content generation.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>nextjs</category>
      <category>node</category>
      <category>video</category>
    </item>
    <item>
      <title>Building an autocomplete-input component in Next.js</title>
      <dc:creator>Joyce Prodipta Banerjee</dc:creator>
      <pubDate>Fri, 06 Oct 2023 12:48:14 +0000</pubDate>
      <link>https://dev.to/banerjeeprodipta/building-an-autocomplete-input-component-in-react-obn</link>
      <guid>https://dev.to/banerjeeprodipta/building-an-autocomplete-input-component-in-react-obn</guid>
      <description>&lt;p&gt;&lt;a href="https://codesandbox.io/p/github/BanerjeeProdipta/autocomplete-input/main?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clnevmydh0007356kxemwi9ic%2522%252C%2522sizes%2522%253A%255B70%252C30%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clnevmydh0003356kazmpjdp6%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clnevmydh0005356ki9xrfbsy%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clnevmydh0006356kier50zhn%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B60%252C40%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clnevmydh0003356kazmpjdp6%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clnevmydh0002356k2k1syura%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252FREADME.md%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522clnevmydh0003356kazmpjdp6%2522%252C%2522activeTabId%2522%253A%2522clnevmydh0002356k2k1syura%2522%257D%252C%2522clnevmydh0006356kier50zhn%2522%253A%257B%2522id%2522%253A%2522clnevmydh0006356kier50zhn%2522%252C%2522activeTabId%2522%253A%2522clnevoloi00g5356k2nmw1ztg%2522%252C%2522tabs%2522%253A%255B%257B%2522type%2522%253A%2522TASK_PORT%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522port%2522%253A3000%252C%2522id%2522%253A%2522clnevoloi00g5356k2nmw1ztg%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522path%2522%253A%2522%252F%2522%257D%255D%257D%252C%2522clnevmydh0005356ki9xrfbsy%2522%253A%257B%2522id%2522%253A%2522clnevmydh0005356ki9xrfbsy%2522%252C%2522activeTabId%2522%253A%2522clnevmydh0004356kcdzqhnbf%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clnevmydh0004356kcdzqhnbf%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TERMINAL%2522%252C%2522shellId%2522%253A%2522clnfww0x2000te6ft3j2i7kld%2522%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522id%2522%253A%2522clnevoi3900em356kvnt0021k%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D" rel="noopener noreferrer"&gt;Sandbox Demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Introduction:&lt;br&gt;
AutoComplete input fields are a common user interface element used to help users efficiently find and select options from a predefined list. In this blog, we'll explore how to create a customizable AutoComplete input component in React with throttling to improve performance. We'll walk through the code for the component and explain its key features.&lt;/p&gt;

&lt;p&gt;Creating the AutoCompleteInput Component:&lt;br&gt;
The &lt;code&gt;AutoCompleteInput&lt;/code&gt; component is designed to provide a user-friendly AutoComplete experience. Let's break down its important components and functionality:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Props&lt;/strong&gt;:&lt;br&gt;
The component takes two props: &lt;code&gt;options&lt;/code&gt; and &lt;code&gt;handleSelection&lt;/code&gt;. The &lt;code&gt;options&lt;/code&gt; prop is an array of strings representing the available options, while &lt;code&gt;handleSelection&lt;/code&gt; is a callback function to handle the selected option.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;State and Refs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;inputRef&lt;/code&gt;: This useRef hook is used to reference the input element for handling clicks outside the component.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;value&lt;/code&gt;: Represents the current value of the input field.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;suggestions&lt;/code&gt;: Stores the list of suggestions based on the user's input.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;getSuggestions Function&lt;/strong&gt;:&lt;br&gt;
This function filters the available options based on the user's input. It converts both the input and options to lowercase for case-insensitive matching.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;onChange Function&lt;/strong&gt;:&lt;br&gt;
Whenever the input field's value changes, this function updates the &lt;code&gt;value&lt;/code&gt; state and recalculates the suggestions based on the new value.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;onSuggestionClick Function&lt;/strong&gt;:&lt;br&gt;
When a suggestion is clicked, this function sets the selected suggestion as the input value and calls the &lt;code&gt;handleSelection&lt;/code&gt; callback with the selected value.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;isSuggestionEmpty Function&lt;/strong&gt;:&lt;br&gt;
This function checks if the suggestions list is empty or contains only an empty string. It is used to conditionally render the suggestions dropdown.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Event Listener for Clicks Outside&lt;/strong&gt;:&lt;br&gt;
An &lt;code&gt;useEffect&lt;/code&gt; hook is used to add a click event listener to the document body. This listener detects clicks outside of the component, allowing it to blur and hide the suggestions dropdown.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Throttling with Lodash&lt;/strong&gt;:&lt;br&gt;
To improve performance and responsiveness, we've added throttling to the &lt;code&gt;onChange&lt;/code&gt; event using the &lt;code&gt;lodash/debounce&lt;/code&gt; function. This reduces the frequency of function calls while the user is typing rapidly.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Installing Lodash:&lt;br&gt;
Before implementing throttling, make sure to install the &lt;code&gt;lodash&lt;/code&gt; library using npm. Run the following command in your project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;lodash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendering the Component:&lt;br&gt;
The &lt;code&gt;AutoCompleteInput&lt;/code&gt; component is rendered within the &lt;code&gt;Home&lt;/code&gt; component. When the user selects a suggestion, the selected value is logged to the console.&lt;/p&gt;

&lt;p&gt;Conclusion:&lt;br&gt;
Creating an AutoComplete input component in React can enhance user experience when searching or selecting items from a list. The provided code demonstrates a basic implementation, and with the added throttling, it ensures smooth performance even with rapid typing.&lt;/p&gt;

&lt;p&gt;Feel free to use this code as a starting point and adapt it to your specific needs. By following the installation instructions for lodash, you can easily add throttling to your React components. Happy coding!&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";
import AutoCompleteInput from "./components/AutoComplete";

const data = ["One", "Two", "Three"];

export default function Home() {
  const handleSelection = (selectedOption: string) =&amp;gt; {
    console.log({ Selected: { selectedOption } });
  };

  return (
    &amp;lt;main className="flex min-h-screen flex-col items-center justify-between p-24"&amp;gt;
      &amp;lt;AutoCompleteInput
        options={data.map((v: string) =&amp;gt; v)}
        handleSelection={handleSelection}
      /&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React, { useEffect, useRef, useState } from "react";
import debounce from "lodash/debounce"; // npm install lodash

interface IPropType {
  options: string[];
  handleSelection: (val: string) =&amp;gt; void;
}

const AutoCompleteInput = (props: IPropType) =&amp;gt; {
  const { options, handleSelection } = props;
  const inputRef = useRef&amp;lt;HTMLInputElement&amp;gt;(null);
  const [value, setValue] = useState("");
  const [suggestions, setSuggestions] = useState([""]);

  const getSuggestions = (inputValue: string) =&amp;gt; {
    if (typeof inputValue !== "string") {
      return [];
    }
    const inputValueLowerCase = inputValue.toLowerCase();
    return options.filter((option) =&amp;gt;
      option.toLowerCase().includes(inputValueLowerCase),
    );
  };

  // Debounce the onChange function
  const debouncedOnChange = debounce((newValue: string) =&amp;gt; {
    setValue(newValue);
    setSuggestions(getSuggestions(newValue));
  }, 100); // Adjust the debounce delay as needed (e.g., 300 milliseconds)

  const onSuggestionClick = (suggestion: string) =&amp;gt; {
    setValue(suggestion);
    handleSelection(suggestion);
    setSuggestions([]);
  };

  const isSuggestionEmpty = () =&amp;gt; {
    if (suggestions.length === 1 &amp;amp;&amp;amp; suggestions[0] === "") {
      return true;
    } else return false;
  };

  // Add a click event listener to the document body to handle clicks outside of the component
  useEffect(() =&amp;gt; {
    const handleDocumentClick = (e: any) =&amp;gt; {
      if (inputRef.current &amp;amp;&amp;amp; !inputRef.current.contains(e.target)) {
        inputRef.current.blur();
        setSuggestions([]);
      }
    };

    document.addEventListener("click", handleDocumentClick);

    return () =&amp;gt; {
      document.removeEventListener("click", handleDocumentClick);
    };
  }, []);

  return (
    &amp;lt;div className="relative"&amp;gt;
      &amp;lt;input
        ref={inputRef}
        className="w-full border border-dark text-black transition-all duration-300 rounded-md px-4 py-3 focus:outline-none"
        type="text"
        placeholder="Search"
        value={value}
        onChange={(e) =&amp;gt; debouncedOnChange(e.target.value)}
        onFocus={() =&amp;gt; {
          setSuggestions(options);
          setValue("");
        }}
      /&amp;gt;
      {!isSuggestionEmpty() &amp;amp;&amp;amp; suggestions.length &amp;gt; 0 &amp;amp;&amp;amp; (
        &amp;lt;ul
          className="bg-white border-blue-500 border-2 rounded hover:cursor-pointer absolute top-14 w-full z-20 max-h-64 overflow-y-auto"
          onPointerLeave={() =&amp;gt; {
            inputRef?.current?.blur();
            setSuggestions([""]);
          }}
        &amp;gt;
          {suggestions.map((suggestion) =&amp;gt; (
            &amp;lt;li
              key={suggestion}
              className="hover:bg-blue-500 hover:text-white transition duration-200 text-sm text-gray-700 p-1"
              onClick={() =&amp;gt; onSuggestionClick(suggestion)}
            &amp;gt;
              {suggestion}
            &amp;lt;/li&amp;gt;
          ))}
        &amp;lt;/ul&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
};

export default AutoCompleteInput;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>nextjs</category>
      <category>tailwindcss</category>
      <category>autocomplete</category>
      <category>input</category>
    </item>
    <item>
      <title>Reusable pagination component using hook</title>
      <dc:creator>Joyce Prodipta Banerjee</dc:creator>
      <pubDate>Fri, 10 Mar 2023 18:48:33 +0000</pubDate>
      <link>https://dev.to/banerjeeprodipta/reusable-pagination-component-using-hook-22mb</link>
      <guid>https://dev.to/banerjeeprodipta/reusable-pagination-component-using-hook-22mb</guid>
      <description>&lt;p&gt;Create a hook for pagination like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useState } from "react";

export default function usePagination() {
    const [offset, setOffset] = useState(0);
    const [limit, setLimit] = useState(10);

    function changeOffset(offsetParam: number) {
        setOffset(offsetParam);
    }
    function changeLimit(limitParam: number) {
        setLimit(limitParam);
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the reusable pagination component UI,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type TypeProps = {
count: number;
changeOffset: (offset: number) =&amp;gt; void;
offset: number;
limit: number;
};

const Pagination = (props: TypeProps) =&amp;gt; {
const { count, changeOffset, offset, limit } = props;

let startingItemNumber = offset + 1;
let endingItemNumber = offset + limit &amp;gt; count ? count : offset + limit;

return (
&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;Showing {startingItemNumber} to {endingItemNumber} of {count} entries
   &amp;lt;/p&amp;gt;
   &amp;lt;button onClick={() =&amp;gt; changeOffset(offset - limit)} disabled= 
     {offset === 0} &amp;gt;
    Prev
   &amp;lt;/button&amp;gt;
   &amp;lt;button onClick={() =&amp;gt; changeOffset(offset + limit)} 
      disabled={endingItemNumber === count}&amp;gt;
    Next
   &amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
  );
};

export default Pagination;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the pagination hook and the pagination component like this in your page&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const List = () =&amp;gt; {
const { limit, offset, changeOffset } = usePagination();
//_e.g. usage of the pagination hook _
const { data, isLoading } = useGetList(limit, offset);


return (
&amp;lt;&amp;gt;
//_Populate list_

 &amp;lt;Pagination
  //make sure count, offset and limit is Number type
  count={data.count}
  offset={data.offset}
  limit={data.limit}
  changeOffset={changeOffset}
 /&amp;gt;
&amp;lt;&amp;gt;
)

export default List;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>hook</category>
      <category>pagination</category>
      <category>reusable</category>
    </item>
    <item>
      <title>Validate file with ZOD</title>
      <dc:creator>Joyce Prodipta Banerjee</dc:creator>
      <pubDate>Fri, 03 Mar 2023 09:33:12 +0000</pubDate>
      <link>https://dev.to/banerjeeprodipta/validate-file-with-zod-20o</link>
      <guid>https://dev.to/banerjeeprodipta/validate-file-with-zod-20o</guid>
      <description>&lt;p&gt;Zod is a TypeScript-first schema validation library with static type inference. You can create validation schemas for either field-level validation or form-level validation1. Here’s an example of how you can use Zod for schema validation for a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Max size is 5MB.
const MAX_FILE_SIZE = 5000000; 

function checkFileType(file: File) {
    if (file?.name) {
        const fileType = file.name.split(".").pop();
        if (fileType === "docx" || fileType === "pdf") return true;
    }
    return false;
}

export const fileSchema = z.object({
z.any()
.refine((file: File) =&amp;gt; file?.length !== 0, "File is required")
.refine((file) =&amp;gt; file.size &amp;lt; MAX_FILE_SIZE, "Max size is 5MB.")
.refine((file) =&amp;gt; checkFileType(file), "Only .pdf, .docx formats are supported."),`
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>zod</category>
      <category>validation</category>
      <category>file</category>
      <category>schema</category>
    </item>
  </channel>
</rss>
