bugfixes, and logging cleanup

This commit is contained in:
Stefan Hardegger
2025-09-21 14:51:21 +02:00
parent 58bb7f8229
commit 0101c0ca2c
11 changed files with 271 additions and 120 deletions

View File

@@ -139,6 +139,15 @@
@apply max-w-full h-auto mx-auto my-6 rounded-lg shadow-sm;
max-height: 80vh; /* Prevent images from being too tall */
display: block;
/* Optimize for performance and prevent reloading */
will-change: auto;
transform: translateZ(0); /* Force hardware acceleration */
backface-visibility: hidden;
image-rendering: optimizeQuality;
/* Prevent layout shifts that might trigger reloads */
box-sizing: border-box;
/* Ensure stable dimensions */
min-height: 1px;
}
.reading-content img[align="left"] {

View File

@@ -11,6 +11,7 @@ import StoryRating from '../../../components/stories/StoryRating';
import TagDisplay from '../../../components/tags/TagDisplay';
import TableOfContents from '../../../components/stories/TableOfContents';
import { sanitizeHtml, preloadSanitizationConfig } from '../../../lib/sanitization';
import { debug } from '../../../lib/debug';
// Memoized content component that only re-renders when content changes
const StoryContent = memo(({
@@ -20,13 +21,50 @@ const StoryContent = memo(({
content: string;
contentRef: React.RefObject<HTMLDivElement>;
}) => {
console.log('🔄 StoryContent component rendering with content length:', content.length);
const renderTime = Date.now();
debug.log('🔄 StoryContent component rendering at', renderTime, 'with content length:', content.length, 'hash:', content.slice(0, 50) + '...');
// Add observer to track image loading events
useEffect(() => {
if (!contentRef.current) return;
const images = contentRef.current.querySelectorAll('img');
debug.log('📸 Found', images.length, 'images in content');
const handleImageLoad = (e: Event) => {
const img = e.target as HTMLImageElement;
debug.log('🖼️ Image loaded:', img.src);
};
const handleImageError = (e: Event) => {
const img = e.target as HTMLImageElement;
debug.log('❌ Image error:', img.src);
};
images.forEach(img => {
img.addEventListener('load', handleImageLoad);
img.addEventListener('error', handleImageError);
debug.log('👀 Monitoring image:', img.src);
});
return () => {
images.forEach(img => {
img.removeEventListener('load', handleImageLoad);
img.removeEventListener('error', handleImageError);
});
};
}, [content]);
return (
<div
ref={contentRef}
className="reading-content"
dangerouslySetInnerHTML={{ __html: content }}
style={{
// Prevent layout shifts that might cause image reloads
minHeight: '100vh',
contain: 'layout style'
}}
/>
);
});
@@ -112,14 +150,14 @@ export default function StoryReadingPage() {
// Debounced function to save reading position
const saveReadingPosition = useCallback(async (position: number) => {
if (!story || position === story.readingPosition) {
console.log('Skipping save - no story or position unchanged:', { story: !!story, position, current: story?.readingPosition });
debug.log('Skipping save - no story or position unchanged:', { story: !!story, position, current: story?.readingPosition });
return;
}
console.log('Saving reading position:', position, 'for story:', story.id);
debug.log('Saving reading position:', position, 'for story:', story.id);
try {
const updatedStory = await storyApi.updateReadingProgress(story.id, position);
console.log('Reading position saved successfully, updated story:', updatedStory.readingPosition);
debug.log('Reading position saved successfully, updated story:', updatedStory.readingPosition);
setStory(prev => prev ? { ...prev, readingPosition: position, lastReadAt: updatedStory.lastReadAt } : null);
} catch (error) {
console.error('Failed to save reading position:', error);
@@ -200,12 +238,12 @@ export default function StoryReadingPage() {
if (story && sanitizedContent && !hasScrolledToPosition) {
// Use a small delay to ensure content is rendered
const timeout = setTimeout(() => {
console.log('Initializing reading position tracking, saved position:', story.readingPosition);
debug.log('Initializing reading position tracking, saved position:', story.readingPosition);
// Check if there's a hash in the URL (for TOC navigation)
const hash = window.location.hash.substring(1);
if (hash && hash.startsWith('heading-')) {
console.log('Auto-scrolling to heading from URL hash:', hash);
debug.log('Auto-scrolling to heading from URL hash:', hash);
const element = document.getElementById(hash);
if (element) {
element.scrollIntoView({
@@ -219,13 +257,13 @@ export default function StoryReadingPage() {
// Otherwise, use saved reading position
if (story.readingPosition && story.readingPosition > 0) {
console.log('Auto-scrolling to saved position:', story.readingPosition);
debug.log('Auto-scrolling to saved position:', story.readingPosition);
const initialPercentage = calculateReadingPercentage(story.readingPosition);
setReadingPercentage(initialPercentage);
scrollToCharacterPosition(story.readingPosition);
} else {
// Even if there's no saved position, mark as ready for tracking
console.log('No saved position, starting fresh tracking');
debug.log('No saved position, starting fresh tracking');
setReadingPercentage(0);
setHasScrolledToPosition(true);
}
@@ -238,8 +276,14 @@ export default function StoryReadingPage() {
// Track reading progress and save position
useEffect(() => {
let ticking = false;
let scrollEventCount = 0;
const handleScroll = () => {
scrollEventCount++;
if (scrollEventCount % 10 === 0) {
debug.log('📜 Scroll event #', scrollEventCount, 'at', Date.now());
}
if (!ticking) {
requestAnimationFrame(() => {
const article = document.querySelector('[data-reading-content]') as HTMLElement;
@@ -278,7 +322,7 @@ export default function StoryReadingPage() {
// 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 });
debug.log('End of story detected:', { nearBottom, highProgress, storyContentFullyVisible, distanceFromBottom, progress });
setHasReachedEnd(true);
setShowEndOfStoryPopup(true);
}
@@ -287,11 +331,11 @@ export default function StoryReadingPage() {
if (hasScrolledToPosition) { // Only save after initial auto-scroll
const characterPosition = getCharacterPositionFromScroll();
const percentage = calculateReadingPercentage(characterPosition);
console.log('Scroll detected, character position:', characterPosition, 'percentage:', percentage);
debug.log('Scroll detected, character position:', characterPosition, 'percentage:', percentage);
setReadingPercentage(percentage);
debouncedSavePosition(characterPosition);
} else {
console.log('Scroll detected but not ready for tracking yet');
debug.log('Scroll detected but not ready for tracking yet');
}
}
ticking = false;

View File

@@ -16,6 +16,7 @@ import { PortableText } from '@portabletext/react';
import Button from '../ui/Button';
import { sanitizeHtmlSync } from '../../lib/sanitization';
import { editorSchema } from '../../lib/portabletext/editorSchema';
import { debug } from '../../lib/debug';
interface PortableTextEditorProps {
value: string; // HTML value for compatibility - will be converted
@@ -394,13 +395,13 @@ function EditorContent({
// Sync HTML value with prop changes
useEffect(() => {
console.log('🔄 Editor value changed:', { valueLength: value?.length, valuePreview: value?.substring(0, 100) });
debug.log('🔄 Editor value changed:', { valueLength: value?.length, valuePreview: value?.substring(0, 100) });
setPortableTextValue(htmlToPortableTextBlocks(value));
}, [value]);
// Debug: log when portableTextValue changes
useEffect(() => {
console.log('📝 Portable text blocks updated:', { blockCount: portableTextValue.length, blocks: portableTextValue });
debug.log('📝 Portable text blocks updated:', { blockCount: portableTextValue.length, blocks: portableTextValue });
}, [portableTextValue]);
// Add a ref to the editor container for direct paste handling
@@ -409,13 +410,13 @@ function EditorContent({
// Global paste event listener to catch ALL paste events
useEffect(() => {
const handleGlobalPaste = (event: ClipboardEvent) => {
console.log('🌍 Global paste event captured');
debug.log('🌍 Global paste event captured');
// Check if the paste is happening within our editor
const target = event.target as Element;
const isInEditor = editorContainerRef.current?.contains(target);
console.log('📋 Paste details:', {
debug.log('📋 Paste details:', {
isInEditor,
targetTag: target?.tagName,
targetClasses: target?.className,
@@ -426,7 +427,7 @@ function EditorContent({
const htmlData = event.clipboardData.getData('text/html');
const textData = event.clipboardData.getData('text/plain');
console.log('📋 Clipboard contents:', {
debug.log('📋 Clipboard contents:', {
htmlLength: htmlData.length,
textLength: textData.length,
hasImages: htmlData.includes('<img'),
@@ -434,7 +435,7 @@ function EditorContent({
});
if (htmlData && htmlData.includes('<img')) {
console.log('📋 Images detected in paste! Attempting to process...');
debug.log('📋 Images detected in paste! Attempting to process...');
// Prevent default paste to handle it completely ourselves
event.preventDefault();
@@ -443,7 +444,7 @@ function EditorContent({
// Convert the pasted HTML to our blocks maintaining order
const pastedBlocks = htmlToPortableTextBlocks(htmlData);
console.log('📋 Converted blocks:', pastedBlocks.map(block => ({
debug.log('📋 Converted blocks:', pastedBlocks.map(block => ({
type: block._type,
key: block._key,
...(block._type === 'image' ? { src: (block as any).src, alt: (block as any).alt } : {}),
@@ -457,7 +458,7 @@ function EditorContent({
const updatedBlocks = [...prev, ...pastedBlocks];
const html = portableTextToHtml(updatedBlocks);
onChange(html);
console.log('📋 Added structured blocks maintaining order:', { pastedCount: pastedBlocks.length, totalBlocks: updatedBlocks.length });
debug.log('📋 Added structured blocks maintaining order:', { pastedCount: pastedBlocks.length, totalBlocks: updatedBlocks.length });
return updatedBlocks;
});
}, 10);
@@ -476,7 +477,7 @@ function EditorContent({
// Handle paste events directly on the editor container (backup approach)
const handleContainerPaste = useCallback((_event: React.ClipboardEvent) => {
console.log('📦 Container paste handler triggered');
debug.log('📦 Container paste handler triggered');
// This might not be reached if global handler prevents default
}, []);
@@ -526,11 +527,11 @@ function EditorContent({
const renderBlock: RenderBlockFunction = useCallback((props) => {
const { schemaType, value, children } = props;
console.log('🎨 Rendering block:', { schemaType: schemaType.name, valueType: value?._type, value });
debug.log('🎨 Rendering block:', { schemaType: schemaType.name, valueType: value?._type, value });
// Handle image blocks
if (schemaType.name === 'image' && isImageBlock(value)) {
console.log('🖼️ Rendering image block:', value);
debug.log('🖼️ Rendering image block:', value);
return (
<div className="my-4 p-3 border border-dashed border-gray-300 rounded-lg bg-gray-50">
<div className="flex items-center gap-2 mb-2">
@@ -654,7 +655,7 @@ export default function PortableTextEditorNew({
storyId,
enableImageProcessing = false
}: PortableTextEditorProps) {
console.log('🎯 Portable Text Editor loaded!', {
debug.log('🎯 Portable Text Editor loaded!', {
valueLength: value?.length,
enableImageProcessing,
hasStoryId: !!storyId

90
frontend/src/lib/debug.ts Normal file
View File

@@ -0,0 +1,90 @@
/**
* Debug logging utility
* Allows conditional logging based on environment or debug flags
*/
// Check if we're in development mode or debug is explicitly enabled
const isDebugEnabled = (): boolean => {
if (typeof window === 'undefined') {
// Server-side: check NODE_ENV
return process.env.NODE_ENV === 'development' || process.env.DEBUG === 'true';
}
// Client-side: check localStorage flag or development mode
try {
return (
process.env.NODE_ENV === 'development' ||
localStorage.getItem('debug') === 'true' ||
window.location.search.includes('debug=true')
);
} catch {
return process.env.NODE_ENV === 'development';
}
};
/**
* Debug logger that only outputs in development or when debug is enabled
*/
export const debug = {
log: (...args: any[]) => {
if (isDebugEnabled()) {
console.log('[DEBUG]', ...args);
}
},
warn: (...args: any[]) => {
if (isDebugEnabled()) {
console.warn('[DEBUG]', ...args);
}
},
error: (...args: any[]) => {
if (isDebugEnabled()) {
console.error('[DEBUG]', ...args);
}
},
group: (label: string) => {
if (isDebugEnabled()) {
console.group(`[DEBUG] ${label}`);
}
},
groupEnd: () => {
if (isDebugEnabled()) {
console.groupEnd();
}
},
time: (label: string) => {
if (isDebugEnabled()) {
console.time(`[DEBUG] ${label}`);
}
},
timeEnd: (label: string) => {
if (isDebugEnabled()) {
console.timeEnd(`[DEBUG] ${label}`);
}
}
};
/**
* Enable debug mode (persists in localStorage)
*/
export const enableDebug = () => {
if (typeof window !== 'undefined') {
localStorage.setItem('debug', 'true');
console.log('Debug mode enabled. Reload page to see debug output.');
}
};
/**
* Disable debug mode
*/
export const disableDebug = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('debug');
console.log('Debug mode disabled. Reload page to hide debug output.');
}
};

View File

@@ -1,5 +1,6 @@
import DOMPurify from 'dompurify';
import { configApi } from './api';
import { debug } from './debug';
interface SanitizationConfig {
allowedTags: string[];
@@ -28,7 +29,7 @@ function filterCssProperties(styleValue: string, allowedProperties: string[]): s
const isAllowed = allowedProperties.includes(property);
if (!isAllowed) {
console.log(`CSS property "${property}" was filtered out (not in allowed list)`);
debug.log(`CSS property "${property}" was filtered out (not in allowed list)`);
}
return isAllowed;
@@ -37,9 +38,9 @@ function filterCssProperties(styleValue: string, allowedProperties: string[]): s
const result = filteredDeclarations.join('; ');
if (declarations.length !== filteredDeclarations.length) {
console.log(`CSS filtering: ${declarations.length} -> ${filteredDeclarations.length} properties`);
console.log('Original:', styleValue);
console.log('Filtered:', result);
debug.log(`CSS filtering: ${declarations.length} -> ${filteredDeclarations.length} properties`);
debug.log('Original:', styleValue);
debug.log('Filtered:', result);
}
return result;
@@ -219,7 +220,7 @@ export function sanitizeHtmlSync(html: string): string {
// If we don't have cached config but there's an ongoing request, wait for it
if (configPromise) {
console.log('Sanitization config loading in progress, using fallback for now');
debug.log('Sanitization config loading in progress, using fallback for now');
} else {
// No config and no ongoing request - try to load it for next time
console.warn('No cached sanitization config available, triggering load for future use');
@@ -229,7 +230,7 @@ export function sanitizeHtmlSync(html: string): string {
}
// Use comprehensive fallback configuration that preserves formatting
console.log('Using fallback sanitization configuration with formatting support');
debug.log('Using fallback sanitization configuration with formatting support');
const fallbackAllowedCssProperties = [
'color', 'font-size', 'font-weight',
'font-style', 'text-align', 'text-decoration', 'margin',