Skip to content

Feat/text-editor-change #234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
342c7db
install: ํ•„์š” ํŒจํ‚ค์ง€ ์ธ์Šคํ†จ
hyoloui Feb 27, 2023
158e3bd
delete(toastUI): ๊ธฐ์กด ์—๋””ํ„ฐ ์‚ญ์ œ
hyoloui Feb 27, 2023
7537290
feat(PostEditor): MDEditor ์ถ”๊ฐ€
hyoloui Feb 27, 2023
4eed639
install: ํ•„์š” ํŒจํ‚ค์ง€ ์ธ์Šคํ†จ
hyoloui Feb 27, 2023
904a088
feat(DetailContent): ๋ทฐ์–ด ์ ์šฉ
hyoloui Feb 27, 2023
9f884da
fix(DetailContent): ๋ทฐ์–ด ๋„š์ด ์ˆ˜์ •
hyoloui Feb 27, 2023
8aef73a
fix(Post): content ์ด๋‹ˆ์…œ string ์ˆ˜์ •
hyoloui Feb 27, 2023
c22a4f2
feat(PostEditor): MDEditor ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ถ”๊ฐ€
hyoloui Feb 27, 2023
d5a14f6
fix(postId): post_id๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
hyoloui Feb 27, 2023
b0cf9d6
update: ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜ ๋ฐ ์‚ญ์ œ
hyoloui Feb 28, 2023
2ba907f
fix(PostEditor): build error ํ•ด๊ฒฐ
hyoloui Feb 28, 2023
adf14dc
fix(console): ์ฝ˜์†” ์ œ๊ฑฐ
hyoloui Feb 28, 2023
015848a
feat(PostEditor): commands toolbar ์ˆ˜์ •
hyoloui Feb 28, 2023
1c177bc
fix(PostEditor): PR ํ”ผ๋“œ๋ฐฑ ๋ฐ˜์˜
hyoloui Feb 28, 2023
6441411
fix(PostEditor): PR ํ”ผ๋“œ๋ฐฑ ๋ฐ˜์˜
hyoloui Feb 28, 2023
79a9c27
fix(PostEditor): PR ํ”ผ๋“œ๋ฐฑ ๋ฐ˜์˜
hyoloui Feb 28, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Components/CreatePost/Post/Post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
postTags,
postTitle,
postTitleBackgroundColor,
postId,
} from "@/lib/recoil";
import supabase from "@/lib/supabase";
import { useState, useEffect } from "react";
Expand All @@ -29,6 +30,7 @@ import ProjectInfo from "./ProjectInfo";
* @TODO user_id ๋ฆฌ์ฝ”์ผ๋กœ ๊ด€๋ฆฌ
*/
const Post: NextPage = () => {
const [isPostId] = useRecoilState(postId);
const [title, setTitle] = useRecoilState(postTitle);
const [subTitle, setSubTitle] = useRecoilState(postSubTitle);
const [titleBackgroundColor, setTitleBackgroundColor] = useRecoilState(
Expand All @@ -54,6 +56,7 @@ const Post: NextPage = () => {
const router = useRouter();

const newPostRow = {
id: isPostId,
title,
sub_title: subTitle,
title_background_color: titleBackgroundColor,
Expand Down Expand Up @@ -162,7 +165,7 @@ const Post: NextPage = () => {
setTag([]);
setIsPublic(true);
setMembers([]);
setContent("ํ”„๋กœ์ ํŠธ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.");
setContent("");
setPostLargeCategory("");
setPostSubCategory("");
};
Expand Down
252 changes: 160 additions & 92 deletions Components/CreatePost/PostEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,77 @@
import "@toast-ui/editor/dist/toastui-editor.css";
import "@toast-ui/editor/dist/theme/toastui-editor-dark.css";
import "tui-color-picker/dist/tui-color-picker.css";
import "@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css";
import "@toast-ui/editor/dist/i18n/ko-kr";
import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css";
import "prismjs/themes/prism.css";

import { Editor } from "@toast-ui/react-editor";
import colorSyntax from "@toast-ui/editor-plugin-color-syntax";
import codeSyntaxHighlightPlugin from "@toast-ui/editor-plugin-code-syntax-highlight";
import Prism from "prismjs";
import { RefObject, useCallback, useEffect } from "react";
import "@uiw/react-md-editor/markdown-editor.css";
import "@uiw/react-markdown-preview/markdown.css";

import { useCallback } from "react";
import dynamic from "next/dynamic";
import supabase from "@/lib/supabase";
import imageCompression from "browser-image-compression";
import { v4 as uuidv4 } from "uuid";
import { useRecoilState } from "recoil";
import { postContent as recoilPostContent } from "@/lib/recoil";
import { postContent as recoilPostContent, postId } from "@/lib/recoil";
import imageCompression from "browser-image-compression";
import type { MDEditorProps } from "@uiw/react-md-editor";
import { NextPage } from "next";
import * as commands from "@uiw/react-md-editor/lib/commands";

/**
* @TODO ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ์‹œ ๋งํฌ ์—ด๋ฆฌ๋Š” ๋ฌธ์ œ ํ•ด๊ฒฐ ํ•„์š”
* @TODO storage ์‚ญ์ œ ๊ตฌํ˜„ ํ•„์š”
* @TODO uuid flag ๊ฝƒ์•„์•ผ ํ•จ >> ๊ฒŒ์‹œ์™€ ์ž„์‹œ์ €์žฅ์˜ ์šฉ๋„๋กœ ๋ถ„๋ฅ˜
*/

interface PostEditorProps {
editorRef: RefObject<Editor>;
}
const MDEditor = dynamic<MDEditorProps>(() => import("@uiw/react-md-editor"), {
ssr: false,
});

const PostEditor = ({ editorRef }: PostEditorProps) => {
const PostEditor: NextPage = () => {
const [isPostId] = useRecoilState(postId);
const [postContent, setPostContent] = useRecoilState(recoilPostContent);
const toolbarItems = [
["heading", "bold", "italic", "strike"],
["hr"],
["ul", "ol", "task"],
["table", "link"],
["image"], // <-- ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ ํˆด๋ฐ”
["code"],
["scrollSync"],
];

// ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
type HookCallback = (url: string, text?: string) => void;

const addImage = useCallback(async (blob: File, dropImage: HookCallback) => {
const img = await compressImg(blob); // ์ด๋ฏธ์ง€ ์••์ถ•
if (!img) return;
const url = await uploadImage(img); // ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ ์„œ๋ฒ„ url
if (!url) return;
dropImage(url, `${blob.name}`); // ์—๋””ํ„ฐ์— ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
}, []);

useEffect(() => {
if (editorRef.current) {
const editorIns = editorRef.current.getInstance();
editorIns.removeHook("addImageBlobHook");
editorIns.addHook("addImageBlobHook", addImage);
}
}, [editorRef, addImage]);

// ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ

const uploadImage = async (blob: File) => {
try {
const imgPath = crypto.randomUUID();
await supabase.storage.from("post-image").upload(imgPath, blob);

// ์ด๋ฏธ์ง€ ์˜ฌ๋ฆฌ๊ธฐ
const urlResult = await supabase.storage
.from("post-image")
.getPublicUrl(imgPath);
return urlResult.data.publicUrl;
} catch (error) {
console.log(error);
return false;
}
};

// //์ด๋ฏธ์ง€ ์••์ถ•
const onImagePasted = useCallback(
async (
dataTransfer: DataTransfer | any // Drag and Drop API
) => {
const files: File[] = []; // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž์œผ๋กœ ๊ฐ€์ ธ์˜จ ํŒŒ์ผ๋“ค
if (dataTransfer.items) {
for (let index = 0; index < dataTransfer.items.length; index += 1) {
const file = dataTransfer.items[index].getAsFile();
if (!file) return;
files.push(file);
}
} else {
const file = dataTransfer[0];

if (!file) return;
files.push(file);
}

const fileId = uuidv4();
files.map(async (file) => {
const compressedFile = await compressImg(file);

if (!compressedFile) return;

const { data: uploadImg } = await supabase.storage
.from("post-image")
.upload(`${isPostId}/${fileId}`, compressedFile);
if (!uploadImg) return;

const { data: insertedMarkdown } = supabase.storage
.from("post-image")
.getPublicUrl(`${isPostId}/${fileId}`);
if (!insertedMarkdown) return;

const insertString = `![${file.name}](${insertedMarkdown.publicUrl})`;
const resultString = insertToTextArea(insertString);

setPostContent(resultString || "");
});
},
[isPostId, setPostContent]
);

// ์ด๋ฏธ์ง€ ์••์ถ•
const compressImg = async (blob: File): Promise<File | void> => {
const options = {
maxSize: 1,
maxSizeMB: 1,
initialQuality: 0.55, // initial 0.7
};
const result = await imageCompression(blob, options)
Expand All @@ -86,35 +80,109 @@ const PostEditor = ({ editorRef }: PostEditorProps) => {
return result;
};

const handleOnEditorChange = () => {
// ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
const editorText = editorRef.current?.getInstance().getMarkdown();
if (editorText === " " || editorText === "" || editorText === undefined) {
return;
// ์—๋””ํ„ฐ์— ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
const insertToTextArea = (intsertString: string) => {
const textarea = document.querySelector("textarea");
if (!textarea) {
return null;
}
// HTML ๋Œ€์‹ ์— Markdown์œผ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
setPostContent(editorText);
let sentence = textarea.value;
const len = sentence.length;
const pos = textarea.selectionStart;
const end = textarea.selectionEnd;

const front = sentence.slice(0, pos);
const back = sentence.slice(pos, len);

sentence = front + intsertString + back;

textarea.value = sentence;
textarea.selectionEnd = end + intsertString.length;
return sentence;
};

return (
<Editor
ref={editorRef}
initialValue={postContent ?? null}
previewStyle="vertical"
height="600px"
initialEditType="markdown"
useCommandShortcut
toolbarItems={toolbarItems}
language="ko-KR"
plugins={[
colorSyntax,
[codeSyntaxHighlightPlugin, { highlighter: Prism }],
]}
hooks={{
// @ts-ignore
addImageBlobHook: addImage,
// div์— ํด๋ž˜์Šค๋ฅผ ์ ์šฉํ•˜์—ฌ ๋‹คํฌ๋ชจ๋“œ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
<MDEditor
value={postContent}
onChange={(value) => {
setPostContent(value || "");
}}
onChange={() => handleOnEditorChange()}
height={600}
onPaste={(event) => {
onImagePasted(event.clipboardData);
}}
onDrop={(event) => {
onImagePasted(event.dataTransfer);
}}
textareaProps={{
placeholder: "Fill in your markdown for the coolest of the cool.",
}}
commands={[
commands.bold,
commands.italic,
commands.strikethrough,
commands.hr,
commands.title,
commands.divider,

commands.link,
commands.group([], {
name: "image",
groupName: "image",
icon: (
<svg
fill="#444541"
height="12"
width="12"
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 489.4 489.4"
>
<g>
<g>
<path
d="M0,437.8c0,28.5,23.2,51.6,51.6,51.6h386.2c28.5,0,51.6-23.2,51.6-51.6V51.6c0-28.5-23.2-51.6-51.6-51.6H51.6
C23.1,0,0,23.2,0,51.6C0,51.6,0,437.8,0,437.8z M437.8,464.9H51.6c-14.9,0-27.1-12.2-27.1-27.1v-64.5l92.8-92.8l79.3,79.3
c4.8,4.8,12.5,4.8,17.3,0l143.2-143.2l107.8,107.8v113.4C464.9,452.7,452.7,464.9,437.8,464.9z M51.6,24.5h386.2
c14.9,0,27.1,12.2,27.1,27.1v238.1l-99.2-99.1c-4.8-4.8-12.5-4.8-17.3,0L205.2,333.8l-79.3-79.3c-4.8-4.8-12.5-4.8-17.3,0
l-84.1,84.1v-287C24.5,36.7,36.7,24.5,51.6,24.5z"
/>
<path
d="M151.7,196.1c34.4,0,62.3-28,62.3-62.3s-28-62.3-62.3-62.3s-62.3,28-62.3,62.3S117.3,196.1,151.7,196.1z M151.7,96
c20.9,0,37.8,17,37.8,37.8s-17,37.8-37.8,37.8s-37.8-17-37.8-37.8S130.8,96,151.7,96z"
/>
</g>
</g>
</svg>
),
// eslint-disable-next-line react/no-unstable-nested-components
children: (handle: any) => {
return (
<div style={{ width: 200, padding: 10 }}>
<input
type="file"
accept="image/*"
onChange={(e) => onImagePasted(e.target.files)}
/>
<button type="button" onClick={() => handle.close()}>
close
</button>
</div>
);
},
buttonProps: { "aria-label": "Insert image" },
}),
commands.quote,
commands.code,
commands.divider,

commands.unorderedListCommand,
commands.orderedListCommand,
commands.checkedListCommand,
commands.divider,
]}
/>
);
};
Expand Down
8 changes: 2 additions & 6 deletions Components/Detail/DetailArticle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@ import {
import supabase from "@/lib/supabase";
import getTextColorByBackgroundColor from "@/utils/detail/getTextColorByBackgroundColor";
import { useQueryClient } from "@tanstack/react-query";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { ClimbingBoxLoader } from "react-spinners";
import styled from "styled-components";

const Viewer = dynamic(() => import("@/Components/Detail/DetailContent"), {
ssr: false,
});
import DetailContent from "./DetailContent";

const DetailArticle = () => {
const {
Expand Down Expand Up @@ -208,7 +204,7 @@ const DetailArticle = () => {
/>
</DetailContentsSide>
<DetailContentsMain>
{content && <Viewer content={content} />}
{content && <DetailContent content={content} />}
</DetailContentsMain>
</DetailContentsContainer>
<RelatedProject />
Expand Down
26 changes: 14 additions & 12 deletions Components/Detail/DetailContent.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import "@toast-ui/editor/dist/toastui-editor-viewer.css";
import "prismjs/themes/prism.css";
import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css";
import "@uiw/react-md-editor/markdown-editor.css";
import "@uiw/react-markdown-preview/markdown.css";

import { Viewer } from "@toast-ui/react-editor";
import codeSyntaxHighlightPlugin from "@toast-ui/editor-plugin-code-syntax-highlight";
import Prism from "prismjs";
import dynamic from "next/dynamic";
import styled from "styled-components";

const MarkdownPreview = dynamic(() => import("@uiw/react-markdown-preview"), {
ssr: false,
});

interface DetailContentProps {
content: string;
}

const DetailContent = ({ content }: DetailContentProps) => {
return (
<Viewer
initialValue={content}
plugins={[[codeSyntaxHighlightPlugin, { highlighter: Prism }]]}
/>
);
return <PreviewContent source={content} />;
};

const PreviewContent = styled(MarkdownPreview)`
width: 60rem;
padding: 1rem;
`;

export default DetailContent;
3 changes: 2 additions & 1 deletion Components/MyPage/UserInfoContainer/UserInfoContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import supabase from "@/lib/supabase";
import Image, { StaticImageData } from "next/image";
import { ChangeEvent, useState } from "react";
import styled from "styled-components";
import { v4 as uuidv4 } from "uuid";
import convertEase64ToFile from "@/utils/commons/convertBase64ToFile";
import { useUserProfile } from "@/hooks/query";
import { useInput } from "@/hooks/common";
Expand Down Expand Up @@ -42,7 +43,7 @@ const UserInfoContainer = () => {
};

const uploadImage = async (file: File) => {
const imgPath = crypto.randomUUID();
const imgPath = uuidv4();
try {
await supabase.storage.from("post-image").upload(imgPath, file);
const { data } = await supabase.storage
Expand Down
Loading