| @@ -0,0 +1,90 @@ | |||
| /* | |||
| Copyright (C) 2025 QuantumNous | |||
| This program is free software: you can redistribute it and/or modify | |||
| it under the terms of the GNU Affero General Public License as | |||
| published by the Free Software Foundation, either version 3 of the | |||
| License, or (at your option) any later version. | |||
| This program is distributed in the hope that it will be useful, | |||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| GNU Affero General Public License for more details. | |||
| You should have received a copy of the GNU Affero General Public License | |||
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
| For commercial licensing, please contact support@quantumnous.com | |||
| */ | |||
| import React, { useState, useEffect, useCallback } from 'react'; | |||
| const ScrollNavigator = ({ sections }) => { | |||
| const [activeIndex, setActiveIndex] = useState(0); | |||
| const getActiveSection = useCallback(() => { | |||
| const container = document.querySelector('.home-snap-container'); | |||
| if (!container) return 0; | |||
| const scrollTop = container.scrollTop; | |||
| const containerHeight = container.clientHeight; | |||
| const scrollCenter = scrollTop + containerHeight * 0.4; | |||
| let closestIndex = 0; | |||
| let closestDistance = Infinity; | |||
| sections.forEach((section, index) => { | |||
| const el = document.getElementById(section.id); | |||
| if (el) { | |||
| const rect = el.getBoundingClientRect(); | |||
| const elCenter = rect.top + rect.height / 2 + scrollTop; | |||
| const distance = Math.abs(elCenter - scrollCenter); | |||
| if (distance < closestDistance) { | |||
| closestDistance = distance; | |||
| closestIndex = index; | |||
| } | |||
| } | |||
| }); | |||
| return closestIndex; | |||
| }, [sections]); | |||
| useEffect(() => { | |||
| const container = document.querySelector('.home-snap-container'); | |||
| if (!container) return; | |||
| const handleScroll = () => { | |||
| const newIndex = getActiveSection(); | |||
| setActiveIndex(newIndex); | |||
| }; | |||
| container.addEventListener('scroll', handleScroll, { passive: true }); | |||
| handleScroll(); // 初始化 | |||
| return () => container.removeEventListener('scroll', handleScroll); | |||
| }, [getActiveSection]); | |||
| const scrollToSection = (id) => { | |||
| const el = document.getElementById(id); | |||
| if (el) { | |||
| el.scrollIntoView({ behavior: 'smooth' }); | |||
| } | |||
| }; | |||
| return ( | |||
| <nav className="scroll-navigator"> | |||
| {sections.map((section, index) => ( | |||
| <button | |||
| key={section.id} | |||
| className={`scroll-navigator-dot ${activeIndex === index ? 'active' : ''}`} | |||
| onClick={() => scrollToSection(section.id)} | |||
| title={section.label} | |||
| aria-label={section.label} | |||
| /> | |||
| ))} | |||
| </nav> | |||
| ); | |||
| }; | |||
| export default ScrollNavigator; | |||