richtext replacement

This commit is contained in:
Stefan Hardegger
2025-09-21 10:10:04 +02:00
parent aae8f8926b
commit b1dbd85346
28 changed files with 3337 additions and 10558 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useState, useEffect, useRef, useCallback, useMemo, memo } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import { storyApi, seriesApi } from '../../../lib/api';
@@ -12,6 +12,27 @@ 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();
@@ -216,58 +237,66 @@ export default function StoryReadingPage() {
// Track reading progress and save position
useEffect(() => {
let ticking = false;
const handleScroll = () => {
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;
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 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');
}
// 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;
}
};
@@ -329,6 +358,11 @@ 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">
@@ -535,10 +569,10 @@ export default function StoryReadingPage() {
</header>
{/* Story Content */}
<div
ref={contentRef}
className="reading-content"
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
<StoryContent
key={`story-content-${story?.id || 'loading'}`}
content={memoizedContent}
contentRef={contentRef}
/>
</article>