Skip to content
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vscode
.env.js
15 changes: 15 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="ko">

<head>
<meta charset="UTF-8">
<title>Fry's Notion</title>
<link rel="stylesheet" href="/style.css" type="text/css" />
</head>

<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

</html>
49 changes: 49 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Router from "./Router.js";
import { routes } from "./constants/routes.js";
import { navigate } from "./utils/navigate.js";

export default function App({ $target }) {
const findMatchedRoute = (pathname) => routes.find((route) => route.path.test(pathname));

this.$target = $target;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ask) 코드를 읽어보니 모두 this.$target = $target 으로 초기 설정을 하셨는데 이유가 뭔가요????? 궁금합니다.. 뭔가 규란님이라면 이유가 있어서 하셨을 것 같아서요 !!

this.state = { currentPage: null };

this.init = () => {
new Router({ onRoute: this.route.bind(this) });
};

this.route = () => {
const { pathname } = location;
const { currentPage } = this.state;
const nextPage = findMatchedRoute(pathname)?.page;

const documentIdFromPath = pathname.split("/documents/")[1];
const documentIdFromHistory = history.state?.documentId || null;

if (documentIdFromPath != documentIdFromHistory || !nextPage) {
navigate("/404", true);
return;
}

const needNewPage =
!currentPage || !(currentPage instanceof nextPage) || !documentIdFromHistory;

if (needNewPage) {
this.setState({
currentPage: new nextPage({
$target: this.$target,
initialState: { documentId: documentIdFromHistory },
}),
});
return;
}

this.state.currentPage.setState({ documentId: documentIdFromHistory });
};

this.setState = (newState) => {
this.state = { ...this.state, ...newState };
};

this.init();
}
25 changes: 25 additions & 0 deletions src/Router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { HISTORY_CHANGE, POP_STATE } from "./constants/routes.js";

export default function Router({ onRoute }) {
this.init = () => {
window.addEventListener(HISTORY_CHANGE, ({ detail }) => {
const { to, isReplace, state } = detail;

if (isReplace || to === location.pathname) {
history.replaceState(state, "", to);
} else {
history.pushState(state, "", to);
}

onRoute();
});

window.addEventListener(POP_STATE, () => {
onRoute();
});

onRoute();
};

this.init();
}
24 changes: 24 additions & 0 deletions src/components/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default function Component({ $target, initialState }) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) 현재 사용되지 않는 파일 같네요! 아마 클래스로 짜시다가 중간에 함수형으로 바꾸셨다고 했는데 그 과정에서 남기신게 아닐까.. 추측해봅니다!

this.$target = $target;
this.state = { ...initialState };

this.init = () => {
this.render();
};

this.setState = () => {
this.render();
};

this.render = () => {
this.$target.innerHTML = `
`;
this.mounted();
};

this.mounted = () => {};

this.setEvent = () => {};

this.init();
}
106 changes: 106 additions & 0 deletions src/components/document/Document.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import DocumentHeader from "./DocumentHeader.js";
import DocumentContent from "./DocumentContent.js";

import API from "../../utils/api.js";
import { setItemToStorage, getItemFromStorage } from "../../utils/storage.js";
import { debounce } from "../../utils/index.js";
import { navigate } from "../../utils/navigate.js";

export default function Document({
$target,
initialState = {
documentId: null,
},
}) {
const fetchDocument = async (documentId) => {
const response = await API.getDocuments(documentId);
Comment on lines +15 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API를 객체로 선언하시다니...! 이렇게 작성하면 실수를 줄일 수 있겠네요!😲


if (!response) {
navigate("/", true);
return;
}

return [{ title: response.title, content: response.content }, response];
};

const handleDocumentEdit = async (text, section = "title") => {
const storedItem = getItemFromStorage("notion", { currentDocument: {} });

storedItem.currentDocument = {
...storedItem.currentDocument,
[section]: text,
tempSavedAt: new Date(),
};
setItemToStorage("notion", { ...storedItem });

const { title, content } = storedItem.currentDocument;
await API.updateDocument(this.state.documentId, { title, content });
};

const renderDocumentById = async ($header, $body) => {
const { documentId } = this.state;
const [{ title, content }, response] = await fetchDocument(documentId);

setItemToStorage("notion", { currentDocument: response });

new DocumentHeader({
$target: $header,
initialState: { title },
onEdit: debounce(handleDocumentEdit, 300),
});
new DocumentContent({
$target: $body,
initialState: { content },
onEdit: debounce(handleDocumentEdit, 300),
});
};

const renderNewDocument = ($header, $body) => {
new DocumentHeader({ $target: $header, onEdit: debounce(handleDocumentEdit, 300) });
new DocumentContent({ $target: $body, onEdit: debounce(handleDocumentEdit, 300) });
};

const renderNoDocument = ($container) => {
$container.innerHTML = `
<h1 style="color: rgb(102, 75, 63, 0.7); font-weight: 800;">Notion에 오신 것을 환영해요!</h1>
<img src="https://media3.giphy.com/media/KjuQizGwJCsgoYdziS/giphy.gif?cid=ecf05e47ly3czt6iu86gd916h6oqna0t6wnb0e95ldri599i&rid=giphy.gif&ct=s" />
`;
};

this.$target = $target;
this.state = initialState;

this.init = () => {
this.render();
};

this.render = () => {
this.$target.innerHTML = `
<div id="document">
<div id="document-header"></div>
<div id="document-body"></div>
</div>
`;

this.mounted();
};

this.mounted = async () => {
const $container = this.$target.querySelector("#document");
const $header = this.$target.querySelector("#document-header");
const $body = this.$target.querySelector("#document-body");

const { documentId } = this.state;

if (documentId === "new") {
renderNewDocument($header, $body);
return;
} else if (!!documentId) {
renderDocumentById($header, $body);
} else {
renderNoDocument($container);
}
};

this.init();
}
41 changes: 41 additions & 0 deletions src/components/document/DocumentContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { addEvent } from "../../utils/event.js";
import { parseNewline } from "../../utils/index.js";

export default function DocumentContent({
$target,
initialState = {
content: "",
},
onEdit,
}) {
this.$target = $target;
this.state = initialState;

this.init = () => {
this.setEvent();
this.render();
};

this.render = () => {
const { content } = this.state;

this.$target.innerHTML = `
<div id="document-editor" name="content" contenteditable="true" placeholder="Type for creating new document">${parseNewline(
content ?? ""
)}</div>
`;
};

this.setEvent = () => {
addEvent(this.$target, "keyup", "[name=content]", (event) => {
onEdit(event.target.innerText, "content");
});
};
Comment on lines +29 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이벤트 리스너를 따로 함수로 만드신 건가요?! 이렇게 코드를 작성할 수도 있군요... 😮


this.setState = (newState) => {
this.state = { ...this.state, ...newState };
this.render();
};

this.init();
}
35 changes: 35 additions & 0 deletions src/components/document/DocumentHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DEFAULT } from "../../constants/config.js";
import { addEvent } from "../../utils/event.js";

export default function DocumentHeader({
$target,
initialState = {
title: "",
},
onEdit,
}) {
this.$target = $target;
this.state = initialState;

this.init = () => {
this.setEvent();
this.render();
};

this.render = () => {
const { title } = this.state;

this.$target.innerHTML = `
<div class="title" name="title" placeholder=${DEFAULT.DOCUMENT_NAME} contenteditable="true">${
title === DEFAULT.DOCUMENT_NAME ? "" : title
}</div>`;
};

this.setEvent = () => {
addEvent(this.$target, "keyup", "[name=title]", (event) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P5 : 저의 경우에는 event type을 keyup으로 해두면 화살표 키나 shift, caps lock 등 입력 값이 변하지 않았음에도 이벤트가 발생해서 유효하지 않은 api가 날라가더라구요. 그래서 input으로 바꿔서 입력값이 변할 때에만 api가 날라갈 수 있게 했었습니다.

그리고 이전에 React에서 겪은 적이 있는데, event type을 keyup으로 해두고 한글을 입력하면 이벤트가 중복해서 2번 발생하는 현상이 있었습니다. 실제로 검색해보니 javascript 자체의 문제더라구요. 한글 관련 에러여서 공식 자료도 찾기 어려웠었습니다. 이런 현상을 피하기 위해서 다른 event type을 고려해보시는 것도 좋을 것 같습니다!

p.s 과제에서 확인해보니, 여기선 keyup으로 해도 이벤트가 중복 발생하진 않네요!

reference : https://blog.naver.com/PostView.naver?blogId=bbak0105&logNo=222371386648&parentCategoryNo=&categoryNo=44&viewDate=&isShowPopularPosts=true&from=search

onEdit(event.target.innerText, "title");
});
};

this.init();
}
60 changes: 60 additions & 0 deletions src/components/sidebar/Details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import DocumentItem from "./DocumentItem.js";
import { createDocumentsListElement, findDocumentElement } from "../../utils/helper.js";

export default function Details({
$target,
initialState = {
id: null,
title: "",
documents: [],
},
onAddButtonClick,
}) {
this.$target = $target;
this.state = initialState;

this.init = () => {
this.render();
};

this.render = () => {
const { documents } = this.state;

this.$target.innerHTML = `
<details id="document-details">
<summary></summary>
<ul>${createDocumentsListElement(documents)}</ul>
</details>
`;

this.mounted();
};

this.mounted = () => {
const $rootDocument = this.$target.querySelector("summary");
const { id, title, documents } = this.state;

new DocumentItem({
$target: $rootDocument,
initialState: { id, title },
onAddButtonClick,
});

documents.forEach(({ id, title, documents }) => {
const $target = findDocumentElement(id);

new Details({
$target,
initialState: { title, id, documents },
onAddButtonClick,
});
});
};

this.setState = (nextState) => {
this.state = { ...this.state, ...nextState };
this.render();
};

this.init();
}
Loading