layout enhancement. Reading position reset
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user