diff --git a/src/BukiVedi.App/web/js/components/tags/index.js b/src/BukiVedi.App/web/js/components/tags/index.js
new file mode 100644
index 0000000..83d7ad5
--- /dev/null
+++ b/src/BukiVedi.App/web/js/components/tags/index.js
@@ -0,0 +1,106 @@
+import { waitForElement, changeTags } from "../../requests/index.js";
+
+export default class TagsComponent {
+ element;
+ constructor({ tags = [], bookid = null, article }) {
+ this.tags = tags;
+ this.article = article;
+ this.bookid = bookid;
+ this.isTextareaMode = false;
+ this.render();
+ }
+
+ get template() {
+ return `
+
+ Теги
+
+
+
${this.getTags(
+ this.tags
+ )}
+ `;
+ }
+ getTags(tags) {
+ return (tags || [])
+ .map(({ id, name }) => {
+ return `
#${name}`;
+ })
+ .join("");
+ }
+
+ getTextarea(tags) {
+ return `
+
+ `;
+ }
+
+ initialize() {
+ this.initEventListeners();
+ }
+
+ initEventListeners() {
+ const tagsEditorListener = this.article.querySelector(
+ ".main-book__tags_title"
+ );
+
+ tagsEditorListener.addEventListener("click", () => {
+ const tagsWrapper = this.article.querySelector(".main-book__tags");
+
+ if (!this.isTextareaMode) {
+ tagsWrapper.innerHTML = this.getTextarea(this.tags);
+ this.article.querySelector(".main-book__textarea").focus();
+ this.isTextareaMode = true;
+ }
+ });
+
+ this.article.addEventListener("keypress", (event) => {
+ if (event.key === "Enter") {
+ if (this.isTextareaMode) {
+ this.closeTextarea();
+ }
+ }
+ });
+
+ document.addEventListener("outside-click", async () => {
+ if (this.isTextareaMode) {
+ this.closeTextarea();
+ }
+ });
+ }
+
+ async closeTextarea() {
+ const tagsWrapper = this.article.querySelector(".main-book__tags");
+ const area = this.article.querySelector(".main-book__textarea");
+
+ const tags = area.value
+ .split(" ")
+ .map((item) => item.trim())
+ .filter((i) => i);
+ if (!!(tags || []).length > 0) {
+ await this.setTags({ bookid: this.bookid, tags });
+ }
+ tagsWrapper.innerHTML = this.getTags(this.tags);
+ this.isTextareaMode = false;
+ }
+
+ async waitRendered() {
+ await waitForElement(".main-book__tags");
+ this.initialize();
+ }
+
+ async render() {
+ this.element = document.createElement("div");
+ this.element.innerHTML = this.template;
+ await this.waitRendered();
+ }
+
+ async setTags({ bookid, tags }) {
+ document.body.classList.add("blur", "spinner");
+ const newTags = await changeTags({ bookid, tags });
+ this.tags = newTags;
+ document.body.classList.remove("spinner");
+ }
+}
diff --git a/src/BukiVedi.App/web/js/constants/index.js b/src/BukiVedi.App/web/js/constants/index.js
index 641cb0b..998ca84 100644
--- a/src/BukiVedi.App/web/js/constants/index.js
+++ b/src/BukiVedi.App/web/js/constants/index.js
@@ -2,11 +2,6 @@ import * as requests from '../requests/index.js';
export const SUCCESS = "success";
export const menuEnum = [
- // {
- // id: 0,
- // label: "В избранное",
- // action: requests.addBookToFavourites,
- // },
{
id: 1,
label: "Авторов в избранное",
@@ -25,11 +20,7 @@ export const menuEnum = [
{
id: 4,
label: "Игнорировать автора",
- action: requests.ignoreAuthor,
- },
- {
- id: 5,
- label: "Теги",
+ action: requests.ignoreAuthors,
},
{
id: 6,
@@ -40,3 +31,21 @@ export const menuEnum = [
label: "Поделиться",
},
];
+
+export const DEFAULT_AUTHOR = {
+ id: 0,
+ name: 'Неизвестно'
+};
+
+export const likeEnum = [
+ {
+ id: 0,
+ label: "Нравится",
+ action: requests.addBookToFavourites,
+ },
+ {
+ id: 1,
+ label: "Не нравится",
+ action: requests.removeBookToFavourites,
+ }
+]
\ No newline at end of file
diff --git a/src/BukiVedi.App/web/js/main/index.js b/src/BukiVedi.App/web/js/main/index.js
index 0050118..e6fa073 100644
--- a/src/BukiVedi.App/web/js/main/index.js
+++ b/src/BukiVedi.App/web/js/main/index.js
@@ -1,13 +1,22 @@
-import { menuEnum } from "../constants/index.js";
-import { fetchData, downloadBook, searchByAuthor } from "../requests/index.js";
+import { menuEnum, likeEnum, DEFAULT_AUTHOR } from "../constants/index.js";
+import {
+ fetchData,
+ downloadBook,
+ searchByAuthor,
+ searchByTag,
+ waitForElement,
+} from "../requests/index.js";
+
+import { stopPropagation } from "../utils/index.js";
+import TagsComponent from "../components/tags/index.js";
export default class BookSection {
subElements = [];
element;
- constructor({ url = "", label = "" } = {}) {
+ constructor({ url = "" } = {}) {
this.url = url;
- this.label = label;
+ this.data = [];
this.render();
}
@@ -29,11 +38,15 @@ export default class BookSection {
booksListener.addEventListener("click", (event) => {
let id, title;
const isLike = event.target.closest("[data-like]");
-
const isMenu = event.target.closest("[data-menu]");
const isAction = event.target.closest("[data-action]");
const isLink = event.target.closest("[data-link]");
const isAuthor = event.target.closest("[data-author]");
+ const isMoreDetails = event.target.closest("[data-details]");
+ const isTagWrapper = event.target.closest("[data-tags-wrapper]");
+ const isTag = event.target.closest("[data-tag]");
+
+ stopPropagation(event);
switch (true) {
case !!isAction:
@@ -62,6 +75,21 @@ export default class BookSection {
id = isAuthor.dataset.author;
this.update({ isByAuthor: true, id });
+ break;
+ case !!isMoreDetails:
+ this.toggleDetails(isMoreDetails);
+
+ break;
+ case !isTagWrapper:
+ const myEvent = new CustomEvent("outside-click");
+ document.dispatchEvent(myEvent);
+ this.closeMenu();
+
+ break;
+ case !!isTag:
+ id = isTag.dataset.tag;
+ this.update({ isByTag: true, id });
+
default:
this.closeMenu();
@@ -75,30 +103,33 @@ export default class BookSection {
getBookBody(data) {
return (data || [])
.map(
- ({
- id,
- authors,
- description,
- format,
- genres,
- imageUrl,
- title,
- series,
- subseries,
- year,
- tags,
- isFavorite,
- }) => {
- const { name, id: authorid } = authors[0] || "Неизвестно";
+ (
+ {
+ id,
+ authors,
+ description,
+ format,
+ genres,
+ imageUrl,
+ title,
+ series,
+ subseries,
+ year,
+ isFavorite,
+ },
+ index
+ ) => {
+ const isLast = !!(index === data?.length - 1);
return `
-
+
+ }" data-like=${isFavorite} data-bookid=${id}>
@@ -118,8 +149,12 @@ export default class BookSection {
+ ${subseries || "Подсерия"}
+
-
- ${title || "Название неизвестно"}
-
- ${series || ""}
-
- ${subseries || ""}
+
+ ${title || "Название неизвестно"}
+
+ ${this.getAuthors(authors)}
+
-
+
+
${description || "Нет описания"}
-
${this.getTags(
- tags
- )}
-
+
+
`;
}
)
@@ -170,10 +208,11 @@ export default class BookSection {
.join("");
}
- getTags(tags) {
- return (tags || [])
+ getAuthors(authors) {
+ return (authors || [DEFAULT_AUTHOR])
.map(({ id, name }) => {
- return `
#${name}`;
+ return `
+ ${name || ""} `;
})
.join("");
}
@@ -201,42 +240,105 @@ export default class BookSection {
}
}
- async makeAction({ id, authorid, bookid }) {
+ toggleDetails(element) {
+ const isOpened = element.classList.contains("opened");
+ const article = element.parentNode.parentNode;
+ const article_body = article.querySelector(".main-book__body");
+
+ if (isOpened) {
+ element.classList.remove("opened");
+ article.classList.add("card_closed");
+ article_body.classList.remove("body_opened");
+ article_body.classList.add("body_closed");
+ } else {
+ element.classList.add("opened");
+ article.classList.remove("card_closed");
+ article_body.classList.remove("body_closed");
+ article_body.classList.add("body_opened");
+ }
+ }
+
+ async countHeight() {
+ await waitForElement(".last");
+
+ const articles = document.querySelectorAll("[data-card]");
+ articles.forEach((article, i) => {
+ const tagWrapper = article.querySelector(".main-book__tags_wrapper");
+ const bookid = article.dataset.bookid;
+ const { tags } = this.data[i] || [];
+ const tagsComponent = new TagsComponent({ tags, article, bookid });
+
+ tagWrapper.append(tagsComponent.element);
+
+ if (article.scrollHeight > 502) {
+ const arrow = article.querySelector(".main-book__arrow_wrapper");
+ arrow.classList.remove("hidden");
+ }
+ });
+ }
+
+ async makeAction({ id, bookid }) {
this.element.classList.add("blur", "spinner");
if (menuEnum[id].action) {
- const data = await menuEnum[id].action({ id, authorid, bookid });
+ const data = await menuEnum[id].action({ id, bookid });
} else {
alert("Напиши меня");
}
this.element.classList.remove("spinner");
- this.closeMenu()
+ this.closeMenu();
}
- makeLike(element) {
- const { bookid: likedBookId } = element.dataset || {};
- this.makeAction({ id: 0, authorid: "", bookid: likedBookId });
- element.classList.add("liked");
+ async makeLike(element) {
+ this.element.classList.add("blur", "spinner");
+
+ const { bookid: likedBookId, like } = element.dataset || {};
+ const isLiked = like === "true";
+ if (!isLiked) {
+ await likeEnum[0].action({ bookid: likedBookId });
+ element.classList.remove("not-liked");
+ element.classList.add("liked");
+ } else {
+ await likeEnum[1].action({ bookid: likedBookId });
+ element.classList.remove("liked");
+ element.classList.add("not-liked");
+ }
+
+ element.dataset.like = !isLiked;
+ this.data = this.data.map(({ isFavorite, id, ...rest }) =>
+ likedBookId === id
+ ? { id, isFavorite: !isFavorite, ...rest }
+ : {
+ id,
+ isFavorite,
+ ...rest,
+ }
+ );
+ console.log("new data", this.data);
+ this.element.classList.remove("spinner");
}
async update(params) {
this.element.classList.add("blur", "spinner");
- let data = [];
if (params?.isByAuthor) {
- data = await searchByAuthor({ id: params.id });
+ this.data = await searchByAuthor({ id: params.id });
+ } else if (params?.isByTag) {
+ this.data = await searchByTag({ id: params.id });
} else {
const query = params;
- data = await fetchData({ query, url: this.url });
+ this.data = await fetchData({ query, url: this.url });
}
- console.log("data", data);
- if (data && Object.values(data).length) {
- this.subElements.body.innerHTML = this.getBookBody(data);
+ console.log("data", this.data);
+
+ if (this.data && Object.values(this.data).length) {
+ this.subElements.body.innerHTML = this.getBookBody(this.data);
this.initialize();
} else {
this.subElements.body.innerHTML = this.getEmptyBody();
}
+ await this.countHeight();
this.element.classList.remove("spinner");
}
@@ -246,7 +348,6 @@ export default class BookSection {
for (const subElement of elements) {
const name = subElement.dataset.element;
-
result[name] = subElement;
}
return result;
@@ -273,3 +374,49 @@ export default class BookSection {
this.element = null;
}
}
+
+// this.data = [
+// {
+// id: "2",
+// authors: [{ id: "1", name: "123" }],
+// description:
+// "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
+// format: "123",
+// genres: [],
+// imageUrl: "",
+// title: "What is Lorem Ipsum?",
+// series: [],
+// subseries: [],
+// year: 1992,
+// tags: [
+// { id: 1, name: "Интересно" },
+// { id: 22, name: "Почитать" },
+// { id: 155, name: "Рекомендовали" },
+// { id: 166, name: "завтра" },
+// { id: 11, name: "наверное" },
+// { id: 221, name: "Почитданетать" },
+// { id: 1515, name: "раздватри" },
+// { id: 1661, name: "возможно" },
+// ],
+// isFavorite: true,
+// },
+// {
+// id: "22",
+// authors: [{ id: "1", name: "123" }],
+// description:
+// "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
+// format: "123",
+// genres: [],
+// imageUrl: "",
+// title: "dsfwqedwqefrwef",
+// series: [],
+// subseries: [],
+// year: 1992,
+// tags: [
+// { id: 1, name: "Интересно" },
+// { id: 22, name: "Почитать" },
+// { id: 155, name: "Рекомендовали" },
+// ],
+// isFavorite: true,
+// },
+// ];
diff --git a/src/BukiVedi.App/web/js/requests/index.js b/src/BukiVedi.App/web/js/requests/index.js
index 3b75058..36fb7f3 100644
--- a/src/BukiVedi.App/web/js/requests/index.js
+++ b/src/BukiVedi.App/web/js/requests/index.js
@@ -1,7 +1,7 @@
import { SUCCESS } from "../constants/index.js";
export const fetchData = ({ query, url }) => {
- console.log('query, url', query, url);
+ console.log("query, url", query, url);
return $.ajax({
contentType: "application/json; charset=utf-8",
dataType: "json",
@@ -35,6 +35,21 @@ export const addBookToFavourites = ({ bookid }) => {
});
};
+export const removeBookToFavourites = ({ bookid }) => {
+ return $.ajax({
+ type: "DELETE",
+ url: `../api/books/${bookid}/favorite`,
+ success: function (data, textStatus, jqXHR) {
+ if (textStatus === SUCCESS) {
+ return data;
+ }
+ },
+ error: function (jqXHR, textStatus, errorThrown) {
+ console.log(jqXHR.statusText);
+ },
+ });
+};
+
export const addAuthorToFavourites = ({ bookid }) => {
return $.ajax({
type: "POST",
@@ -65,6 +80,21 @@ export const readLater = ({ bookid }) => {
});
};
+export const removeFromReadLater = ({ bookid }) => {
+ return $.ajax({
+ type: "DELETE",
+ url: `../api/books/${bookid}/read`,
+ success: function (data, textStatus, jqXHR) {
+ if (textStatus === SUCCESS) {
+ return data;
+ }
+ },
+ error: function (jqXHR, textStatus, errorThrown) {
+ console.log(jqXHR.statusText);
+ },
+ });
+};
+
export const ignoreBook = ({ bookid }) => {
return $.ajax({
type: "POST",
@@ -80,7 +110,22 @@ export const ignoreBook = ({ bookid }) => {
});
};
-export const ignoreAuthor = ({ bookid }) => {
+export const stopIgnoreBook = ({ bookid }) => {
+ return $.ajax({
+ type: "DELETE",
+ url: `../api/books/${bookid}/block`,
+ success: function (data, textStatus, jqXHR) {
+ if (textStatus === SUCCESS) {
+ return data;
+ }
+ },
+ error: function (jqXHR, textStatus, errorThrown) {
+ console.log(jqXHR.statusText);
+ },
+ });
+};
+
+export const ignoreAuthors = ({ bookid }) => {
return $.ajax({
type: "POST",
url: `../api/books/${bookid}/author/block`,
@@ -99,28 +144,28 @@ export const downloadBook = ({ id, title, format }) => {
$.ajax({
type: "GET",
url: `../api/books/download/${id}`,
- responseType: 'blob',
+ responseType: "blob",
success: function (data, textStatus, jqXHR) {
if (textStatus === SUCCESS) {
- const name = title.replace(' ', '_')
+ const name = title.replace(" ", "_");
const url = window.URL.createObjectURL(new Blob([data]));
- const link = document.createElement('a');
+ const link = document.createElement("a");
link.href = url;
- link.setAttribute('download', `${name}.${format}`);
+ link.setAttribute("download", `${name}.${format}`);
document.body.appendChild(link);
- link.click();
+ link.click();
}
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR.statusText);
},
});
-}
+};
export const searchByAuthor = ({ id }) => {
return $.ajax({
@@ -139,3 +184,63 @@ export const searchByAuthor = ({ id }) => {
},
});
};
+
+export const waitForElement = (selector) => {
+ return new Promise((resolve) => {
+ if (document.querySelector(selector)) {
+ return resolve(document.querySelector(selector));
+ }
+
+ const observer = new MutationObserver((mutations) => {
+ if (document.querySelector(selector)) {
+ observer.disconnect();
+ resolve(document.querySelector(selector));
+ }
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ });
+ });
+};
+
+export const changeTags = ({ bookid, tags }) => {
+ return $.ajax({
+ contentType: "application/json; charset=utf-8",
+ dataType: "json",
+ type: "POST",
+ url: "/api/tags",
+ data: JSON.stringify({ bookid, names: tags }),
+ success: function (data, textStatus, jqXHR) {
+ if (textStatus === SUCCESS) {
+ return data;
+ }
+ },
+ error: function (jqXHR, textStatus, errorThrown) {
+ console.log(jqXHR.statusText);
+ },
+ });
+
+ // return [
+ // {id: "6611598e1468849d1b00570d", name: "шиза"}
+ // ];
+};
+
+export const searchByTag = ({ id }) => {
+ return $.ajax({
+ contentType: "application/json; charset=utf-8",
+ dataType: "json",
+ type: "POST",
+ url: `../api/tags/${id}/books`,
+ success: function (data, textStatus, jqXHR) {
+ if (textStatus === SUCCESS) {
+ return data;
+ }
+ return [];
+ },
+ error: function (jqXHR, textStatus, errorThrown) {
+ console.log(jqXHR.statusText);
+ },
+ });
+};
diff --git a/src/BukiVedi.App/web/js/utils/index.js b/src/BukiVedi.App/web/js/utils/index.js
new file mode 100644
index 0000000..9b8387a
--- /dev/null
+++ b/src/BukiVedi.App/web/js/utils/index.js
@@ -0,0 +1,4 @@
+export const stopPropagation = event => {
+ event.stopImmediatePropagation();
+ event.stopPropagation();
+}
\ No newline at end of file
diff --git a/src/BukiVedi.App/web/login.html b/src/BukiVedi.App/web/login.html
index c27a51c..b42b1f7 100644
--- a/src/BukiVedi.App/web/login.html
+++ b/src/BukiVedi.App/web/login.html
@@ -23,21 +23,21 @@
-
+