From 5ebed9748d8f97ab9c312a092f132beb9fe3af76 Mon Sep 17 00:00:00 2001 From: Swaim Sahay Date: Sat, 22 Nov 2025 17:13:03 +0530 Subject: [PATCH 1/2] feat: add scroll to top button with progress indicator - Created ScrollToTop component with smooth animations - Added circular progress ring showing scroll position - Gradient purple design with hover effects and tooltip - Optimized scroll event handling with requestAnimationFrame - WCAG compliant with proper ARIA attributes - Responsive and mobile-friendly Closes #204 --- Frontend/src/App.tsx | 2 + Frontend/src/components/ui/scroll-to-top.tsx | 94 ++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 Frontend/src/components/ui/scroll-to-top.tsx diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 60f7ecd..11411b0 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -20,6 +20,7 @@ import PublicRoute from "./components/PublicRoute"; import Dashboard from "./pages/Brand/Dashboard"; import BasicDetails from "./pages/BasicDetails"; import Onboarding from "./components/Onboarding"; +import ScrollToTop from "./components/ui/scroll-to-top"; function App() { const [isLoading, setIsLoading] = useState(true); @@ -45,6 +46,7 @@ function App() { return ( + {/* Public Routes */} } /> diff --git a/Frontend/src/components/ui/scroll-to-top.tsx b/Frontend/src/components/ui/scroll-to-top.tsx new file mode 100644 index 0000000..b347556 --- /dev/null +++ b/Frontend/src/components/ui/scroll-to-top.tsx @@ -0,0 +1,94 @@ +import { useState, useEffect } from "react"; +import { ArrowUp } from "lucide-react"; + +export default function ScrollToTop() { + const [isVisible, setIsVisible] = useState(false); + const [scrollProgress, setScrollProgress] = useState(0); + + // Show button when page is scrolled down and track scroll progress + useEffect(() => { + const toggleVisibility = () => { + const scrolled = window.scrollY; + const windowHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight; + const progress = (scrolled / windowHeight) * 100; + + setScrollProgress(progress); + + if (scrolled > 300) { + setIsVisible(true); + } else { + setIsVisible(false); + } + }; + + window.addEventListener("scroll", toggleVisibility); + toggleVisibility(); // Initial check + + return () => { + window.removeEventListener("scroll", toggleVisibility); + }; + }, []); + + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }; + + return ( +
+ + + {/* Tooltip */} +
+ Back to top +
+
+
+ ); +} From 53c352c052965fce0a8f64d9fcece9fb7fa1ba16 Mon Sep 17 00:00:00 2001 From: Swaim Sahay Date: Sat, 22 Nov 2025 17:32:49 +0530 Subject: [PATCH 2/2] fix: optimize scroll performance and fix progress ring - Add requestAnimationFrame throttling for scroll events - Fix division by zero on non-scrollable pages - Calculate SVG radius dynamically to match rendered size - Add proper cleanup and accessibility attributes --- Frontend/src/components/ui/scroll-to-top.tsx | 102 +++++++++++++------ 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/Frontend/src/components/ui/scroll-to-top.tsx b/Frontend/src/components/ui/scroll-to-top.tsx index b347556..1296906 100644 --- a/Frontend/src/components/ui/scroll-to-top.tsx +++ b/Frontend/src/components/ui/scroll-to-top.tsx @@ -1,31 +1,60 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { ArrowUp } from "lucide-react"; export default function ScrollToTop() { const [isVisible, setIsVisible] = useState(false); const [scrollProgress, setScrollProgress] = useState(0); + const [circleRadius, setCircleRadius] = useState(20); + const rafIdRef = useRef(null); + const svgRef = useRef(null); - // Show button when page is scrolled down and track scroll progress useEffect(() => { - const toggleVisibility = () => { + const updateScrollState = () => { const scrolled = window.scrollY; - const windowHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight; - const progress = (scrolled / windowHeight) * 100; + const documentHeight = document.documentElement.scrollHeight; + const clientHeight = document.documentElement.clientHeight; + const scrollableHeight = documentHeight - clientHeight; - setScrollProgress(progress); + const progress = scrollableHeight > 0 + ? Math.min(100, Math.max(0, (scrolled / scrollableHeight) * 100)) + : 0; - if (scrolled > 300) { - setIsVisible(true); - } else { - setIsVisible(false); + setScrollProgress(progress); + setIsVisible(scrolled > 300); + rafIdRef.current = null; + }; + + const handleScroll = () => { + if (rafIdRef.current === null) { + rafIdRef.current = requestAnimationFrame(updateScrollState); } }; - window.addEventListener("scroll", toggleVisibility); - toggleVisibility(); // Initial check + window.addEventListener("scroll", handleScroll, { passive: true }); + updateScrollState(); return () => { - window.removeEventListener("scroll", toggleVisibility); + window.removeEventListener("scroll", handleScroll); + if (rafIdRef.current !== null) { + cancelAnimationFrame(rafIdRef.current); + } + }; + }, []); + + useEffect(() => { + const updateCircleRadius = () => { + if (svgRef.current) { + const svgRect = svgRef.current.getBoundingClientRect(); + const computedRadius = (Math.min(svgRect.width, svgRect.height) / 2) * 0.45; + setCircleRadius(Math.max(computedRadius, 1)); + } + }; + + updateCircleRadius(); + window.addEventListener("resize", updateCircleRadius, { passive: true }); + + return () => { + window.removeEventListener("resize", updateCircleRadius); }; }, []); @@ -36,25 +65,34 @@ export default function ScrollToTop() { }); }; + const circumference = 2 * Math.PI * circleRadius; + const offset = circumference * (1 - scrollProgress / 100); + return (
- - - {/* Tooltip */} -
- Back to top -
+
);