From 1a0edec7128e1358b2b7d4b8c08785e2b031f08e Mon Sep 17 00:00:00 2001 From: Will Miao Date: Tue, 3 Mar 2026 21:18:12 +0800 Subject: [PATCH] feat: enhance supporters modal with auto-scrolling and visual improvements - Add auto-scrolling functionality to supporters list with user interaction controls (pause on hover, manual scroll) - Implement gradient overlays at top/bottom for credits-like appearance - Style custom scrollbar with subtle hover effects for better UX - Adjust padding and positioning to ensure all supporters remain visible during scroll --- static/css/components/modal/support-modal.css | 44 ++++++- static/js/services/supportersService.js | 109 +++++++++++++++++- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/static/css/components/modal/support-modal.css b/static/css/components/modal/support-modal.css index a058f6fb..f52e5106 100644 --- a/static/css/components/modal/support-modal.css +++ b/static/css/components/modal/support-modal.css @@ -378,6 +378,29 @@ flex: 1; display: flex; flex-direction: column; + position: relative; /* Base for masks */ +} + +/* Optional: Fading effect for credits feel at top and bottom */ +.all-supporters-group::before, +.all-supporters-group::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 40px; + pointer-events: none; + z-index: 2; +} + +.all-supporters-group::before { + top: 30px; /* Below the title */ + background: linear-gradient(to bottom, var(--lora-surface), transparent); +} + +.all-supporters-group::after { + bottom: 0; + background: linear-gradient(to top, var(--lora-surface), transparent); } .all-supporters-group .supporters-group-title { @@ -391,8 +414,27 @@ line-height: 2.2; max-height: 550px; overflow-y: auto; - padding: var(--space-2) 0; + padding: var(--space-2) 0 40px 0; /* Extra padding at bottom for final visibility */ color: var(--text-color); + scroll-behavior: auto; /* Ensure manual scroll is immediate */ +} + +/* Subtle scrollbar for credits look */ +.supporters-all-list::-webkit-scrollbar { + width: 4px; +} + +.supporters-all-list::-webkit-scrollbar-track { + background: transparent; +} + +.supporters-all-list::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.05); + border-radius: 4px; +} + +.supporters-all-list:hover::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); } .supporter-name-item { diff --git a/static/js/services/supportersService.js b/static/js/services/supportersService.js index 868a52f8..3e846efb 100644 --- a/static/js/services/supportersService.js +++ b/static/js/services/supportersService.js @@ -60,6 +60,110 @@ export function clearSupportersCache() { supportersData = null; } +let autoScrollRequest = null; +let autoScrollTimeout = null; +let isUserInteracting = false; +let isHovering = false; +let currentScrollPos = 0; + +/** + * Handle user interaction to stop auto-scroll + */ +function handleInteraction() { + isUserInteracting = true; +} + +/** + * Handle mouse enter to pause auto-scroll + */ +function handleMouseEnter() { + isHovering = true; +} + +/** + * Handle mouse leave to resume auto-scroll + */ +function handleMouseLeave() { + isHovering = false; +} + +/** + * Initialize auto-scrolling for the supporters list like movie credits + * @param {HTMLElement} container The scrollable container + */ +function initAutoScroll(container) { + if (!container) return; + + // Stop any existing animation and clear any pending timeout + if (autoScrollRequest) { + cancelAnimationFrame(autoScrollRequest); + autoScrollRequest = null; + } + if (autoScrollTimeout) { + clearTimeout(autoScrollTimeout); + autoScrollTimeout = null; + } + + // Reset state for new scroll + isUserInteracting = false; + isHovering = false; + container.scrollTop = 0; + currentScrollPos = 0; + + const scrollSpeed = 0.4; // Pixels per frame (~24px/sec at 60fps) + + const step = () => { + // Stop animation if container is hidden or no longer in DOM + if (!container.offsetParent) { + autoScrollRequest = null; + return; + } + + if (!isHovering && !isUserInteracting) { + const prevScrollTop = container.scrollTop; + currentScrollPos += scrollSpeed; + container.scrollTop = currentScrollPos; + + // Check if we reached the bottom + if (container.scrollTop === prevScrollTop && currentScrollPos > 1) { + const isAtBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1; + if (isAtBottom) { + autoScrollRequest = null; + return; + } + } + } else { + // Keep currentScrollPos in sync if user scrolls manually or pauses + currentScrollPos = container.scrollTop; + } + + autoScrollRequest = requestAnimationFrame(step); + }; + + // Remove existing listeners before adding to avoid duplicates + container.removeEventListener('mouseenter', handleMouseEnter); + container.removeEventListener('mouseleave', handleMouseLeave); + container.removeEventListener('wheel', handleInteraction); + container.removeEventListener('touchstart', handleInteraction); + container.removeEventListener('mousedown', handleInteraction); + + // Event listeners to handle user control + container.addEventListener('mouseenter', handleMouseEnter); + container.addEventListener('mouseleave', handleMouseLeave); + + // Use { passive: true } for better scroll performance + container.addEventListener('wheel', handleInteraction, { passive: true }); + container.addEventListener('touchstart', handleInteraction, { passive: true }); + container.addEventListener('mousedown', handleInteraction); + + // Initial delay before starting the credits-style scroll + autoScrollTimeout = setTimeout(() => { + if (container.scrollHeight > container.clientHeight) { + autoScrollRequest = requestAnimationFrame(step); + } + }, 1800); +} + /** * Render supporters in the support modal */ @@ -69,9 +173,7 @@ export async function renderSupporters() { // Update subtitle with total count const subtitleEl = document.getElementById('supportersSubtitle'); if (subtitleEl) { - // Get the translation key and replace count const originalText = subtitleEl.textContent; - // Replace the count in the text (simple approach) subtitleEl.textContent = originalText.replace(/\d+/, supporters.totalCount); } @@ -100,5 +202,8 @@ export async function renderSupporters() { `; }) .join(''); + + // Initialize the auto-scroll effect + initAutoScroll(supportersGrid); } }