-
Notifications
You must be signed in to change notification settings - Fork 28
[Mission4/김민재] - Project_Notion_VanillaJS #16
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
base: 3/#4_minjae
Are you sure you want to change the base?
Changes from all commits
a3791c7
dfcb1a5
b729d29
cc8da73
58880ea
e701bc6
ab4ada1
21e6fb6
5c66953
9b976db
d5ef51f
99ba91b
44b6780
95f7373
7b9451e
e686a19
df7f49e
eeb2c72
dca573d
2949465
c86403e
e79b992
d38ffa4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| constants.js | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <link rel="stylesheet" href="/style.css" /> | ||
| <title>Minjae의 Notion</title> | ||
| </head> | ||
| <body> | ||
| <main id="app"></main> | ||
| <script src="/src/main.js" type="module"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import EditorContainer from "./components/editpage/EditorContainer.js"; | ||
| import Sidebar from "./components/sidebar/Sidebar.js"; | ||
| import { request } from "./api.js"; | ||
| import { initRouter, push } from "./router.js"; | ||
| import { validation } from "./validation.js"; | ||
| import LandingPage from "./components/editpage/LandingPage.js"; | ||
|
|
||
| export default function App({ $target }) { | ||
| validation(new.target, "App"); | ||
|
|
||
| this.state = { | ||
| documentsList: [], | ||
| editorDocument: { | ||
| docId: null, | ||
| doc: { | ||
| title: "", | ||
| content: "", | ||
| documents: [], | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const sidebar = new Sidebar({ | ||
| $target, | ||
| initialState: this.state.documentsList, | ||
| }); | ||
|
|
||
| new LandingPage({ $target }); | ||
|
|
||
| const editContainer = new EditorContainer({ | ||
| $target, | ||
| initialState: this.state.editorDocument, | ||
| }); | ||
|
|
||
| this.setState = (nextState) => { | ||
| this.state = nextState; | ||
| editContainer.setState(this.state.editorDocument); | ||
| }; | ||
|
|
||
| this.init = async () => { | ||
| const docs = await request("/documents"); | ||
| this.setState({ | ||
| ...this.state, | ||
| documentsList: docs, | ||
| }); | ||
| await sidebar.setState(docs); | ||
| this.route(); | ||
| }; | ||
|
|
||
| this.route = () => { | ||
| const { pathname } = window.location; | ||
| const editArea = document.querySelector(".editContainer"); | ||
| const landingPage = document.querySelector(".landingPage"); | ||
|
|
||
| if (pathname === "/") { | ||
| editArea.style.display = "none"; | ||
| landingPage.style.display = "block"; | ||
| } else if (pathname.indexOf("/documents/") === 0) { | ||
| editArea.style.display = "block"; | ||
| landingPage.style.display = "none"; | ||
| const [, , docId] = pathname.split("/"); | ||
| //같은 문서 두번 클릭시 내용 삭제 방지 | ||
| if (this.state.editorDocument.docId === docId) { | ||
| return; | ||
| } | ||
| //뒤로가기 대비 방어코드 | ||
| if (docId === null) { | ||
| push("/"); | ||
| } | ||
| this.setState({ | ||
| ...this.state, | ||
| editorDocument: { docId }, | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| this.init(); | ||
|
|
||
| window.addEventListener("popstate", () => this.route()); | ||
|
|
||
| initRouter(() => this.route()); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { API_END_POINT, X_USERNAME } from "./constants.js"; | ||
|
|
||
| export const request = async (url, options = {}) => { | ||
| try { | ||
| const res = await fetch(`${API_END_POINT}${url}`, { | ||
| ...options, | ||
| headers: { | ||
| "x-username": X_USERNAME, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| }); | ||
|
|
||
| if (res.ok) { | ||
| return await res.json(); | ||
| } | ||
|
|
||
| throw new Error("API 처리중 뭔가 이상합니다!"); | ||
| } catch (e) { | ||
| alert(e.message); | ||
| history.replaceState(null, null, "/"); | ||
| location.reload(); | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { validation, checkDifference } from "../../validation.js"; | ||
|
|
||
| export default function BreadCrumb({ $target, initialState, clickPath }) { | ||
| validation(new.target, "BreadCrumb"); | ||
|
|
||
| const $breadCrumb = document.createElement("nav"); | ||
| $breadCrumb.className = "linkContainer"; | ||
| this.state = initialState; | ||
|
|
||
| this.setState = (nextState) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. state도 validation을 거치면 좋을 것 같습니다.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 전체적으로 validation 적용해보도록 하겠습니다. 감사합니다! |
||
| if (typeof nextState !== "object") throw new Error("변경될 상태가 객체가 아닙니다."); | ||
| if (checkDifference(this.state, nextState)) return; | ||
| this.state = nextState; | ||
| this.render(); | ||
| }; | ||
|
|
||
| const recursiveBC = (id, path) => { | ||
| const $curLi = document.getElementById(id); | ||
| if ($curLi) { | ||
| const $ul = $curLi.closest("ul"); | ||
|
|
||
| if ($ul.className === "child" || $ul.className === "child--show") { | ||
| path.push([$curLi.querySelector("span").innerHTML, id]); | ||
|
|
||
| recursiveBC($ul.closest("li").id, path); | ||
| } else { | ||
| path.push([$curLi.querySelector("span").innerHTML, id]); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const renderBreadCrumb = (id) => { | ||
| const path = []; | ||
|
|
||
| recursiveBC(id, path); | ||
|
|
||
| return path | ||
| .reverse() | ||
| .map((el) => `<span class="link" data-id=${el[1]} >${el[0]}</span>`) | ||
| .join(" / "); | ||
|
Comment on lines
+37
to
+40
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| }; | ||
|
|
||
| $breadCrumb.addEventListener("click", (e) => { | ||
| const $span = e.target.closest("span"); | ||
|
|
||
| if ($span) { | ||
| const { id } = $span.dataset; | ||
| clickPath(id); | ||
| } | ||
| }); | ||
|
|
||
| this.render = () => { | ||
| const { docId } = this.state; | ||
|
|
||
| $breadCrumb.innerHTML = ` | ||
| <div> | ||
| ${docId ? renderBreadCrumb(docId) : ""} | ||
| </div> | ||
| `; | ||
| }; | ||
|
|
||
| $target.prepend($breadCrumb); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { validation } from "../../validation.js"; | ||
|
|
||
| export default function Editor({ $target, initialState, onEdit }) { | ||
| validation(new.target, "Editor"); | ||
| const $editor = document.createElement("div"); | ||
| $editor.className = "editor"; | ||
| $target.appendChild($editor); | ||
|
|
||
| $editor.innerHTML = ` | ||
| <div style="display: flex; flex-direction: column"> | ||
| <input type="text" placeholder="제목 없음" name="title" style="height:100px" /> | ||
| <textarea name="content" placeholder="내용을 입력하세요" style="height:75vh;"></textarea> | ||
| </div> | ||
| `; | ||
|
|
||
| this.state = initialState; | ||
|
|
||
| this.setState = (nextState) => { | ||
| this.state = nextState; | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| $editor.querySelector("[name=title]").value = this.state.title; | ||
| $editor.querySelector("[name=content]").value = this.state.content; | ||
| }; | ||
|
|
||
| this.render(); | ||
|
|
||
| $editor.addEventListener("keyup", (e) => { | ||
| const { tagName: targetTag, value } = e.target; | ||
| let nextState = {}; | ||
| if (targetTag === "INPUT") { | ||
| nextState = { ...this.state, title: value }; | ||
| } else { | ||
| nextState = { | ||
| ...this.state, | ||
| content: value, | ||
| }; | ||
| } | ||
|
|
||
| this.setState(nextState); | ||
| onEdit(this.state); | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { request } from "../../api.js"; | ||
| import BreadCrumb from "./BreadCrumb.js"; | ||
| import Editor from "./Editor.js"; | ||
| import { push } from "../../router.js"; | ||
| import SubLink from "./SubLink.js"; | ||
| import { validation } from "../../validation.js"; | ||
|
|
||
| //여기에선 docId만 핸들, Editor에는 Doc만 | ||
| export default function EditorContainer({ $target, initialState }) { | ||
| validation(new.target, "EditorContainer"); | ||
|
|
||
| const $editorContainer = document.createElement("section"); | ||
| $editorContainer.className = "editContainer"; | ||
|
|
||
| this.state = initialState; | ||
|
|
||
| let timer = null; | ||
|
|
||
| const breadCrumb = new BreadCrumb({ | ||
| $target: $editorContainer, | ||
| initialState: this.state, | ||
| clickPath: (id) => { | ||
| changeHoverEffect(this.state, id); | ||
| push(`/documents/${id}`); | ||
| }, | ||
| }); | ||
|
|
||
| const editor = new Editor({ | ||
| $target: $editorContainer, | ||
| initialState: this.state.doc, | ||
| onEdit: (post) => { | ||
| document.getElementById(post.id).getElementsByTagName("span")[0].innerText = post.title; | ||
|
|
||
| if (timer !== null) { | ||
| clearTimeout(timer); | ||
| } | ||
| timer = setTimeout(async () => { | ||
| await request(`/documents/${post.id}`, { | ||
| method: "PUT", | ||
| body: JSON.stringify(post), | ||
| }); | ||
| }, 500); | ||
| }, | ||
| }); | ||
|
|
||
| const subLink = new SubLink({ | ||
| $target: $editorContainer, | ||
| initialState: this.state, | ||
| clickLink: (id) => { | ||
| changeHoverEffect(this.state, id); | ||
| push(`/documents/${id}`); | ||
| }, | ||
| }); | ||
|
|
||
| this.setState = async (nextState) => { | ||
| if (this.state.docId !== nextState.docId) { | ||
| this.state = nextState; | ||
| await fetchDoc(); | ||
| return; | ||
| } | ||
| this.state = nextState; | ||
|
|
||
| breadCrumb.setState(this.state); | ||
| subLink.setState(this.state); | ||
| editor.setState(this.state.doc || { title: "", content: "" }); | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| $target.appendChild($editorContainer); | ||
| }; | ||
|
|
||
| const fetchDoc = async () => { | ||
| const { docId } = this.state; | ||
| const doc = await request(`/documents/${docId}`); | ||
|
|
||
| this.setState({ | ||
| ...this.state, | ||
| doc, | ||
| }); | ||
| }; | ||
|
|
||
| const changeHoverEffect = (prev, curr) => { | ||
| const { docId } = prev; | ||
| if (docId) { | ||
| const $prevLi = document.getElementById(docId); | ||
| const $currLi = document.getElementById(curr); | ||
| $prevLi.querySelector("p").style = ""; | ||
| $currLi.querySelector("p").style.backgroundColor = "rgba(0, 0, 0, 0.1)"; | ||
| } | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { validation } from "../../validation.js"; | ||
|
|
||
| export default function LandingPage({ $target }) { | ||
| validation(new.target, "LandingPage"); | ||
|
|
||
| const $landingPage = document.createElement("section"); | ||
| $landingPage.className = "landingPage"; | ||
| $target.appendChild($landingPage); | ||
|
|
||
| this.render = () => { | ||
| $landingPage.innerHTML = ` | ||
| <h1 class="landingTitle"> | ||
| Notion에 오신 것을 환영합니다. | ||
| </h1> | ||
| <p class="landingP">페이지를 추가하여 새 문서를 작성해보세요.</p> | ||
| `; | ||
| }; | ||
|
|
||
| this.render(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { validation } from "../../validation.js"; | ||
| export default function SubLink({ $target, initialState, clickLink }) { | ||
| validation(new.target, "SubLink"); | ||
|
|
||
| const $subLink = document.createElement("footer"); | ||
| $subLink.className = "linkContainer"; | ||
| $target.appendChild($subLink); | ||
|
|
||
| this.state = initialState; | ||
|
|
||
| this.setState = (nextState) => { | ||
| this.state = nextState; | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| const { doc } = this.state; | ||
|
|
||
| $subLink.innerHTML = | ||
| doc.documents.length > 0 | ||
| ? doc.documents.map((el) => `<div class="link" data-id=${el.id}>${el.title}</div>`).join("") | ||
| : ``; | ||
| }; | ||
|
|
||
| $subLink.addEventListener("click", (e) => { | ||
| const { id } = e.target.closest("div").dataset; | ||
| if (id) { | ||
| const $curLi = document.getElementById(id); | ||
| const $parentUl = $curLi.closest("ul"); | ||
|
|
||
| $parentUl.closest("li").querySelector(".toggleFold").innerText = "▼"; | ||
| $parentUl.className = "child--show"; | ||
| clickLink(id); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initial state 관리 방식 좋네요 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다!🙇🏻♂️ 필요한 state들만 사용해보려고 노력해봤습니다.