diff --git a/src/main/resources/static/contentUtility.js b/src/main/resources/static/contentUtility.js index ff48f52..6c47123 100644 --- a/src/main/resources/static/contentUtility.js +++ b/src/main/resources/static/contentUtility.js @@ -1,28 +1,33 @@ -import {searchBarTimeout, searchSection, lastQuery} from "./main.js" -import Dataset from "./dataset.js" // TODO consider renaming this to "searchUtility.js" -export function fetchQuery(fetchString, clearResults) { + +import { searchBarTimeout, searchSection, lastQuery } from "./main.js" +import Dataset from "./dataset.js" + +export async function fetchQuery(fetchString, clearResults) { clearTimeout(searchBarTimeout); - fetch(fetchString) - .then(resp => resp.json()) - .then((data) => { - parseContent(data.content, clearResults); - lastQuery.totalPages = data.totalPages; - if (clearResults) { - lastQuery.currentPage = 0; - } - }); + const response = await fetch(fetchString); + const data = await response.json(); + + parseContent(data.content, clearResults); + lastQuery.totalPages = data.totalPages; + if (clearResults) { + lastQuery.currentPage = 0; + } } function parseContent(content, clearResults) { + const nothingFoundElement = searchSection.querySelector("#nothing-found"); + if (content.length === 0) { - searchSection.querySelector("#nothing-found ").classList.remove("hidden"); + nothingFoundElement.classList.remove("hidden"); } else { - searchSection.querySelector("#nothing-found").classList.add("hidden"); - const datasets = content.map(dataset => new Dataset(dataset)); + nothingFoundElement.classList.add("hidden"); + + const datasets = content.map(dataset => Dataset.get(dataset.id) ?? new Dataset(dataset)); if (clearResults) { Array.from(searchSection.querySelectorAll(".datasets .dataset")).forEach(e => e.remove()); } + for (const dataset of datasets) { searchSection.querySelector(".datasets") .appendChild(dataset.createDatasetHTMLElement()); diff --git a/src/main/resources/static/dataset.js b/src/main/resources/static/dataset.js index 29170a8..1c5d40a 100644 --- a/src/main/resources/static/dataset.js +++ b/src/main/resources/static/dataset.js @@ -1,6 +1,8 @@ -import { vote } from "./main.js"; +import { DATASET_ENDPOINT, getBaseURL } from "./main.js"; export default class Dataset { + static #datasets = new Map(); + #abstract; #author; #categories; @@ -13,8 +15,13 @@ export default class Dataset { #upvotes; #url; #votes; + #elements = []; - constructor({abst: shortDescription, author, categories, date, description, id, rating, title, type, upvotes, url, votes}) { + static get(id) { + return this.#datasets.get(id); + } + + constructor({ abst: shortDescription, author, categories, date, description, id, rating, title, type, upvotes, url, votes }) { this.#abstract = shortDescription; this.#author = author; this.#categories = categories; @@ -27,38 +34,179 @@ export default class Dataset { this.#upvotes = upvotes; this.#url = url; this.#votes = votes; + + Dataset.#datasets.set(id, this); } + // Getters + get abstract() { + return this.#abstract; + } + + get author() { + return this.#author; + } + + get categories() { + return this.#categories; + } + + get date() { + return this.#date; + } + + get description() { + return this.#description; + } + + get id() { + return this.#id; + } + + get rating() { + return this.#rating; + } + + get title() { + return this.#title; + } + + get type() { + return this.#type; + } + + get upvotes() { + return this.#upvotes; + } + + get url() { + return this.#url; + } + + get votes() { + return this.#votes; + } + + get mainElement() { + return this.#elements[0]; + } + + get elements() { + return this.#elements; + } + + // Main methods + + // Only on main page createDatasetHTMLElement() { - let template = document.querySelector("#dataset-template"); - const clone = template.content.cloneNode(true); - clone.querySelector(".dataset").dataset.id = this.#id; - clone.querySelector("h3").innerText = this.#title; - clone.querySelector("p").innerText = this.#description; - clone.querySelector("span").innerText = this.#upvotes; - let votedIDs = window.localStorage; - // depending on whether the button has been up/downvoted, its according button get disabled and hidden - if (votedIDs.getItem(this.#id)) { - let votedButton = clone.querySelector(votedIDs.getItem(this.#id)? ".upvote-btn":".downvote-btn"); - votedButton.classList.add("isVoted"); - votedButton.disabled = true; - let notVotedButton = clone.querySelector(votedIDs.getItem(this.#id)? ".downvote-btn":".upvote-btn"); - notVotedButton.style.visibility = "hidden"; + const clone = this.#createTemplateInstance("dataset-template"); + if (clone == null) { + return null; } - // Event Listeners - clone.querySelector(".upvote-btn").addEventListener("click", () => { - vote(this.#id, true); - }); + clone.querySelector(".dataset").dataset.id = this.#id; + clone.querySelector(".dataset-title").innerText = this.#title; + clone.querySelector(".dataset-description").innerText = this.#description; + clone.querySelector(".upvote-count").innerText = this.#upvotes; - clone.querySelector(".downvote-btn").addEventListener("click", () => { - vote(this.#id, false); - }) + this.#elements.push(clone.children[0]); + this.#createUpvoteButtons(clone); return clone; } - getID() { - return this.#id; + // Only on details page + createUpvoteComponent() { + let clone = this.#createTemplateInstance("voting-template") + + clone.querySelector(".upvote-count").innerText = this.#upvotes; + + this.#elements.push(clone.children[0]); + this.#createUpvoteButtons(clone); + + return clone; + } + + #createUpvoteButtons(templateInstance) { + if (this.storageGetKey("is-voted", false)) { + // The template instance (clone) has to be pushed before this can work + this.#disableVoteBtns(); + } + + // Event Listeners + templateInstance.querySelector(".upvote-btn").addEventListener("click", () => { + this.vote(); + }); + + templateInstance.querySelector(".downvote-btn").addEventListener("click", () => { + this.vote(false); + }) + } + + #createTemplateInstance(templateId) { + let template = document.getElementById(templateId); + if (template == null) { + return null; + } + + return template.content.cloneNode(true); + } + + isMainElement(element) { + return this.#elements.length > 0 && this.#elements[0].isEqualNode(element); + } + + async vote(up = true) { + const fetchURL = new URL( + `${DATASET_ENDPOINT}/id/${this.#id}/${up ? "up" : "down"}vote`, + getBaseURL(), + ); + + const response = await fetch(fetchURL, { + method: "PUT", + headers: { + 'Content-Type': 'application/json', + } + }); + const data = await response.json(); + + this.#upvotes = data.upvotes; + for (const element of this.#elements) { + element.querySelector(".upvote-count").innerText = this.#upvotes; + } + + this.storageSetKey("vote-type", up ? "upvote" : "downvote"); + this.storageSetKey("is-voted", true); + this.#disableVoteBtns(); + } + + #disableVoteBtns() { + if (this.#elements.length > 0) { + const voteType = this.storageGetKey("vote-type", null); + + this.#elements.flatMap(e => Array.from(e.querySelectorAll(".upvote .btn"))) + .forEach(btn => { + btn.disabled = true; + + if (btn.classList.contains(`${voteType}-btn`)) { + btn.classList.add("isVoted"); + } + }); + } + } + + storageGet() { + const json = localStorage.getItem(this.#id); + return json != null ? JSON.parse(json) : {}; + } + + storageSetKey(key, value) { + let currentStorage = this.storageGet(); + currentStorage[key] = value; + localStorage.setItem(this.#id, JSON.stringify(currentStorage)); + } + + storageGetKey(key, defaultValue) { + return this.storageGet()?.[key] ?? defaultValue; } } diff --git a/src/main/resources/static/main.js b/src/main/resources/static/main.js index 32caf41..b359763 100644 --- a/src/main/resources/static/main.js +++ b/src/main/resources/static/main.js @@ -1,14 +1,14 @@ -import {fetchQuery} from "./contentUtility.js"; +import { fetchQuery } from "./contentUtility.js"; import Dataset from "./dataset.js"; -const apiEndpoint = "/api/v1/datasets"; -const baseURL = location.origin; +export const DATASET_ENDPOINT = "/api/v1/datasets"; +export const getBaseURL = () => location.origin; + const defaultPagingValue = 20; export const lastQuery = { totalPages: 0, - currentPage: 0 + currentPage: 0, }; -const votedIDs = window.localStorage; // definition of all buttons & sections const addButton = document.getElementById("add-btn"); @@ -69,24 +69,6 @@ resetButton.addEventListener("click", () => { updateSections(); }); -// Consider moving this to datasets.js completely // TODO: we dont need them, do we? there in dataset.js -const upvoteButtonClickListener = e => { - const entryID = e.target.parentElement.parentElement.dataset.id; - vote(entryID, true); -}; -for (const upvoteButton of upvoteButtons) { - upvoteButton.addEventListener("click", upvoteButtonClickListener); -} - -// Consider moving this to datasets.js completely // TODO: we dont need them, do we? there in dataset.js -const downvoteButtonClickListener = e => { - const entryID = e.target.parentElement.parentElement.dataset.id; - vote(entryID, false); -}; -for (const downvoteButton of downvoteButtons) { - downvoteButton.addEventListener("click", downvoteButtonClickListener); -} - // functions of the main page function navigateToAdd() { window.location.href = "/add"; //TODO: move to EventListner? @@ -121,42 +103,18 @@ function getSortQuery() { // creates query for the whole toolbar, so that searching, sorting and filtering are always combined function createQuery() { updateSections(); - let queryURL = new URL(apiEndpoint + "/search", baseURL); + let queryURL = new URL(DATASET_ENDPOINT + "/search", getBaseURL()); queryURL.searchParams.append("search", getSearchQuery()); + let filterQuery = getFilterQuery(); queryURL.searchParams.append(filterQuery[0], filterQuery[1]); + let sortQuery = getSortQuery(); queryURL.searchParams.append("sort", sortQuery[0]); queryURL.searchParams.append("direction", sortQuery[1]); queryURL.searchParams.append("size", defaultPagingValue.toString(10)); - return queryURL; -} -export function vote(entryID, up) { - const fetchURL = new URL( - `${apiEndpoint}/id/${entryID}/${up ? "up" : "down"}vote`, - baseURL, - ); - fetch(fetchURL, { - method: "PUT", - headers: { - 'Content-Type': 'application/json', - } - }) - .then(resp => resp.json()) - .then((data) => { - console.log(data.upvotes); // TODO: remove, check einbauen: data.id === entryID? - let dataSets = document.querySelectorAll('[data-id= ' + CSS.escape(entryID) + ']'); - for (const dataSetElement of dataSets) { - dataSetElement.querySelector("span").innerText = data.upvotes; - let votedButton = dataSetElement.querySelector(up? ".upvote-btn":".downvote-btn"); - votedButton.classList.add("isVoted"); - votedButton.disabled = true; - let notVotedButton = dataSetElement.querySelector(up? ".downvote-btn":".upvote-btn"); - notVotedButton.style.visibility = "hidden"; - votedIDs.setItem(dataSetElement.getAttribute("data-id"), up); - } - }); + return queryURL; } // updates the page display. If no query is present, the initial page is shown, otherwise the search results. @@ -178,7 +136,7 @@ function updateSections() { // fetches the further categories used in the filter function function fetchCategories() { const fetchURL = new URL( - "api/v1/categories", baseURL); + "api/v1/categories", getBaseURL()); fetch(fetchURL) .then(resp => resp.json()) .then((data) => { @@ -191,32 +149,30 @@ function fetchCategories() { } // fetches entries for the initial page -function fetchInitialEntries() { - let recentsQueryURL = new URL(apiEndpoint + "/search", baseURL); +async function fetchInitialEntries() { + let recentsQueryURL = new URL(DATASET_ENDPOINT + "/search", getBaseURL()); recentsQueryURL.searchParams.append("sort", "date"); recentsQueryURL.searchParams.append("direction", "desc"); recentsQueryURL.searchParams.append("size", "6"); - fetch(recentsQueryURL) - .then(resp => resp.json()) - .then((data) => { - const datasets = data.content.map(dataset => new Dataset(dataset)); - for (const dataset of datasets) { - document.querySelector("#recents .datasets") - .appendChild(dataset.createDatasetHTMLElement()); - } - }); - let topVotedQueryURL = new URL(apiEndpoint + "/search", baseURL); + const recentsResponse = await fetch(recentsQueryURL); + const recentsData = await recentsResponse.json(); + + const recentsDatasets = recentsData.content.map(dataset => new Dataset(dataset)); + for (const recentDataset of recentsDatasets) { + document.querySelector("#recents .datasets").appendChild(recentDataset.createDatasetHTMLElement()); + } + + let topVotedQueryURL = new URL(DATASET_ENDPOINT + "/search", getBaseURL()); topVotedQueryURL.searchParams.append("sort", "upvotes"); topVotedQueryURL.searchParams.append("direction", "desc"); topVotedQueryURL.searchParams.append("size", "1"); - fetch(topVotedQueryURL) - .then(resp => resp.json()) - .then((data) => { - let dataset = new Dataset(data.content[0]); - document.querySelector("#top .datasets") - .appendChild(dataset.createDatasetHTMLElement()); - }); + + const topVotedResponse = await fetch(topVotedQueryURL); + const topVotedData = await topVotedResponse.json(); + const topVotedDataset = new Dataset(topVotedData.content[0]); + + document.querySelector("#top .datasets").appendChild(topVotedDataset.createDatasetHTMLElement()); } window.onload = function () { diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 5f8cbdd..c6e00e9 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -19,8 +19,8 @@
  • -

    title

    -

    Simply daily accountability phone call, powered by AI

    +

    title

    +

    Simply daily accountability phone call, powered by AI