diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index af9752d9b..ad00e6a0f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,6 +20,7 @@ import ApplicationFormPage from './pages/ApplicationFormPage/ApplicationFormPage import ApplicantsTab from './pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab'; import ApplicantDetailPage from './pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage'; import ClubUnionPage from './pages/ClubUnionPage/ClubUnionPage'; +import { ScrollToTopButton } from '@/components/common/ScrollToTopButton/ScrollToTopButton'; import 'swiper/css'; const queryClient = new QueryClient(); @@ -29,6 +30,7 @@ const App = () => { + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.styles.ts b/frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.styles.ts new file mode 100644 index 000000000..72d377c80 --- /dev/null +++ b/frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.styles.ts @@ -0,0 +1,21 @@ +import styled from 'styled-components'; + +export const ScrollButton = styled.button<{ $isVisible: boolean }>` + position: fixed; + bottom: 80px; + right: 30px; + z-index: 1000; + opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)}; + visibility: ${({ $isVisible }) => ($isVisible ? 'visible' : 'hidden')}; + transition: opacity 0.3s, visibility 0.3s; + background: none; + border: none; + cursor: pointer; + padding: 0; + + img { + width: 50px; + height: 50px; + display: block; + } +`; \ No newline at end of file diff --git a/frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.tsx b/frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.tsx new file mode 100644 index 000000000..73d43e108 --- /dev/null +++ b/frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.tsx @@ -0,0 +1,23 @@ +import { useScrollTrigger } from '@/hooks/useScrollTrigger'; +import scrollButtonIcon from '@/assets/images/icons/scroll_icon.svg'; +import * as Styled from './ScrollToTopButton.styles'; + + +export const ScrollToTopButton = () => { + const { isTriggered } = useScrollTrigger(); + + const handleScrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return ( + + Scroll to top + + ); +}; \ No newline at end of file diff --git a/frontend/src/hooks/useScrollTrigger.ts b/frontend/src/hooks/useScrollTrigger.ts new file mode 100644 index 000000000..64fbb6041 --- /dev/null +++ b/frontend/src/hooks/useScrollTrigger.ts @@ -0,0 +1,39 @@ +import { useEffect, useState, useCallback } from "react"; + +interface ScrollTriggerOptions { + threshold?: number; + direction?: 'up' | 'down'; + passive?: boolean; + onChange?: (next: boolean) => void; +} + + +export const useScrollTrigger = ({ + threshold = 10, + direction = 'down', + passive = true, + onChange, +}: ScrollTriggerOptions = {}) => { + const [isTriggered, setIsTriggered] = useState(false); + + const handleScroll = useCallback(() => { + const scrollY = window.scrollY; + const shouldShowButton = + direction === 'down' + ? scrollY > threshold + : scrollY < threshold; + + setIsTriggered((prev) => { + if (shouldShowButton === prev) return prev; + onChange?.(shouldShowButton); + return shouldShowButton; + }) + }, [ direction, threshold, onChange]); + + useEffect(() => { + window.addEventListener('scroll', handleScroll, { passive }); + return () => window.removeEventListener('scroll', handleScroll); + }, [handleScroll, passive]); + + return { isTriggered }; +}; \ No newline at end of file