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
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>