Crafting a Seamless Multi-Check Items List with Auto-Select Tabs in Next.js Using Material-UI

Photo by RetroSupply on Unsplash

Crafting a Seamless Multi-Check Items List with Auto-Select Tabs in Next.js Using Material-UI

Welcome, fellow developers! In this tutorial, we'll embark on a journey to create a feature-rich multi-check items list in your Next.js application, utilizing the power of Material-UI. We'll dive into the process of fetching data from a custom API, generating dynamic tabs, and implementing an auto-select mechanism for a smoother user experience.

Let's get started:

Setting Up Your Next.js App

Begin by creating a new Next.js app with the following command:

npx create-next-app multi-check-items-list

Now, let's integrate Material-UI into our project:

npm install @mui/material @emotion/react @emotion/styled

Crafting a Dynamic API for Items Listing

Our API, located at src/pages/api/getItemsCustomizations.ts, is designed to provide a structured list of customizable items. We've organized these items into categories and subcategories, ensuring a clear hierarchy for a more user-friendly interface.

// src/pages/api/getItemsCustomizations.ts
import type { NextApiRequest, NextApiResponse } from "next";

type Data = {
  id: number;
  parentId: number | "";
  name: string;
};

const categoriesList: Data[] = [
  {
    id: 1,
    parentId: "",
    name: "Super Mega Deal",
  },
  {
    id: 2,
    parentId: 1,
    name: "Super Mega Deal - Original",
  },
  {
    id: 3,
    parentId: 1,
    name: "Super Mega Deal - Spicy",
  },
  {
    id: 4,
    parentId: 1,
    name: "Super Mega Deal - Mix",
  },
  {
    id: 5,
    parentId: "",
    name: "Would you like to add extra side to your meal?",
  },
  {
    id: 6,
    parentId: 5,
    name: "Pop Corn Large",
  },
  {
    id: 7,
    parentId: 5,
    name: "Twister Sandwich",
  },
  {
    id: 8,
    parentId: 5,
    name: "Rice",
  },
  {
    id: 9,
    parentId: "",
    name: "Select Your Favorite Side Item",
  },
  {
    id: 10,
    parentId: 9,
    name: "Family Fries",
  },
  {
    id: 11,
    parentId: 9,
    name: "Coleslaw Salad Large",
  },
  {
    id: 12,
    parentId: 9,
    name: "Family Spicy Fries",
  },
  {
    id: 13,
    parentId: "",
    name: "Special Deal",
  },
  {
    id: 14,
    parentId: 13,
    name: "Special Deal - Original",
  },
  {
    id: 15,
    parentId: 13,
    name: "Special Deal - Spicy",
  },
  {
    id: 16,
    parentId: 13,
    name: "Special Deal - Mix",
  },
  {
    id: 17,
    parentId: "",
    name: "Deluxe Deal",
  },
  {
    id: 18,
    parentId: 17,
    name: "Deluxe Deal - Original",
  },
  {
    id: 19,
    parentId: 17,
    name: "Deluxe Deal - Spicy",
  },
  {
    id: 20,
    parentId: 17,
    name: "Deluxe Deal - Mix",
  },
];

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data[]>
) {
  const getCustomizationsMainCategories = categoriesList.filter(
    (item) => item.parentId === ""
  );
  const getCustomizations = getCustomizationsMainCategories.map((item) => {
    return { ...item, children: getCustomizationsChildren(item.id) };
  });
  res.status(200).json(getCustomizations);
}

const getCustomizationsChildren = (parentId: number | "") => {
  return categoriesList.filter((item) => item.parentId === parentId);
};

Building the Multi-Check Component

In src/pages/index.tsx, we've implemented a comprehensive multi-check component. This component features dynamic tabs, auto-selection on scroll, and a visually appealing design. The code is thoroughly commented to guide you through each step.

// src/pages/index.tsx
import React, { useEffect, useRef, useState } from "react";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import { Checkbox, Grid } from "@mui/material";
import Card from "@mui/material/Card";
import Typography from "@mui/material/Typography";

interface CustomizedOption {
  id: number;
  name: string;
  children: { id: number; name: string }[];
}

const buttonStyle = {
  cursor: "pointer",
  padding: "10px",
  boxShadow: "0px 0px 1px 1px green",
  margin: "10px",
  borderRadius: "10px",
};

const Home: React.FC = () => {
  const [isTabClick, setIsTabClick] = useState(false);
  const [open, setOpen] = useState(false);
  const [customizedOptionsList, setCustomizedOptionsList] = useState<
    CustomizedOption[]
  >([]);
  const [selectedId, setSelectedId] = useState(0);
  const [checkedIds, setCheckedIds] = useState<number[]>([]);

  const handleClickOpen = () => {
    setOpen(true);
  };

  const handleClose = () => {
    setOpen(false);
  };

  useEffect(() => {
    getCustomizationsList();
  }, []);

  const getCustomizationsList = async () => {
    const response = await fetch(
      "http://localhost:3000/api/getItemsCustomizations"
    );
    const data = await response.json();
    setCustomizedOptionsList(data);
  };

  const handleTabClick = (item: CustomizedOption) => {
    setIsTabClick(true);
    setSelectedId(item?.id);
    const anchorNodeElem = document.createElement("a");
    anchorNodeElem.href = `#MAIN_SECTION_${item?.id}`;
    anchorNodeElem.click();
  };

  const handleChange = (item: { id: number }) => {
    if (checkedIds.includes(item?.id)) {
      setCheckedIds(checkedIds.filter((itemR) => itemR !== item?.id));
      return;
    }
    setCheckedIds([...checkedIds, item?.id]);
  };

  const ActionAreaCard: React.FC<{ customizedOption: CustomizedOption }> = ({
    customizedOption,
  }) => {
    let callback = (entries: any, observer: any) => {
      entries.forEach((entry: any) => {
        if (entry?.isIntersecting && !isTabClick) {
          setSelectedId(customizedOption?.id);
        }
      });
    };

    const sectionRef: any = useRef(null);

    useEffect(() => {
      let options = {
        root: null,
        rootMargin: "0px",
        threshold: 1.0,
      };

      let observer = new IntersectionObserver(callback, options);
      sectionRef?.current ? observer.observe(sectionRef.current) : "";

      return () => {
        observer.disconnect();
      };
    }, []);

    return (
      <Card ref={sectionRef} sx={{ maxWidth: 345 }}>
        <Typography gutterBottom variant="h5" component="div">
          {customizedOption?.name}
        </Typography>
        {customizedOption?.children.map((item, index) => (
          <div key={index} style={{ display: "flex", alignItems: "center" }}>
            <Checkbox
              checked={checkedIds.includes(item?.id)}
              onChange={() => handleChange(item)}
            />
            <Typography variant="body2" color="text.secondary">
              {item?.name}
            </Typography>
          </div>
        ))}
      </Card>
    );
  };

  return (
    <React.Fragment>
      <Button onClick={handleClickOpen}>Open food item customizations</Button>
      <Dialog open={open} onClose={handleClose}>
        <Grid container spacing={2}>
          <Grid item xs={12}>
            <h2 style={{ textAlign: "center" }}>Food item customizations</h2>
          </Grid>
          <Grid item xs={4}>
            <aside style={{ maxHeight: "65vh", overflowY: "auto" }}>
              <nav>
                <ul style={{ listStyle: "none" }}>
                  {customizedOptionsList.map((item, index) => (
                    <li
                      onClick={() => handleTabClick(item)}
                      style={{
                        ...buttonStyle,
                        color: item.id === selectedId ? "purple" : undefined,
                        background:
                          item.id === selectedId ? "yellow" : undefined,
                      }}
                      key={item.id}
                    >
                      {item.name}
                    </li>
                  ))}
                </ul>
              </nav>
            </aside>
          </Grid>
          <Grid item xs={8}>
            <div
              id={"KALI"}
              style={{
                maxHeight: "65vh",
                overflowY: "auto",
                display: "grid",
                gap: "5rem",
              }}
            >
              {customizedOptionsList.map((item, index) => (
                <div
                  key={index}
                  id={`MAIN_SECTION_${item?.id}`}
                  onMouseEnter={() => setIsTabClick(false)}
                >
                  <ActionAreaCard customizedOption={item} />
                </div>
              ))}
            </div>
          </Grid>
        </Grid>
      </Dialog>
    </React.Fragment>
  );
};

export default Home;

Enhancing User Experience

To ensure a seamless user experience, we've incorporated smooth scrolling behavior in our global styles file (src/styles/globals.css). This provides users with a pleasant and effortless navigation experience.

// src/styles/globals.css
html {
  scroll-behavior: smooth;
}

Conclusion

And there you have it – a powerful, feature-rich multi-check items list in your Next.js application! This tutorial covers everything from setting up your project to implementing a polished user interface with Material-UI.

How You Can Support:

If you found this tutorial valuable and it has saved you time or effort, consider supporting my work by buying me a coffee. Your support fuels future tutorials and helps maintain the quality of content. Every contribution is greatly appreciated!

Buy Me a Coffee - Your generosity keeps the coding fuel flowing!

Thank you for choosing this tutorial, and happy coding!