From 598f752b120019608af520f19b231e2b1afaa679 Mon Sep 17 00:00:00 2001
From: Rj Manhas <117674421+RjManhas@users.noreply.github.com>
Date: Wed, 12 Nov 2025 12:03:10 -0700
Subject: [PATCH] feat: hide the arrow buttons on scroll lists when at either
end of the list (#61)
---
.../components/carousels/EpisodeCarousel.tsx | 73 +++++++++---
src/components/player/atoms/Episodes.tsx | 89 ++++++++++-----
.../components/CarouselNavButtons.tsx | 53 ++++++++-
.../discover/components/CategoryButtons.tsx | 105 +++++++++++++-----
src/stores/__old/watched/store.ts | 2 +-
5 files changed, 248 insertions(+), 74 deletions(-)
diff --git a/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx b/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx
index b6eeacb6..9fec2494 100644
--- a/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx
+++ b/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx
@@ -45,6 +45,39 @@ export function EpisodeCarousel({
const updateItem = useProgressStore((s) => s.updateItem);
const confirmModal = useModal("season-watch-confirm");
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
+ const [canScrollRight, setCanScrollRight] = useState(false);
+
+ const updateScrollState = () => {
+ if (!carouselRef.current) {
+ setCanScrollLeft(false);
+ setCanScrollRight(false);
+ return;
+ }
+
+ const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;
+ const isAtStart = scrollLeft <= 1;
+ const isAtEnd = scrollLeft + clientWidth >= scrollWidth - 1;
+
+ setCanScrollLeft(!isAtStart);
+ setCanScrollRight(!isAtEnd);
+ };
+
+ useEffect(() => {
+ const carousel = carouselRef.current;
+ if (!carousel) return;
+
+ updateScrollState();
+
+ carousel.addEventListener("scroll", updateScrollState);
+ window.addEventListener("resize", updateScrollState);
+
+ return () => {
+ carousel.removeEventListener("scroll", updateScrollState);
+ window.removeEventListener("resize", updateScrollState);
+ };
+ }, []);
+
const handleScroll = (direction: "left" | "right") => {
if (!carouselRef.current) return;
@@ -530,15 +563,17 @@ export function EpisodeCarousel({
{/* Episodes Carousel */}
{/* Left scroll button */}
-
-
-
+ {canScrollLeft && (
+
+
+
+ )}
{/* Right scroll button */}
-
-
-
+ {canScrollRight && (
+
+
+
+ )}
);
diff --git a/src/components/player/atoms/Episodes.tsx b/src/components/player/atoms/Episodes.tsx
index 2d081980..8b9bf424 100644
--- a/src/components/player/atoms/Episodes.tsx
+++ b/src/components/player/atoms/Episodes.tsx
@@ -766,6 +766,39 @@ export function EpisodesView({
],
);
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
+ const [canScrollRight, setCanScrollRight] = useState(false);
+
+ const updateScrollState = () => {
+ if (!carouselRef.current) {
+ setCanScrollLeft(false);
+ setCanScrollRight(false);
+ return;
+ }
+
+ const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;
+ const isAtStart = scrollLeft <= 1;
+ const isAtEnd = scrollLeft + clientWidth >= scrollWidth - 1;
+
+ setCanScrollLeft(!isAtStart);
+ setCanScrollRight(!isAtEnd);
+ };
+
+ useEffect(() => {
+ const carousel = carouselRef.current;
+ if (!carousel) return;
+
+ updateScrollState();
+
+ carousel.addEventListener("scroll", updateScrollState);
+ window.addEventListener("resize", updateScrollState);
+
+ return () => {
+ carousel.removeEventListener("scroll", updateScrollState);
+ window.removeEventListener("resize", updateScrollState);
+ };
+ }, []);
+
const handleScroll = (direction: "left" | "right") => {
if (!carouselRef.current) return;
@@ -916,20 +949,22 @@ export function EpisodesView({
content = (
{/* Horizontal scroll buttons */}
-
-
+ )}
{/* Right scroll button */}
-
-
handleScroll("right")}
+ {canScrollRight && (
+
-
-
-
+ handleScroll("right")}
+ >
+
+
+
+ )}
);
}
diff --git a/src/pages/discover/components/CarouselNavButtons.tsx b/src/pages/discover/components/CarouselNavButtons.tsx
index bd9d3261..24cbdae3 100644
--- a/src/pages/discover/components/CarouselNavButtons.tsx
+++ b/src/pages/discover/components/CarouselNavButtons.tsx
@@ -1,3 +1,5 @@
+import { useCallback, useEffect, useState } from "react";
+
import { Icon, Icons } from "@/components/Icon";
import { Flare } from "@/components/utils/Flare";
@@ -11,9 +13,12 @@ interface CarouselNavButtonsProps {
interface NavButtonProps {
direction: "left" | "right";
onClick: () => void;
+ visible: boolean;
}
-function NavButton({ direction, onClick }: NavButtonProps) {
+function NavButton({ direction, onClick, visible }: NavButtonProps) {
+ if (!visible) return null;
+
return (
{
+ const carousel = carouselRefs.current[categorySlug];
+ if (!carousel) {
+ setCanScrollLeft(false);
+ setCanScrollRight(false);
+ return;
+ }
+
+ const { scrollLeft, scrollWidth, clientWidth } = carousel;
+ const isAtStart = scrollLeft <= 1;
+ const isAtEnd = scrollLeft + clientWidth >= scrollWidth - 1;
+
+ setCanScrollLeft(!isAtStart);
+ setCanScrollRight(!isAtEnd);
+ }, [categorySlug, carouselRefs]);
+
+ useEffect(() => {
+ const carousel = carouselRefs.current[categorySlug];
+ if (!carousel) return;
+
+ updateScrollState();
+
+ carousel.addEventListener("scroll", updateScrollState);
+ window.addEventListener("resize", updateScrollState);
+
+ return () => {
+ carousel.removeEventListener("scroll", updateScrollState);
+ window.removeEventListener("resize", updateScrollState);
+ };
+ }, [categorySlug, carouselRefs, updateScrollState]);
+
const handleScroll = (direction: "left" | "right") => {
const carousel = carouselRefs.current[categorySlug];
if (!carousel) return;
@@ -76,8 +115,16 @@ export function CarouselNavButtons({
return (
<>
- handleScroll("left")} />
- handleScroll("right")} />
+ handleScroll("left")}
+ visible={canScrollLeft}
+ />
+ handleScroll("right")}
+ visible={canScrollRight}
+ />
>
);
}
diff --git a/src/pages/discover/components/CategoryButtons.tsx b/src/pages/discover/components/CategoryButtons.tsx
index 6bb6bfc8..8f7f1c0e 100644
--- a/src/pages/discover/components/CategoryButtons.tsx
+++ b/src/pages/discover/components/CategoryButtons.tsx
@@ -1,3 +1,5 @@
+import { useCallback, useEffect, useState } from "react";
+
import { Icon, Icons } from "@/components/Icon";
interface CategoryButtonsProps {
@@ -15,34 +17,84 @@ export function CategoryButtons({
isMobile,
showAlwaysScroll,
}: CategoryButtonsProps) {
- const renderScrollButton = (direction: "left" | "right") => (
-
- {
- const element = document.getElementById(
- `button-carousel-${categoryType}`,
- );
- if (element) {
- element.scrollBy({
- left: direction === "left" ? -200 : 200,
- behavior: "smooth",
- });
- }
- }}
- >
-
-
-
- );
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
+ const [canScrollRight, setCanScrollRight] = useState(false);
+
+ const updateScrollState = useCallback(() => {
+ const element = document.getElementById(`button-carousel-${categoryType}`);
+ if (!element) {
+ setCanScrollLeft(false);
+ setCanScrollRight(false);
+ return;
+ }
+
+ const { scrollLeft, scrollWidth, clientWidth } = element;
+ const isAtStart = scrollLeft <= 1;
+ const isAtEnd = scrollLeft + clientWidth >= scrollWidth - 1;
+
+ setCanScrollLeft(!isAtStart);
+ setCanScrollRight(!isAtEnd);
+ }, [categoryType]);
+
+ useEffect(() => {
+ const element = document.getElementById(`button-carousel-${categoryType}`);
+ if (!element) return;
+
+ updateScrollState();
+
+ element.addEventListener("scroll", updateScrollState);
+ window.addEventListener("resize", updateScrollState);
+
+ return () => {
+ element.removeEventListener("scroll", updateScrollState);
+ window.removeEventListener("resize", updateScrollState);
+ };
+ }, [categoryType, updateScrollState]);
+
+ useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ updateScrollState();
+ }, 0);
+ return () => clearTimeout(timeoutId);
+ }, [categories, categoryType, updateScrollState]);
+
+ const renderScrollButton = (direction: "left" | "right") => {
+ const shouldShow = direction === "left" ? canScrollLeft : canScrollRight;
+
+ if (!shouldShow && !showAlwaysScroll && !isMobile) return null;
+
+ return (
+
+ {
+ const element = document.getElementById(
+ `button-carousel-${categoryType}`,
+ );
+ if (element) {
+ element.scrollBy({
+ left: direction === "left" ? -200 : 200,
+ behavior: "smooth",
+ });
+ }
+ }}
+ >
+
+
+
+ );
+ };
return (
- {(showAlwaysScroll || isMobile) && renderScrollButton("left")}
+ {(showAlwaysScroll || isMobile || canScrollLeft) &&
+ renderScrollButton("left")}
- {(showAlwaysScroll || isMobile) && renderScrollButton("right")}
+ {(showAlwaysScroll || isMobile || canScrollRight) &&
+ renderScrollButton("right")}
);
}
diff --git a/src/stores/__old/watched/store.ts b/src/stores/__old/watched/store.ts
index 7d5739ad..5bc2f6fe 100644
--- a/src/stores/__old/watched/store.ts
+++ b/src/stores/__old/watched/store.ts
@@ -1,10 +1,10 @@
import { useProgressStore } from "@/stores/progress";
+import { createVersionedStore } from "../migrations";
import { OldData, migrateV2Videos } from "./migrations/v2";
import { migrateV3Videos } from "./migrations/v3";
import { migrateV4Videos } from "./migrations/v4";
import { WatchedStoreData } from "./types";
-import { createVersionedStore } from "../migrations";
export const VideoProgressStore = createVersionedStore()
.setKey("video-progress")