diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/controler/CategoryController.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/controler/CategoryController.java new file mode 100644 index 0000000..06454ed --- /dev/null +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/controler/CategoryController.java @@ -0,0 +1,18 @@ +package de.uni_passau.fim.PADAS.group3.DataDash.controler; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.uni_passau.fim.PADAS.group3.DataDash.model.Category; + +import org.springframework.web.bind.annotation.GetMapping; + + +@RestController +@RequestMapping("/api/v1/categories") +public class CategoryController { + @GetMapping + public Category[] getMethodName() { + return Category.values(); + } +} diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/controler/DatasetController.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/controler/DatasetController.java index 64f92cd..824c5ff 100644 --- a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/controler/DatasetController.java +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/controler/DatasetController.java @@ -4,6 +4,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.web.bind.annotation.*; + +import de.uni_passau.fim.PADAS.group3.DataDash.model.Category; import de.uni_passau.fim.PADAS.group3.DataDash.model.Dataset; import de.uni_passau.fim.PADAS.group3.DataDash.model.DatasetService; import de.uni_passau.fim.PADAS.group3.DataDash.model.Type; @@ -51,42 +53,42 @@ public class DatasetController { datasetService.deleteDataset(id); } - @PostMapping("/id/{id}/upvote") + @PutMapping("/id/{id}/upvote") public Dataset upvote(@PathVariable("id") UUID id) { datasetService.upvoteDataset(id); - return null; + return datasetService.getDatasetById(id); } - @PostMapping("/id/{id}/downvote") + @PutMapping("/id/{id}/downvote") public Dataset downvote(@PathVariable("id") UUID id) { datasetService.downvoteDataset(id); - return null; // new ResponseEntity<>(null, HttpStatus.OK); + return getDatasetById(id); // new ResponseEntity<>(null, HttpStatus.OK); } - @PostMapping("/id/{id}/vote") - public String postMethodName(@PathVariable("id") UUID id, + @PutMapping("/id/{id}/stars") + public Dataset postMethodName(@PathVariable("id") UUID id, @RequestParam("stars") int stars) { if (stars > 0 && stars < 6) { datasetService.voteDataset(id, stars); - return null; } - return "Invalid vote"; + return datasetService.getDatasetById(id); } @GetMapping - public Page getDatasetsByDateAfter(@RequestParam(value = "author", required = false) String author, + public Page getDatasetsByDateAfter(@RequestParam(value = "author", required = false) String author, @RequestParam(value = "title", required = false) String title, @RequestParam(value = "description", required = false) String description, @RequestParam(value = "abst", required = false) String abst, @RequestParam(value = "type", required = false) Type type, - @RequestParam(value = "min-raiting", required = false) Float raiting, + @RequestParam(value = "min-rating", required = false) Float rating, @RequestParam(value = "page", required = false, defaultValue = "0") int page, @RequestParam(value = "size", required = false, defaultValue = "20") int size, @RequestParam(value = "sort", required = false, defaultValue = "upvotes") String sort, - @RequestParam(value = "direction", required = false, defaultValue = "desc") String direction) { + @RequestParam(value = "direction", required = false, defaultValue = "desc") String direction, + @RequestParam(value = "category", required = false) Category category) { Pageable pageable = PageRequest.of(page, size, Sort.by(direction.equals("asc") ? Sort.Direction.ASC : Sort.Direction.DESC, sort)); - return datasetService.getDatasetsByOptionalCriteria(title, description, author, abst, type, raiting, pageable); + return datasetService.getDatasetsByOptionalCriteria(title, description, author, abst, type, rating, category,pageable); } @GetMapping("/search") @@ -94,10 +96,12 @@ public class DatasetController { @RequestParam(value = "page", required = false, defaultValue = "0") int page, @RequestParam(value = "size", required = false, defaultValue = "20") int size, @RequestParam(value = "sort", required = false, defaultValue = "upvotes") String sort, - @RequestParam(value = "direction", required = false, defaultValue = "desc") String direction) { + @RequestParam(value = "direction", required = false, defaultValue = "desc") String direction, + @RequestParam(value = "category", required = false, defaultValue = "%") String category, + @RequestParam(value = "type", required = false, defaultValue = "%") String type){ Pageable pageable = PageRequest.of(page, size, Sort.by(direction.equals("asc") ? Sort.Direction.ASC : Sort.Direction.DESC, sort)); - return datasetService.searchByOptionalCriteria(search, pageable); + return datasetService.searchByOptionalCriteria(search, category, type, pageable); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/controler/PageController.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/controler/PageController.java new file mode 100644 index 0000000..f1a0f6f --- /dev/null +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/controler/PageController.java @@ -0,0 +1,12 @@ +package de.uni_passau.fim.PADAS.group3.DataDash.controler; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class PageController { + @GetMapping("/add") + public String getAddPage() { + return "add"; + } +} diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/Category.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/Category.java new file mode 100644 index 0000000..b7cc30e --- /dev/null +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/Category.java @@ -0,0 +1,14 @@ +package de.uni_passau.fim.PADAS.group3.DataDash.model; + +public enum Category { + HEALTH, + ENVIRONMENT, + ECONOMY, + POLITICS, + TECHNOLOGY, + SPORTS, + SCIENCE, + CULTURE, + EDUCATION, + OTHER +} diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/Dataset.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/Dataset.java index 52cd1b4..7d694fb 100644 --- a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/Dataset.java +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/Dataset.java @@ -3,6 +3,7 @@ package de.uni_passau.fim.PADAS.group3.DataDash.model; import java.net.URL; import java.time.LocalDate; import java.util.UUID; +import java.sql.Date; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -29,7 +30,7 @@ public class Dataset { private String author; - private LocalDate date; + private Date date; private float raiting; @@ -38,10 +39,10 @@ public class Dataset { private int upvotes; private URL url; + @Enumerated(EnumType.STRING) + private Category categorie; - private String[] categories; - - public Dataset(String title, String abst, String description, String author, URL url, String[] categories, Type type) { + public Dataset(String title, String abst, String description, String author, URL url, Category categories, Type type) { this.raiting = 0; this.votes = 0; @@ -50,8 +51,8 @@ public class Dataset { setAbst(abst); setDescription(description); setAuthor(author); - setDate(LocalDate.now()); - setCategories(categories); + setDate(LocalDate.now()); + setCategorie(categories); setType(type); setUrl(url); } @@ -68,12 +69,12 @@ public class Dataset { return author; } - public String[] getCategories() { - return categories; + public Category getCategorie() { + return categorie; } public LocalDate getDate() { - return date; + return date.toLocalDate(); } public String getDescription() { @@ -116,12 +117,12 @@ public class Dataset { this.author = author; } - public void setCategories(String[] categories) { - this.categories = categories; + public void setCategorie(Category categories) { + this.categorie = categories; } public void setDate(LocalDate localDate) { - this.date = localDate; + this.date = Date.valueOf(localDate); } public void setDescription(String description) { diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/DatasetService.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/DatasetService.java index c34e55b..a3ac52d 100644 --- a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/DatasetService.java +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/DatasetService.java @@ -1,5 +1,6 @@ package de.uni_passau.fim.PADAS.group3.DataDash.model; +import java.time.LocalDate; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -26,6 +27,7 @@ public class DatasetService { } public void addDataset(Dataset dataset) { + dataset.setDate(LocalDate.now()); datasetRepository.save(dataset); } @@ -85,18 +87,17 @@ public class DatasetService { } public Page getDatasetsByOptionalCriteria(String title, String description, String author, String abst, - Type type, Float raiting, Pageable pageable) { - String[] categories = null; + Type type, Float raiting, Category category, Pageable pageable) { return datasetRepository.findByOptionalCriteria(Optional.ofNullable(title), Optional.ofNullable(description), - Optional.ofNullable(author), Optional.ofNullable(abst), Optional.ofNullable(type), - Optional.ofNullable(categories), Optional.ofNullable(raiting), pageable); + Optional.ofNullable(author), Optional.ofNullable(abst), Optional.ofNullable(type), Optional.ofNullable(category), + Optional.ofNullable(raiting), pageable); } - public Page searchByOptionalCriteria(String search, Pageable pageable) { - if (search.equals("%")) { - System.out.println("searching for all datasets"); - return datasetRepository.findAll(pageable); - } - return datasetRepository.searchByOptionalCriteria(Optional.ofNullable(search), pageable); + public Page searchByOptionalCriteria(String search, String categories, String type, Pageable pageable) { + //TODO: make it not Crash + Category category = categories.equals("%") ? null : Category.valueOf(categories); + Type t = type.equals("%") ? null : Type.valueOf(type); + + return datasetRepository.searchByOptionalCriteria(Optional.ofNullable(search), Optional.ofNullable(category), Optional.ofNullable(t),pageable); } } \ No newline at end of file diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/LoadDummyDatabase.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/LoadDummyDatabase.java index ba8f166..dd6d8e0 100644 --- a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/LoadDummyDatabase.java +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/LoadDummyDatabase.java @@ -1,8 +1,7 @@ package de.uni_passau.fim.PADAS.group3.DataDash.model; -import java.net.URL; -import java.sql.Date; import java.util.List; +import java.util.Random; import org.slf4j.LoggerFactory; import org.springframework.boot.CommandLineRunner; @@ -23,8 +22,10 @@ public class LoadDummyDatabase { return args -> { for (int i = 0; i < 100; i++) { - Dataset dataset = new Dataset("Title" + i, "Abst" + i, "Description" + i, "Author" + i,null, new String[]{"Category" + i}, Type.API); - repository.save(dataset); + Dataset dataset = new Dataset("Title" + i, "Abst" + i, "Description" + i, "Author" + i,null, Category.EDUCATION, Type.API); + for (int j = 0; j < new Random().nextInt(50); j++) { + dataset.upvote(); + } log.info("Preloading" + repository.save(dataset)); } List s = repository.findByTitleLike("%Title%"); diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/dataRepository.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/dataRepository.java index b2934d8..17fde64 100644 --- a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/dataRepository.java +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/model/dataRepository.java @@ -11,49 +11,64 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +public interface dataRepository extends JpaRepository { -public interface dataRepository extends JpaRepository{ + Dataset getDatasetById(UUID id); - Dataset getDatasetById(UUID id); - List findByTitle(String title); - List findByTitleLike(String title); - List findByAuthorLike(String author); - List findByType(Type type); - List findByAuthor(String author); - List findByAbstLike(String abst); - List findByDescriptionLike(String description); - List findByCategories(String[] categories); - List findByRaitingGreaterThan(float raiting); - List findByVotesGreaterThan(int votes); - List findByDateAfter(Date date); - List findByDateBefore(Date date); - List findByDateBetween(Date date1, Date date2); - @SuppressWarnings("null") - Page findAll(Pageable pageable); + List findByTitle(String title); - @Query("SELECT d FROM Dataset d WHERE " + - "(COALESCE(:title, '') = '' OR d.title LIKE :title) AND " + - "(COALESCE(:description, '') = '' OR d.description LIKE :description) AND" + - "(COALESCE(:author, '') = '' OR d.author LIKE :author) AND" + - "(COALESCE(:abst, '') = '' OR d.abst LIKE :abst) AND" + - "(:type IS NULL OR d.type = :type) AND"+ - "(:categories IS NULL OR d.categories = :categories) AND" + - "(:raiting IS NULL OR d.raiting > :raiting)") - PagefindByOptionalCriteria(@Param("title") Optional title, - @Param("description") Optional description, - @Param("author") Optional author, - @Param("abst") Optional abst, - @Param("type") Optional type, - @Param("categories") Optional categories, - @Param("raiting") Optional raiting, - Pageable pageable); + List findByTitleLike(String title); + List findByAuthorLike(String author); - @Query("SELECT d FROM Dataset d WHERE " + - "(LOWER(d.title) LIKE LOWER(:search)) OR " + - "(LOWER(d.description) LIKE LOWER(:search)) OR " + - "(LOWER(d.author) LIKE LOWER(:search))") - PagesearchByOptionalCriteria(@Param("search") Optional search, - Pageable pageable); - + List findByType(Type type); + + List findByAuthor(String author); + + List findByAbstLike(String abst); + + List findByDescriptionLike(String description); + + List findByRaitingGreaterThan(float raiting); + + List findByVotesGreaterThan(int votes); + + List findByDateAfter(Date date); + + List findByDateBefore(Date date); + + List findByCategorie(Category categorie); + + List findByDateBetween(Date date1, Date date2); + + @SuppressWarnings("null") + Page findAll(Pageable pageable); + + @Query("SELECT d FROM Dataset d WHERE " + + "(COALESCE(:title, '') = '' OR d.title LIKE :title) AND " + + "(COALESCE(:description, '') = '' OR d.description LIKE :description) AND" + + "(COALESCE(:author, '') = '' OR d.author LIKE :author) AND" + + "(COALESCE(:abst, '') = '' OR d.abst LIKE :abst) AND" + + "(:type IS NULL OR d.type = :type) AND" + + "(:categorie IS NULL OR d.categorie = :categorie) AND" + + "(:raiting IS NULL OR d.raiting > :raiting)") + Page findByOptionalCriteria(@Param("title") Optional title, + @Param("description") Optional description, + @Param("author") Optional author, + @Param("abst") Optional abst, + @Param("type") Optional type, + @Param("categorie") Optional categories, + @Param("raiting") Optional raiting, + Pageable pageable); + + @Query("SELECT d FROM Dataset d WHERE " + + "((LOWER(d.title) LIKE LOWER(:search)) OR " + + "(LOWER(d.description) LIKE LOWER(:search)) OR " + + "(LOWER(d.author) LIKE LOWER(:search))) AND" + + "(:categorie IS NULL OR d.categorie = :categorie) AND" + + "(:type IS NULL OR d.type = :type)") + Page searchByOptionalCriteria(@Param("search") Optional search, + @Param("categorie") Optional categories, + @Param("type") Optional type, + Pageable pageable); } \ No newline at end of file diff --git a/src/main/resources/static/add.css b/src/main/resources/static/add.css index a4b4b92..6d62a6f 100644 --- a/src/main/resources/static/add.css +++ b/src/main/resources/static/add.css @@ -4,20 +4,6 @@ --accent-color: oklch(65.33% 0.158 247.76); } -form label:after { - content: ":"; -} - -form :is(input[type=text], textarea) { - background-color: var(--fg-color); - border: none; - border-radius: .25lh; - color: var(--text-color, white); - padding: .5em; - font-family: sans-serif; -} - -/* quick info box */ form { display: grid; grid-template-columns: 1fr 1fr auto; @@ -30,17 +16,57 @@ form > * { gap: 1rem; } -form :is(input[type=text], textarea) { +form label:after { + content: ":"; +} + +/* text entries */ +form :is(input[type=text], input[type=url], textarea) { + background-color: var(--fg-color); + border: none; + border-radius: .25lh; + color: var(--text-color, white); + padding: .5em; + font-family: sans-serif; flex-grow: 1; } +/* focus outlines */ +:is(form :is(input[type=text], input[type=url], textarea), .btn):focus-visible { + outline: 2px solid var(--accent-color); +} + +.btn, #is-dataset-toggle { + transition: outline ease-in 100ms; +} + +/* input validation */ +form :is(input[type=text], input[type=url], textarea):user-valid { + --validation-color: lime; +} + +form :is(input[type=text], input[type=url], textarea):user-invalid { + --validation-color: red; + outline-style: solid; +} + +form :is(input[type=text], input[type=url], textarea):is(:user-valid, :user-invalid) { + outline-color: var(--validation-color); +} + +form :is(input[type=text], input[type=url], textarea):is(:user-valid, :user-invalid):not(:focus-visible) { + outline-width: 1px; +} + /* switch */ label:has(#is-dataset) { gap: 0; } #is-dataset { - display: none; + top: -100vh; + left: -100vw; + position: absolute; } #is-dataset-toggle { @@ -69,6 +95,10 @@ label:has(#is-dataset) { filter: drop-shadow(rgba(0, 0, 0, .8) 0 0 .25rem); } +#is-dataset:focus-visible + #is-dataset-toggle { + outline: 2px solid var(--accent-color); +} + #is-dataset:not(:checked) + #is-dataset-toggle::before { left: 0; } @@ -79,7 +109,11 @@ label:has(#is-dataset) { /* short description box */ form :has(#short-description) { - grid-column: 1 / 3; + grid-column: 1 / 2; +} + +form :has(#url) { + grid-column: 2 / 4; } /* full description box */ @@ -116,13 +150,18 @@ form :has(#short-description) { --drop-shadow-opacity: .5; --drop-shadow-offset-y: 0; --drop-shadow-blur: .25rem; - filter: drop-shadow( + --drop-shadow: drop-shadow( rgba(0, 0, 0, var(--drop-shadow-opacity)) 0 var(--drop-shadow-offset-y) var(--drop-shadow-blur) ); + filter: var(--drop-shadow); } -.btn:hover { +.btn:focus-visible, #is-dataset:focus-visible + #is-dataset-toggle { + outline-offset: 2px; +} + +.btn:not(:disabled):hover { background-color: color-mix(in oklab, var(--btn-color) 80%, var(--bg-color)); --drop-shadow-opacity: .8; --drop-shadow-offset-y: .25rem; @@ -132,3 +171,7 @@ form :has(#short-description) { .btn.suggested { --btn-color: var(--accent-color); } + +.btn:disabled { + filter: var(--drop-shadow) grayscale(.5) brightness(.5); +} diff --git a/src/main/resources/static/add.js b/src/main/resources/static/add.js new file mode 100644 index 0000000..280eb67 --- /dev/null +++ b/src/main/resources/static/add.js @@ -0,0 +1,59 @@ +const form = document.forms[0]; +const { + title: titleEntry, + author: authorEntry, + ["is-dataset"]: isDatasetSwitch, + ["short-description"]: shortDescriptionEntry, + url: urlEntry, + ["full-description"]: fullDescriptionEntry, + ["btn-add"]: addBtn, + ["btn-cancel"]: cancelBtn, +} = form.elements; + +const validationListener = () => { + addBtn.disabled = !form.checkValidity(); +}; + +// Register validationListener on all required inputs that must be valid +[ + titleEntry, + authorEntry, + shortDescriptionEntry, + urlEntry, + fullDescriptionEntry, +].forEach(input => input.addEventListener("input", validationListener)); + +form.addEventListener("submit", e => { + e.preventDefault(); + if (!form.reportValidity()) return; + + // Create the request body + const newContent = { + title: titleEntry.value, + author: authorEntry.value, + abst: shortDescriptionEntry.value, + url: urlEntry.value, + description: fullDescriptionEntry.value, + type: isDatasetSwitch.checked ? "API" : "DATASET", + categories: [], + }; + + console.debug(newContent); + + // Don't allow several requests to be sent at the same time + addBtn.disabled = true; + + fetch("/api/v1/datasets", { + method: "POST", + body: JSON.stringify(newContent), + headers: { + "Content-Type": "application/json;charset=utf-8" + } + }).then(response => { + if (response.status == 200) { + location.assign("/"); + } else { + addBtn.disabled = !form.checkValidity(); + } + }); +}); diff --git a/src/main/resources/static/contentUtility.js b/src/main/resources/static/contentUtility.js index 61c9f36..deb5658 100644 --- a/src/main/resources/static/contentUtility.js +++ b/src/main/resources/static/contentUtility.js @@ -1,4 +1,5 @@ -import { searchBarTimeout } from "./main.js" +import {searchBarTimeout, searchSection} from "./main.js" +import Dataset from "./dataset.js" export function fetchQuery(fetchString) { clearTimeout(searchBarTimeout); @@ -10,5 +11,15 @@ export function fetchQuery(fetchString) { } function parseContent(content) { - //TODO: method for parsing query results + if (content.length === 0) { + searchSection.querySelector("#nothing-found ").classList.remove("hidden"); + } else { + searchSection.querySelector("#nothing-found").classList.add("hidden"); + const datasets = content.map(dataset => new Dataset(dataset)); + 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 new file mode 100644 index 0000000..27245e5 --- /dev/null +++ b/src/main/resources/static/dataset.js @@ -0,0 +1,51 @@ +import { vote } from "./main.js"; + +export default class Dataset { + #abstract; + #author; + #categories; + #date; + #description; + #id; + #rating; + #title; + #type; + #upvotes; + #url; + #votes; + + constructor({abst: shortDescription, author, categories, date, description, id, rating, title, type, upvotes, url, votes}) { + this.#abstract = shortDescription; + this.#author = author; + this.#categories = categories; + this.#date = date; + this.#description = description; + this.#id = id; + this.#rating = rating; + this.#title = title; + this.#type = type; + this.#upvotes = upvotes; + this.#url = url; + this.#votes = votes; + } + + 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; + + // Event Listeners + clone.querySelector(".upvote-btn").addEventListener("click", () => { + vote(this.#id, true); + }); + + clone.querySelector(".downvote-btn").addEventListener("click", () => { + vote(this.#id, false); + }) + + return clone; + } +} diff --git a/src/main/resources/static/main.css b/src/main/resources/static/main.css index 3c2d591..c62d21e 100644 --- a/src/main/resources/static/main.css +++ b/src/main/resources/static/main.css @@ -42,6 +42,7 @@ header { left: 0; z-index: 1; } + #tool-bar { display: flex; flex-direction: row; @@ -61,6 +62,25 @@ header { color: var(--text-color); } +#nothing-found { + height: 60vh; + padding: 20vh 0; +} + +#nothing-found-bg { + background: url("sad-looking-glass.svg") center no-repeat; + background-size: contain; + width: 100%; + height: 40vh; +} + +#nothing-found-text { + text-align: center; +} + +.hidden { + display: none; +} #search-entry:focus-visible { outline: none; } @@ -98,7 +118,7 @@ header { gap: .5em; } /* Buttons */ -.upvote-btn, .downvote-btn, #search-btn, #filter-btn, #sort-btn { +.upvote-btn, .downvote-btn, #search-btn, #filter-btn, #sort-btn, #reset-tools-btn { background: var(--icon-url) no-repeat; background-size: contain; border: none; @@ -128,6 +148,11 @@ header { --icon-size: 1rem; } +#reset-tools-btn { + --icon-url: url(reset.svg); + --icon-size: 1rem; +} + #sort-btn { --icon-url: url(sort.svg); --icon-size: 1rem; diff --git a/src/main/resources/static/main.js b/src/main/resources/static/main.js index 91dd9e0..77af7e6 100644 --- a/src/main/resources/static/main.js +++ b/src/main/resources/static/main.js @@ -1,4 +1,5 @@ -import { fetchQuery } from "./contentUtility.js"; +import {fetchQuery} from "./contentUtility.js"; +import Dataset from "./dataset.js"; const apiEndpoint = "/api/v1/datasets"; const baseURL = location.origin; @@ -9,17 +10,22 @@ const lastQuery = { currentPage: 0 }; -// definition of all buttons +// definition of all buttons & sections const addButton = document.getElementById("add-btn"); const filterButton = document.getElementById("filter-btn"); const searchButton = document.getElementById("search-btn"); const searchBar = document.getElementById("search-entry"); const sortButton = document.getElementById("sort-btn"); +const resetButton = document.getElementById("reset-tools-btn"); const upvoteButtons = document.getElementsByClassName("upvote-btn"); const downvoteButtons = document.getElementsByClassName("downvote-btn"); +export const searchSection = document.getElementById("search"); +const recentSection = document.getElementById("recents"); +const mostLikedSection = document.getElementById("top"); // ID of the timeout, because we need to cancel it at some point export let searchBarTimeout; +const searchDelay = 500; // Event listeners addButton.addEventListener("click", () => { @@ -28,35 +34,43 @@ addButton.addEventListener("click", () => { filterButton.addEventListener("change", () => { const filterString = filterButton.value; - filter(filterString); + if (filterString !== filterButton.querySelector("#default-filter").value) { + fetchQuery(createQuery()); + } }); searchButton.addEventListener("click", () => { - const searchString = searchBar.value; - search(searchString); + fetchQuery(createQuery()); + }); searchBar.addEventListener("input", () => { + updateSections(); clearTimeout(searchBarTimeout); searchBarTimeout = setTimeout(() => { - const searchString = searchBar.value; - search(searchString); - }, 1000); + fetchQuery(createQuery()); + }, searchDelay); }); searchBar.addEventListener('keypress', function (e) { if (e.key === 'Enter') { - const searchString = searchBar.value; - search(searchString); + fetchQuery(createQuery()); } -}) - -sortButton.addEventListener("change", () => { - const sortString = sortButton.value; - sort(sortString); }); -const upvoteButtonClickListener = e => { +sortButton.addEventListener("change", () => { + fetchQuery(createQuery()); +}); + +resetButton.addEventListener("click", () => { + searchBar.value = ""; + filterButton.value = filterButton.querySelector("#default-filter").value; + sortButton.value = sortButton.querySelector("#default-sort").value; + updateSections(); +}); + +// Consider moving this to datasets.js completely +const upvoteButtonClickListener = e => { const entryID = e.target.parentElement.parentElement.dataset.id; vote(entryID, true); }; @@ -64,6 +78,7 @@ for (const upvoteButton of upvoteButtons) { upvoteButton.addEventListener("click", upvoteButtonClickListener); } +// Consider moving this to datasets.js completely const downvoteButtonClickListener = e => { const entryID = e.target.parentElement.parentElement.dataset.id; vote(entryID, false); @@ -74,54 +89,134 @@ for (const downvoteButton of downvoteButtons) { // functions of the main page function navigateToAdd() { - //TODO: url to add page not yet implemented, add here + window.location.href = "/add"; } -function filter(filterString) { - filterString = filterString.toUpperCase(); - - let fetchURL = new URL(apiEndpoint, baseURL); - fetchURL.searchParams.append("type", filterString); - fetchURL.searchParams.append("size", defaultPagingValue); - - console.log(fetchURL); // TODO: remove - fetchQuery(fetchURL); -} - -function search(searchString) { - let fetchURL = new URL(apiEndpoint + "/search", baseURL); - fetchURL.searchParams.append("search", searchString.length == 0 ? "%" : searchString); - - console.log(fetchURL); // TODO: remove - fetchQuery(fetchURL); -} - -function sort(sortString) { - let query = sortString.toLowerCase().split(" "); - if (query[1] === "a-z" || query[1] === "↑") { - query[1] = "asc"; +function getFilterQuery() { + let filterString= filterButton.value.toUpperCase(); + if (filterString === "NONE") { + return ["type", "%"] + } else if (document.querySelector('#filter-btn option:checked').parentElement.label === "Standard categories") { + return ["type", filterString]; } else { - query[1] = "desc"; + return ["category", filterString]; } - - let fetchURL = new URL(apiEndpoint, baseURL); - fetchURL.searchParams.append("sort", query[0]); - fetchURL.searchParams.append("direction", query[1]); - - console.log(fetchURL); // TODO: remove - fetchQuery(fetchURL); } -function vote(entryID, up) { +function getSearchQuery() { + let searchString = searchBar.value; + return (searchString.length === 0 ? "%" : ("%" + searchString + "%")); +} + +function getSortQuery() { + let sortString = sortButton.value.toLowerCase().split(" "); + if (sortString[1] === "a-z" || sortString[1] === "↑" || sortString[1] === "oldest-newest") { + sortString[1] = "asc"; + } else { + sortString[1] = "desc"; + } + return sortString +} + +// 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); + 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, ); - - console.log(fetchURL); // TODO: remove - fetch(fetchURL); + 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 dataset = document.querySelector('[data-id= ' + CSS.escape(entryID) + ']') + dataset.querySelector("span").innerText = data.upvotes; + }); } function incrementPageCount() { lastQuery.currentPage++; } + +// updates the page display. If no query is present, the initial page is shown, otherwise the search results. +function updateSections() { + if (searchBar.value === "" && sortButton.value === sortButton.querySelector("#default-sort").value + && filterButton.value === filterButton.querySelector("#default-filter").value) { + searchSection.classList.add("hidden"); + recentSection.classList.remove("hidden"); + mostLikedSection.classList.remove("hidden"); + resetButton.classList.add("hidden"); + } else { + searchSection.classList.remove("hidden"); + recentSection.classList.add("hidden"); + mostLikedSection.classList.add("hidden"); + resetButton.classList.remove("hidden"); + } +} + +// fetches the further categories used in the filter function +function fetchCategories() { + const fetchURL = new URL( + "api/v1/categories" , baseURL); + fetch(fetchURL) + .then(resp => resp.json()) + .then((data) => { + for (let i = 0; i < data.length; i++) { + let category = data[i].toLowerCase(); + category = category.charAt(0).toUpperCase() + category.slice(1); + document.getElementById("other-categories").appendChild(new Option(category)); + } + }); +} + +// fetches entries for the initial page +function fetchInitialEntries() { + let recentsQueryURL = new URL(apiEndpoint + "/search", baseURL); + 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); + topVotedQueryURL.searchParams.append("sort", "upvotes"); + topVotedQueryURL.searchParams.append("direction", "desc"); + topVotedQueryURL.searchParams.append("size", "1"); + fetch(topVotedQueryURL) + .then(resp => resp.json()) + .then((data) => { + document.querySelector("#top .datasets") + .appendChild(new Dataset(data.content[0]).createDatasetHTMLElement()); + }); +} + +window.onload = function () { + fetchCategories(); + fetchInitialEntries(); + updateSections(); + if (searchBar.value !== "") { + fetchQuery(createQuery()); + } +} diff --git a/src/main/resources/static/reset.svg b/src/main/resources/static/reset.svg new file mode 100644 index 0000000..6915802 --- /dev/null +++ b/src/main/resources/static/reset.svg @@ -0,0 +1,52 @@ + + + + diff --git a/src/main/resources/static/sad-looking-glass.svg b/src/main/resources/static/sad-looking-glass.svg index 7ad3841..ab99a6e 100644 --- a/src/main/resources/static/sad-looking-glass.svg +++ b/src/main/resources/static/sad-looking-glass.svg @@ -1,2 +1,7 @@ - + + + + + + diff --git a/src/main/resources/templates/add.html b/src/main/resources/templates/add.html index d8472c1..30888c3 100644 --- a/src/main/resources/templates/add.html +++ b/src/main/resources/templates/add.html @@ -5,6 +5,7 @@ DataDash – Add dataset/API +
@@ -19,15 +20,15 @@ how to use it.

-
+ - - + + - +