Add mdx support for Events content

This commit is contained in:
ArtemBozhko
2026-10-09 12:51:43 +03:00
parent e2d097f918
commit 52326657d3
10 changed files with 5065 additions and 68 deletions
+82 -19
View File
@@ -1,6 +1,23 @@
import { useRef, useState } from 'react';
import type { FormEvent, ChangeEvent } from 'react';
import { createBrowserSupabase } from '../../lib/supabase';
import {
MDXEditor,
headingsPlugin,
listsPlugin,
linkPlugin,
imagePlugin,
quotePlugin,
thematicBreakPlugin,
markdownShortcutPlugin,
toolbarPlugin,
UndoRedo,
BoldItalicUnderlineToggles,
ListsToggle,
BlockTypeSelect,
InsertImage,
} from '@mdxeditor/editor';
import '@mdxeditor/editor/style.css';
import type { Dict } from '../../lib/i18n';
import type { EventRow } from '../../lib/db';
@@ -9,6 +26,11 @@ interface Props {
event?: EventRow | null;
}
interface ContentEditorProps {
content: string;
onChange: (content: string) => void;
}
function pad(n: number): string {
return String(n).padStart(2, '0');
}
@@ -30,6 +52,55 @@ function slugify(value: string): string {
.slice(0, 80);
}
async function imageUploadHandler(file: File): Promise<string> {
try {
const ext = file.name.split('.').pop()?.toLowerCase() || 'jpg';
const path = `content-images/${crypto.randomUUID()}.${ext}`;
const supabase = createBrowserSupabase();
const { data, error: uploadError } = await supabase.storage
.from('event-content-images')
.upload(path, file, { upsert: false, contentType: file.type });
if (uploadError) throw uploadError;
if (!data) throw new Error('No upload path');
const { data: publicUrl } = supabase.storage.from('event-content-images').getPublicUrl(data.path);
return publicUrl.publicUrl;
} catch {
throw new Error('Image upload failed');
}
}
function ContentEditor({ content, onChange }: ContentEditorProps) {
return (
<MDXEditor
markdown={content}
onChange={onChange}
plugins={[
headingsPlugin(),
listsPlugin(),
linkPlugin(),
imagePlugin({ imageUploadHandler: imageUploadHandler }),
quotePlugin(),
thematicBreakPlugin(),
markdownShortcutPlugin(),
toolbarPlugin({
toolbarContents: () => (
<>
<UndoRedo />
<BoldItalicUnderlineToggles />
<BlockTypeSelect />
<ListsToggle />
<InsertImage />
</>
),
}),
]}
contentEditableClassName="prose prose-sm max-w-none textarea"
/>
);
}
export default function EventEditor({ dict: t, event }: Props) {
const [titleUa, setTitleUa] = useState(event?.title?.ua ?? '');
const [titleEn, setTitleEn] = useState(event?.title?.en ?? '');
@@ -94,6 +165,10 @@ export default function EventEditor({ dict: t, event }: Props) {
setError(t.common.error);
return;
}
if (descriptionUa.trim().length > 20000 || descriptionEn.trim().length > 20000) {
setError(t.common.error);
return;
}
const payload = {
title: { ua: titleUa.trim(), en: titleEn.trim() },
@@ -158,25 +233,13 @@ export default function EventEditor({ dict: t, event }: Props) {
</div>
</div>
<div className="grid-2">
<div className="form-field">
<label htmlFor="desc-ua">{t.admin.eventDescUa}</label>
<textarea
className="textarea"
id="desc-ua"
value={descriptionUa}
onChange={(e) => setDescriptionUa(e.target.value)}
/>
</div>
<div className="form-field">
<label htmlFor="desc-en">{t.admin.eventDescEn}</label>
<textarea
className="textarea"
id="desc-en"
value={descriptionEn}
onChange={(e) => setDescriptionEn(e.target.value)}
/>
</div>
<div className="form-field">
<label htmlFor="desc-ua">{t.admin.eventDescUa}</label>
<ContentEditor content={descriptionUa} onChange={setDescriptionUa} />
</div>
<div className="form-field">
<label htmlFor="desc-en">{t.admin.eventDescEn}</label>
<ContentEditor content={descriptionEn} onChange={setDescriptionEn} />
</div>
<div className="grid-2">