Files
thrilltrack-explorer/src/components/admin/MarkdownEditor.tsx
2025-10-17 17:30:00 +00:00

116 lines
3.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import MDEditor from '@uiw/react-md-editor';
import { useTheme } from '@/components/theme/ThemeProvider';
import { useAutoSave } from '@/hooks/useAutoSave';
import { CheckCircle2, Loader2, AlertCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import rehypeSanitize from 'rehype-sanitize';
interface MarkdownEditorProps {
value: string;
onChange: (value: string) => void;
onSave?: (value: string) => Promise<void>;
autoSave?: boolean;
height?: number;
placeholder?: string;
}
export function MarkdownEditor({
value,
onChange,
onSave,
autoSave = false,
height = 600,
placeholder = 'Write your content in markdown...'
}: MarkdownEditorProps) {
const { theme } = useTheme();
const [mounted, setMounted] = useState(false);
// Auto-save integration
const { isSaving, lastSaved, error } = useAutoSave({
data: value,
onSave: onSave || (async () => {}),
debounceMs: 3000,
enabled: autoSave && !!onSave,
isValid: value.length > 0
});
useEffect(() => {
setMounted(true);
}, []);
// Prevent hydration mismatch
if (!mounted) {
return (
<div
className="border border-input rounded-lg bg-muted/50 flex items-center justify-center"
style={{ height }}
>
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
);
}
const getLastSavedText = () => {
if (!lastSaved) return null;
const seconds = Math.floor((Date.now() - lastSaved.getTime()) / 1000);
if (seconds < 60) return `Saved ${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
return `Saved ${minutes}m ago`;
};
return (
<div className="space-y-2">
<div data-color-mode={theme}>
<MDEditor
value={value}
onChange={(val) => onChange(val || '')}
height={height}
preview="live"
hideToolbar={false}
enableScroll={true}
textareaProps={{
placeholder
}}
previewOptions={{
rehypePlugins: [[rehypeSanitize]],
className: 'prose dark:prose-invert max-w-none p-4'
}}
/>
</div>
{/* Auto-save status indicator */}
{autoSave && onSave && (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{isSaving && (
<>
<Loader2 className="h-3 w-3 animate-spin" />
<span>Saving...</span>
</>
)}
{!isSaving && lastSaved && !error && (
<>
<CheckCircle2 className="h-3 w-3 text-green-600 dark:text-green-400" />
<span>{getLastSavedText()}</span>
</>
)}
{error && (
<>
<AlertCircle className="h-3 w-3 text-destructive" />
<span className="text-destructive">Failed to save: {error}</span>
</>
)}
</div>
)}
{/* Word and character count */}
<div className="flex justify-between text-xs text-muted-foreground">
<span>Supports markdown formatting with live preview</span>
<span>
{value.split(/\s+/).filter(Boolean).length} words · {value.length} characters
</span>
</div>
</div>
);
}