@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef, useCallback, useMemo, memo } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { storyApi, seriesApi } from '../../../lib/api';
|
||||
@@ -12,27 +12,6 @@ import TagDisplay from '../../../components/tags/TagDisplay';
|
||||
import TableOfContents from '../../../components/stories/TableOfContents';
|
||||
import { sanitizeHtml, preloadSanitizationConfig } from '../../../lib/sanitization';
|
||||
|
||||
// Memoized content component that only re-renders when content changes
|
||||
const StoryContent = memo(({
|
||||
content,
|
||||
contentRef
|
||||
}: {
|
||||
content: string;
|
||||
contentRef: React.RefObject<HTMLDivElement>;
|
||||
}) => {
|
||||
console.log('🔄 StoryContent component rendering with content length:', content.length);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="reading-content"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
StoryContent.displayName = 'StoryContent';
|
||||
|
||||
export default function StoryReadingPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
@@ -237,66 +216,58 @@ export default function StoryReadingPage() {
|
||||
|
||||
// Track reading progress and save position
|
||||
useEffect(() => {
|
||||
let ticking = false;
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
const article = document.querySelector('[data-reading-content]') as HTMLElement;
|
||||
if (article) {
|
||||
const scrolled = window.scrollY;
|
||||
const articleTop = article.offsetTop;
|
||||
const articleHeight = article.scrollHeight;
|
||||
const windowHeight = window.innerHeight;
|
||||
const article = document.querySelector('[data-reading-content]') as HTMLElement;
|
||||
if (article) {
|
||||
const scrolled = window.scrollY;
|
||||
const articleTop = article.offsetTop;
|
||||
const articleHeight = article.scrollHeight;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
const progress = Math.min(100, Math.max(0,
|
||||
((scrolled - articleTop + windowHeight) / articleHeight) * 100
|
||||
));
|
||||
const progress = Math.min(100, Math.max(0,
|
||||
((scrolled - articleTop + windowHeight) / articleHeight) * 100
|
||||
));
|
||||
|
||||
setReadingProgress(progress);
|
||||
setReadingProgress(progress);
|
||||
|
||||
// Multi-method end-of-story detection
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
const windowBottom = scrolled + windowHeight;
|
||||
const distanceFromBottom = documentHeight - windowBottom;
|
||||
// Multi-method end-of-story detection
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
const windowBottom = scrolled + windowHeight;
|
||||
const distanceFromBottom = documentHeight - windowBottom;
|
||||
|
||||
// Method 1: Distance from bottom (most reliable)
|
||||
const nearBottom = distanceFromBottom <= 200;
|
||||
// Method 1: Distance from bottom (most reliable)
|
||||
const nearBottom = distanceFromBottom <= 200;
|
||||
|
||||
// Method 2: High progress but only as secondary check
|
||||
const highProgress = progress >= 98;
|
||||
// Method 2: High progress but only as secondary check
|
||||
const highProgress = progress >= 98;
|
||||
|
||||
// Method 3: Check if story content itself is fully visible
|
||||
const storyContentElement = contentRef.current;
|
||||
let storyContentFullyVisible = false;
|
||||
if (storyContentElement) {
|
||||
const contentRect = storyContentElement.getBoundingClientRect();
|
||||
const contentBottom = scrolled + contentRect.bottom;
|
||||
const documentContentHeight = Math.max(documentHeight - 300, contentBottom); // Account for footer padding
|
||||
storyContentFullyVisible = windowBottom >= documentContentHeight;
|
||||
}
|
||||
// Method 3: Check if story content itself is fully visible
|
||||
const storyContentElement = contentRef.current;
|
||||
let storyContentFullyVisible = false;
|
||||
if (storyContentElement) {
|
||||
const contentRect = storyContentElement.getBoundingClientRect();
|
||||
const contentBottom = scrolled + contentRect.bottom;
|
||||
const documentContentHeight = Math.max(documentHeight - 300, contentBottom); // Account for footer padding
|
||||
storyContentFullyVisible = windowBottom >= documentContentHeight;
|
||||
}
|
||||
|
||||
// Trigger end detection if user is near bottom AND (has high progress OR story content is fully visible)
|
||||
if (nearBottom && (highProgress || storyContentFullyVisible) && !hasReachedEnd && hasScrolledToPosition) {
|
||||
console.log('End of story detected:', { nearBottom, highProgress, storyContentFullyVisible, distanceFromBottom, progress });
|
||||
setHasReachedEnd(true);
|
||||
setShowEndOfStoryPopup(true);
|
||||
}
|
||||
// Trigger end detection if user is near bottom AND (has high progress OR story content is fully visible)
|
||||
if (nearBottom && (highProgress || storyContentFullyVisible) && !hasReachedEnd && hasScrolledToPosition) {
|
||||
console.log('End of story detected:', { nearBottom, highProgress, storyContentFullyVisible, distanceFromBottom, progress });
|
||||
setHasReachedEnd(true);
|
||||
setShowEndOfStoryPopup(true);
|
||||
}
|
||||
|
||||
// Save reading position and update percentage (debounced)
|
||||
if (hasScrolledToPosition) { // Only save after initial auto-scroll
|
||||
const characterPosition = getCharacterPositionFromScroll();
|
||||
const percentage = calculateReadingPercentage(characterPosition);
|
||||
console.log('Scroll detected, character position:', characterPosition, 'percentage:', percentage);
|
||||
setReadingPercentage(percentage);
|
||||
debouncedSavePosition(characterPosition);
|
||||
} else {
|
||||
console.log('Scroll detected but not ready for tracking yet');
|
||||
}
|
||||
}
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
// Save reading position and update percentage (debounced)
|
||||
if (hasScrolledToPosition) { // Only save after initial auto-scroll
|
||||
const characterPosition = getCharacterPositionFromScroll();
|
||||
const percentage = calculateReadingPercentage(characterPosition);
|
||||
console.log('Scroll detected, character position:', characterPosition, 'percentage:', percentage);
|
||||
setReadingPercentage(percentage);
|
||||
debouncedSavePosition(characterPosition);
|
||||
} else {
|
||||
console.log('Scroll detected but not ready for tracking yet');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -358,11 +329,6 @@ export default function StoryReadingPage() {
|
||||
const nextStory = findNextStory();
|
||||
const previousStory = findPreviousStory();
|
||||
|
||||
// Memoize the sanitized content to prevent re-processing on scroll
|
||||
const memoizedContent = useMemo(() => {
|
||||
return sanitizedContent;
|
||||
}, [sanitizedContent]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen theme-bg flex items-center justify-center">
|
||||
@@ -569,10 +535,10 @@ export default function StoryReadingPage() {
|
||||
</header>
|
||||
|
||||
{/* Story Content */}
|
||||
<StoryContent
|
||||
key={`story-content-${story?.id || 'loading'}`}
|
||||
content={memoizedContent}
|
||||
contentRef={contentRef}
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="reading-content"
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
|
||||
/>
|
||||
</article>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user