Skip to content

Commit 0cd685a

Browse files
authored
Merge pull request #82 from boostcampwm-2024/feature-fe-#81
EditorTitle 컴포넌트 추가
2 parents 94ca108 + 9b944b0 commit 0cd685a

File tree

7 files changed

+231
-124
lines changed

7 files changed

+231
-124
lines changed

frontend/src/App.tsx

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

33
import Sidebar from "./components/sidebar";
44
import HoverTrigger from "./components/HoverTrigger";
5-
import EditorView from "./components/EditorView";
5+
import EditorView from "./components/editor/EditorView";
66
import SideWrapper from "./components/layout/SideWrapper";
77
import Canvas from "./components/canvas";
88

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
interface EditorTitleProps {
2+
title?: string;
3+
onTitleChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
4+
}
5+
6+
export default function EditorTitle({
7+
title,
8+
onTitleChange,
9+
}: EditorTitleProps) {
10+
return (
11+
<div className="p-12 pb-0">
12+
<input
13+
type="text"
14+
className="w-full text-xl font-bold outline-none"
15+
defaultValue={"제목없음"}
16+
value={title}
17+
onChange={onTitleChange}
18+
/>
19+
</div>
20+
);
21+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import usePageStore from "@/store/usePageStore";
2+
import Editor from ".";
3+
import {
4+
usePage,
5+
useUpdatePage,
6+
useOptimisticUpdatePage,
7+
} from "@/hooks/usePages";
8+
import EditorLayout from "../layout/EditorLayout";
9+
import EditorTitle from "./EditorTitle";
10+
import { EditorInstance } from "novel";
11+
import { useState } from "react";
12+
import SaveStatus from "./ui/SaveStatus";
13+
import { useDebouncedCallback } from "use-debounce";
14+
15+
export default function EditorView() {
16+
const { currentPage } = usePageStore();
17+
const { data, isLoading } = usePage(currentPage);
18+
const pageTitle = data?.title ?? "제목없음";
19+
const pageContent = data?.content ?? {};
20+
21+
const updatePageMutation = useUpdatePage();
22+
const optimisticUpdatePageMutation = useOptimisticUpdatePage({
23+
id: currentPage ?? 0,
24+
});
25+
26+
const [saveStatus, setSaveStatus] = useState<"saved" | "unsaved">("saved");
27+
28+
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
29+
setSaveStatus("unsaved");
30+
31+
optimisticUpdatePageMutation.mutate(
32+
{
33+
pageData: { title: e.target.value, content: pageContent },
34+
},
35+
{
36+
onSuccess: () => setSaveStatus("saved"),
37+
onError: () => setSaveStatus("unsaved"),
38+
},
39+
);
40+
};
41+
42+
const handleEditorUpdate = useDebouncedCallback(
43+
async ({ editor }: { editor: EditorInstance }) => {
44+
if (currentPage === null) {
45+
return;
46+
}
47+
48+
const json = editor.getJSON();
49+
50+
setSaveStatus("unsaved");
51+
updatePageMutation.mutate(
52+
{ id: currentPage, pageData: { title: pageTitle, content: json } },
53+
{
54+
onSuccess: () => setSaveStatus("saved"),
55+
onError: () => setSaveStatus("unsaved"),
56+
},
57+
);
58+
},
59+
500,
60+
);
61+
62+
if (isLoading || !data || currentPage === null) {
63+
return <div>로딩 중,,</div>;
64+
}
65+
66+
return (
67+
<EditorLayout>
68+
<SaveStatus saveStatus={saveStatus} />
69+
<EditorTitle title={pageTitle} onTitleChange={handleTitleChange} />
70+
<Editor
71+
key={currentPage}
72+
initialContent={pageContent}
73+
pageId={currentPage}
74+
onEditorUpdate={handleEditorUpdate}
75+
/>
76+
</EditorLayout>
77+
);
78+
}

frontend/src/components/editor/index.tsx

Lines changed: 65 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from "react";
1+
import { useState } from "react";
22
import {
33
EditorRoot,
44
EditorCommand,
@@ -22,127 +22,83 @@ import { MathSelector } from "./selectors/math-selector";
2222
import { TextButtons } from "./selectors/text-buttons";
2323
import { ColorSelector } from "./selectors/color-selector";
2424

25-
import { useDebouncedCallback } from "use-debounce";
26-
import { useUpdatePage } from "@/hooks/usePages";
27-
2825
const extensions = [...defaultExtensions, slashCommand];
2926

27+
type EditorUpdateEvent = {
28+
editor: EditorInstance;
29+
};
3030
interface EditorProp {
3131
pageId: number;
32-
initialValue?: JSONContent;
33-
onChange?: (value: JSONContent) => void;
32+
initialContent?: JSONContent;
33+
onEditorUpdate?: (event: EditorUpdateEvent) => void;
3434
}
3535

36-
// TODO: 나중에 title input 추가해야함
37-
const Editor = ({ pageId, initialValue }: EditorProp) => {
38-
const [initialContent, setInitialContent] = useState<null | JSONContent>(
39-
initialValue === undefined ? null : initialValue,
40-
);
41-
42-
const updatePageMutation = useUpdatePage(pageId);
43-
36+
const Editor = ({ initialContent, onEditorUpdate }: EditorProp) => {
4437
const [openNode, setOpenNode] = useState(false);
4538
const [openColor, setOpenColor] = useState(false);
4639
const [openLink, setOpenLink] = useState(false);
47-
const [saveStatus, setSaveStatus] = useState("Saved");
48-
49-
const debouncedUpdates = useDebouncedCallback(
50-
async (editor: EditorInstance) => {
51-
if (pageId === undefined) return;
52-
53-
const json = editor.getJSON();
54-
55-
const response = await updatePageMutation.mutateAsync({
56-
id: pageId,
57-
pageData: {
58-
title: "제목 없음",
59-
content: json,
60-
},
61-
});
62-
if (response) {
63-
setSaveStatus("Saved");
64-
}
65-
},
66-
500,
67-
);
68-
69-
useEffect(() => {
70-
const content = window.localStorage.getItem(pageId.toString());
71-
if (content) setInitialContent(JSON.parse(content));
72-
}, [pageId]);
7340

7441
return (
75-
<div className="relative h-[720px] w-[520px] overflow-auto border-muted bg-background bg-white sm:rounded-lg sm:border sm:shadow-lg">
76-
<div className="absolute right-5 top-5 z-10 mb-5 flex gap-2">
77-
<div className="rounded-lg bg-accent px-2 py-1 text-sm text-muted-foreground">
78-
{saveStatus}
79-
</div>
80-
</div>
81-
<EditorRoot>
82-
<EditorContent
83-
initialContent={initialContent === null ? undefined : initialContent}
84-
className=""
85-
extensions={extensions}
86-
editorProps={{
87-
handleDOMEvents: {
88-
keydown: (_view, event) => handleCommandNavigation(event),
89-
},
90-
attributes: {
91-
class: `prose prose-lg prose-headings:font-title font-default focus:outline-none max-w-full`,
92-
},
93-
}}
94-
slotAfter={<ImageResizer />}
95-
onUpdate={({ editor }) => {
96-
debouncedUpdates(editor);
97-
setSaveStatus("Unsaved");
42+
<EditorRoot>
43+
<EditorContent
44+
initialContent={initialContent}
45+
className=""
46+
extensions={extensions}
47+
editorProps={{
48+
handleDOMEvents: {
49+
keydown: (_view, event) => handleCommandNavigation(event),
50+
},
51+
attributes: {
52+
class: `prose prose-lg prose-headings:font-title font-default focus:outline-none max-w-full`,
53+
},
54+
}}
55+
slotAfter={<ImageResizer />}
56+
onUpdate={onEditorUpdate}
57+
>
58+
<EditorCommand className="z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border border-muted bg-background px-1 py-2 shadow-md transition-all">
59+
<EditorCommandEmpty className="px-2 text-muted-foreground">
60+
No results
61+
</EditorCommandEmpty>
62+
<EditorCommandList>
63+
{suggestionItems.map((item) => (
64+
<EditorCommandItem
65+
value={item.title}
66+
onCommand={(val) => item.command?.(val)}
67+
className="flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:cursor-pointer hover:bg-accent aria-selected:bg-accent"
68+
key={item.title}
69+
>
70+
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-muted bg-background">
71+
{item.icon}
72+
</div>
73+
<div>
74+
<p className="font-medium">{item.title}</p>
75+
<p className="text-xs text-muted-foreground">
76+
{item.description}
77+
</p>
78+
</div>
79+
</EditorCommandItem>
80+
))}
81+
</EditorCommandList>
82+
</EditorCommand>
83+
<EditorBubble
84+
tippyOptions={{
85+
placement: "top",
9886
}}
87+
className="flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl"
9988
>
100-
<EditorCommand className="z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border border-muted bg-background px-1 py-2 shadow-md transition-all">
101-
<EditorCommandEmpty className="px-2 text-muted-foreground">
102-
No results
103-
</EditorCommandEmpty>
104-
<EditorCommandList>
105-
{suggestionItems.map((item) => (
106-
<EditorCommandItem
107-
value={item.title}
108-
onCommand={(val) => item.command?.(val)}
109-
className="flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:cursor-pointer hover:bg-accent aria-selected:bg-accent"
110-
key={item.title}
111-
>
112-
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-muted bg-background">
113-
{item.icon}
114-
</div>
115-
<div>
116-
<p className="font-medium">{item.title}</p>
117-
<p className="text-xs text-muted-foreground">
118-
{item.description}
119-
</p>
120-
</div>
121-
</EditorCommandItem>
122-
))}
123-
</EditorCommandList>
124-
</EditorCommand>
125-
<EditorBubble
126-
tippyOptions={{
127-
placement: "top",
128-
}}
129-
className="flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl"
130-
>
131-
{" "}
132-
<Separator orientation="vertical" />
133-
<NodeSelector open={openNode} onOpenChange={setOpenNode} />
134-
<Separator orientation="vertical" />
135-
<LinkSelector open={openLink} onOpenChange={setOpenLink} />
136-
<Separator orientation="vertical" />
137-
<MathSelector />
138-
<Separator orientation="vertical" />
139-
<TextButtons />
140-
<Separator orientation="vertical" />
141-
<ColorSelector open={openColor} onOpenChange={setOpenColor} />
142-
</EditorBubble>
143-
</EditorContent>
144-
</EditorRoot>
145-
</div>
89+
<Separator orientation="vertical" />
90+
<NodeSelector open={openNode} onOpenChange={setOpenNode} />
91+
<Separator orientation="vertical" />
92+
<LinkSelector open={openLink} onOpenChange={setOpenLink} />
93+
<Separator orientation="vertical" />
94+
<MathSelector />
95+
<Separator orientation="vertical" />
96+
<TextButtons />
97+
<Separator orientation="vertical" />
98+
<ColorSelector open={openColor} onOpenChange={setOpenColor} />
99+
</EditorBubble>
100+
</EditorContent>
101+
</EditorRoot>
146102
);
147103
};
148104

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export interface SaveStatusProps {
2+
saveStatus: "saved" | "unsaved";
3+
}
4+
5+
export default function SaveStatus({ saveStatus }: SaveStatusProps) {
6+
return (
7+
<div className="absolute right-5 top-5 z-10">
8+
<div className="rounded-lg bg-accent px-2 py-1 text-sm text-muted-foreground">
9+
{saveStatus}
10+
</div>
11+
</div>
12+
);
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
interface EditorLayoutProps {
2+
children: React.ReactNode;
3+
}
4+
5+
export default function EditorLayout({ children }: EditorLayoutProps) {
6+
return (
7+
<div className="relative h-[720px] w-[520px] overflow-auto border-muted bg-background bg-white sm:rounded-lg sm:border sm:shadow-lg">
8+
{children}
9+
</div>
10+
);
11+
}

0 commit comments

Comments
 (0)