Skip to content

Commit 507183f

Browse files
committed
Markdown editor integration
1 parent 1d8efc4 commit 507183f

7 files changed

+4285
-733
lines changed

app/globals.css

+62-18
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,6 @@
22
@tailwind components;
33
@tailwind utilities;
44

5-
body {
6-
font-family: Arial, Helvetica, sans-serif;
7-
}
8-
9-
@layer utilities {
10-
.text-balance {
11-
text-wrap: balance;
12-
}
13-
}
14-
155
@layer base {
166
:root {
177
--background: 0 0% 100%;
@@ -33,13 +23,14 @@ body {
3323
--border: 214.3 31.8% 91.4%;
3424
--input: 214.3 31.8% 91.4%;
3525
--ring: 222.2 84% 4.9%;
26+
--radius: 0.5rem;
3627
--chart-1: 12 76% 61%;
3728
--chart-2: 173 58% 39%;
3829
--chart-3: 197 37% 24%;
3930
--chart-4: 43 74% 66%;
4031
--chart-5: 27 87% 67%;
41-
--radius: 0.5rem;
4232
}
33+
4334
.dark {
4435
--background: 222.2 84% 4.9%;
4536
--foreground: 210 40% 98%;
@@ -72,21 +63,74 @@ body {
7263
* {
7364
@apply border-border;
7465
}
75-
/* body {
76-
@apply bg-background text-foreground;
77-
} */
66+
7867
h1 {
7968
@apply text-4xl md:text-6xl xl:text-7xl font-bold;
8069
}
70+
8171
h2 {
82-
@apply text-4xl lg:text-4xl font-medium;
72+
@apply text-xl lg:text-4xl font-medium;
8373
}
74+
8475
h3 {
8576
@apply text-2xl lg:text-4xl font-medium;
8677
}
87-
78+
79+
a {
80+
@apply hover:cursor-pointer;
81+
}
82+
8883
button {
89-
@apply hover:cursor-pointer
90-
bg-purple-600 hover:bg-purple-700;
84+
@apply hover:cursor-pointer bg-purple-600 hover:bg-purple-700;
85+
}
86+
87+
/* Markdown content styles */
88+
.markdown-content {
89+
@apply mx-auto;
90+
}
91+
92+
.markdown-content h1 {
93+
@apply text-3xl font-bold mt-8 mb-4;
94+
}
95+
96+
.markdown-content h2 {
97+
@apply text-2xl font-semibold mt-6 mb-3;
98+
}
99+
100+
.markdown-content h3 {
101+
@apply text-xl font-medium mt-4 mb-2;
102+
}
103+
104+
.markdown-content p {
105+
@apply mb-4 leading-7;
106+
}
107+
108+
.markdown-content ul,
109+
.markdown-content ol {
110+
@apply mb-4 pl-6;
111+
}
112+
113+
.markdown-content li {
114+
@apply mb-2 list-disc;
115+
}
116+
117+
.markdown-content a {
118+
@apply text-blue-600 hover:underline;
119+
}
120+
121+
.markdown-content blockquote {
122+
@apply border-l-4 border-gray-300 pl-4 italic my-4;
123+
}
124+
125+
.markdown-content code {
126+
@apply bg-gray-100 rounded px-1 py-0.5 font-mono text-sm;
127+
}
128+
129+
.markdown-content pre {
130+
@apply bg-gray-100 rounded p-4 overflow-x-auto my-4;
131+
}
132+
133+
.markdown-content pre code {
134+
@apply bg-transparent p-0;
91135
}
92136
}

app/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const fontSans = FontSans({
1414
});
1515

1616
export const metadata: Metadata = {
17-
title: "SpeakEasyAI Demo",
17+
title: "SpeaklyAI",
1818
description:
1919
"Convert your video or voice into a Blog Post in seconds with the power of AI!",
2020
icons: {

components/content/content-editor.tsx

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"use client";
2+
3+
import { useCallback, useState } from "react";
4+
import BgGradient from "../common/bg-gradient";
5+
import { ForwardRefEditor } from "./forward-ref-editor";
6+
import { useFormState, useFormStatus } from "react-dom";
7+
import { updatePostAction } from "@/actions/edit-actions";
8+
import { Button } from "../ui/button";
9+
import { Download, Edit2, Loader2 } from "lucide-react";
10+
11+
function SubmitButton() {
12+
const { pending } = useFormStatus();
13+
return (
14+
<Button
15+
type="submit"
16+
className={`w-40 bg-gradient-to-r from-purple-900 to-indigo-600 hover:from-purple-600 hover:to-indigo-900 text-white font-semibold py-2 px-4 rounded-full shadow-lg transform transition duration-200 ease-in-out hover:scale-105 focus:outline-none focus:ring-2`}
17+
disabled={pending}
18+
>
19+
{pending ? (
20+
<span className="flex items-center justify-center">
21+
<Loader2 className="w-5 h-5 mr-2 animate-spin" /> Updating...
22+
</span>
23+
) : (
24+
<span className="flex items-center justify-center">
25+
<Edit2 className="w-5 h-5 mr-2" />
26+
Update Text
27+
</span>
28+
)}
29+
</Button>
30+
);
31+
}
32+
33+
const initialState = {
34+
success: false,
35+
};
36+
37+
type UploadState = {
38+
success: boolean;
39+
};
40+
41+
type UploadAction = (
42+
state: UploadState,
43+
formData: FormData
44+
) => Promise<UploadState>;
45+
46+
export default function ContentEditor({
47+
posts,
48+
}: {
49+
posts: Array<{ content: string; title: string; id: string }>;
50+
}) {
51+
const [content, setContent] = useState(posts[0].content);
52+
const [isChanged, setIsChanged] = useState(false);
53+
54+
const updatedPostActionWithId = updatePostAction.bind(null, {
55+
postId: posts[0].id,
56+
content,
57+
});
58+
59+
const [state, formAction] = useFormState<UploadState, FormData>(
60+
updatedPostActionWithId as unknown as UploadAction,
61+
initialState
62+
);
63+
64+
const handleContentChange = (value: string) => {
65+
setContent(value);
66+
setIsChanged(true);
67+
};
68+
69+
const handleExport = useCallback(() => {
70+
const filename = `${posts[0].title || "blog-post"}.md`;
71+
72+
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
73+
const url = URL.createObjectURL(blob);
74+
const link = document.createElement("a");
75+
link.href = url;
76+
link.download = filename;
77+
document.body.appendChild(link);
78+
link.click();
79+
document.body.removeChild(link);
80+
URL.revokeObjectURL(url);
81+
}, [content, posts]);
82+
83+
return (
84+
<form action={formAction} className="flex flex-col gap-2">
85+
<div className="flex justify-between items-center border-b-2 border-gray-200/50 pb-4">
86+
<div>
87+
<h2 className="text-2xl font-bold text-gray-800 mb-2 flex items-center gap-2">
88+
📝 Edit your post
89+
</h2>
90+
<p className="text-gray-600">Start editing your blog post below...</p>
91+
</div>
92+
<div className="flex gap-4">
93+
<SubmitButton></SubmitButton>
94+
<Button
95+
onClick={handleExport}
96+
className="w-40 bg-gradient-to-r from-amber-500 to-amber-900 hover:from-amber-600 hover:to-amber-700 text-white font-semibold py-2 px-4 rounded-full shadow-lg transform transition duration-200 ease-in-out hover:scale-105 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-opacity-50"
97+
>
98+
<Download className="w-5 h-5 mr-2" />
99+
Export
100+
</Button>
101+
</div>
102+
</div>
103+
<BgGradient className="opacity-20">
104+
<ForwardRefEditor
105+
markdown={posts[0].content}
106+
className="markdown-content border-dotted border-gray-200 border-2 p-4 rounded-md animate-in ease-in-out duration-75"
107+
onChange={handleContentChange}
108+
></ForwardRefEditor>
109+
</BgGradient>
110+
</form>
111+
);
112+
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"use client";
2+
3+
import { MDXEditorMethods, MDXEditorProps } from "@mdxeditor/editor";
4+
import dynamic from "next/dynamic";
5+
import { forwardRef } from "react";
6+
7+
// ForwardRefEditor.tsx
8+
9+
// This is the only place InitializedMDXEditor is imported directly.
10+
const Editor = dynamic(() => import("./mdx-editor"), {
11+
// Make sure we turn SSR off
12+
ssr: false,
13+
});
14+
15+
// This is what is imported by other components. Pre-initialized with plugins, and ready
16+
// to accept other props, including a ref.
17+
export const ForwardRefEditor = forwardRef<MDXEditorMethods, MDXEditorProps>(
18+
(props, ref) => <Editor {...props} editorRef={ref} />
19+
);
20+
21+
// TS complains without the following line
22+
ForwardRefEditor.displayName = "ForwardRefEditor";

components/content/mdx-editor.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"use client";
2+
// InitializedMDXEditor.tsx
3+
import type { ForwardedRef } from "react";
4+
import {
5+
headingsPlugin,
6+
listsPlugin,
7+
quotePlugin,
8+
thematicBreakPlugin,
9+
markdownShortcutPlugin,
10+
MDXEditor,
11+
type MDXEditorMethods,
12+
type MDXEditorProps,
13+
} from "@mdxeditor/editor";
14+
15+
// Only import this to the next file
16+
export default function InitializedMDXEditor({
17+
editorRef,
18+
...props
19+
}: { editorRef: ForwardedRef<MDXEditorMethods> | null } & MDXEditorProps) {
20+
return (
21+
<MDXEditor
22+
plugins={[
23+
// Example Plugin Usage
24+
headingsPlugin(),
25+
listsPlugin(),
26+
quotePlugin(),
27+
thematicBreakPlugin(),
28+
markdownShortcutPlugin(),
29+
]}
30+
{...props}
31+
ref={editorRef}
32+
/>
33+
);
34+
}

0 commit comments

Comments
 (0)