153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
|
import { ImagePickerDialog } from '@/components/widgets/ImagePickerDialog';
|
|
import { supabase } from '@/integrations/supabase/client';
|
|
|
|
|
|
// Lazy load the heavy editor component
|
|
const MilkdownEditorInternal = React.lazy(() => import('@/components/lazy-editors/MilkdownEditorInternal'));
|
|
|
|
interface MarkdownEditorProps {
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
className?: string;
|
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
|
}
|
|
|
|
const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
|
|
value,
|
|
onChange,
|
|
placeholder = "Enter description...",
|
|
className = "",
|
|
onKeyDown
|
|
}) => {
|
|
const [activeTab, setActiveTab] = useState<'editor' | 'raw'>('editor');
|
|
const [imagePickerOpen, setImagePickerOpen] = useState(false);
|
|
const pendingImageResolveRef = useRef<((url: string) => void) | null>(null);
|
|
|
|
// Handler for image upload - opens the ImagePickerDialog
|
|
const handleImageUpload = useCallback((_file?: File): Promise<string> => {
|
|
console.log('[handleImageUpload] Called from image-block, opening ImagePickerDialog');
|
|
return new Promise((resolve) => {
|
|
pendingImageResolveRef.current = resolve;
|
|
console.log('[handleImageUpload] Resolve function stored in ref');
|
|
setImagePickerOpen(true);
|
|
});
|
|
}, []);
|
|
|
|
// Handler for image selection from picker
|
|
const handleImageSelect = useCallback(async (pictureId: string) => {
|
|
console.log('[handleImageSelect] Selected picture ID:', pictureId);
|
|
try {
|
|
// Fetch the image URL from Supabase
|
|
const { data, error } = await supabase
|
|
.from('pictures')
|
|
.select('image_url')
|
|
.eq('id', pictureId)
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
const imageUrl = data.image_url;
|
|
|
|
const resolveFunc = pendingImageResolveRef.current;
|
|
|
|
if (resolveFunc) {
|
|
pendingImageResolveRef.current = null;
|
|
resolveFunc(imageUrl);
|
|
}
|
|
|
|
setImagePickerOpen(false);
|
|
} catch (error) {
|
|
console.error('[handleImageSelect] Error fetching image:', error);
|
|
if (pendingImageResolveRef.current) {
|
|
pendingImageResolveRef.current('');
|
|
pendingImageResolveRef.current = null;
|
|
}
|
|
setImagePickerOpen(false);
|
|
}
|
|
}, []);
|
|
|
|
const handleRawChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
onChange(e.target.value);
|
|
}, [onChange]);
|
|
|
|
return (
|
|
<>
|
|
<div className={`border rounded-md bg-background ${className}`}>
|
|
<div className="flex border-b">
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('editor')}
|
|
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${activeTab === 'editor'
|
|
? 'border-primary text-primary'
|
|
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
|
}`}
|
|
>
|
|
Editor
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('raw')}
|
|
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${activeTab === 'raw'
|
|
? 'border-primary text-primary'
|
|
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
|
}`}
|
|
>
|
|
Markdown
|
|
</button>
|
|
</div>
|
|
{activeTab === 'editor' && (
|
|
<React.Suspense fallback={<div className="p-3 text-muted-foreground">Loading editor...</div>}>
|
|
<MilkdownEditorInternal
|
|
value={value}
|
|
onChange={onChange}
|
|
className={className}
|
|
/>
|
|
</React.Suspense>
|
|
)}
|
|
{activeTab === 'raw' && (
|
|
<textarea
|
|
value={value || ''}
|
|
onChange={handleRawChange}
|
|
onKeyDown={onKeyDown}
|
|
placeholder={placeholder}
|
|
className="w-full p-3 bg-transparent border-0 rounded-b-md focus:ring-0 focus:outline-none resize-none font-mono text-sm"
|
|
style={{ height: '120px', minHeight: '120px' }}
|
|
aria-label="Raw markdown input"
|
|
autoFocus
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Image Picker Dialog */}
|
|
<ImagePickerDialog
|
|
isOpen={imagePickerOpen}
|
|
onClose={() => {
|
|
setImagePickerOpen(false);
|
|
// Reject the promise if closed without selection
|
|
if (pendingImageResolveRef.current) {
|
|
pendingImageResolveRef.current('');
|
|
pendingImageResolveRef.current = null;
|
|
}
|
|
}}
|
|
onSelect={handleImageSelect}
|
|
currentValue={null}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
MarkdownEditor.displayName = 'MarkdownEditor';
|
|
|
|
// Memoize with custom comparison
|
|
export default React.memo(MarkdownEditor, (prevProps, nextProps) => {
|
|
// Re-render if value, placeholder, className, or onKeyDown change
|
|
return (
|
|
prevProps.value === nextProps.value &&
|
|
prevProps.placeholder === nextProps.placeholder &&
|
|
prevProps.className === nextProps.className &&
|
|
prevProps.onKeyDown === nextProps.onKeyDown
|
|
// onChange is intentionally omitted to prevent unnecessary re-renders
|
|
);
|
|
}); |