Skip to content

Commit f4bf255

Browse files
authored
Merge pull request #43 from boostcampwm-2024/feature-fe-#6
에디터 컴포넌트 추가
2 parents 99480c3 + 2de5e22 commit f4bf255

File tree

10 files changed

+2291
-9
lines changed

10 files changed

+2291
-9
lines changed

frontend/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44
"version": "0.0.0",
55
"type": "module",
66
"scripts": {
7-
"dev": "vite",
7+
"dev": "vite --host",
88
"build": "tsc -b && vite build",
99
"lint": "eslint .",
10-
"preview": "vite preview"
10+
"preview": "vite preview --host"
1111
},
1212
"dependencies": {
1313
"@tanstack/react-query": "^5.59.19",
1414
"autoprefixer": "^10.4.20",
15+
"class-variance-authority": "^0.7.0",
1516
"clsx": "^2.1.1",
17+
"highlight.js": "^11.10.0",
18+
"lowlight": "^3.1.0",
19+
"novel": "^0.5.0",
1620
"postcss": "^8.4.47",
1721
"react": "^18.3.1",
1822
"react-dom": "^18.3.1",
@@ -22,6 +26,7 @@
2226
},
2327
"devDependencies": {
2428
"@eslint/js": "^9.13.0",
29+
"@tailwindcss/typography": "^0.5.15",
2530
"@types/react": "^18.3.12",
2631
"@types/react-dom": "^18.3.1",
2732
"@vitejs/plugin-react": "^4.3.3",

frontend/src/App.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
22

3+
import Sidebar from "./components/sidebar";
4+
import HoverTrigger from "./components/HoverTrigger";
5+
import Editor from "./components/editor";
6+
import { defaultEditorContent } from "./components/editor/content";
7+
import SideWrapper from "./components/layout/SideWrapper";
8+
39
const queryClient = new QueryClient();
410

511
function App() {
612
return (
713
<QueryClientProvider client={queryClient}>
8-
<div className="text-4xl">hiiii</div>{" "}
14+
<div className="h-screen bg-[#231F20]">
15+
<SideWrapper side="right">
16+
<Editor initialValue={defaultEditorContent} />
17+
</SideWrapper>
18+
<HoverTrigger className="w-64">
19+
<Sidebar />
20+
</HoverTrigger>
21+
</div>
922
</QueryClientProvider>
1023
);
1124
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
export const defaultEditorContent = {
2+
type: "doc",
3+
content: [
4+
{
5+
type: "heading",
6+
attrs: { level: 1 },
7+
content: [{ type: "text", text: "그라운드 룰 작성" }],
8+
},
9+
{
10+
type: "heading",
11+
attrs: { level: 2 },
12+
content: [{ type: "text", text: "코어 시간" }],
13+
},
14+
{
15+
type: "codeBlock",
16+
attrs: { language: "js" },
17+
content: [
18+
{ type: "text", text: "const boostcamp = {\n growth: True;\n}" },
19+
],
20+
},
21+
{
22+
type: "orderedList",
23+
attrs: { tight: true, start: 1 },
24+
content: [
25+
{
26+
type: "listItem",
27+
content: [
28+
{
29+
type: "paragraph",
30+
content: [
31+
{
32+
type: "text",
33+
text: "📅 데일리 스크럼",
34+
},
35+
],
36+
},
37+
],
38+
},
39+
{
40+
type: "listItem",
41+
content: [
42+
{
43+
type: "text",
44+
text: "🧑🏻‍💻 코드 리뷰 & 머지",
45+
},
46+
],
47+
},
48+
{
49+
type: "listItem",
50+
content: [
51+
{
52+
type: "text",
53+
text: "📝 문서화",
54+
},
55+
],
56+
},
57+
{
58+
type: "listItem",
59+
content: [
60+
{
61+
type: "text",
62+
text: "📢 모더레이터 (각종 회의 및 스크럼)",
63+
},
64+
],
65+
},
66+
],
67+
},
68+
69+
{
70+
type: "paragraph",
71+
content: [
72+
{
73+
type: "math",
74+
attrs: {
75+
latex: "E = mc^2",
76+
},
77+
},
78+
],
79+
},
80+
],
81+
};
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import {
2+
TiptapImage,
3+
TiptapLink,
4+
UpdatedImage,
5+
TaskList,
6+
TaskItem,
7+
HorizontalRule,
8+
StarterKit,
9+
Placeholder,
10+
AIHighlight,
11+
CodeBlockLowlight,
12+
Mathematics,
13+
} from "novel/extensions";
14+
import { UploadImagesPlugin } from "novel/plugins";
15+
import { cx } from "class-variance-authority";
16+
import { all, createLowlight } from "lowlight";
17+
18+
const aiHighlight = AIHighlight;
19+
const placeholder = Placeholder;
20+
const tiptapLink = TiptapLink.configure({
21+
HTMLAttributes: {
22+
class: cx(
23+
"text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer",
24+
),
25+
},
26+
});
27+
const lowlight = createLowlight(all);
28+
29+
const tiptapImage = TiptapImage.extend({
30+
addProseMirrorPlugins() {
31+
return [
32+
UploadImagesPlugin({
33+
imageClass: cx("opacity-40 rounded-lg border border-stone-200"),
34+
}),
35+
];
36+
},
37+
}).configure({
38+
allowBase64: true,
39+
HTMLAttributes: {
40+
class: cx("rounded-lg border border-muted"),
41+
},
42+
});
43+
44+
const updatedImage = UpdatedImage.configure({
45+
HTMLAttributes: {
46+
class: cx("rounded-lg border border-muted"),
47+
},
48+
});
49+
50+
const taskList = TaskList.configure({
51+
HTMLAttributes: {
52+
class: cx("not-prose pl-2 "),
53+
},
54+
});
55+
const taskItem = TaskItem.configure({
56+
HTMLAttributes: {
57+
class: cx("flex gap-2 items-start my-4"),
58+
},
59+
nested: true,
60+
});
61+
62+
const horizontalRule = HorizontalRule.configure({
63+
HTMLAttributes: {
64+
class: cx("mt-4 mb-6 border-t border-muted-foreground"),
65+
},
66+
});
67+
68+
const starterKit = StarterKit.configure({
69+
bulletList: {
70+
HTMLAttributes: {
71+
class: cx("list-disc list-outside leading-3 -mt-2"),
72+
},
73+
},
74+
orderedList: {
75+
HTMLAttributes: {
76+
class: cx("list-decimal list-outside leading-3 -mt-2"),
77+
},
78+
},
79+
listItem: {
80+
HTMLAttributes: {
81+
class: cx("leading-normal -mb-2"),
82+
},
83+
},
84+
blockquote: {
85+
HTMLAttributes: {
86+
class: cx("border-l-4 border-primary"),
87+
},
88+
},
89+
codeBlock: {
90+
HTMLAttributes: {
91+
class: cx(
92+
"rounded-md bg-muted text-muted-foreground border p-5 font-mono font-medium bg-slate-200",
93+
),
94+
},
95+
},
96+
code: {
97+
HTMLAttributes: {
98+
class: cx("rounded-md bg-muted px-1.5 py-1 font-mono font-medium"),
99+
spellcheck: "false",
100+
},
101+
},
102+
paragraph: {
103+
HTMLAttributes: {
104+
class: cx(""),
105+
},
106+
},
107+
horizontalRule: false,
108+
dropcursor: {
109+
color: "#DBEAFE",
110+
width: 4,
111+
},
112+
gapcursor: false,
113+
});
114+
115+
const codeBlockLowlight = CodeBlockLowlight.configure({
116+
// configure lowlight: common / all / use highlightJS in case there is a need to specify certain language grammars only
117+
// common: covers 37 language grammars which should be good enough in most cases
118+
lowlight,
119+
});
120+
121+
const mathematics = Mathematics.configure({
122+
HTMLAttributes: {
123+
class: cx("text-foreground rounded p-1 hover:bg-accent cursor-pointer"),
124+
},
125+
katexOptions: {
126+
throwOnError: false,
127+
},
128+
});
129+
export const defaultExtensions = [
130+
starterKit,
131+
placeholder,
132+
tiptapLink,
133+
tiptapImage,
134+
updatedImage,
135+
taskList,
136+
taskItem,
137+
horizontalRule,
138+
aiHighlight,
139+
codeBlockLowlight,
140+
mathematics,
141+
];
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {
2+
EditorRoot,
3+
EditorCommand,
4+
EditorCommandItem,
5+
EditorCommandEmpty,
6+
EditorContent,
7+
type JSONContent,
8+
EditorCommandList,
9+
EditorBubble,
10+
EditorBubbleItem,
11+
} from "novel";
12+
import { ImageResizer, handleCommandNavigation } from "novel/extensions";
13+
import { defaultExtensions } from "./extensions";
14+
15+
import { slashCommand } from "./slash-commands";
16+
17+
import "./prosemirror.css";
18+
19+
const extensions = [...defaultExtensions, slashCommand];
20+
21+
interface EditorProp {
22+
initialValue?: JSONContent;
23+
onChange?: (value: JSONContent) => void;
24+
}
25+
const Editor = ({ initialValue }: EditorProp) => {
26+
return (
27+
<EditorRoot>
28+
<EditorContent
29+
initialContent={initialValue}
30+
className="w-[520px] rounded-xl border bg-white p-2"
31+
{...(initialValue && { initialContent: initialValue })}
32+
extensions={extensions}
33+
editorProps={{
34+
handleDOMEvents: {
35+
keydown: (_view, event) => handleCommandNavigation(event),
36+
},
37+
attributes: {
38+
class: `prose prose-lg prose-headings:font-title font-default focus:outline-none max-w-full`,
39+
},
40+
}}
41+
slotAfter={<ImageResizer />}
42+
>
43+
<EditorCommand className="border-muted bg-background z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border px-1 py-2 shadow-md transition-all">
44+
<EditorCommandEmpty className="text-muted-foreground px-2">
45+
No results
46+
</EditorCommandEmpty>
47+
<EditorCommandList>
48+
<EditorCommandItem
49+
value={"Text"}
50+
onCommand={(val) => console.log(val)}
51+
className={`hover:bg-accent aria-selected:bg-accent flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm`}
52+
>
53+
<div>
54+
<p className="font-medium">Text</p>
55+
<p className="text-muted-foreground text-xs">텍스트</p>
56+
</div>
57+
</EditorCommandItem>
58+
<EditorCommandItem
59+
value={"Text"}
60+
onCommand={(val) => console.log(val)}
61+
className={`hover:bg-accent aria-selected:bg-accent flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm`}
62+
>
63+
<div>
64+
<p className="font-medium">Image</p>
65+
<p className="text-muted-foreground text-xs">이미지</p>
66+
</div>
67+
</EditorCommandItem>
68+
</EditorCommandList>
69+
</EditorCommand>
70+
<EditorBubble
71+
tippyOptions={{
72+
placement: "top",
73+
}}
74+
className="border-muted bg-background flex w-fit max-w-[90vw] overflow-hidden rounded-md border shadow-xl"
75+
>
76+
<EditorBubbleItem>안녕</EditorBubbleItem>
77+
</EditorBubble>
78+
</EditorContent>
79+
</EditorRoot>
80+
);
81+
};
82+
83+
export default Editor;

0 commit comments

Comments
 (0)