Skip to content

Commit

Permalink
Merge pull request #141 from systemphil/feature/custom-jsx-editor
Browse files Browse the repository at this point in the history
feature/custom jsx for editor
  • Loading branch information
Firgrep authored Feb 3, 2025
2 parents 8f420ac + 07f2b9d commit 9f1b188
Show file tree
Hide file tree
Showing 15 changed files with 333 additions and 101 deletions.
3 changes: 3 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"Clientside",
"clsx",
"cockroachdb",
"Collingwood",
"cuid",
"Daseiendes",
"Dasein",
Expand All @@ -47,6 +48,7 @@
"Geraets",
"Guyer",
"Hackett",
"Halkyon",
"headeronly",
"Houlgate",
"Identität",
Expand Down Expand Up @@ -98,6 +100,7 @@
"tollere",
"Trisokkas",
"Übergang",
"Unfreedom",
"unmittelbare",
"unseparated",
"unseparatedness",
Expand Down
2 changes: 1 addition & 1 deletion features/courses/components/CourseCardDeleteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function CourseCardDeleteButton({
>
Delete Course
</button>
<Dialog open={isDialogOpen} onClose={onClose}>
<Dialog open={isDialogOpen} onClose={onClose} disableScrollLock>
<DialogContent>
Are you sure you want to delete this {modelName} item? This
action cannot be undone.
Expand Down
2 changes: 1 addition & 1 deletion features/courses/components/CourseMaterialCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function CourseMaterialCard({
>
<ButtonDeleteCross />
</button>
<Dialog open={isDialogOpen} onClose={onClose}>
<Dialog open={isDialogOpen} onClose={onClose} disableScrollLock>
<DialogContent>
Are you sure you want to delete this {modelName} item? This
action cannot be undone.
Expand Down
87 changes: 42 additions & 45 deletions features/courses/components/LessonFrontPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { Heading } from "lib/components/ui/Heading";
import { VideoDataLoader } from "lib/components/VideoDataLoader";
import { Suspense } from "react";
import { Loading } from "lib/components/animations/Loading";
import { FadeIn } from "lib/components/animations/FadeIn";
import { Paragraph } from "lib/components/ui/Paragraph";

export async function LessonFrontPage({ lessonSlug }: { lessonSlug: string }) {
Expand All @@ -34,53 +33,51 @@ export async function LessonFrontPage({ lessonSlug }: { lessonSlug: string }) {
const xxl = "2xl:gap-8 2xl:px-20";

return (
<FadeIn>
<div
className={`flex flex-col justify-center items-center gap-2 ${md} ${lg} ${xl} ${xxl} mb-20`}
>
<div className="min-h-[500px] flex w-full md:col-span-3 md:order-2">
{lessonData.video ? (
<Suspense fallback={<Loading.SkeletonFullPage />}>
<VideoDataLoader videoEntry={lessonData.video} />
</Suspense>
) : (
<div>No video content</div>
)}
<div
className={`flex flex-col justify-center items-center gap-2 ${md} ${lg} ${xl} ${xxl} mb-20`}
>
<div className="min-h-[500px] flex w-full md:col-span-3 md:order-2">
{lessonData.video ? (
<Suspense fallback={<Loading.SkeletonFullPage />}>
<VideoDataLoader videoEntry={lessonData.video} />
</Suspense>
) : (
<div>No video content</div>
)}
</div>
<div className="md:col-span-1 md:p-2 md:order-1 lg:row-span-2">
<div className="flex flex-col justify-start">
<Link
href={`/symposia/courses/${lessonData.course.slug}`}
className="text-base self-center md:self-start text-primary dark:text-gray-500 opacity-70 transition hover:opacity-100 p-2"
>
{`<- Back to ${lessonData.course.name}`}
</Link>
<TableOfLessons
lessons={lessonData.course.lessons}
courseSlug={lessonData.course.slug}
/>
</div>
<div className="md:col-span-1 md:p-2 md:order-1 lg:row-span-2">
<div className="flex flex-col justify-start">
<Link
href={`/symposia/courses/${lessonData.course.slug}`}
className="text-base self-center md:self-start text-primary dark:text-gray-500 opacity-70 transition hover:opacity-100 p-2"
>
{`<- Back to ${lessonData.course.name}`}
</Link>
<TableOfLessons
lessons={lessonData.course.lessons}
courseSlug={lessonData.course.slug}
/>
</div>
</div>
<div className="w-full px-2 md:px-0 md:w-auto md:col-span-4 md:m-4 md:order-3 lg:col-span-2 lg:row-span-2">
<div className="py-3">
<Heading as="h3">{lessonData.name}</Heading>
<Paragraph>{lessonData.description}</Paragraph>
</div>
<div className="w-full px-2 md:px-0 md:w-auto md:col-span-4 md:m-4 md:order-3 lg:col-span-2 lg:row-span-2">
<div className="py-3">
<Heading as="h3">{lessonData.name}</Heading>
<Paragraph>{lessonData.description}</Paragraph>
</div>

{lessonData?.content?.mdxCompiled ? (
<MDXRenderer data={lessonData.content.mdxCompiled} />
) : (
<div>No lesson content</div>
)}
</div>
<div className="md:col-span-4 md:m-4 md:order-4 lg:col-start-2 lg:col-span-3">
{lessonData?.transcript?.mdxCompiled ? (
<MDXRenderer data={lessonData.transcript.mdxCompiled} />
) : (
<div>No transcript</div>
)}
</div>
{lessonData?.content?.mdxCompiled ? (
<MDXRenderer data={lessonData.content.mdxCompiled} />
) : (
<div>No lesson content</div>
)}
</div>
<div className="md:col-span-4 md:m-4 md:order-4 lg:col-start-2 lg:col-span-3">
{lessonData?.transcript?.mdxCompiled ? (
<MDXRenderer data={lessonData.transcript.mdxCompiled} />
) : (
<div>No transcript</div>
)}
</div>
</FadeIn>
</div>
);
}
39 changes: 39 additions & 0 deletions features/editor/components/ButtonInsertTeacherProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DialogButton, insertJsx$, usePublisher } from "@mdxeditor/editor";

export const ButtonInsertTeacherProfile = () => {
const insertJsx = usePublisher(insertJsx$);
// grab the insertDirective action (a.k.a. publisher) from the
// state management system of the directivesPlugin

return (
<DialogButton
autocompleteSuggestions={["filip", "ahilleas"]}
tooltipTitle="Insert teacher profile"
submitButtonTitle="Insert teacher"
dialogInputPlaceholder="name:optional-title"
buttonContent="🧑‍🏫"
onSubmit={(input) => {
const inputLowercase = input.toLocaleLowerCase();
if (
inputLowercase &&
(inputLowercase.includes("filip") ||
inputLowercase.includes("ahilleas"))
) {
insertJsx({
name: "EmbedTeacherProfile",
kind: "text",
props: { teacherInput: inputLowercase },
children: [
{
type: "text",
value: `Teacher profile ${inputLowercase}`,
},
],
});
} else {
alert("Unsupported teacher name");
}
}}
/>
);
};
35 changes: 35 additions & 0 deletions features/editor/components/ButtonInsertYouTube.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DialogButton, insertJsx$, usePublisher } from "@mdxeditor/editor";

export const ButtonInsertYouTube = () => {
const insertJsx = usePublisher(insertJsx$);
// grab the insertDirective action (a.k.a. publisher) from the
// state management system of the directivesPlugin

return (
<DialogButton
tooltipTitle="Insert Youtube video"
submitButtonTitle="Insert video"
dialogInputPlaceholder="Paste the youtube video URL"
buttonContent="YT"
onSubmit={(url) => {
const videoId = new URL(url).searchParams.get("v");
const videoUrl = `https://www.youtube.com/embed/${videoId}`;
if (videoId && videoUrl) {
insertJsx({
name: "EmbedYT",
kind: "text",
props: { src: videoUrl },
children: [
{
type: "text",
value: `YouTube video ${videoId}`,
},
],
});
} else {
alert("Invalid YouTube URL");
}
}}
/>
);
};
31 changes: 30 additions & 1 deletion features/editor/components/EditorInternals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import {
CreateLink,
DiffSourceToggleWrapper,
EditorInFocus,
GenericJsxEditor,
InsertAdmonition,
InsertCodeBlock,
InsertFrontmatter,
InsertImage,
InsertTable,
InsertThematicBreak,
JsxComponentDescriptor,
ListsToggle,
MDXEditor,
MDXEditorMethods,
Expand All @@ -36,6 +38,7 @@ import {
frontmatterPlugin,
headingsPlugin,
imagePlugin,
jsxPlugin,
linkDialogPlugin,
linkPlugin,
listsPlugin,
Expand All @@ -52,6 +55,8 @@ import { Loading } from "lib/components/animations/Loading";
import { actionUploadImage } from "lib/server/actions";
import { sleep } from "lib/utils";
import { actionUpdateMdxModelById } from "../server/actions";
import { ButtonInsertYouTube } from "./ButtonInsertYouTube";
import { ButtonInsertTeacherProfile } from "./ButtonInsertTeacherProfile";

/**
* Context to hold the state of mutation loading as passing props did not work with the MDXEditor Toolbar.
Expand All @@ -72,6 +77,26 @@ export default function EditorInternals({ material, title }: EditorProps) {
const editorRef = React.useRef<MDXEditorMethods>(null);
const [isLoading, setIsLoading] = useState(false);

/**
* Custom JSX components used in Markdown must be registered here.
*/
const jsxComponentDescriptors: JsxComponentDescriptor[] = [
{
name: "EmbedYT",
kind: "text",
props: [{ name: "src", type: "string" }],
hasChildren: true,
Editor: GenericJsxEditor,
},
{
name: "EmbedTeacherProfile",
kind: "text",
props: [{ name: "teacher", type: "string" }],
hasChildren: true,
Editor: GenericJsxEditor,
},
];

const handleSave = async () => {
const markdownValue = editorRef.current?.getMarkdown();
if (!markdownValue) {
Expand Down Expand Up @@ -121,8 +146,9 @@ export default function EditorInternals({ material, title }: EditorProps) {
className="border-2 border-gray-200 rounded-lg full-demo-mdxeditor"
ref={editorRef}
markdown={material.mdx}
contentEditableClassName="prose dark:prose-invert max-w-none"
contentEditableClassName="!prose dark:!prose-invert max-w-none"
plugins={[
jsxPlugin({ jsxComponentDescriptors }),
listsPlugin(),
quotePlugin(),
headingsPlugin(),
Expand Down Expand Up @@ -271,6 +297,9 @@ const DefaultToolbar: React.FC<DefaultToolbarProps> = ({
]}
/>

<ButtonInsertTeacherProfile />
<ButtonInsertYouTube />

<Separator />
<TooltipWrap title="Debug: Print to console">
<Button
Expand Down
Loading

0 comments on commit 9f1b188

Please sign in to comment.