diff --git a/api/api.js b/api/api.js new file mode 100644 index 00000000..f56fd2d7 --- /dev/null +++ b/api/api.js @@ -0,0 +1,21 @@ +import { API_END_POINT, HEADER } from "./apiConstans.js"; + +export const request = async (url, options = {}) => { + try { + const res = await fetch(`${API_END_POINT}${url}`, { + ...options, + headers: { + "Content-Type": "application/json", + "x-username": HEADER, + }, + }); + + if (res.ok) { + return await res.json(); + } + + throw new Error("API 처리중 뭔가 이상합니다!"); + } catch (error) { + alert(error.message); + } +}; diff --git a/components/Content/Content.js b/components/Content/Content.js new file mode 100644 index 00000000..30a4c947 --- /dev/null +++ b/components/Content/Content.js @@ -0,0 +1,99 @@ +import ContentEditor from "./ContentEditor.js"; +import { request } from "../../api/api.js"; +import { push } from "../../utils/router.js"; + +export default function Content({ $target, initialState }) { + const $content = document.createElement("div"); + $content.classList.add("layout-content"); + + this.state = initialState; + + const post = { + title: "", + content: "", + }; + + let timer = null; + + const contentEditor = new ContentEditor({ + $target: $content, + initialState: post, + onEditing: (post) => { + if (timer !== null) { + clearTimeout(timer); + } + + timer = setTimeout(async () => { + const isNew = this.state.documentId === "new"; + + if (isNew) { + const createdPost = await request("/documents", { + method: "post", + body: JSON.stringify(post), + }); + + this.setState({ + documentId: createdPost.id, + }); + + push(`/documents/${createdPost.id}`); + } else { + await request(`/documents/${post.id}`, { + method: "put", + body: JSON.stringify(post), + }); + } + }, 500); + }, + }); + + this.setState = async (nextState) => { + if (this.state.documentId !== nextState.documentId) { + this.state = nextState; + + if (this.state.documentId === "new") { + const document = { + title: "", + content: "", + }; + + contentEditor.setState(document); + contentEditor.render(); + this.render(); + } else { + await fetchPost(); + } + + return; + } + + this.state = nextState; + this.render(); + contentEditor.setState( + this.state.document || { + title: "", + content: "", + } + ); + contentEditor.render(); + }; + + this.render = () => { + $target.appendChild($content); + }; + + const fetchPost = async () => { + const { documentId } = this.state; + + if (documentId !== "new") { + const document = await request(`/documents/${documentId}`, { + method: "get", + }); + + this.setState({ + ...this.state, + document, + }); + } + }; +} diff --git a/components/Content/ContentEditor.js b/components/Content/ContentEditor.js new file mode 100644 index 00000000..de3d925b --- /dev/null +++ b/components/Content/ContentEditor.js @@ -0,0 +1,51 @@ +export default function ContentEditor({ + $target, + initialState = { + title: "", + content: "", + }, + onEditing, +}) { + const $contentEditor = document.createElement("div"); + + this.state = initialState; + + this.setState = (nextState) => { + this.state = nextState; + }; + + $contentEditor.innerHTML = ` + + + `; + + $target.appendChild($contentEditor); + + this.render = () => { + $contentEditor.querySelector("[name=title]").value = this.state.title; + $contentEditor.querySelector("[name=content]").value = this.state.content; + }; + + this.render(); + + $contentEditor.addEventListener("keyup", (e) => { + const { target } = e; + const name = target.getAttribute("name"); + const nextState = { + ...this.state, + [name]: target.value, + }; + + this.setState(nextState); + onEditing(this.state); + }); +} diff --git a/components/SideBar/SideBar.js b/components/SideBar/SideBar.js new file mode 100644 index 00000000..c7b08ddb --- /dev/null +++ b/components/SideBar/SideBar.js @@ -0,0 +1,25 @@ +import SideBarList from "./SideBarList.js"; +import { request } from "../../api/api.js"; + +export default function SideBar({ $target }) { + const $sideBar = document.createElement("div"); + $sideBar.classList.add("layout-sidebar"); + + const sideBarList = new SideBarList({ + $target: $sideBar, + initialState: [], + }); + + this.setState = async () => { + const documents = await request("/documents", { + method: "get", + }); + + sideBarList.setState(documents); + this.render(); + }; + + this.render = async () => { + $target.appendChild($sideBar); + }; +} diff --git a/components/SideBar/SideBarItem.js b/components/SideBar/SideBarItem.js new file mode 100644 index 00000000..31274c6e --- /dev/null +++ b/components/SideBar/SideBarItem.js @@ -0,0 +1,14 @@ +export default function SideBarItem(item) { + return ` +
  • + + ${item.title} + + + + +
  • + `; +} diff --git a/components/SideBar/SideBarList.js b/components/SideBar/SideBarList.js new file mode 100644 index 00000000..290e403f --- /dev/null +++ b/components/SideBar/SideBarList.js @@ -0,0 +1,63 @@ +import SideBarItem from "./SideBarItem.js"; +import { request } from "../../api/api.js"; +import { push } from "../../utils/router.js"; + +export default function SideBarList({ $target, initialState }) { + const $sideBarList = document.createElement("div"); + + this.state = initialState; + + this.setState = (nextState) => { + this.state = nextState; + this.render(); + }; + + this.render = () => { + if (!this.state) return; + + $sideBarList.innerHTML = ` + + `; + + $target.appendChild($sideBarList); + }; + + this.render(); + + $sideBarList.addEventListener("click", async (e) => { + const $item = e.target; + const { id } = $item.parentElement.dataset; + + if ($item.className === "item-load") { + push(`/documents/${id}`); + + return; + } + + if ($item.className === "item-remove") { + await request(`/documents/${id}`, { + method: "delete", + }); + + push(`/`); + + return; + } + + if ($item.className === "item-add") { + const tempPost = { + title: "", + parent: id, + }; + const createdPost = await request("/documents", { + method: "post", + body: JSON.stringify(tempPost), + }); + + // this.setState() + push(`/documents/${createdPost.id}`); + } + }); +} diff --git a/index.html b/index.html new file mode 100644 index 00000000..4d714f06 --- /dev/null +++ b/index.html @@ -0,0 +1,10 @@ + + + Notion + + + +
    + + + diff --git a/index.js b/index.js new file mode 100644 index 00000000..acf90b87 --- /dev/null +++ b/index.js @@ -0,0 +1,5 @@ +import App from "./pages/App.js"; + +const $target = document.querySelector("#app"); + +new App({ $target }); diff --git a/pages/App.js b/pages/App.js new file mode 100644 index 00000000..d4cd5eef --- /dev/null +++ b/pages/App.js @@ -0,0 +1,40 @@ +import SideBar from "../components/SideBar/SideBar.js"; +import Content from "../components/Content/Content.js"; +import { initRouter } from "../utils/router.js"; + +export default function App({ $target }) { + const $layout = document.createElement("div"); + $layout.classList.add("layout-main"); + + const sideBar = new SideBar({ + $target: $layout, + }); + const content = new Content({ + $target: $layout, + initialState: { + documentId: "new", + }, + }); + + this.route = async () => { + $target.innerHTML = ``; + + const { pathname } = window.location; + + if (pathname === "/") { + await sideBar.setState(); + await content.setState({ documentId: "new" }); + } else if (pathname.indexOf("/documents/" === 0)) { + const [, , documentId] = pathname.split("/"); + + await sideBar.setState(); + await content.setState({ documentId }); + } + + $target.appendChild($layout); + }; + + this.route(); + + initRouter(() => this.route()); +} diff --git a/style/style.css b/style/style.css new file mode 100644 index 00000000..0bc79744 --- /dev/null +++ b/style/style.css @@ -0,0 +1,41 @@ +.layout-main { + display: flex; + width: 100%; + height: 100%; + color: #37352f; + background: #ffffff; +} + +.layout-sidebar { + width: 20%; + max-width: 280px; + min-width: 200px; + height: 100%; + font-size: 14px; + font-weight: bold; + color: #37352f; + background: #fbfbfa; + overflow: auto; + float: left; +} + +.layout-content { + width: 80%; + height: 100%; + float: left; +} + +.sidebar-list-ul { + list-style: none; +} + +.editor-title { + width: 100%; + height: 5%; + max-height: 60px; +} + +.editor-content { + width: 100%; + height: 95%; +} diff --git a/utils/localStorage.js b/utils/localStorage.js new file mode 100644 index 00000000..ace8af8b --- /dev/null +++ b/utils/localStorage.js @@ -0,0 +1,19 @@ +const localStorage = window.localStorage; + +export const getItem = (key, defaultValue) => { + try { + const storedValue = localStorage.getItem(key); + + return storedValue ? JSON.parse(storedValue) : defaultValue; + } catch (error) { + return defaultValue; + } +}; + +export const setItem = (key, value) => { + localStorage.setItem(key, JSON.stringify(value)); +}; + +export const removeItem = (key) => { + localStorage.removeItem(key); +}; diff --git a/utils/router.js b/utils/router.js new file mode 100644 index 00000000..67482796 --- /dev/null +++ b/utils/router.js @@ -0,0 +1,22 @@ +const ROUTE_CHANGE_EVENT_NAME = "route-change"; + +export const initRouter = (onRoute) => { + window.addEventListener(ROUTE_CHANGE_EVENT_NAME, (e) => { + const { nextUrl } = e.detail; + + if (nextUrl) { + history.pushState(null, null, nextUrl); + onRoute(); + } + }); +}; + +export const push = (nextUrl) => { + window.dispatchEvent( + new CustomEvent(ROUTE_CHANGE_EVENT_NAME, { + detail: { + nextUrl, + }, + }) + ); +};