mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
116 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|