diff --git a/.eslintrc b/.eslintrc index d30435505..0002e9777 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,6 +5,9 @@ "jest": true, "es6": true }, + "parserOptions": { + "ecmaVersion": 2017 + }, "rules": { "complexity": [0, 11], "consistent-return": 2, diff --git a/src/library/css/article.css b/src/library/css/article.css new file mode 100644 index 000000000..fada05a4c --- /dev/null +++ b/src/library/css/article.css @@ -0,0 +1,118 @@ +.article_header, +.article_filter { + color: #444c63; + font-size: 22px; + height: 70px; + background-color: #eef1f7; + border-bottom: 1px solid #dadfea; + border-top: 1px solid #f4f6fa; +} + +.article_header-label { + margin: 23px; +} + +.article_filter { + display: flex; + align-items: center; + justify-content: space-between; +} + +.article_filter ul { + display: flex; + width: 355px; +} + +.article_filter li { + padding: 4px 11px; + margin: 0; + border-radius: 10px; +} + +.article_filter li:hover { + background-color: #97b3ce; +} + +.article_books { + display: flex; + flex-wrap: wrap; +} + +.article_books img { + width: 200px; + height: 275px; +} + +.article_books h3 { + color: #444c63; + font-size: 16px; + font-weight: normal; + margin: 0; +} + +.article_books p { + color: #6f7d95; + font-size: 12px; + margin: 0; +} + +.article_books_book { + margin: 30px 20px; + width: 200px; +} + +.star_selected { + fill: #ffab00; + stroke: #ffab00; + stroke-width: 2px; +} + +.star_unselected { + stroke: #ffab00; + fill: white; + stroke-width: 2px; +} + +.star_tmpselected { + fill: #ffab00; + stroke: #ffab00; +} + +.star_tmpunselected { + fill: white; + stroke: #facc6f; +} + +.article_search { + width: 400px; + height: 32px; + border: 1px solid rgba(140, 151, 178, 0.4); + border-radius: 16px; + padding: 0 17px; + outline: none; +} + +.article_search::-moz-placeholder { + color: #8c97b2; +} + +.article_search::-webkit-input-placeholder { + color: #8c97b2; +} + +.article_search-img { + position: absolute; + top: 9px; + right: 30px; + width: 13px; + height: 13px; +} + +.article_search-label { + position: relative; + height: 32px; +} + +.article_filter .selected-filter { + font-weight: bold; +} \ No newline at end of file diff --git a/src/library/css/footer.css b/src/library/css/footer.css new file mode 100644 index 000000000..6de2576a6 --- /dev/null +++ b/src/library/css/footer.css @@ -0,0 +1,20 @@ +.footer__item { + float: right; + background-color: #1f2637; + width: 55px; + height: 45px; + display: flex; + align-items: center; + justify-content: center; +} + +.footer__item:hover { + background-color: #15a4fa; + ; +} + +.footer__item img { + float: center; + width: 14px; + height: 14px; +} \ No newline at end of file diff --git a/src/library/css/header.css b/src/library/css/header.css new file mode 100644 index 000000000..c3d4e6c5a --- /dev/null +++ b/src/library/css/header.css @@ -0,0 +1,34 @@ +.header__button { + color: #78829d; + font-size: 13px; + margin: 15px; +} + +.header__marker { + border: 2px solid #19202e; + border-radius: 100%; +} + +.header__user { + color: #ffffff; + font-size: 13px; + width: 158px; + background-color: #15a4fa; + height: 100%; + border-top-right-radius: 4px; + display: flex; + justify-content: space-around; + align-items: center; +} + +.header__user-icons { + width: 12px; + height: 12px; +} + +.header__user-photo { + width: 31px; + height: 32px; + border: 2px solid #eef1f7; + border-radius: 100%; +} \ No newline at end of file diff --git a/src/library/css/library.css b/src/library/css/library.css new file mode 100644 index 000000000..feab89dfa --- /dev/null +++ b/src/library/css/library.css @@ -0,0 +1,66 @@ +@font-face { + font-family: "Proxima Nova"; + src: url(../fonts/12093.ttf); +} + +.library { + display: grid; + margin: 0; + font-family: "Proxima Nova"; + grid-template-areas: "header header" "navigation article" "footer footer"; + grid-template-columns: 200px 1fr; +} + +.header { + grid-area: header; + height: 45px; + background-color: #2f364a; + display: flex; + flex-wrap: nowrap; + justify-content: flex-end; + align-items: center; + border-top-right-radius: 4px; + position: fixed; + width: 100%; + z-index: 1; +} + +.navigation { + height: 100%; + grid-area: navigation; + background-color: #181e2c; + margin: 44px 0; +} + +.article { + grid-area: article; + margin: 45px 0; +} + +.footer { + height: 45px; + grid-area: footer; + background-color: #181e2c; + position: fixed; + bottom: 0px; + width: 100%; +} + +.library ul { + padding: 0; +} + +.library li { + color: #78829d; + font-size: 13px; + text-transform: capitalize; + padding: 0 0 0 46px; + list-style-type: none; + position: relative; + cursor: pointer; +} + +.library li:hover { + color: white; + background-color: #15a4fa; +} \ No newline at end of file diff --git a/src/library/css/modalWindow.css b/src/library/css/modalWindow.css new file mode 100644 index 000000000..fec962744 --- /dev/null +++ b/src/library/css/modalWindow.css @@ -0,0 +1,67 @@ +.modal-window { + display: none; + position: absolute; + position: fixed; + z-index: 2; + margin: auto; + width: 100%; + height: 100%; + justify-content: center; + align-items: center; + background-color: rgba(49, 48, 48, 0.5) +} + +.modal-window label { + width: 180px; + display: inline-block; + margin: 5px 10px; +} + +.modal-window form { + background-color: #1f2637; + color: #78829d; + padding: 30px; + border-radius: 10px; + border: 1px solid black; +} + +.modal-window p { + margin: 20px 20px 5px 20px; +} + +.modal-window input { + border-radius: 3px; +} + +.modal-window__buttons { + text-align: center; +} + +.modal-window__button { + border-radius: 3px; + width: 90px; + height: 20px; + background-color: #f2795a; + color: white; + border: none; + font-size: 14px; + margin: 10px; +} + +.modal-window__button:hover { + background-color: #f05a34; +} + +.modal-window i { + color: red; + margin: 5px; +} + +.modal-window__hint { + color: red; + display: none; +} + +.modal-window__container-hint { + height: 20px; +} \ No newline at end of file diff --git a/src/library/css/navigation.css b/src/library/css/navigation.css new file mode 100644 index 000000000..48c51c5a8 --- /dev/null +++ b/src/library/css/navigation.css @@ -0,0 +1,153 @@ +.navigation__module { + background-color: #1f2637; + margin: 4px 0; + border-bottom: 1px solid #141824; + border-top: 1px solid #78829d; +} + +.navigation__item-history p { + font-size: 12px; + color: #6f7d95; +} + +.navigation__item-history strong { + font-size: 12px; + font-weight: normal; + color: #97b3ce; +} + +.navigation__item-history { + margin: 25px 25px 25px 46px; + position: relative; +} + +.navigation__menu li::before { + content: ""; + position: absolute; + width: 14px; + height: 14px; + top: 11px; + left: 21px; +} + +.navigation__menu li:nth-child(1)::before { + background-image: url(../img/book.svg); +} + +.navigation__menu li:nth-child(1):hover::before { + background-image: url(../img/book_hover.svg); +} + +.navigation__menu li:nth-child(2)::before { + background-image: url(../img/globe.svg); +} + +.navigation__menu li:nth-child(2):hover::before { + background-image: url(../img/globe_hover.svg); +} + +.navigation__menu li:nth-child(3)::before { + background-image: url(../img/shoppingtrolley.svg); +} + +.navigation__menu li:nth-child(3):hover::before { + background-image: url(../img/shoppingtrolley_hover.svg); +} + +.navigation__menu li:nth-child(4)::before { + background-image: url(../img/star.svg); +} + +.navigation__menu li:nth-child(4):hover::before { + background-image: url(../img/star_hover.svg); +} + +.navigation__menu li:nth-child(5)::before { + background-image: url(../img/list_icon.svg); +} + +.navigation__menu li:nth-child(5):hover::before { + background-image: url(../img/list_icon_hover.svg); +} + +.navigation__menu li:nth-child(6)::before { + background-image: url(../img/clock.svg); +} + +.navigation__menu li:nth-child(6):hover::before { + background-image: url(../img/clock_hover.svg); +} + +.navigation__menu-filter li::before { + content: ""; + width: 6px; + height: 6px; + position: absolute; + top: 16px; + left: 25px; + border-radius: 100%; +} + +.navigation__menu-filter li:nth-child(1)::before { + background-color: #e64c66; +} + +.navigation__menu-filter li:nth-child(2)::before { + background-color: #ffab00; +} + +.navigation__menu-filter li:nth-child(3)::before { + background-color: #00bfdd; +} + +.navigation__menu-filter li:nth-child(4)::before { + background-color: #7874cf; +} + +.navigation__clock::before { + content: ""; + position: absolute; + width: 14px; + height: 14px; + background-image: url(../img/clock.svg); + top: 10px; + left: -25px; +} + +.navigation__menu-filter li { + height: 38px; + line-height: 38px; + display: inline-block; + width: 154px; +} + +.navigation__menu a { + height: 38px; + line-height: 38px; + display: inline-block; +} + +.navigation__module-button-add { + text-transform: uppercase; + width: 160px; + height: 44px; + background-color: #f2795a; + border-radius: 4px; + color: white; + border: none; + margin: 35px 20px; + font-size: 14px; +} + +.navigation__module-button-add:hover { + background-color: #f05a34; +} + +.navigation__module-button-add img { + margin: 0 10px; +} + +.navigation__menu-filter .selected-filter { + background-color: #37415c; + box-shadow: inset 0 0 5px #181d2b; +} \ No newline at end of file diff --git a/src/library/fonts/12093.ttf b/src/library/fonts/12093.ttf new file mode 100644 index 000000000..7d0024750 Binary files /dev/null and b/src/library/fonts/12093.ttf differ diff --git a/src/library/img/+.png b/src/library/img/+.png new file mode 100644 index 000000000..ae2f10646 Binary files /dev/null and b/src/library/img/+.png differ diff --git a/src/library/img/arrowDown.png b/src/library/img/arrowDown.png new file mode 100644 index 000000000..8a96fdaf9 Binary files /dev/null and b/src/library/img/arrowDown.png differ diff --git a/src/library/img/avatar-01.png b/src/library/img/avatar-01.png new file mode 100644 index 000000000..00fb845db Binary files /dev/null and b/src/library/img/avatar-01.png differ diff --git a/src/library/img/book.svg b/src/library/img/book.svg new file mode 100644 index 000000000..db5a3478e --- /dev/null +++ b/src/library/img/book.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/library/img/book_hover.svg b/src/library/img/book_hover.svg new file mode 100644 index 000000000..e7831bd18 --- /dev/null +++ b/src/library/img/book_hover.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/library/img/clock.svg b/src/library/img/clock.svg new file mode 100644 index 000000000..d1a751331 --- /dev/null +++ b/src/library/img/clock.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/library/img/clock_hover.svg b/src/library/img/clock_hover.svg new file mode 100644 index 000000000..ed445aeb6 --- /dev/null +++ b/src/library/img/clock_hover.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/library/img/footer_hint.png b/src/library/img/footer_hint.png new file mode 100644 index 000000000..6eff1a81a Binary files /dev/null and b/src/library/img/footer_hint.png differ diff --git a/src/library/img/footer_settings.png b/src/library/img/footer_settings.png new file mode 100644 index 000000000..b87249a5a Binary files /dev/null and b/src/library/img/footer_settings.png differ diff --git a/src/library/img/globe.svg b/src/library/img/globe.svg new file mode 100644 index 000000000..3cb328b36 --- /dev/null +++ b/src/library/img/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/library/img/globe_hover.svg b/src/library/img/globe_hover.svg new file mode 100644 index 000000000..d21052b54 --- /dev/null +++ b/src/library/img/globe_hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/library/img/list_icon.svg b/src/library/img/list_icon.svg new file mode 100644 index 000000000..8fe8f2760 --- /dev/null +++ b/src/library/img/list_icon.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/library/img/list_icon_hover.svg b/src/library/img/list_icon_hover.svg new file mode 100644 index 000000000..3f64295e8 --- /dev/null +++ b/src/library/img/list_icon_hover.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/library/img/search.png b/src/library/img/search.png new file mode 100644 index 000000000..0875f22f1 Binary files /dev/null and b/src/library/img/search.png differ diff --git a/src/library/img/shoppingtrolley.svg b/src/library/img/shoppingtrolley.svg new file mode 100644 index 000000000..a8a6c47b7 --- /dev/null +++ b/src/library/img/shoppingtrolley.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/library/img/shoppingtrolley_hover.svg b/src/library/img/shoppingtrolley_hover.svg new file mode 100644 index 000000000..921cc369a --- /dev/null +++ b/src/library/img/shoppingtrolley_hover.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/library/img/star.svg b/src/library/img/star.svg new file mode 100644 index 000000000..83d5aad61 --- /dev/null +++ b/src/library/img/star.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/library/img/star_hover.svg b/src/library/img/star_hover.svg new file mode 100644 index 000000000..275badae4 --- /dev/null +++ b/src/library/img/star_hover.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/library/img/star_rating.svg b/src/library/img/star_rating.svg new file mode 100644 index 000000000..22909e959 --- /dev/null +++ b/src/library/img/star_rating.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/library/library.html b/src/library/library.html new file mode 100644 index 000000000..f3243bcff --- /dev/null +++ b/src/library/library.html @@ -0,0 +1,148 @@ + + + + + + Library + + + + + + + + + +
+
+

Help Center

+
+
+
+

Our Support

+
+
+ foto +

John Doe

+ +
+
+ +
+
+

Browse Available Books

+
+
+ + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/library/scripts/app.js b/src/library/scripts/app.js new file mode 100644 index 000000000..4a2aad743 --- /dev/null +++ b/src/library/scripts/app.js @@ -0,0 +1 @@ +new MainController().init(); \ No newline at end of file diff --git a/src/library/scripts/app/mainController.js b/src/library/scripts/app/mainController.js new file mode 100644 index 000000000..a7c88611a --- /dev/null +++ b/src/library/scripts/app/mainController.js @@ -0,0 +1,13 @@ +function MainController() { + return Controller.call(this); +} +MainController.prototype = Object.create(MainController.prototype); +MainController.prototype.constructor = Controller; +MainController.prototype.init = function () { + this.bookController = new BookController(); + this.bookController.init(); + this.historyController = new HistoryController(); + this.bookController.setOnHistoryEventHandler(this.historyController.onNewEvent.bind(this.historyController)); + this.historyController.init(); +}; + diff --git a/src/library/scripts/books/bookController.js b/src/library/scripts/books/bookController.js new file mode 100644 index 000000000..02820d4bb --- /dev/null +++ b/src/library/scripts/books/bookController.js @@ -0,0 +1,156 @@ +var urlBooks = "https://rsu-library-api.herokuapp.com/books"; + +function BookController() { + Controller.call(this); + this.addBookController = new ModalWindowController(); + this.addBookController.init(); + this.addBookController.setonAddBookEventHandler((book) => { + this.addBook(book); + message = `You added a new book ${book.title} by ${book.author.firstName} ${book.author.lastName}`; + this.onHistoryEventHandler(message, new Date()); + }); + this.onHistoryEventHandler = function() {}; + this.lastFilteredCategory = null; + this.view = new BookView(); + return this; +} +BookController.prototype = Object.create(Controller.prototype); +BookController.prototype.constructor = BookController; +BookController.prototype.setOnHistoryEventHandler = function(cb) { + this.onHistoryEventHandler = cb; +}; +BookController.prototype.onBtnAllBooksHandler = function() { + this.view.changeCategorySelection(event.target) + this.store.dropSort(); +}; +BookController.prototype.onBtnMostRecentHandler = function() { + this.view.changeCategorySelection(event.target) + this.store.sort(book => book.updatedAt, true); +}; +BookController.prototype.onBtnMostPopular = function() { + this.view.changeCategorySelection(event.target) + this.store.sort(book => book.rating, true); +}; +BookController.prototype.onBtnFreeBooksHandler = function() { + this.view.changeCategorySelection(event.target) + this.store.sort(book => book.cost, false); +}; +BookController.prototype.onBtnCategoryFilterHandler = function(category, event) { + this.view.changeCategorySelection(event.target) + var store = this.store; + store.beginUpdate(); + var message; + try { + store.removePropFilter("CategoryFilter"); + if (this.lastFilteredCategory === category) { + message = `You have canceled filter by category ${category}`; + this.lastFilteredCategory = null; + } else { + this.lastFilteredCategory = category; + message = `You have applied filter by category ${category}`; + store.addPropFilter(function(item) { return item.categories.includes(category); }, "CategoryFilter"); + } + store.filter(); + } finally { + store.endUpdate(); + this.onHistoryEventHandler(message, new Date()); + } +}; +BookController.prototype.onStarClickHandlerWrapper = function(bookId, rating) { + var store = this.store; + var index = store.findIndex(book => book.id === bookId); + if (index >= 0) { + var book = store.getRealAt(index); + book.rating = rating; + } + store.changed(); +}; +BookController.prototype.onSearchFieldChangeHandler = function() { + var text = this.items.inpSearch.value.toLowerCase(); + const { store } = this; + store.beginUpdate(); + try { + store.removePropFilter("SearchAuthorAndBook"); + if (text && text !== "") { + store.addPropFilter(function(item) { + return item.author.firstName.toLowerCase().includes(text) || + item.author.lastName.toLowerCase().includes(text) || + item.title.toLowerCase().includes(text); + }, "SearchAuthorAndBook"); + } + store.filter(); + } finally { + this.onHistoryEventHandler(`You have applied search by word ${text}`, new Date()); + store.endUpdate(); + } +}; +BookController.prototype.LoadBooks = async function() { + await this.getDataFromServer(); +}; +BookController.prototype.getDataFromServer = async function() { + const response = await request(urlBooks); + const { store } = this; + store.beginUpdate(); + try { + if (response instanceof Array) { + for (const item of response) { + this.addBook(item); + } + } + } finally { + store.endUpdate(); + } +}; +BookController.prototype.addBook = function(bookSource) { + const book = new Book(); + book.id = generateId(); + const { author } = bookSource; + if (author) { + if (author.firstName) book.author.firstName = author.firstName; + if (author.lastName) book.author.lastName = author.lastName; + } + const { categories } = bookSource; + if (categories && (categories instanceof Array)) { + for (const item of categories) { + book.categories.push(item); + } + } + if (bookSource.cost) book.cost = bookSource.cost; + if (bookSource.createdAt) book.createdAt = bookSource.createdAt; + if (bookSource.image_url) book.image_url = bookSource.image_url; + if (bookSource.rating) book.rating = bookSource.rating; + if (bookSource.title) book.title = bookSource.title; + if (bookSource.updatedAt) book.updatedAt = bookSource.updatedAt; + this.store.add(book); +}; +BookController.prototype.onBtnAddBookHandler = function() { + this.addBookController.setActive(); +}; + +BookController.prototype.prepareListeners = function() { + Controller.prototype.prepareListeners.call(this); + var items = this.items; + items.btnAllBooks.addEventListener("click", this.onBtnAllBooksHandler.bind(this)); + items.btnMostRecent.addEventListener("click", this.onBtnMostRecentHandler.bind(this)); + items.btnMostPopular.addEventListener("click", this.onBtnMostPopular.bind(this)); + items.btnFreeBooks.addEventListener("click", this.onBtnFreeBooksHandler.bind(this)); + items.inpSearch.addEventListener("input", debounce(this.onSearchFieldChangeHandler.bind(this), 1000)); + items.btnMustReadTitles.addEventListener("click", (event) => this.onBtnCategoryFilterHandler("must_read", event)); + items.btnBestOfList.addEventListener("click", (event) => this.onBtnCategoryFilterHandler("best", event)); + items.btnClassicNovels.addEventListener("click", (event) => this.onBtnCategoryFilterHandler("classic", event)); + items.btnNonFiction.addEventListener("click", (event) => this.onBtnCategoryFilterHandler("non_fiction", event)); + + items.btnAddBook.addEventListener("click", this.onBtnAddBookHandler.bind(this)); + this.view.setOnStarClickHandler(this.onStarClickHandlerWrapper.bind(this)); +}; +BookController.prototype.prepareStore = async function() { + Controller.prototype.prepareStore.call(this); + await this.LoadBooks(); +}; +BookController.prototype.validation = function() { + var items = this.items; + if (items.title) { + console.log(this.items); + } + return false; +}; \ No newline at end of file diff --git a/src/library/scripts/books/bookModel.js b/src/library/scripts/books/bookModel.js new file mode 100644 index 000000000..a7f74774d --- /dev/null +++ b/src/library/scripts/books/bookModel.js @@ -0,0 +1,10 @@ +function Book() { + this.title = undefined; + this.author = { firstName: undefined, lastName: undefined }; + this.categories = []; + this.cost = undefined; + this.createdAt = new Date(); + this.updatedAt = new Date(); + this.image_url = undefined; + this.rating = undefined; +} diff --git a/src/library/scripts/books/bookView.js b/src/library/scripts/books/bookView.js new file mode 100644 index 000000000..07b984723 --- /dev/null +++ b/src/library/scripts/books/bookView.js @@ -0,0 +1,178 @@ +var bookIdSymb = Symbol("bookID"); +var starParentSymb = Symbol("parentStar"); +var starRatingSymb = Symbol("ratingStar"); +var isSelectedSymb = Symbol("selected"); +var groupSymb = Symbol("group"); + +function BookView() { + View.call(this); + this.onStarClickHandler = null; + this.containerName = "books"; + this.prepareItems(); + return this; +} +BookView.prototype = Object.create(View.prototype); +BookView.prototype.constructor = BookView; +BookView.prototype.setOnStarClickHandler = function(cb) { + this.onStarClickHandler = cb; +}; +BookView.prototype.onStarClickHandlerWrapper = function(event) { + var id = event.target[bookIdSymb]; + var rating = event.target[starRatingSymb]; + if (this.onStarClickHandler) this.onStarClickHandler(id, rating); +}; +BookView.prototype.updateContainer = function() { + View.prototype.updateContainer.call(this); + const { container } = this; + for (const book of this.store) { + container.appendChild(this.createBook(book)); + } + var starCont = container.getElementsByTagName("path"); + for (let i = 0; i < starCont.length; i++) { + starCont[i].addEventListener("mouseover", this.onMouseOverStar.bind(this)); + starCont[i].addEventListener("mouseout", this.onMouseOutOfStar.bind(this)); + starCont[i].addEventListener("click", this.onStarClickHandlerWrapper.bind(this)); + } +}; +BookView.prototype.fillstarsTmp = function(rating, id) { + var tmpsStar = document.getElementById(id).getElementsByTagName("svg"); + for (var i = 0; i < 5; i++) { + var svgClass = (rating > i) ? + " star_tmpselected" : + " star_tmpunselected"; + var tmpClass = tmpsStar[i].getAttribute("class") + svgClass; + tmpsStar[i].setAttribute("class", tmpClass); + } +}; +BookView.prototype.dellfillstarsTmp = function(id) { + var star = document.getElementById(id).getElementsByTagName("svg"); + for (var i = 0; i < 5; i++) { + var clazz = star[i].getAttribute("class").split(" ")[0]; + star[i].setAttribute("class", clazz); + } +}; +BookView.prototype.onMouseOverStar = function(event) { + if (event.target.id !== "") { + var rating = event.target[starRatingSymb]; + var parent = event.target[starParentSymb]; + this.fillstarsTmp(rating, parent); + } +}; +BookView.prototype.onMouseOutOfStar = function(event) { + var parent = event.target[starParentSymb]; + if (parent) this.dellfillstarsTmp(parent); +}; +BookView.prototype.addStars = function(book, bookElement) { + var id = book.id; + var rating = document.createElement("div"); + rating.className = "booksCont_stars"; + rating.id = id.toString(); + rating.innerHTML += star + star + star + star + star; + bookElement.appendChild(rating); + this.setStarsId(bookElement, id); + this.fillstars(book.rating, bookElement); +}; +BookView.prototype.setStarsId = function(book, id) { + var stars = book.getElementsByTagName("path"); + for (var i = 0; i < 5; i++) { + stars[i].setAttribute("id", "book_" + id + "star" + (i + 1)); + stars[i][bookIdSymb] = id; + stars[i][starParentSymb] = "book_" + id; + stars[i][starRatingSymb] = i + 1; + } +}; +BookView.prototype.fillstars = function(rating, bookElement) { + var stars = bookElement.getElementsByTagName("svg"); + for (var i = 0; i < 5; i++) { + var svgClass = (rating > i) ? + "star_selected" : + "star_unselected"; + stars[i].setAttribute("class", svgClass); + } +}; +BookView.prototype.createBook = function(book) { + var bookElement = document.createElement("div"); + var img = document.createElement("img"); + var title = document.createElement("h3"); + var author = document.createElement("p"); + bookElement.className = "article_books_book"; + bookElement.id = "book_" + book.id; + img.src = book.image_url; + title.innerHTML = book.title; + author.innerHTML = "by " + book.author.firstName + " " + book.author.lastName; + bookElement.appendChild(img); + bookElement.appendChild(title); + bookElement.appendChild(author); + this.addStars(book, bookElement); + return bookElement; +}; +BookView.prototype.prepareItems = function() { + var items = this.items; + items.btnAllBooks = document.getElementById("allBooks"); + items.btnMostRecent = document.getElementById("mostRecent"); + items.btnMostPopular = document.getElementById("mostPopular"); + items.btnFreeBooks = document.getElementById("freeBooks"); + items.topFilter = [ + items.btnAllBooks, + items.btnMostRecent, + items.btnMostPopular, + items.btnFreeBooks + ]; + items.topFilter.forEach(btn => { + const item = btn; + item[isSelectedSymb] = false; + item[groupSymb] = "topFilter"; + }); + this.setSelectionItem(items.btnAllBooks); + + items.inpSearch = document.getElementById("searchBox"); + items.btnMustReadTitles = document.getElementById("MustReadTitles"); + items.btnBestOfList = document.getElementById("BestOfList"); + items.btnClassicNovels = document.getElementById("ClassicNovels"); + items.btnNonFiction = document.getElementById("NonFiction"); + items.leftFilter = [ + items.btnMustReadTitles, + items.btnBestOfList, + items.btnClassicNovels, + items.btnNonFiction + ]; + items.leftFilter.forEach(btn => { + const item = btn; + item[isSelectedSymb] = false; + item[groupSymb] = "leftFilter"; + }); + items.btnAddBook = document.getElementById("buttonAdd"); +}; +BookView.prototype.changeCategorySelection = function(target) { + const item = target; + if (item[groupSymb] === "topFilter") { + this.items.topFilter.forEach(btn => { + if (item !== btn) this.removeSelectionItem(btn); + }) + this.setSelectionItem(item); + } + if (item[groupSymb] === "leftFilter") { + this.items.leftFilter.forEach(btn => { + if (item !== btn) this.removeSelectionItem(btn); + }) + if (item[isSelectedSymb]) { + this.removeSelectionItem(item); + } else { + this.setSelectionItem(item); + } + } +}; +BookView.prototype.setSelectionItem = function(elem) { + const item = elem; + if (!item[isSelectedSymb]) { + item[isSelectedSymb] = true + item.className = "selected-filter"; + } +}; +BookView.prototype.removeSelectionItem = function(elem) { + const item = elem; + if (item[isSelectedSymb]) { + item[isSelectedSymb] = false; + item.className = ""; + } +}; \ No newline at end of file diff --git a/src/library/scripts/history/historyController.js b/src/library/scripts/history/historyController.js new file mode 100644 index 000000000..131ae69f5 --- /dev/null +++ b/src/library/scripts/history/historyController.js @@ -0,0 +1,21 @@ +function HistoryController() { + Controller.call(this); + this.view = new HistoryView(); + return this; +} +HistoryController.prototype = Object.create(Controller.prototype); +HistoryController.prototype.constructor = HistoryController; +HistoryController.prototype.onNewEvent = function (event, time) { + var record = new HistoryRecord(); + record.event = event; + record.time = time; + var store = this.store; + store.beginUpdate(); + try { + store.add(record); + store.sort((item) => item.time, true); + } + finally { + store.endUpdate(); + } +}; \ No newline at end of file diff --git a/src/library/scripts/history/historyModel.js b/src/library/scripts/history/historyModel.js new file mode 100644 index 000000000..a607b6d72 --- /dev/null +++ b/src/library/scripts/history/historyModel.js @@ -0,0 +1,4 @@ +function HistoryRecord() { + this.event = ""; + this.cost = new Date(); +} diff --git a/src/library/scripts/history/historyView.js b/src/library/scripts/history/historyView.js new file mode 100644 index 000000000..e9420b736 --- /dev/null +++ b/src/library/scripts/history/historyView.js @@ -0,0 +1,27 @@ +function HistoryView() { + var _this = View.call(this); + _this.containerName = "history"; + return _this; +} +HistoryView.prototype = Object.create(View.prototype); +HistoryView.prototype.constructor = HistoryView; +HistoryView.prototype.updateContainer = function() { + View.prototype.updateContainer.call(this); + const { container } = this; + for (const record of this.store) { + container.appendChild(this.createRecord(record)); + } +}; +HistoryView.prototype.createRecord = function(record) { + var recordElement = document.createElement("div"); + recordElement.className = "navigation__item-history"; + var message = document.createElement("p"); + message.innerHTML = record.event; + var time = document.createElement("p"); + time.className = "navigation__clock"; + var currentTime = new Date(); + time.innerHTML = Math.round((currentTime.getTime() - record.time.getTime()) / 1000 / 60) + " minutes ago"; + recordElement.appendChild(message); + recordElement.appendChild(time); + return recordElement; +}; \ No newline at end of file diff --git a/src/library/scripts/lib/container.js b/src/library/scripts/lib/container.js new file mode 100644 index 000000000..d582b38f9 --- /dev/null +++ b/src/library/scripts/lib/container.js @@ -0,0 +1,3 @@ +function ItemContainer() { + this.items = {}; +} \ No newline at end of file diff --git a/src/library/scripts/lib/controller.js b/src/library/scripts/lib/controller.js new file mode 100644 index 000000000..29dfae6ae --- /dev/null +++ b/src/library/scripts/lib/controller.js @@ -0,0 +1,18 @@ +function Controller() { + return ItemContainer.call(this); +} +Controller.prototype = Object.create(ItemContainer.prototype); +Controller.prototype.constructor = Controller; +Controller.prototype.init = function () { + this.store = new Store(); + this.view.initView(this.store); + this.items = this.view.getItems(); + this.prepareListeners(); + this.prepareStore(); +}; +Controller.prototype.prepareListeners = function () { + var _this = this; + this.store.subscribe(() => _this.view.onUpdate()); +}; +Controller.prototype.prepareStore = function () { +}; diff --git a/src/library/scripts/lib/star.js b/src/library/scripts/lib/star.js new file mode 100644 index 000000000..29442256d --- /dev/null +++ b/src/library/scripts/lib/star.js @@ -0,0 +1 @@ +var star = ''; diff --git a/src/library/scripts/lib/store.js b/src/library/scripts/lib/store.js new file mode 100644 index 000000000..3de7a7251 --- /dev/null +++ b/src/library/scripts/lib/store.js @@ -0,0 +1,113 @@ +function Store() { + this.items = []; + this.sortedIndexes = []; + this.filteredIndexes = []; + this.filterRules = []; + this.delegateList = []; + this.suppressedChanges = 0; +} +Store.prototype.changed = function() { + if (this.suppressedChanges === 0) { + for (const delegate of this.delegateList) { + delegate(); + } + } +}; +Store.prototype.beginUpdate = function() { + this.suppressedChanges++; +}; +Store.prototype.endUpdate = function() { + if (this.suppressedChanges === 0) { + throw new Error("Cant resume unsupressed events"); + } + this.suppressedChanges--; + this.changed(); +}; +Store.prototype.add = function(item) { + this.items.push(item); + this.sortedIndexes.push(this.sortedIndexes.length); + this.filter(); +}; +Store.prototype.resetFilteredIndexes = function() { + this.filteredIndexes = [...this.sortedIndexes]; +}; +Store.prototype.filter = function() { + this.resetFilteredIndexes(); + for (const rule of this.filterRules) { + this.applyFilterRule(rule.prop); + } + this.changed(); +}; +Store.prototype.applyFilterRule = function(rule) { + var result = []; + var filteredIndexes = this.filteredIndexes; + for (const index of filteredIndexes) { + if (rule(this.getRealAt(index))) { + result.push(index); + } + } + this.filteredIndexes = result; +}; +Store.prototype.sort = function(propGetter, desc) { + var items = this.items; + var res = desc ? -1 : 1; + this.sortedIndexes.sort(function(a, b) { + if (propGetter(items[a]) > propGetter(items[b])) { + return res; + } + if (propGetter(items[a]) < propGetter(items[b])) { + return -res; + } + return 0; + }); + this.filter(); +}; +Store.prototype.dropSort = function() { + var sortedIndexes = this.sortedIndexes; + for (var i = 0; i < sortedIndexes.length; i++) { + sortedIndexes[i] = i; + } + this.filter(); +}; +Store.prototype.addPropFilter = function(propFunc, keyValue) { + this.filterRules.push({ key: keyValue, prop: propFunc }); +}; +Store.prototype.removePropFilter = function(key) { + var index = this.filterRules.findIndex(function(obj) { return obj.key === key; }); + if (index >= 0) { + this.filterRules.splice(index, 1); + } +}; +Store.prototype.findIndex = function(cb) { + var items = this.items; + for (var i = 0; i < items.length; i++) { + var value = items[i]; + if (cb(value)) { + return i; + } + } + return -1; +}; +Store.prototype.getRealAt = function(index) { + return this.items[index]; +}; +Store.prototype.getFilteredAt = function(index) { + return this.getRealAt(this.filteredIndexes[index]); +}; +Store.prototype.getAt = function(index) { + return this.getFilteredAt(index); +}; +Store.prototype.subscribe = function(delegate) { + this.delegateList.push(delegate); +}; +Store.prototype.unsubscribe = function(delegate) { + var index = this.delegateList.indexOf(delegate); + if (index >= 0) { + this.delegateList.splice(index, 1); + } +}; +Store.prototype[Symbol.iterator] = function*() { + for (const item of this.filteredIndexes) { + yield this.getRealAt(item); + } +}; \ No newline at end of file diff --git a/src/library/scripts/lib/view.js b/src/library/scripts/lib/view.js new file mode 100644 index 000000000..2cbc2330d --- /dev/null +++ b/src/library/scripts/lib/view.js @@ -0,0 +1,27 @@ + +function View() { + return ItemContainer.call(this) || this; +} +View.prototype = Object.create(ItemContainer.prototype); +View.prototype.constructor = View; +View.prototype.prepareContainer = function (containerName) { + this.container = document.getElementById(containerName); +}; +View.prototype.clearContainer = function () { + this.container.innerHTML = ""; +}; +View.prototype.updateContainer = function () { + this.clearContainer(); +}; + +View.prototype.initView = function (store) { + this.store = store; + this.prepareContainer(this.containerName); + this.updateContainer(); +}; +View.prototype.getItems = function () { + return this.items; +}; +View.prototype.onUpdate = function () { + this.updateContainer(); +}; \ No newline at end of file diff --git a/src/library/scripts/modalWindow/modalWindowController.js b/src/library/scripts/modalWindow/modalWindowController.js new file mode 100644 index 000000000..d13671420 --- /dev/null +++ b/src/library/scripts/modalWindow/modalWindowController.js @@ -0,0 +1,73 @@ +function ModalWindowController() { + Controller.call(this); + this.view = new ModalWindowView(); + this.onAddBookEventHandler = function () { }; + return this; +}; +ModalWindowController.prototype = Object.create(Controller.prototype); +ModalWindowController.prototype.constructor = ModalWindowController; +ModalWindowController.prototype.setonAddBookEventHandler = function (cb) { + this.onAddBookEventHandler = cb; +}; +ModalWindowController.prototype.setActive = function () { + this.view.onShow(); +}; +ModalWindowController.prototype.hide = function () { + this.view.onHide(); +}; + +ModalWindowController.prototype.addBook = function (e) { + if (this.validation()) { + const bookSource = this.createBookSource(); + this.onAddBookEventHandler(bookSource); + this.hide(); + this.view.updateContainer(); + } +}; + +ModalWindowController.prototype.createBookSource = function () { + console.log("click"); + const book = new Book(); + const { items } = this; + book.author = {}; + book.author.firstName = items.AuthorName.value; + book.author.lastName = items.AuthorNameLast.value; + book.categories = []; + if (items.rdbMustRead.checked) { + book.categories.push("must_read"); + } + if (items.rdbBest.checked) { + book.categories.push("best"); + } + if (items.rdbClassic.checked) { + book.categories.push("classic"); + } + if (items.rdbNonFiction.checked) { + book.categories.push("non_fiction"); + } + book.cost = items.Cost.value; + book.image_url = items.ImageUrl.value; + book.rating = 0; + book.title = items.Title.value; + book.createdAt = new Date(); + book.updatedAt = new Date(); + return book; +} +ModalWindowController.prototype.validation = function () { + const itemsModal = this.view.items; + let result; + if (itemsModal.Title.value.length !== 0 && itemsModal.AuthorName.value.length !== 0 && itemsModal.AuthorNameLast.value.length !== 0 && itemsModal.Cost.value.length !== 0 && itemsModal.ImageUrl.value.length !== 0) { + result = true; + } else { + this.view.onShowHint(); + result = false; + } + return result; +}; + +ModalWindowController.prototype.prepareListeners = function () { + Controller.prototype.prepareListeners.call(this); + var items = this.items; + items.btnResetForm.addEventListener("click", this.hide.bind(this)); + items.btnSbmtForm.addEventListener("click", this.addBook.bind(this)); +}; \ No newline at end of file diff --git a/src/library/scripts/modalWindow/modalWindowView.js b/src/library/scripts/modalWindow/modalWindowView.js new file mode 100644 index 000000000..89ff9e0b0 --- /dev/null +++ b/src/library/scripts/modalWindow/modalWindowView.js @@ -0,0 +1,51 @@ +function ModalWindowView() { + View.call(this); + this.containerName = "addBooKModal"; + this.prepareItems(); + return this; +} +ModalWindowView.prototype = Object.create(View.prototype); +ModalWindowView.prototype.constructor = ModalWindowView; +ModalWindowView.prototype.updateContainer = function () { + this.items.AuthorName.value = ""; + this.items.AuthorNameLast.value = ""; + this.items.rdbMustRead.checked= false; + this.items.rdbBest.checked= false; + this.items.rdbClassic.checked= false; + this.items.rdbNonFiction.checked= false; + this.items.Cost.value = ""; + this.items.ImageUrl.value = ""; + this.items.Title.value = ""; + this.onHideHint(); +}; +ModalWindowView.prototype.onShow = function () { + this.container.style.display = "flex"; +} +ModalWindowView.prototype.onHide = function () { + this.container.style.display = "none"; +} +ModalWindowView.prototype.onHideHint = function () { + this.items.Hint.style.display = "none"; +} +ModalWindowView.prototype.onShowHint = function () { + this.items.Hint.style.display = "block"; +} + +ModalWindowView.prototype.prepareItems = function () { + var items = this.items; + items.AuthorName = document.getElementById("nameAuthor"); + items.AuthorNameLast = document.getElementById("lastnameAuthor"); + items.rdbMustRead = document.getElementById("radioMustRead"); + items.rdbBest = document.getElementById("radioBest"); + items.rdbClassic = document.getElementById("radioClassic"); + items.rdbNonFiction = document.getElementById("radioNon"); + items.Cost = document.getElementById("cost"); + items.ImageUrl = document.getElementById("image"); + items.Rating = 1; + items.Title = document.getElementById("title"); + + items.Hint = document.getElementById("formHint"); + + items.btnSbmtForm = document.getElementById("formSubmit"); + items.btnResetForm = document.getElementById("formReset"); +}; \ No newline at end of file diff --git a/src/library/scripts/utils/debounce.js b/src/library/scripts/utils/debounce.js new file mode 100644 index 000000000..87048ae0a --- /dev/null +++ b/src/library/scripts/utils/debounce.js @@ -0,0 +1,10 @@ +function debounce(cb, delay) { + var timer = null; + return function(...args) { + if (timer) clearTimeout(timer); + timer = setTimeout(function() { + cb.apply(this, args); + timer = null; + }, delay); + }; +} \ No newline at end of file diff --git a/src/library/scripts/utils/idGenerator.js b/src/library/scripts/utils/idGenerator.js new file mode 100644 index 000000000..100b2ce49 --- /dev/null +++ b/src/library/scripts/utils/idGenerator.js @@ -0,0 +1,5 @@ +var globalId = 0; + +function generateId() { + return globalId++; +} \ No newline at end of file diff --git a/src/library/scripts/utils/request.js b/src/library/scripts/utils/request.js new file mode 100644 index 000000000..e4cebf8b3 --- /dev/null +++ b/src/library/scripts/utils/request.js @@ -0,0 +1,12 @@ +async function request(url) { + try { + var response = await fetch(url); + if (response.ok) { + return await response.json(); + } + throw new Error(response.statusText); + } catch (err) { + alert("Server request failed: " + err); + } + return undefined; +} \ No newline at end of file