layout enhancement. Reading position reset

This commit is contained in:
Stefan Hardegger
2025-09-16 09:34:27 +02:00
parent f92dcc5314
commit c92308c24a
5 changed files with 452 additions and 65 deletions

View File

@@ -24,6 +24,9 @@ export default function StoryReadingPage() {
const [hasScrolledToPosition, setHasScrolledToPosition] = useState(false);
const [showToc, setShowToc] = useState(false);
const [hasHeadings, setHasHeadings] = useState(false);
const [showEndOfStoryPopup, setShowEndOfStoryPopup] = useState(false);
const [hasReachedEnd, setHasReachedEnd] = useState(false);
const [resettingPosition, setResettingPosition] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -194,13 +197,41 @@ export default function StoryReadingPage() {
const articleTop = article.offsetTop;
const articleHeight = article.scrollHeight;
const windowHeight = window.innerHeight;
const progress = Math.min(100, Math.max(0,
const progress = Math.min(100, Math.max(0,
((scrolled - articleTop + windowHeight) / articleHeight) * 100
));
setReadingProgress(progress);
// 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 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;
}
// 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 (debounced)
if (hasScrolledToPosition) { // Only save after initial auto-scroll
const characterPosition = getCharacterPositionFromScroll();
@@ -220,11 +251,11 @@ export default function StoryReadingPage() {
clearTimeout(saveTimeoutRef.current);
}
};
}, [story, hasScrolledToPosition, getCharacterPositionFromScroll, debouncedSavePosition]);
}, [story, hasScrolledToPosition, getCharacterPositionFromScroll, debouncedSavePosition, hasReachedEnd]);
const handleRatingUpdate = async (newRating: number) => {
if (!story) return;
try {
await storyApi.updateRating(story.id, newRating);
setStory(prev => prev ? { ...prev, rating: newRating } : null);
@@ -233,6 +264,25 @@ export default function StoryReadingPage() {
}
};
const handleResetReadingPosition = async () => {
if (!story) return;
try {
setResettingPosition(true);
await storyApi.updateReadingProgress(story.id, 0);
setStory(prev => prev ? { ...prev, readingPosition: 0 } : null);
setShowEndOfStoryPopup(false);
setHasReachedEnd(false);
// DON'T scroll immediately - let user stay at current position
// The reset will take effect when they next open the story
} catch (error) {
console.error('Failed to reset reading position:', error);
} finally {
setResettingPosition(false);
}
};
const findNextStory = (): Story | null => {
if (!story?.seriesId || seriesStories.length <= 1) return null;
@@ -350,6 +400,47 @@ export default function StoryReadingPage() {
</>
)}
{/* End of Story Popup */}
{showEndOfStoryPopup && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black bg-opacity-50 z-50"
onClick={() => setShowEndOfStoryPopup(false)}
/>
{/* Popup Modal */}
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 max-w-md w-full mx-4">
<div className="theme-card theme-shadow rounded-lg p-6">
<div className="text-center">
<h3 className="text-lg font-semibold theme-header mb-3">
🎉 Story Complete!
</h3>
<p className="theme-text mb-6">
You've reached the end of "{story?.title}". Would you like to reset your reading position so the story starts from the beginning next time you open it?
</p>
<div className="flex gap-3 justify-center">
<Button
variant="ghost"
onClick={() => setShowEndOfStoryPopup(false)}
>
Keep Current Position
</Button>
<Button
variant="primary"
onClick={handleResetReadingPosition}
loading={resettingPosition}
>
Reset for Next Time
</Button>
</div>
</div>
</div>
</div>
</>
)}
{/* Story Content */}
<main className="max-w-4xl mx-auto px-4 py-8">
<article data-reading-content>