Skip to content

Commit c0e0d2e

Browse files
authored
Merge pull request #67 from boostcampwm-2024/feature-fe-#66
에디터 content 자동 저장 기능 추가
2 parents 54908eb + 10c7877 commit c0e0d2e

File tree

3 files changed

+84
-89
lines changed

3 files changed

+84
-89
lines changed

frontend/src/api/page.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ export const deletePage = async (id: number) => {
3434
return res.data;
3535
};
3636

37-
export const updatePage = async (id: number, pageData: PageRequest) => {
37+
export const updatePage = async (id: number, pageData: JSONContent) => {
3838
const url = `/page/${id}`;
3939

40-
const res = await Patch<null, PageRequest>(url, pageData);
40+
const res = await Patch<null, JSONContent>(url, pageData);
4141
return res.data;
4242
};

frontend/src/components/editor/index.tsx

Lines changed: 80 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { ColorSelector } from "./selectors/color-selector";
2424

2525
import { useDebouncedCallback } from "use-debounce";
2626

27+
import { useUpdatePage } from "@/hooks/usePages";
28+
2729
const extensions = [...defaultExtensions, slashCommand];
2830

2931
interface EditorProp {
@@ -40,31 +42,22 @@ const Editor = ({ pageId, initialValue }: EditorProp) => {
4042
const [openNode, setOpenNode] = useState(false);
4143
const [openColor, setOpenColor] = useState(false);
4244
const [openLink, setOpenLink] = useState(false);
45+
const [saveStatus, setSaveStatus] = useState("Saved");
4346

44-
const highlightCodeblocks = (content: string) => {
45-
const doc = new DOMParser().parseFromString(content, "text/html");
46-
doc.querySelectorAll("pre code").forEach((el) => {
47-
// @ts-expect-error - highlightElement is not in the types
48-
// https://highlightjs.readthedocs.io/en/latest/api.html?highlight=highlightElement#highlightelement
49-
hljs.highlightElement(el);
50-
});
51-
return new XMLSerializer().serializeToString(doc);
52-
};
47+
const updatePageMutation = useUpdatePage();
5348

5449
const debouncedUpdates = useDebouncedCallback(
5550
async (editor: EditorInstance) => {
5651
if (pageId === undefined) return;
5752

5853
const json = editor.getJSON();
59-
window.localStorage.setItem(
60-
"html-content",
61-
highlightCodeblocks(editor.getHTML()),
62-
);
63-
window.localStorage.setItem(pageId.toString(), JSON.stringify(json));
64-
window.localStorage.setItem(
65-
"markdown",
66-
editor.storage.markdown.getMarkdown(),
67-
);
54+
const response = await updatePageMutation.mutateAsync({
55+
id: pageId,
56+
pageData: json,
57+
});
58+
if (response) {
59+
setSaveStatus("Saved");
60+
}
6861
},
6962

7063
500,
@@ -75,72 +68,78 @@ const Editor = ({ pageId, initialValue }: EditorProp) => {
7568
if (content) setInitialContent(JSON.parse(content));
7669
}, [pageId]);
7770

78-
console.log(initialContent);
79-
8071
return (
81-
<EditorRoot>
82-
<EditorContent
83-
initialContent={initialContent === null ? undefined : initialContent}
84-
className="relative h-[720px] w-[520px] overflow-auto border-muted bg-background bg-white sm:rounded-lg sm:border sm:shadow-lg"
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-
}}
98-
>
99-
<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">
100-
<EditorCommandEmpty className="px-2 text-muted-foreground">
101-
No results
102-
</EditorCommandEmpty>
103-
<EditorCommandList>
104-
{suggestionItems.map((item) => (
105-
<EditorCommandItem
106-
value={item.title}
107-
onCommand={(val) => item.command?.(val)}
108-
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"
109-
key={item.title}
110-
>
111-
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-muted bg-background">
112-
{item.icon}
113-
</div>
114-
<div>
115-
<p className="font-medium">{item.title}</p>
116-
<p className="text-xs text-muted-foreground">
117-
{item.description}
118-
</p>
119-
</div>
120-
</EditorCommandItem>
121-
))}
122-
</EditorCommandList>
123-
</EditorCommand>
124-
<EditorBubble
125-
tippyOptions={{
126-
placement: "top",
72+
<div className="relative h-[720px] w-[520px] overflow-auto border-muted bg-background bg-white sm:rounded-lg sm:border sm:shadow-lg">
73+
<div className="absolute right-5 top-5 z-10 mb-5 flex gap-2">
74+
<div className="rounded-lg bg-accent px-2 py-1 text-sm text-muted-foreground">
75+
{saveStatus}
76+
</div>
77+
</div>
78+
<EditorRoot>
79+
<EditorContent
80+
initialContent={initialContent === null ? undefined : initialContent}
81+
className=""
82+
extensions={extensions}
83+
editorProps={{
84+
handleDOMEvents: {
85+
keydown: (_view, event) => handleCommandNavigation(event),
86+
},
87+
attributes: {
88+
class: `prose prose-lg prose-headings:font-title font-default focus:outline-none max-w-full`,
89+
},
90+
}}
91+
slotAfter={<ImageResizer />}
92+
onUpdate={({ editor }) => {
93+
debouncedUpdates(editor);
94+
setSaveStatus("Unsaved");
12795
}}
128-
className="flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl"
12996
>
130-
{" "}
131-
<Separator orientation="vertical" />
132-
<NodeSelector open={openNode} onOpenChange={setOpenNode} />
133-
<Separator orientation="vertical" />
134-
<LinkSelector open={openLink} onOpenChange={setOpenLink} />
135-
<Separator orientation="vertical" />
136-
<MathSelector />
137-
<Separator orientation="vertical" />
138-
<TextButtons />
139-
<Separator orientation="vertical" />
140-
<ColorSelector open={openColor} onOpenChange={setOpenColor} />
141-
</EditorBubble>
142-
</EditorContent>
143-
</EditorRoot>
97+
<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">
98+
<EditorCommandEmpty className="px-2 text-muted-foreground">
99+
No results
100+
</EditorCommandEmpty>
101+
<EditorCommandList>
102+
{suggestionItems.map((item) => (
103+
<EditorCommandItem
104+
value={item.title}
105+
onCommand={(val) => item.command?.(val)}
106+
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"
107+
key={item.title}
108+
>
109+
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-muted bg-background">
110+
{item.icon}
111+
</div>
112+
<div>
113+
<p className="font-medium">{item.title}</p>
114+
<p className="text-xs text-muted-foreground">
115+
{item.description}
116+
</p>
117+
</div>
118+
</EditorCommandItem>
119+
))}
120+
</EditorCommandList>
121+
</EditorCommand>
122+
<EditorBubble
123+
tippyOptions={{
124+
placement: "top",
125+
}}
126+
className="flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl"
127+
>
128+
{" "}
129+
<Separator orientation="vertical" />
130+
<NodeSelector open={openNode} onOpenChange={setOpenNode} />
131+
<Separator orientation="vertical" />
132+
<LinkSelector open={openLink} onOpenChange={setOpenLink} />
133+
<Separator orientation="vertical" />
134+
<MathSelector />
135+
<Separator orientation="vertical" />
136+
<TextButtons />
137+
<Separator orientation="vertical" />
138+
<ColorSelector open={openColor} onOpenChange={setOpenColor} />
139+
</EditorBubble>
140+
</EditorContent>
141+
</EditorRoot>
142+
</div>
144143
);
145144
};
146145

frontend/src/hooks/usePages.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
updatePage,
77
type PageRequest,
88
} from "@/api/page";
9+
import { JSONContent } from "novel";
910

1011
export const usePages = () => {
1112
const { data, isError } = useQuery({
@@ -40,13 +41,8 @@ export const useDeletePage = () => {
4041
};
4142

4243
export const useUpdatePage = () => {
43-
const queryClient = useQueryClient();
44-
4544
return useMutation({
46-
mutationFn: ({ id, pageData }: { id: number; pageData: PageRequest }) =>
45+
mutationFn: ({ id, pageData }: { id: number; pageData: JSONContent }) =>
4746
updatePage(id, pageData),
48-
onSuccess: () => {
49-
queryClient.invalidateQueries({ queryKey: ["pages"] });
50-
},
5147
});
5248
};

0 commit comments

Comments
 (0)