mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 13:31:13 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
239
src-old/components/admin/MarkdownEditor.tsx
Normal file
239
src-old/components/admin/MarkdownEditor.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
MDXEditor,
|
||||
headingsPlugin,
|
||||
listsPlugin,
|
||||
quotePlugin,
|
||||
thematicBreakPlugin,
|
||||
markdownShortcutPlugin,
|
||||
linkPlugin,
|
||||
linkDialogPlugin,
|
||||
imagePlugin,
|
||||
tablePlugin,
|
||||
codeBlockPlugin,
|
||||
codeMirrorPlugin,
|
||||
diffSourcePlugin,
|
||||
toolbarPlugin,
|
||||
UndoRedo,
|
||||
BoldItalicUnderlineToggles,
|
||||
CodeToggle,
|
||||
ListsToggle,
|
||||
BlockTypeSelect,
|
||||
CreateLink,
|
||||
InsertImage,
|
||||
InsertTable,
|
||||
InsertThematicBreak,
|
||||
DiffSourceToggleWrapper,
|
||||
type MDXEditorMethods
|
||||
} from '@mdxeditor/editor';
|
||||
import '@mdxeditor/editor/style.css';
|
||||
import '@/styles/mdx-editor-theme.css';
|
||||
import { useTheme } from '@/components/theme/ThemeProvider';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import { invokeWithTracking } from '@/lib/edgeFunctionTracking';
|
||||
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
|
||||
import { useAutoSave } from '@/hooks/useAutoSave';
|
||||
import { CheckCircle2, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
|
||||
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): React.JSX.Element {
|
||||
const { theme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
|
||||
const editorRef = useRef<MDXEditorMethods>(null);
|
||||
|
||||
// Resolve "system" theme to actual theme based on OS preference
|
||||
useEffect(() => {
|
||||
if (theme === 'system') {
|
||||
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setResolvedTheme(isDark ? 'dark' : 'light');
|
||||
} else {
|
||||
setResolvedTheme(theme);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
// Listen for OS theme changes when in system mode
|
||||
useEffect(() => {
|
||||
if (theme !== 'system') return;
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handler = (e: MediaQueryListEvent): void => {
|
||||
setResolvedTheme(e.matches ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
}, [theme]);
|
||||
|
||||
// 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 = (): string | null => {
|
||||
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
|
||||
className="border border-input rounded-lg overflow-hidden"
|
||||
style={{ minHeight: height }}
|
||||
>
|
||||
<MDXEditor
|
||||
ref={editorRef}
|
||||
className={cn('mdxeditor', resolvedTheme === 'dark' && 'dark-theme')}
|
||||
markdown={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
contentEditableClassName="prose dark:prose-invert max-w-none p-4 min-h-[500px] mdx-content-area"
|
||||
plugins={[
|
||||
headingsPlugin(),
|
||||
listsPlugin(),
|
||||
quotePlugin(),
|
||||
thematicBreakPlugin(),
|
||||
markdownShortcutPlugin(),
|
||||
linkPlugin(),
|
||||
linkDialogPlugin(),
|
||||
imagePlugin({
|
||||
imageUploadHandler: async (file: File): Promise<string> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const { data, error } = await invokeWithTracking(
|
||||
'upload-image',
|
||||
formData,
|
||||
undefined
|
||||
);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Return Cloudflare CDN URL
|
||||
const imageUrl = getCloudflareImageUrl((data as { id: string }).id, 'public');
|
||||
if (!imageUrl) throw new Error('Failed to generate image URL');
|
||||
|
||||
return imageUrl;
|
||||
} catch (error: unknown) {
|
||||
handleError(error, {
|
||||
action: 'Upload markdown image',
|
||||
metadata: { fileName: file.name }
|
||||
});
|
||||
throw new Error(error instanceof Error ? error.message : 'Failed to upload image');
|
||||
}
|
||||
}
|
||||
}),
|
||||
tablePlugin(),
|
||||
codeBlockPlugin({ defaultCodeBlockLanguage: 'js' }),
|
||||
codeMirrorPlugin({
|
||||
codeBlockLanguages: {
|
||||
js: 'JavaScript',
|
||||
ts: 'TypeScript',
|
||||
tsx: 'TypeScript (React)',
|
||||
jsx: 'JavaScript (React)',
|
||||
css: 'CSS',
|
||||
html: 'HTML',
|
||||
python: 'Python',
|
||||
bash: 'Bash',
|
||||
json: 'JSON',
|
||||
sql: 'SQL'
|
||||
}
|
||||
}),
|
||||
diffSourcePlugin({ viewMode: 'rich-text', diffMarkdown: '' }),
|
||||
toolbarPlugin({
|
||||
toolbarContents: () => (
|
||||
<>
|
||||
<UndoRedo />
|
||||
<BoldItalicUnderlineToggles />
|
||||
<CodeToggle />
|
||||
<BlockTypeSelect />
|
||||
<ListsToggle />
|
||||
<CreateLink />
|
||||
<InsertImage />
|
||||
<InsertTable />
|
||||
<InsertThematicBreak />
|
||||
<DiffSourceToggleWrapper>
|
||||
<span className="text-sm">Source</span>
|
||||
</DiffSourceToggleWrapper>
|
||||
</>
|
||||
)
|
||||
})
|
||||
]}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user