diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a11c21ab --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/etc.txt \ No newline at end of file diff --git a/README.md b/README.md index 82b85e81..d19818cc 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ -# ๐Ÿ“Œ 4์ฃผ์ฐจ ํ”„๋กœ์ ํŠธ[Project1] +# VanillaJS๋กœ ๋งŒ๋“  ๋…ธ์…˜ ํด๋กœ๋‹ ํ”„๋กœ์ ํŠธ -## ํ•„์ˆ˜ ํ”„๋กœ์ ํŠธ +## ๋ฐฐํฌ ์ฃผ์†Œ +[๐Ÿ”ณ JooNotion](https://joo-notion.vercel.app/) -- ํ”„๋กœ์ ํŠธ ๊ธฐํ•œ - - ํ”„๋กœ์ ํŠธ ์ˆ˜ํ–‰ ๊ธฐ๊ฐ„ : 2022๋…„ 11์›” 8์ผ(ํ™”) ~ 2022๋…„ 11์›” 16์ผ(์ˆ˜) - - ๋ฉ˜ํ‹ฐ ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๊ธฐ๊ฐ„ : 2022๋…„ 11์›” 17์ผ(๋ชฉ) ~ 2022๋…„ 11์›” 20์ผ(์ผ) - - ๋ฉ˜ํ†  ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๊ธฐ๊ฐ„ : 2022๋…„ 11์›” 17์ผ(๋ชฉ) ~ 2022๋…„ 11์›” 22์ผ(ํ™”) - - ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฐ˜์˜ ๊ธฐ๊ฐ„ : 2022๋…„ 11์›” 23์ผ(์ˆ˜) ~ 2022๋…„ 11์›” 25์ผ(๊ธˆ) -- ๋‚ด์šฉ - - **Day 17 [ํ”„๋กœ์ ํŠธ] ๋…ธ์…˜ ํด๋กœ๋‹ ์š”๊ตฌ์‚ฌํ•ญ** ํ™•์ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค. +## ๊ธฐ๋Šฅ ๊ตฌํ˜„ +- History API๋ฅผ ์ด์šฉํ•ด SPA ํ˜•ํƒœ๋กœ ๊ฐœ๋ฐœ +- ๋ฌธ์„œ ์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ +- ํ•˜์œ„๋ฌธ์„œ ์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ + - ํ•˜์œ„๋ฌธ์„œ์˜ ํ† ๊ธ€ ์ƒํƒœ๋ฅผ LocalStorage์— ์ €์žฅํ•˜์—ฌ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์œ ์ง€๋˜๋„๋ก ๊ตฌํ˜„ +- ๋ฌธ์„œ ํŽธ์ง‘ ์‹œ API ํ˜ธ์ถœ์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ๋””๋ฐ”์šด์‹ฑ ๊ตฌํ˜„ + - ๋งˆ์ง€๋ง‰ ์ž…๋ ฅ ํ›„ 1์ดˆ๊ฐ„ ์ž…๋ ฅ์ด ์—†์œผ๋ฉด ์ž๋™ ์ €์žฅ + +## ํ”„๋กœ์ ํŠธ ํšŒ๊ณ  +ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ ์ƒ๊ธด ๋ฌธ์ œ์— ๋Œ€ํ•ด ๊ณ ๋ฏผํ•˜๊ณ  ํ•ด๊ฒฐ๊ณผ์ •์„ ๊ธฐ๋กํ–ˆ์Šต๋‹ˆ๋‹ค. +
+[๋ธ”๋กœ๊ทธ ์ฃผ์†Œ](https://woogie-97.tistory.com/10) diff --git a/config.js b/config.js new file mode 100644 index 00000000..d3ab90d7 --- /dev/null +++ b/config.js @@ -0,0 +1 @@ +export const OPENED_LIST_KEY = "local-save-open-list"; diff --git a/index.html b/index.html new file mode 100644 index 00000000..03c31891 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + ๐Ÿ”ณ JooNotion + + +
+ + + diff --git a/src/App.js b/src/App.js new file mode 100644 index 00000000..cf56108f --- /dev/null +++ b/src/App.js @@ -0,0 +1,89 @@ +import PostEdit from "./components/PostEdit.js"; +import PostsPage from "./components/PostsPage.js"; +import HomePage from "./pages/HomePage.js"; +import { initRouter } from "./routes/router.js"; +import NotFound from "./pages/NotFound.js"; +import { request } from "./api/index.js"; + +export default function App({ $target }) { + const $postListContainer = document.createElement("div"); + const $homeContainer = document.createElement("div"); + const $postEditContainer = document.createElement("div"); + + $homeContainer.className = "post-edit-container"; + $postListContainer.className = "post-list-container"; + $postEditContainer.className = "post-edit-container"; + + $target.appendChild($postListContainer); + $target.appendChild($homeContainer); + $target.appendChild($postEditContainer); + + this.state = { + selectedPost: {}, + }; + + const homePage = new HomePage({ + $target: $homeContainer, + }); + + const notFound = new NotFound({ + $target, + }); + + /** + * PostList ๊ฐ์‹ธ๋Š” ์ปดํฌ๋„ŒํŠธ + */ + const postsPage = new PostsPage({ + $target: $postListContainer, + }); + + /** + * Editor ๊ฐ์‹ธ๋Š” ์ปดํฌ๋„ŒํŠธ + */ + const postEdit = new PostEdit({ + $target: $postEditContainer, + initialState: this.state, + updatePost: (updatePost) => { + postsPage.setState(updatePost); + }, + }); + + postsPage.setState(); + + this.init = async () => { + const { pathname } = window.location; + if (pathname === "/") { + $postEditContainer.style.display = "none"; + $homeContainer.style.display = "flex"; + } else if (pathname.indexOf("/documents/") === 0) { + $postEditContainer.style.display = "flex"; + $homeContainer.style.display = "none"; + + const [, , id] = pathname.split("/"); + if (id === "new") { + postEdit.setState({ + ...this.state, + id, + }); + } else { + const post = await request(`/documents/${id}`, { + method: "GET", + }); + postsPage.setState(post); + postEdit.setState(post); + } + } else { + notFound.render(); + $postListContainer.style.display = "none"; + $postEditContainer.style.display = "none"; + $homeContainer.style.display = "none"; + } + }; + + this.init(); + initRouter(() => this.init()); + + window.addEventListener("popstate", () => { + this.init(); + }); +} diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 00000000..8b9cfa97 --- /dev/null +++ b/src/api/index.js @@ -0,0 +1,19 @@ +const API_END_POINT = "https://kdt-frontend.programmers.co.kr"; + +export const request = async (url, options = {}) => { + try { + const res = await fetch(`${API_END_POINT}${url}`, { + ...options, + headers: { + "Content-Type": "application/json", + "x-username": "woogie", + }, + }); + if (res.ok) { + return await res.json(); + } + throw new Error("API ํ˜ธ์ถœ ์‹คํŒจ!!!!!!!"); + } catch (error) { + alert(error); + } +}; diff --git a/src/assets/css/fontello.css b/src/assets/css/fontello.css new file mode 100644 index 00000000..9bad1512 --- /dev/null +++ b/src/assets/css/fontello.css @@ -0,0 +1,61 @@ +@font-face { + font-family: 'fontello'; + src: url('../font/fontello.eot?10205068'); + src: url('../font/fontello.eot?10205068#iefix') format('embedded-opentype'), + url('../font/fontello.woff2?10205068') format('woff2'), + url('../font/fontello.woff?10205068') format('woff'), + url('../font/fontello.ttf?10205068') format('truetype'), + url('../font/fontello.svg?10205068#fontello') format('svg'); + font-weight: normal; + font-style: normal; +} +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'fontello'; + src: url('../font/fontello.svg?10205068#fontello') format('svg'); + } +} +*/ +[class^="icon-"]:before, [class*=" icon-"]:before { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: never; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + + /* Font smoothing. That was taken from TWBS */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} + +.icon-trash:before { content: '\e803'; } /* '๎ ƒ' */ +.icon-plus-1:before { content: '\e804'; } /* '๎ „' */ +.icon-down-open:before { content: '\f004'; } /* '๏€„' */ +.icon-right-open:before { content: '\f006'; } /* '๏€†' */ +.icon-circle-thin:before { content: '\f1db'; } /* '๏‡›' */ diff --git a/src/assets/css/libs/_mixins.scss b/src/assets/css/libs/_mixins.scss new file mode 100644 index 00000000..d98ffa3d --- /dev/null +++ b/src/assets/css/libs/_mixins.scss @@ -0,0 +1,52 @@ +@import "./vars.scss"; + +@mixin base-button { + button { + font-family: "Noto Sans KR", Sans-serif; + border: 0; + color: $color-base-font; + background-color: transparent; + } + + button:hover { + cursor: pointer; + } +} + +@mixin input-title { + .title { + cursor: text; + font-size: 2.5rem; + font-weight: 700; + line-height: 4rem; + outline: none; + } + + .title:empty:before { + content: attr(placeholder); + font-size: 2.5rem; + font-weight: 700; + line-height: 4rem; + color: #ccc; + outline: none; + } +} + +@mixin input-content { + .content { + height: 600px; + cursor: text; + font-size: 1rem; + line-height: 3rem; + outline: none; + } + + .content:empty::before { + content: attr(placeholder); + top: 0; + font-size: 1rem; + line-height: 3rem; + color: #ccc; + outline: none; + } +} diff --git a/src/assets/css/libs/_vars.scss b/src/assets/css/libs/_vars.scss new file mode 100644 index 00000000..a82526b3 --- /dev/null +++ b/src/assets/css/libs/_vars.scss @@ -0,0 +1,16 @@ +// font +$color-home-font: #c7c7c7; +$color-base-font: #5e5e5e; + +// background +$color-post-edit-background: #ffffff; +$color-post-list-background: rgb(251 251 250); + +// hover +$color-post-list-hover: rgb(235, 235, 235); +$color-post-button-hover: rgb(209, 209, 209); + +// button +$color-modal-close-btn-background: #6b6b6b; +$color-post-create-btn-background: rgb(35, 131, 226); +$color-white: #ffffff; diff --git a/src/assets/css/modal.scss b/src/assets/css/modal.scss new file mode 100644 index 00000000..6e28e958 --- /dev/null +++ b/src/assets/css/modal.scss @@ -0,0 +1,75 @@ +@import "./libs/vars.scss"; +@import "./libs/mixins.scss"; + +.container-modal { + display: none; + + .modal { + z-index: 2; + position: absolute; + max-width: 970px; + margin: 0 auto; + width: 100%; + height: 400px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: $color-post-edit-background; + padding: 2rem 4rem; + border-radius: 4px; + box-shadow: rgb(15 15 15 / 5%) 0px 0px 0px 1px, + rgb(15 15 15 / 10%) 0px 5px 10px, rgb(15 15 15 / 20%) 0px 15px 40px; + + .modal-title { + position: relative; + + h2 { + font-size: 1.5rem; + font-weight: bold; + } + + .title-input-container { + position: relative; + margin-top: 1.5rem; + height: 300px; + + @include input-title; + } + } + + .modal-btn-container { + display: flex; + justify-content: end; + gap: 1rem; + + button { + padding: 8px 1rem; + border-radius: 10px; + color: $color-white; + font-size: 1rem; + } + + .close { + background-color: $color-modal-close-btn-background; + } + + .create-post-btn { + background-color: $color-post-create-btn-background; + } + } + } + + .overlay { + z-index: 1; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); + } +} + +.post-modal-show { + display: block; +} diff --git a/src/assets/css/postList.scss b/src/assets/css/postList.scss new file mode 100644 index 00000000..281aab7f --- /dev/null +++ b/src/assets/css/postList.scss @@ -0,0 +1,132 @@ +@import "./libs/vars.scss"; +@import "./libs/mixins.scss"; + +.post-list-container { + background-color: $color-post-list-background; + max-width: 300px; + min-width: 260px; + width: 100%; + + .nav-header { + padding: 1rem 0; + text-align: center; + } + + .nav-header:hover { + cursor: pointer; + } + + @include base-button; + + ul { + max-height: 870px; + overflow-y: scroll; + .post-list { + font-size: 14px; + padding-left: 4px; + color: $color-base-font; + + .list-flex { + display: flex; + position: relative; + justify-content: space-between; + align-items: center; + padding: 8px 4px; + + .open-folder:hover { + background-color: $color-post-button-hover; + } + + span { + padding: 2px; + } + + .post-title { + display: inline-block; + vertical-align: bottom; + max-width: 150px; + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .delete-create-btn-container { + display: none; + position: absolute; + right: 4px; + + button { + padding: 1px; + } + } + } + + .list-flex:hover { + cursor: pointer; + background-color: $color-post-list-hover; + + .delete-create-btn-container { + display: block; + z-index: 5; + + button { + cursor: pointer; + } + + button:hover { + background-color: $color-post-button-hover; + } + } + } + + .list-active { + background-color: $color-post-list-hover; + } + + .child-ul { + padding-left: 24px; + } + + .empty { + color: $color-home-font; + padding: 8px 0 8px 24px; + } + + .hide { + display: none; + } + } + } + + .empty-posts-list { + display: flex; + flex-direction: column; + text-align: center; + gap: 10px; + padding-top: 3rem; + } + + .create-post-btn-container { + padding: 1rem 0; + width: 300px; + text-align: center; + position: fixed; + bottom: 0; + background-color: $color-post-list-background; + + button { + color: #000000; + font-size: 14px; + font-weight: bold; + } + + button:hover { + cursor: pointer; + } + } + + .create-post-btn-container:hover { + cursor: pointer; + background-color: $color-post-list-hover; + } +} diff --git a/src/assets/css/reset.css b/src/assets/css/reset.css new file mode 100644 index 00000000..9e40b8d4 --- /dev/null +++ b/src/assets/css/reset.css @@ -0,0 +1,130 @@ +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} + +body { + line-height: 1; +} + +ol, +ul { + list-style: none; +} + +blockquote, +q { + quotes: none; +} + +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/src/assets/css/style.css b/src/assets/css/style.css new file mode 100644 index 00000000..0e08b35c --- /dev/null +++ b/src/assets/css/style.css @@ -0,0 +1,298 @@ +@import "./reset.css"; +.container-modal { + display: none; +} +.container-modal .modal { + z-index: 2; + position: absolute; + max-width: 970px; + margin: 0 auto; + width: 100%; + height: 400px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #ffffff; + padding: 2rem 4rem; + border-radius: 4px; + box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 5px 10px, rgba(15, 15, 15, 0.2) 0px 15px 40px; +} +.container-modal .modal .modal-title { + position: relative; +} +.container-modal .modal .modal-title h2 { + font-size: 1.5rem; + font-weight: bold; +} +.container-modal .modal .modal-title .title-input-container { + position: relative; + margin-top: 1.5rem; + height: 300px; +} +.container-modal .modal .modal-title .title-input-container .title { + cursor: text; + font-size: 2.5rem; + font-weight: 700; + line-height: 4rem; + outline: none; +} +.container-modal .modal .modal-title .title-input-container .title:empty:before { + content: attr(placeholder); + font-size: 2.5rem; + font-weight: 700; + line-height: 4rem; + color: #ccc; + outline: none; +} +.container-modal .modal .modal-btn-container { + display: flex; + justify-content: end; + gap: 1rem; +} +.container-modal .modal .modal-btn-container button { + padding: 8px 1rem; + border-radius: 10px; + color: #ffffff; + font-size: 1rem; +} +.container-modal .modal .modal-btn-container .close { + background-color: #6b6b6b; +} +.container-modal .modal .modal-btn-container .create-post-btn { + background-color: rgb(35, 131, 226); +} +.container-modal .overlay { + z-index: 1; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); +} + +.post-modal-show { + display: block; +} + +.post-list-container { + background-color: rgb(251, 251, 250); + max-width: 300px; + min-width: 260px; + width: 100%; +} +.post-list-container .nav-header { + padding: 1rem 0; + text-align: center; +} +.post-list-container .nav-header:hover { + cursor: pointer; +} +.post-list-container button { + font-family: "Noto Sans KR", Sans-serif; + border: 0; + color: #5e5e5e; + background-color: transparent; +} +.post-list-container button:hover { + cursor: pointer; +} +.post-list-container ul { + max-height: 870px; + overflow-y: scroll; +} +.post-list-container ul .post-list { + font-size: 14px; + padding-left: 4px; + color: #5e5e5e; +} +.post-list-container ul .post-list .list-flex { + display: flex; + position: relative; + justify-content: space-between; + align-items: center; + padding: 8px 4px; +} +.post-list-container ul .post-list .list-flex .open-folder:hover { + background-color: rgb(209, 209, 209); +} +.post-list-container ul .post-list .list-flex span { + padding: 2px; +} +.post-list-container ul .post-list .list-flex .post-title { + display: inline-block; + vertical-align: bottom; + max-width: 150px; + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.post-list-container ul .post-list .list-flex .delete-create-btn-container { + display: none; + position: absolute; + right: 4px; +} +.post-list-container ul .post-list .list-flex .delete-create-btn-container button { + padding: 1px; +} +.post-list-container ul .post-list .list-flex:hover { + cursor: pointer; + background-color: rgb(235, 235, 235); +} +.post-list-container ul .post-list .list-flex:hover .delete-create-btn-container { + display: block; + z-index: 5; +} +.post-list-container ul .post-list .list-flex:hover .delete-create-btn-container button { + cursor: pointer; +} +.post-list-container ul .post-list .list-flex:hover .delete-create-btn-container button:hover { + background-color: rgb(209, 209, 209); +} +.post-list-container ul .post-list .list-active { + background-color: rgb(235, 235, 235); +} +.post-list-container ul .post-list .child-ul { + padding-left: 24px; +} +.post-list-container ul .post-list .empty { + color: #c7c7c7; + padding: 8px 0 8px 24px; +} +.post-list-container ul .post-list .hide { + display: none; +} +.post-list-container .empty-posts-list { + display: flex; + flex-direction: column; + text-align: center; + gap: 10px; + padding-top: 3rem; +} +.post-list-container .create-post-btn-container { + padding: 1rem 0; + width: 300px; + text-align: center; + position: fixed; + bottom: 0; + background-color: rgb(251, 251, 250); +} +.post-list-container .create-post-btn-container button { + color: #000000; + font-size: 14px; + font-weight: bold; +} +.post-list-container .create-post-btn-container button:hover { + cursor: pointer; +} +.post-list-container .create-post-btn-container:hover { + cursor: pointer; + background-color: rgb(235, 235, 235); +} + +.global-container { + display: flex; + height: 100vh; + font-family: "Noto Sans KR", Sans-serif; +} +.global-container .post-list-container, +.global-container .post-edit-container { + display: inline-block; +} +.global-container .post-edit-container { + display: flex; + width: 100%; + max-width: 900px; + min-width: 400px; + margin: 0 auto; + justify-content: center; + align-items: center; + padding: 0 4rem; + background-color: #ffffff; +} +.global-container .post-edit-container .home h1 { + font-size: 3rem; + font-weight: bold; + color: #c7c7c7; +} +.global-container .post-edit-container .editor { + width: 100%; + max-height: 850px; + overflow-y: scroll; + display: flex; + flex-direction: column; + gap: 2rem; + padding-right: 3rem; +} +.global-container .post-edit-container .editor .title-input-container { + padding-top: 5rem; +} +.global-container .post-edit-container .editor .title-input-container .title { + cursor: text; + font-size: 2.5rem; + font-weight: 700; + line-height: 4rem; + outline: none; +} +.global-container .post-edit-container .editor .title-input-container .title:empty:before { + content: attr(placeholder); + font-size: 2.5rem; + font-weight: 700; + line-height: 4rem; + color: #ccc; + outline: none; +} +.global-container .post-edit-container .editor .post-date-container { + display: flex; + flex-direction: column; + gap: 0.5rem; + text-align: end; +} +.global-container .post-edit-container .editor .post-date-container span { + color: #c7c7c7; +} +.global-container .post-edit-container .editor .content-input-container .content { + height: 600px; + cursor: text; + font-size: 1rem; + line-height: 3rem; + outline: none; +} +.global-container .post-edit-container .editor .content-input-container .content:empty::before { + content: attr(placeholder); + top: 0; + font-size: 1rem; + line-height: 3rem; + color: #ccc; + outline: none; +} +.global-container .post-edit-container .editor::-webkit-scrollbar { + display: none; +} +.global-container .not-found { + width: 100%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} +.global-container .not-found h1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: 1rem; +} +.global-container .not-found button { + font-family: "Noto Sans KR", Sans-serif; + border: 0; + color: #5e5e5e; + background-color: transparent; +} +.global-container .not-found button:hover { + cursor: pointer; +} +.global-container .not-found button { + font-size: 1rem; +} + +/*# sourceMappingURL=style.css.map */ diff --git a/src/assets/css/style.css.map b/src/assets/css/style.css.map new file mode 100644 index 00000000..ab697246 --- /dev/null +++ b/src/assets/css/style.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["style.scss","modal.scss","libs/_vars.scss","libs/_mixins.scss","postList.scss"],"names":[],"mappings":"AAAQ;ACGR;EACC;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBCX2B;EDY3B;EACA;EACA;;AAGA;EACC;;AAEA;EACC;EACA;;AAGD;EACC;EACA;EACA;;AEjBH;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;AFSA;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA,OChCU;EDiCV;;AAGD;EACC,kBCvC+B;;AD0ChC;EACC,kBC1C+B;;AD+ClC;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIF;EACC;;;AGtED;EACC,kBFE4B;EED5B;EACA;EACA;;AAEA;EACC;EACA;;AAGD;EACC;;ADZD;EACC;EACA;EACA,ODJgB;ECKhB;;AAGD;EACC;;ACSD;EACC;EACA;;AACA;EACC;EACA;EACA,OFxBe;;AE0Bf;EACC;EACA;EACA;EACA;EACA;;AAEA;EACC,kBF1BqB;;AE6BtB;EACC;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;;AAEA;EACC;;AAKH;EACC;EACA,kBFxDoB;;AE0DpB;EACC;EACA;;AAEA;EACC;;AAGD;EACC,kBFlEoB;;AEuEvB;EACC,kBFzEoB;;AE4ErB;EACC;;AAGD;EACC,OFzFc;EE0Fd;;AAGD;EACC;;AAKH;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA,kBF5G2B;;AE8G3B;EACC;EACA;EACA;;AAGD;EACC;;AAIF;EACC;EACA,kBFxHsB;;;AFFxB;EACC;EACA;EACA;;AAEA;AAAA;EAEC;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBErB2B;;AFwB1B;EACC;EACA;EACA,OE/Bc;;AFmChB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;;AG9BH;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;AHoBC;EACC;EACA;EACA;EACA;;AAEA;EACC,OExDa;;ACkCjB;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;AHiBA;EACC;;AAIF;EACC;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;;AG/EF;EACC;EACA;EACA,ODJgB;ECKhB;;AAGD;EACC;;AH4EA;EACC","file":"style.css"} \ No newline at end of file diff --git a/src/assets/css/style.scss b/src/assets/css/style.scss new file mode 100644 index 00000000..75abf789 --- /dev/null +++ b/src/assets/css/style.scss @@ -0,0 +1,92 @@ +@import "./reset.css"; +@import "./libs/vars.scss"; +@import "./libs/mixins.scss"; + +@import "./modal.scss"; +@import "./postList.scss"; + +.global-container { + display: flex; + height: 100vh; + font-family: "Noto Sans KR", Sans-serif; + + .post-list-container, + .post-edit-container { + display: inline-block; + } + + .post-edit-container { + display: flex; + width: 100%; + max-width: 900px; + min-width: 400px; + margin: 0 auto; + justify-content: center; + align-items: center; + padding: 0 4rem; + background-color: $color-post-edit-background; + + .home { + h1 { + font-size: 3rem; + font-weight: bold; + color: $color-home-font; + } + } + + .editor { + width: 100%; + max-height: 850px; + overflow-y: scroll; + display: flex; + flex-direction: column; + gap: 2rem; + padding-right: 3rem; + + .title-input-container { + padding-top: 5rem; + @include input-title; + } + + .post-date-container { + display: flex; + flex-direction: column; + gap: 0.5rem; + text-align: end; + + span { + color: $color-home-font; + } + } + + .content-input-container { + @include input-content; + } + } + + .editor::-webkit-scrollbar { + display: none; + } + } + + .not-found { + width: 100%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + + h1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: 1rem; + } + + @include base-button; + + button { + font-size: 1rem; + } + } +} diff --git a/src/assets/font/fontello.eot b/src/assets/font/fontello.eot new file mode 100644 index 00000000..ac02012c Binary files /dev/null and b/src/assets/font/fontello.eot differ diff --git a/src/assets/font/fontello.svg b/src/assets/font/fontello.svg new file mode 100644 index 00000000..e67ea068 --- /dev/null +++ b/src/assets/font/fontello.svg @@ -0,0 +1,20 @@ + + + +Copyright (C) 2022 by original authors @ fontello.com + + + + + + + + + + + + + + + + diff --git a/src/assets/font/fontello.ttf b/src/assets/font/fontello.ttf new file mode 100644 index 00000000..6adc48fa Binary files /dev/null and b/src/assets/font/fontello.ttf differ diff --git a/src/assets/font/fontello.woff b/src/assets/font/fontello.woff new file mode 100644 index 00000000..b15dbc46 Binary files /dev/null and b/src/assets/font/fontello.woff differ diff --git a/src/assets/font/fontello.woff2 b/src/assets/font/fontello.woff2 new file mode 100644 index 00000000..0735f65e Binary files /dev/null and b/src/assets/font/fontello.woff2 differ diff --git a/src/components/CreatePostButton.js b/src/components/CreatePostButton.js new file mode 100644 index 00000000..4738ff5c --- /dev/null +++ b/src/components/CreatePostButton.js @@ -0,0 +1,37 @@ +import { $createPostBtn } from "../utils/templates.js"; +import CreatePostModal from "./modal/CreatePostModal.js"; + +export default function CreatePostButton({ + $target, + initialState, + createPost, +}) { + const $createPostBtnContainer = document.createElement("div"); + $createPostBtnContainer.className = "create-post-btn-container"; + $target.appendChild($createPostBtnContainer); + + this.state = initialState; + + this.setState = (nextState) => { + this.state = nextState; + }; + + this.render = () => { + $createPostBtnContainer.innerHTML = $createPostBtn(); + }; + + this.render(); + + $createPostBtnContainer.addEventListener("click", () => { + $createPostModal.setState({ + link: "/documents/new", + modalOpen: true, + }); + }); + + const $createPostModal = new CreatePostModal({ + $target, + initialState: { link: "/documents/new", modalOpen: false }, + createPost, + }); +} diff --git a/src/components/Editor.js b/src/components/Editor.js new file mode 100644 index 00000000..53bc3cdc --- /dev/null +++ b/src/components/Editor.js @@ -0,0 +1,42 @@ +import { $editPost } from "../utils/templates.js"; + +export default function Editor({ $target, initialState, onEditing }) { + const $editor = document.createElement("div"); + $editor.className = "editor"; + this.state = initialState; + this.body = initialState; + + $target.appendChild($editor); + + this.setState = (nextState) => { + this.state = nextState; + this.body = nextState; + this.render(); + $editor.querySelector('div[name="title"]').innerText = this.state.title; + $editor.querySelector('div[name="content"]').innerHTML = + this.state.content === null + ? "" + : this.state.content.replace(/\n/g, "
"); + }; + + this.render = () => { + $editor.innerHTML = $editPost(this.state); + }; + + $editor.addEventListener("keyup", (e) => { + const { target } = e; + + const post = { + id: this.body.id, + title: this.body.title, + content: this.body.content, + }; + if (target.className.includes("title")) { + this.body.title = target.innerText; + } else { + this.body.content = target.innerText; + } + + onEditing(post); + }); +} diff --git a/src/components/PostEdit.js b/src/components/PostEdit.js new file mode 100644 index 00000000..45cbb3c9 --- /dev/null +++ b/src/components/PostEdit.js @@ -0,0 +1,62 @@ +import { request } from "../api/index.js"; +import Editor from "./Editor.js"; + +export default function PostEdit({ $target, initialState, updatePost }) { + this.state = initialState; + + let timer = null; + const editor = new Editor({ + $target, + initialState: this.state, + onEditing: (post) => { + if (timer !== null) { + clearTimeout(timer); + } + timer = setTimeout(async () => { + const updatedPost = await request(`/documents/${post.id}`, { + method: "PUT", + body: JSON.stringify(post), + }); + updatePost(updatedPost); + }, 1000); + }, + }); + + this.setState = async (nextState) => { + if (this.state.id !== nextState.id) { + this.state = nextState; + if (this.state.id === "new") { + this.render(); + editor.setState(this.state); + } else { + await fetchPost(); + } + return; + } + + this.state = nextState; + this.render(); + + editor.setState( + this.state || { + title: "", + content: "", + } + ); + }; + + this.render = () => { + editor.setState(this.state); + }; + + const fetchPost = async () => { + const { id } = this.state; + if (id !== "new") { + const post = await request(`/documents/${id}`); + this.setState({ + ...this.state, + post, + }); + } + }; +} diff --git a/src/components/PostList.js b/src/components/PostList.js new file mode 100644 index 00000000..7a8913e8 --- /dev/null +++ b/src/components/PostList.js @@ -0,0 +1,123 @@ +import { OPENED_LIST_KEY } from "../../config.js"; +import { getItem, removeItem, setItem } from "../utils/storage.js"; +import { $onLoadParentList } from "../utils/templates.js"; +import CreatePostModal from "./modal/CreatePostModal.js"; + +export default function PostList({ + $target, + removePost, + clickPost, + createPost, +}) { + const $postList = document.createElement("div"); + const $emptyPostDiv = document.createElement("div"); + $emptyPostDiv.className = "empty-posts-list"; + + $target.appendChild($postList); + + this.state = { + posts: [], + selectedPostId: null, + openedId: [], + }; + this.setState = ({ postsList, selectedPost }) => { + this.state.posts = postsList; + this.state.selectedPostId = + selectedPost === undefined ? null : selectedPost.id; + this.state.openedId = getItem(OPENED_LIST_KEY, []); + this.render(); + }; + + this.setOpenedLists = (nextState) => { + this.state.openedId = [...nextState]; + setItem(OPENED_LIST_KEY, this.state.openedId); + }; + + this.render = () => { + if (this.state.posts.length > 0) { + $postList.innerHTML = `${$onLoadParentList( + this.state.posts, + this.state.selectedPostId, + this.state.openedId + )}`; + } else { + $postList.innerHTML = ""; + $postList.appendChild($emptyPostDiv); + $emptyPostDiv.innerHTML = ` + ์•„์ง ์ž‘์„ฑ๋œ ๊ธ€์ด ์—†์–ด์š”. + ์˜ค๋Š˜ ํ•˜๋ฃจ๋ฅผ ๊ธฐ๋กํ•ด๋ณด์„ธ์š”! ๐Ÿ“ + + `; + } + }; + + $postList.addEventListener("click", (e) => { + const $li = e.target.closest(".list-flex"); + const $childLi = $li.nextElementSibling; + + if ($li && $li.dataset.id !== undefined) { + const { className } = e.target; + const { id } = $li.dataset; + + if (className.includes("open-folder")) { + if (className.includes("icon-right-open")) { + e.target.classList.replace("icon-right-open", "icon-down-open"); + this.state.openedId.includes(id) + ? null + : this.setOpenedLists([...this.state.openedId, id]); + } else { + e.target.classList.replace("icon-down-open", "icon-right-open"); + this.setOpenedLists( + this.state.openedId.filter((item) => item !== id) + ); + } + + if ($childLi.className.includes("hide")) { + $childLi.classList.remove("hide"); + } else { + $childLi.classList.add("hide"); + } + } else if (className.includes("delete-page-btn")) { + removePost(id, this.state.openedId); + } else if (className.includes("create-child-page-btn")) { + $createPostModal.setState({ + link: `/documents/${id}`, + modalOpen: true, + createdChildPost: (newChildId) => { + if (newChildId) { + this.setOpenedLists([...this.state.openedId, id]); + $childLi.classList.remove("hide"); + } + }, + }); + } else { + clickPost(id); + } + } + }); + + /** + * ํฌ์ŠคํŠธ ๋ฆฌ์ŠคํŠธ ๋น„์–ด์žˆ์„๋•Œ + * ๊ธ€ ์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ + */ + $emptyPostDiv.addEventListener("click", (e) => { + const { className } = e.target; + if (className.includes("first-create-page-btn")) { + $createPostModal.setState({ + link: "/documents/new", + modalOpen: true, + }); + } + }); + + /** + * ๋ฃจํŠธ ํŽ˜์ด์ง€ ์ƒ์„ฑ ๋ชจ๋‹ฌ + */ + const $createPostModal = new CreatePostModal({ + $target, + initialState: { link: `/documents/new`, modalOpen: false }, + createPost, + }); + + this.render(); +} diff --git a/src/components/PostsPage.js b/src/components/PostsPage.js new file mode 100644 index 00000000..435f0cad --- /dev/null +++ b/src/components/PostsPage.js @@ -0,0 +1,60 @@ +import PostsPageHeader from "./PostsPageHeader.js"; +import CreatePostButton from "./CreatePostButton.js"; +import PostList from "./PostList.js"; +import { request } from "../api/index.js"; +import { push } from "../routes/router.js"; + +export default function PostsPage({ $target }) { + const $postPage = document.createElement("div"); + + this.setState = async (nextState) => { + const postsList = await request("/documents", { + method: "GET", + }); + postList.setState({ postsList, selectedPost: nextState }); + this.render(); + }; + + new PostsPageHeader({ $target: $postPage }); + const postList = new PostList({ + $target: $postPage, + + removePost: async (id, openedId) => { + const post = await request(`/documents/${id}`, { + method: "GET", + }); + if (confirm(`์ œ๋ชฉ : ${post.title}\nํ•ด๋‹น ๊ธ€์„ ์‚ญ์ œํ• ๊นŒ์š”?`)) { + const res = await request(`/documents/${post.id}`, { + method: "DELETE", + }); + if (res.id) { + this.setState(); + postList.setOpenedLists(openedId.filter((item) => item !== id)); + push("/"); + } + } + }, + + clickPost: (id) => { + push(`/documents/${id}`); + }, + + createPost: (createdPost) => { + this.setState(createdPost); + }, + }); + + new CreatePostButton({ + $target, + initialState: { link: "/documents/new", modalOpen: false }, + createPost: (createdPost) => { + this.setState(createdPost); + }, + }); + + this.render = () => { + $target.appendChild($postPage); + }; + + this.render(); +} diff --git a/src/components/PostsPageHeader.js b/src/components/PostsPageHeader.js new file mode 100644 index 00000000..b37cf113 --- /dev/null +++ b/src/components/PostsPageHeader.js @@ -0,0 +1,17 @@ +import { $postsPageHeader } from "../utils/templates.js"; + +export default function PostsPageHeader({ $target }) { + const $navHeader = document.createElement("div"); + + $target.appendChild($navHeader); + + this.render = () => { + $navHeader.innerHTML = $postsPageHeader(); + }; + + $navHeader.addEventListener("click", () => { + window.location = window.location.origin; + }); + + this.render(); +} diff --git a/src/components/modal/CreatePostModal.js b/src/components/modal/CreatePostModal.js new file mode 100644 index 00000000..490a915f --- /dev/null +++ b/src/components/modal/CreatePostModal.js @@ -0,0 +1,76 @@ +import { $createPostModal } from "../../utils/templates.js"; +import { modalClose, modalShow } from "./handler.js"; +import { push } from "../../routes/router.js"; +import { request } from "../../api/index.js"; + +export default function CreatePostModal({ $target, initialState, createPost }) { + const $modal = document.createElement("div"); + $target.appendChild($modal); + + this.state = initialState; + this.body = { title: "" }; + + this.setState = (nextState) => { + this.state = nextState; + if (this.state.modalOpen) { + modalShow($modal); + modalEventListener(); + } else modalClose($modal); + }; + + this.setBody = (nextState) => { + this.body = nextState; + }; + + const modalEventListener = () => { + const $overlay = document.querySelectorAll(".overlay"); + $overlay.forEach((item) => { + item.addEventListener("click", () => { + modalClose($modal); + }); + }); + }; + + this.render = () => { + $modal.innerHTML = $createPostModal(); + }; + + $modal.addEventListener("keyup", (e) => { + const { target } = e; + const name = target.getAttribute("name"); + + let state = { ...this.state }; + const [, , id] = state.link.split("/"); + this.setBody({ + parent: id === "new" ? null : id, + [name]: target.innerText, + }); + }); + + $modal.addEventListener("click", async (e) => { + const { target } = e; + const $closeBtn = target.closest(".close"); + const $createBtn = target.closest(".create-post-btn"); + + if ($createBtn) { + if (this.body.title.length === 0) { + alert("๊ธ€ ์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”!"); + return; + } + const createdPost = await request("/documents", { + method: "POST", + body: JSON.stringify(this.body), + }); + if (createdPost.id) { + modalClose($modal); + push(`/documents/${createdPost.id}`); + this.state.createdChildPost(createdPost.id); + createPost(createdPost); + } + } else if ($closeBtn) { + modalClose($modal); + } + }); + + this.render(); +} diff --git a/src/components/modal/handler.js b/src/components/modal/handler.js new file mode 100644 index 00000000..92c5ea75 --- /dev/null +++ b/src/components/modal/handler.js @@ -0,0 +1,12 @@ +// ๋ชจ๋‹ฌ ์ž…๋ ฅ ์ฐฝ ๋‹ซ๊ธฐ +const modalClose = ($target) => { + $target.querySelector(".container-modal").classList.remove("post-modal-show"); + $target.querySelector('div[name="title"]').innerText = ""; +}; + +// ๋ชจ๋‹ฌ ์ž…๋ ฅ ์ฐฝ ๋„์šฐ๊ธฐ +const modalShow = ($target) => { + $target.querySelector(".container-modal").classList.add("post-modal-show"); +}; + +export { modalShow, modalClose }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..3876e42a --- /dev/null +++ b/src/index.js @@ -0,0 +1,5 @@ +import App from "./App.js"; + +const $target = document.querySelector("#app"); + +new App({ $target }); diff --git a/src/pages/HomePage.js b/src/pages/HomePage.js new file mode 100644 index 00000000..bb1ec099 --- /dev/null +++ b/src/pages/HomePage.js @@ -0,0 +1,13 @@ +import { $home } from "../utils/templates.js"; + +export default function HomePage({ $target }) { + const $homePage = document.createElement("div"); + + $target.appendChild($homePage); + + this.render = () => { + $homePage.innerHTML = $home(); + }; + + this.render(); +} diff --git a/src/pages/NotFound.js b/src/pages/NotFound.js new file mode 100644 index 00000000..911b0d93 --- /dev/null +++ b/src/pages/NotFound.js @@ -0,0 +1,24 @@ +export default function NotFound({ $target }) { + const $notFound = document.createElement("div"); + $notFound.className = "not-found"; + + $target.appendChild($notFound); + + this.render = () => { + $notFound.innerHTML = ` +

ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๐Ÿ˜ข

+ + `; + }; + + this.init = () => { + $notFound.addEventListener("click", (e) => { + const $button = e.target.closest("button"); + if ($button) { + window.location = window.location.origin; + } + }); + }; + + this.init(); +} diff --git a/src/routes/router.js b/src/routes/router.js new file mode 100644 index 00000000..090b5bcc --- /dev/null +++ b/src/routes/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, + }, + }) + ); +}; diff --git a/src/utils/storage.js b/src/utils/storage.js new file mode 100644 index 00000000..b95c768d --- /dev/null +++ b/src/utils/storage.js @@ -0,0 +1,18 @@ +const storage = window.localStorage; + +export const setItem = (key, value) => { + storage.setItem(key, JSON.stringify(value)); +}; + +export const getItem = (key, defaultValue) => { + try { + const storedValue = storage.getItem(key); + return storedValue ? JSON.parse(storedValue) : defaultValue; + } catch (error) { + return defaultValue; + } +}; + +export const removeItem = (key) => { + storage.removeItem(key); +}; diff --git a/src/utils/templates.js b/src/utils/templates.js new file mode 100644 index 00000000..10e748b7 --- /dev/null +++ b/src/utils/templates.js @@ -0,0 +1,209 @@ +const $home = () => { + return ` +
+

๐Ÿ”ณ JooNotion

+
+ `; +}; + +const formatTime = (time) => { + if (isNaN(time)) return; + + const date = ` + ${time.getFullYear()}-${String(time.getMonth() + 1).padStart(2, "0")}-${String( + time.getDate() + ).padStart(2, "0")} + / ${String(time.getHours()).padStart(2, "0")} : ${String( + time.getMinutes() + ).padStart(2, "0")}`; + + return date; +}; + +/** + * ์—๋””ํ„ฐ ํ…œํ”Œ๋ฆฟ + * @param {*} param0 + * @returns + */ +const $editPost = ({ title, content, createdAt, updatedAt }) => { + content = content === null ? "" : content; + return ` +
+
${title}
+
+
+ ์ƒ์„ฑ๋‚ ์งœ : ${formatTime(new Date(createdAt))} + ์ˆ˜์ •๋‚ ์งœ : ${formatTime(new Date(updatedAt))} +
+
+
${content}
+
+ `; +}; + +/** + * ๋ฆฌ์ŠคํŠธ ํ…œํ”Œ๋ฆฟ + * @param {*} posts + * @returns + */ +const $listContent = (post, selectedPostId, openedId) => { + let isIncluded = false; + if (openedId.length > 0 && openedId !== undefined) { + isIncluded = openedId.includes(post.id.toString()); + } + return ` +
+
+ + + ${post.title} + +
+
+ + +
+
+ `; +}; + +/** + * ์ž์‹ ๋ฆฌ์ŠคํŠธ ์ถœ๋ ฅ + * @param {*} posts + * @returns + */ +const $onLoadChildList = (posts, selectedPostId, openedId, hasIncluded) => { + if (Array.isArray(posts)) { + return ` +
+ ${posts + .map((post) => { + const isChildIncluded = openedId.includes(post.id.toString()); + return ` + ${$listContent(post, selectedPostId, openedId)} + ${ + post.documents.length > 0 + ? `${$onLoadChildList( + post.documents, + selectedPostId, + openedId, + isChildIncluded + )}` + : `${$emptyPage(isChildIncluded)}` + } + `; + }) + .join("")} +
+ `; + } +}; + +/** + * ๋ถ€๋ชจ ๋ฆฌ์ŠคํŠธ ์ „์ฒด ์ถœ๋ ฅ + * @param {*} posts + * @returns + */ +const $onLoadParentList = (posts, selectedPostId, openedId) => { + if (Array.isArray(posts)) { + return ` + + `; + } +}; + +/** + * ํฌ์ŠคํŠธ ๋ฆฌ์ŠคํŠธ ํ—ค๋” + * @returns + */ +const $postsPageHeader = () => { + return ` + + `; +}; + +/** + * ํ•˜์œ„ ํŽ˜์ด์ง€ ์—†์Œ + * @returns + */ +const $emptyPage = (hasIncluded) => { + return ` +
+ ํ•˜์œ„ ํŽ˜์ด์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. +
+ `; +}; + +/** + * ํŽ˜์ด์ง€ ์ถ”๊ฐ€ ๋ฒ„ํŠผ + * @returns + */ +const $createPostBtn = () => { + return ` +
+ +
+ `; +}; + +/** + * ํฌ์ŠคํŠธ ์ž‘์„ฑ ๋ชจ๋‹ฌ์ฐฝ + * @returns html + * + */ +const $createPostModal = () => { + return ` +
+
+ +
+ `; +}; + +export { + $home, + $editPost, + $postsPageHeader, + $onLoadParentList, + $emptyPage, + $createPostBtn, + $createPostModal, +}; diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..98c2c171 --- /dev/null +++ b/vercel.json @@ -0,0 +1,3 @@ +{ + "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }] +}