Search code examples
htmlreactjstailwind-css

How can i have a progress indicator and video buffer rate that remains within the boundaries of each video chapters like Youtube does?


I am making a custom video player. this video may contain chapters, so i map through these chapters and make a div for each one with a gap between them like youtube does. problem is the progress rate indicator and the buffer rate indicator overlaps with the chapters and covers the gap. Here is picture of what i made: Supposedly there was a gap for the chapter where I marked, but the progress indicator rate covered that gap.

this is what I want the progress bar to be like: As you can see the progress rate didn't cover the chapters gap

Here is the code: https://github.com/mohammadmansour200/my-react-app


Solution

  • You could consider having two copies of the chapter elements, on top of each other. Use clip-path() to "trim" the progress bar to the correct width.

    To do this, first replace the progress element with a copy of the chapter elements, but colored the same black. Here, I've kept it DRY by refactoring the chapter elements to a new component:

    function Chapters({ color }) {
      return (
        <div className="absolute flex w-full -translate-y-[3px] flex-row-reverse gap-1">
          {APIData.chapter.map((chapter) => {
            const chapterWidth = `${
              (unformatTimestamp(chapter.chapterStart) /
                unformatTimestamp(APIData.duration)) *
              100
            }%`;
    
            return (
              <div
                key={chapter.chapterStart}
                style={{
                  width: chapterWidth,
                }}
                className={`h-[6px] ${color} ${
                  chapterWidth === "0%" ? "flex-grow" : ""
                }`}
              ></div>
            );
          })}
        </div>
      );
    }
    
    {/* Video chapters */}
    <Chapters color="bg-black/50" />
    {/* Video progress rate */}
    <Chapters color="bg-black" />
    

    const { useState, useRef } = React;
    
    const APIData = {
      duration: "5:00",
      chapter: [
        {
          chapterTitle: "test",
          chapterStart: "0:00",
        },
        {
          chapterTitle: "test 2",
          chapterStart: "3:34",
        },
      ],
    };
    
    function unformatTimestamp(duration) {
      const splittedDuration = duration.split(":");
    
      const hours = splittedDuration.length === 2 ? undefined : splittedDuration[0];
      const minutes =
        hours === undefined ? splittedDuration[0] : splittedDuration[1];
      const seconds =
        hours === undefined ? splittedDuration[1] : splittedDuration[2];
    
      if (hours === undefined && minutes === "0") {
        return parseInt(seconds);
      } else if (minutes !== undefined && hours === undefined) {
        const calcSeconds = parseInt(minutes) * 60 + parseInt(seconds);
        return calcSeconds;
      } else if (hours !== undefined) {
        const calcSeconds =
          parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);
        return calcSeconds;
      }
    }
    
    function Chapters({ color }) {
      return (
        <div className="absolute flex w-full -translate-y-[3px] flex-row-reverse gap-1">
          {APIData.chapter.map((chapter) => {
            const chapterWidth = `${
              (unformatTimestamp(chapter.chapterStart) /
                unformatTimestamp(APIData.duration)) *
              100
            }%`;
    
            return (
              <div
                key={chapter.chapterStart}
                style={{
                  width: chapterWidth,
                }}
                className={`h-[6px] ${color} ${
                  chapterWidth === "0%" ? "flex-grow" : ""
                }`}
              ></div>
            );
          })}
        </div>
      );
    }
    
    function App() {
      const [progress, setProgress] = useState(0);
      const sliderElRef = useRef(null);
      function onSliderInput() {
        setProgress(sliderElRef.current.value);
      }
      console.log(progress);
    
      return (
        <div className="flex w-full items-center gap-2 text-xs font-semibold opacity-85">
          0:00
          <div className="relative flex w-full items-center">
            {/* Video slider */}
            <input
              ref={sliderElRef}
              onInput={onSliderInput}
              max="100"
              min="0"
              step="0.01"
              type="range"
              defaultValue="0"
              className="progress-bar absolute z-[4] h-[6px]  w-full cursor-pointer select-none appearance-none rounded-2xl bg-transparent"
            />
            <div className="relative w-full">
              {APIData.chapter.length !== 0 && APIData.chapter !== undefined && (
                <React.Fragment>
                  {/* Video chapters */}
                  <Chapters color="bg-black/50" />
                  {/* Video progress rate */}
                  <Chapters color="bg-black" />
                </React.Fragment>
              )}
    
              {/* Video buffer rate */}
              <div className="absolute w-full -translate-y-[3px]">
                <div
                  style={{ width: "10%" }}
                  className="h-[6px] rounded-full bg-black/40"
                ></div>
              </div>
            </div>
          </div>
          5:00
        </div>
      );
    }
    
    ReactDOM.createRoot(document.getElementById("root")).render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdn.tailwindcss.com/3.4.3"></script>
    
    <div id="root"></div>

    Then, apply clip-path: inset() to the progress element, clipping from the right edge only, using the invert of the progress variable value:

    function Chapters({ color, style = {} }) {
      return (
        <div … style={style}>
    
    {/* Video progress rate */}
    <Chapters … style={{ clipPath: `inset(0 ${100 - progress}% 0 0)` }} />
    

    const { useState, useRef } = React;
    
    const APIData = {
      duration: "5:00",
      chapter: [
        {
          chapterTitle: "test",
          chapterStart: "0:00",
        },
        {
          chapterTitle: "test 2",
          chapterStart: "3:34",
        },
      ],
    };
    
    function unformatTimestamp(duration) {
      const splittedDuration = duration.split(":");
    
      const hours = splittedDuration.length === 2 ? undefined : splittedDuration[0];
      const minutes =
        hours === undefined ? splittedDuration[0] : splittedDuration[1];
      const seconds =
        hours === undefined ? splittedDuration[1] : splittedDuration[2];
    
      if (hours === undefined && minutes === "0") {
        return parseInt(seconds);
      } else if (minutes !== undefined && hours === undefined) {
        const calcSeconds = parseInt(minutes) * 60 + parseInt(seconds);
        return calcSeconds;
      } else if (hours !== undefined) {
        const calcSeconds =
          parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);
        return calcSeconds;
      }
    }
    
    function Chapters({ color, style = {} }) {
      return (
        <div className="absolute flex w-full -translate-y-[3px] flex-row-reverse gap-1" style={style}>
          {APIData.chapter.map((chapter) => {
            const chapterWidth = `${
              (unformatTimestamp(chapter.chapterStart) /
                unformatTimestamp(APIData.duration)) *
              100
            }%`;
    
            return (
              <div
                key={chapter.chapterStart}
                style={{
                  width: chapterWidth,
                }}
                className={`h-[6px] ${color} ${
                  chapterWidth === "0%" ? "flex-grow" : ""
                }`}
              ></div>
            );
          })}
        </div>
      );
    }
    
    function App() {
      const [progress, setProgress] = useState(0);
      const sliderElRef = useRef(null);
      function onSliderInput() {
        setProgress(sliderElRef.current.value);
      }
      console.log(progress);
    
      return (
        <div className="flex w-full items-center gap-2 text-xs font-semibold opacity-85">
          0:00
          <div className="relative flex w-full items-center">
            {/* Video slider */}
            <input
              ref={sliderElRef}
              onInput={onSliderInput}
              max="100"
              min="0"
              step="0.01"
              type="range"
              defaultValue="0"
              className="progress-bar absolute z-[4] h-[6px]  w-full cursor-pointer select-none appearance-none rounded-2xl bg-transparent"
            />
            <div className="relative w-full">
              {APIData.chapter.length !== 0 && APIData.chapter !== undefined && (
                <React.Fragment>
                  {/* Video chapters */}
                  <Chapters color="bg-black/50" />
                  {/* Video progress rate */}
                  <Chapters color="bg-black" style={{ clipPath: `inset(0 ${100 - progress}% 0 0)` }} />
                </React.Fragment>
              )}
    
              {/* Video buffer rate */}
              <div className="absolute w-full -translate-y-[3px]">
                <div
                  style={{ width: "10%" }}
                  className="h-[6px] rounded-full bg-black/40"
                ></div>
              </div>
            </div>
          </div>
          5:00
        </div>
      );
    }
    
    ReactDOM.createRoot(document.getElementById("root")).render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdn.tailwindcss.com/3.4.3"></script>
    
    <div id="root"></div>