Skip to content

Commit e8c41ac

Browse files
committed
feat: add scroll-to-top button on all pages
- Create reusable ScrollToTop component with smooth animations - Add ChevronUp icon from lucide-react (no asset needed) - Button appears after scrolling 300px - Smooth fade-in/fade-out animations with Framer Motion - Hover effects with scale and lift animations - Responsive design with mobile-friendly sizing - Dark mode compatible - Accessible with proper ARIA labels - Remove duplicate scroll-to-top button from NewsDetailPage - Add component to App.tsx to appear on all pages
1 parent c26c275 commit e8c41ac

File tree

3 files changed

+59
-27
lines changed

3 files changed

+59
-27
lines changed

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect } from 'react';
22
import { RouterProvider } from 'react-router-dom';
33
import router from '@/routes';
4+
import ScrollToTop from '@/components/shared/ScrollToTop';
45

56
const App = () => {
67
useEffect(() => {
@@ -25,6 +26,7 @@ const App = () => {
2526
return (
2627
<div className="min-h-screen flex flex-col bg-white dark:bg-gray-900">
2728
<RouterProvider router={router} />
29+
<ScrollToTop />
2830
</div>
2931
);
3032
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useState, useEffect } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
import { ChevronUp } from 'lucide-react';
4+
5+
const ScrollToTop = () => {
6+
const [isVisible, setIsVisible] = useState(false);
7+
8+
useEffect(() => {
9+
const toggleVisibility = () => {
10+
if (window.scrollY > 300) {
11+
setIsVisible(true);
12+
} else {
13+
setIsVisible(false);
14+
}
15+
};
16+
17+
window.addEventListener('scroll', toggleVisibility);
18+
19+
return () => {
20+
window.removeEventListener('scroll', toggleVisibility);
21+
};
22+
}, []);
23+
24+
const scrollToTop = () => {
25+
window.scrollTo({
26+
top: 0,
27+
behavior: 'smooth',
28+
});
29+
};
30+
31+
return (
32+
<AnimatePresence>
33+
{isVisible && (
34+
<motion.button
35+
onClick={scrollToTop}
36+
className="fixed bottom-6 right-6 sm:bottom-8 sm:right-8 z-50 p-3 sm:p-4 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
37+
aria-label="Scroll to top"
38+
initial={{ opacity: 0, scale: 0.5, y: 20 }}
39+
animate={{ opacity: 1, scale: 1, y: 0 }}
40+
exit={{ opacity: 0, scale: 0.5, y: 20 }}
41+
whileHover={{ scale: 1.1, y: -2 }}
42+
whileTap={{ scale: 0.95 }}
43+
transition={{
44+
type: 'spring',
45+
stiffness: 500,
46+
damping: 30,
47+
}}
48+
>
49+
<ChevronUp className="h-5 w-5 sm:h-6 sm:w-6" aria-hidden="true" />
50+
</motion.button>
51+
)}
52+
</AnimatePresence>
53+
);
54+
};
55+
56+
export default ScrollToTop;
57+

src/pages/News/NewsDetailPage.tsx

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -361,33 +361,6 @@ const NewsDetailPage: React.FC = () => {
361361
</div>
362362
</div>
363363
)}
364-
365-
{/* Back to Top Button */}
366-
<div className="fixed bottom-8 right-8 z-50">
367-
<motion.button
368-
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
369-
className="p-3 bg-blue-600 text-white rounded-full shadow-lg hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400"
370-
aria-label="Back to top"
371-
whileHover={{ scale: 1.05 }}
372-
whileTap={{ scale: 0.95 }}
373-
>
374-
<svg
375-
xmlns="http://www.w3.org/2000/svg"
376-
className="h-6 w-6"
377-
fill="none"
378-
viewBox="0 0 24 24"
379-
stroke="currentColor"
380-
aria-hidden="true"
381-
>
382-
<path
383-
strokeLinecap="round"
384-
strokeLinejoin="round"
385-
strokeWidth={2}
386-
d="M5 15l7-7 7 7"
387-
/>
388-
</svg>
389-
</motion.button>
390-
</div>
391364
</div>
392365

393366
{/* Image Modal */}

0 commit comments

Comments
 (0)