mono/packages/ui/src/components/MarkdownEditor.tsx
2026-01-20 10:34:09 +01:00

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
);
});