diff --git a/pom.xml b/pom.xml index 36bb031..cdd484e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.0 + 3.3.1 de.uni-passau.fim.PADAS.group3 @@ -22,11 +22,6 @@ org.springframework.boot spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-thymeleaf - org.springframework.boot @@ -57,13 +52,6 @@ runtime - - org.mockito - mockito-core - 4.5.1 - test - - diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/Dataset.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/Dataset.java index 8a269c9..36796e3 100644 --- a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/Dataset.java +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/Dataset.java @@ -52,7 +52,7 @@ public class Dataset { private String licence; - private static final List sortable = Arrays.asList("author", "title", "upvotes", "date"); + private static final List sortable = Arrays.asList("author", "title", "upvotes", "raiting", "date"); @ManyToOne private Category categorie; diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetController.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetController.java index a844374..d0a00ca 100644 --- a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetController.java +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetController.java @@ -71,7 +71,7 @@ public class DatasetController { if (datasetService.getDatasetById(id) == null) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } - if (!(stars > 0 && stars < 6)) { + if (!(stars >= 0 && stars < 6)) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } datasetService.voteDataset(id, stars); diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetService.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetService.java index 52e6ef6..757739e 100644 --- a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetService.java +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetService.java @@ -17,44 +17,44 @@ public class DatasetService { private dataRepository datasetRepository; private CategoryRepository categoryRepository; - public DatasetService(dataRepository datasetRepository, CategoryRepository categoryRepository) { + DatasetService(dataRepository datasetRepository, CategoryRepository categoryRepository) { this.datasetRepository = datasetRepository; this.categoryRepository = categoryRepository; } - public Dataset getDatasetById(UUID id) { + Dataset getDatasetById(UUID id) { return datasetRepository.getDatasetById(id); } - public Dataset addDataset(Dataset dataset) { + Dataset addDataset(Dataset dataset) { dataset.setDate(LocalDate.now()); return datasetRepository.save(dataset); } - public void voteDataset(UUID id, int vote) { + void voteDataset(UUID id, int vote) { Dataset dataset = datasetRepository.getDatasetById(id); dataset.vote(vote); datasetRepository.save(dataset); } - public void deleteDataset(UUID id) { + void deleteDataset(UUID id) { Dataset dataset = datasetRepository.getDatasetById(id); datasetRepository.delete(dataset); } - public void upvoteDataset(UUID id) { + void upvoteDataset(UUID id) { Dataset dataset = datasetRepository.getDatasetById(id); dataset.upvote(); datasetRepository.save(dataset); } - public void downvoteDataset(UUID id) { + void downvoteDataset(UUID id) { Dataset dataset = datasetRepository.getDatasetById(id); dataset.downvote(); datasetRepository.save(dataset); } - public Page searchByOptionalCriteria(String search, String categories, String type, Pageable pageable) { + Page searchByOptionalCriteria(String search, String categories, String type, Pageable pageable) { Category category = categories.equals("%") ? null : categoryRepository.getCategoryById(UUID.fromString(categories)); Type t = type.equals("%") ? null : Type.valueOf(type); diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/category/CategoryController.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/category/CategoryController.java index 3ce13b0..f68be67 100644 --- a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/category/CategoryController.java +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/category/CategoryController.java @@ -37,8 +37,8 @@ public class CategoryController { } @ResponseStatus(HttpStatus.CREATED) @PostMapping - public void createCategory(@RequestBody CategoryDto dto) { - categoryService.addCategory(dto); + public Category createCategory(@RequestBody CategoryDto dto) { + return categoryService.addCategory(dto); } diff --git a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/category/CategoryService.java b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/category/CategoryService.java index 3e2698f..ced75f3 100644 --- a/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/category/CategoryService.java +++ b/src/main/java/de/uni_passau/fim/PADAS/group3/DataDash/category/CategoryService.java @@ -8,22 +8,22 @@ import java.util.UUID; public class CategoryService { private CategoryRepository categoryRepository; - public CategoryService(CategoryRepository categoryRepository) { + CategoryService(CategoryRepository categoryRepository) { this.categoryRepository = categoryRepository; } - public void addCategory(CategoryDto category) { + Category addCategory(CategoryDto category) { Category cat = new Category(category.getName()); - categoryRepository.save(cat); + return categoryRepository.save(cat); } - public List getAllCategories() { + List getAllCategories() { List tmp = categoryRepository.findAll(); List s = tmp.stream().map(CategoryDtoMapper::toDto).toList(); return s; } - public CategoryDto getCategoryById(UUID id) { + CategoryDto getCategoryById(UUID id) { Category c = categoryRepository.getCategoryById(id); if (c == null) { return null; diff --git a/src/main/resources/static/add.css b/src/main/resources/static/add.css index 5bbac6e..979d061 100644 --- a/src/main/resources/static/add.css +++ b/src/main/resources/static/add.css @@ -171,3 +171,30 @@ form :has(#url) { .btn:disabled { filter: var(--drop-shadow) grayscale(.5) brightness(.5); } + +#category[value="new"] { + display: none; +} + +label[for="category"] { + width: 0; + user-select: none; + overflow: hidden; +} + +span:has(#category) { + gap: unset; +} + +#new-category-group:not(.hidden) { + display: flex; + justify-content: stretch; + gap: var(--gap-small); + width: 100%; +} + +#new-category-group button { + background-image: url("./sort.svg"); + background-size: contain; + background-origin: content-box; +} diff --git a/src/main/resources/static/add.html b/src/main/resources/static/add.html index 30888c3..3c9f9c8 100644 --- a/src/main/resources/static/add.html +++ b/src/main/resources/static/add.html @@ -27,7 +27,7 @@ - + @@ -48,6 +48,29 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/static/add.js b/src/main/resources/static/add.js index 53292ad..819e55e 100644 --- a/src/main/resources/static/add.js +++ b/src/main/resources/static/add.js @@ -7,10 +7,16 @@ const { ["is-dataset"]: isDatasetSwitch, ["short-description"]: shortDescriptionEntry, url: urlEntry, + ["terms-of-use"]: termsOfUseEntry, + license: licenseEntry, + category: categorySpinner, + ["new-category"]: newCategoryEntry, + ["change-category-btn"]: changeCategoryBtn, ["full-description"]: fullDescriptionEntry, ["btn-add"]: addBtn, ["btn-cancel"]: cancelBtn, } = form.elements; +const newCategoryGroup = document.getElementById("new-category-group"); const validationListener = () => { addBtn.disabled = !form.checkValidity(); @@ -22,44 +28,127 @@ const validationListener = () => { authorEntry, shortDescriptionEntry, urlEntry, + termsOfUseEntry, + licenseEntry, + newCategoryEntry, fullDescriptionEntry, ].forEach(input => input.addEventListener("input", validationListener)); +// Category spinner +const categorySpinnerSet = (...args) => { + if (args.length > 0) { + categorySpinner.value = args[0]; + } + + categorySpinner.setAttribute("value", categorySpinner.value); + + if (categorySpinner.value == "new") { + newCategoryGroup.classList.remove("hidden"); + newCategoryEntry.disabled = false; + newCategoryEntry.focus(); + } else { + newCategoryGroup.classList.add("hidden"); + newCategoryEntry.disabled = true; + } +}; + +const getCategory = () => { + return categorySpinner.value == "new" + ? newCategoryEntry.value + : categorySpinner.value; +} + +categorySpinner.addEventListener("input", e => { + categorySpinnerSet(); + validationListener(); +}); + +changeCategoryBtn.addEventListener("click", e => { + e.preventDefault(); + categorySpinnerSet(""); + validationListener(); +}); + +let categoriesResponse = await fetch(`${location.origin}/api/v1/categories`); +let categories = []; +if (!categoriesResponse.ok) { + console.warn("Could not load categories!"); +} else { + categories = await categoriesResponse.json(); + for (const category of categories) { + let option = document.createElement("option"); + option.value = category.id; + option.text = category.name; + categorySpinner.add(option); + } +} + +// Form listeners cancelBtn.addEventListener("click", () => { window.location.href = location.origin; }) form.addEventListener("submit", async e => { e.preventDefault(); - if (!form.reportValidity()) return; + if (!form.reportValidity()) { + addBtn.disabled = true; + return; + } + + let categoryID = categorySpinner.value; + + if (categoryID == "new") { + const newCategoryName = newCategoryEntry.value.trim(); + + if (!categories.map(c => c.name).includes(newCategoryName)) { + // Try to add the new category + const newCategoryResponse = await fetch(`/api/v1/categories`, { + method: "POST", + headers: { "Content-Type": "application/json;charset=utf-8" }, + body: JSON.stringify({ name: newCategoryName }), + }); + + if (!newCategoryResponse.ok) { + newCategoryEntry.setCustomValidity( + `Could not create new category: ${newCategoryResponse.statusText}` + ); + form.reportValidity(); + return; + } + + const newCategory = await newCategoryResponse.json(); + categoryID = newCategory.id; + } + } // Create the request body const newContent = { title: titleEntry.value, author: authorEntry.value, + type: isDatasetSwitch.checked ? "API" : "DATASET", abst: shortDescriptionEntry.value, url: urlEntry.value, + termsOfUse: termsOfUseEntry.value, + licence: licenseEntry.value, + categorie: { + id: categoryID, + }, 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; let response = await fetch("/api/v1/datasets", { method: "POST", + headers: { "Content-Type": "application/json;charset=utf-8" }, body: JSON.stringify(newContent), - headers: { - "Content-Type": "application/json;charset=utf-8" - } - }); let data = await response.json(); + let dataset = new Dataset(data); dataset.storageSetKey("created-locally", true); + if (response.ok) { location.assign("/"); } else { diff --git a/src/main/resources/static/dataset.js b/src/main/resources/static/dataset.js index e7f0934..87984cd 100644 --- a/src/main/resources/static/dataset.js +++ b/src/main/resources/static/dataset.js @@ -23,7 +23,7 @@ export default class Dataset { return this.#datasets.get(id); } - constructor({ abst: shortDescription, author, categorie, date, description, id, raiting, title, type, upvotes, url, votes, license, termsOfUse }) { + constructor({ abst: shortDescription, author, categorie, date, description, id, raiting, title, type, upvotes, url, votes, licence: license, termsOfUse }) { this.#shortDescription = shortDescription; this.#author = author; this.#category = categorie; diff --git a/src/main/resources/static/details.css b/src/main/resources/static/details.css index 86bb412..92d1372 100644 --- a/src/main/resources/static/details.css +++ b/src/main/resources/static/details.css @@ -79,6 +79,17 @@ h1 { grid-column: 1 / 3; } +#rating-input { + mask-image: url("stars.svg"); + -webkit-mask-image: url("stars.svg"); + mask-size: contain; + mask-mode: alpha; + width: 5lh; + height: 1lh; + margin-inline: .5ch; + background: linear-gradient(to right, yellow 33%, black 33%); +} + #rating { color: color-mix(in oklab, var(--text-color) 80%, black); color: transparent; @@ -208,6 +219,51 @@ a { } } +#details-btns { + grid-column: 1 / 4; + justify-content: end; + gap: 1rem; + display: flex; +} + +#delete-btn { + background: #861c1c; +} + +/* button styling to be revisited */ +.btn { + padding: .5lh 1lh; + border: none; + border-radius: .5lh; + --btn-color: var(--fg-color); + background-color: var(--btn-color); + color: var(--text-color); + font-weight: bold; + font-size: 1rem; + transition: background-color 100ms, filter 200ms; + transition-timing-function: ease-out; + --drop-shadow-opacity: .5; + --drop-shadow-offset-y: 0; + --drop-shadow-blur: .25rem; + --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: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; + --drop-shadow-blur: .4rem; +} + + #nothing-found-bg { background-position-x: calc(50% + 3cqh); } diff --git a/src/main/resources/static/details.html b/src/main/resources/static/details.html index e91405e..ff27926 100644 --- a/src/main/resources/static/details.html +++ b/src/main/resources/static/details.html @@ -24,6 +24,7 @@

Title

4 + Lorem ipsum dolor sit amet consectetur adipisicing elit. Perspiciatis recusandae laborum odio corrupti voluptas quisquam dicta, quibusdam ipsum qui exercitationem. https://example.com/dataset @@ -58,6 +59,10 @@ ipsam nobis quis.

+
+ + +
diff --git a/src/main/resources/static/details.js b/src/main/resources/static/details.js index e770a5c..3f80529 100644 --- a/src/main/resources/static/details.js +++ b/src/main/resources/static/details.js @@ -13,23 +13,35 @@ const category = document.getElementById("category"); const license = document.getElementById("license"); const termsOfUse = document.getElementById("terms-of-use"); const fullDescription = document.getElementById("full-description"); +const backButton = document.getElementById("back-btn"); +const deleteButton = document.getElementById("delete-btn"); + +let dataset = null; +let currentRating = 0; +let isRated = false; const currentLocation = new URL(location.href); if (currentLocation.searchParams.has("id")) { const id = currentLocation.searchParams.get("id"); const response = await fetch(`${currentLocation.origin}/api/v1/datasets/id/${id}`); - console.dir(response); if (response.ok) { const data = await response.json(); - const dataset = new Dataset(data); - console.dir(data, dataset); + dataset = new Dataset(data); const upvoteComponent = dataset.createUpvoteComponent(); + console.log(dataset.storageGet()); + debugger + if (dataset.storageGetKey("created-locally", false)) { + deleteButton.classList.remove("hidden"); + } + isRated = dataset.storageGetKey("is-rated", false) + + title.innerText = dataset.title; title.dataset.type = dataset.type.toLowerCase(); rating.value = dataset.rating; - ratingText.innerText = dataset.rating; + ratingText.innerText = parseFloat(dataset.rating).toFixed(1); shortDescription.innerText = dataset.shortDescription; url.href = dataset.url; url.innerText = dataset.url; @@ -57,3 +69,53 @@ if (currentLocation.searchParams.has("id")) { mainPage.classList.add("hidden"); notFoundPage.classList.remove("hidden"); } + +backButton.addEventListener("click", () => { + window.location.href = location.origin; +}) + +deleteButton.addEventListener("click", () => { + if (dataset != null) { + fetch(`${currentLocation.origin}/api/v1/datasets/id/` + dataset.id, { + method: 'DELETE' + }).then(resp => { + if (resp.ok) { + window.location.href = location.origin; + } + }); + } +}); + +rating.addEventListener("mousemove", (event) => { + if (!isRated) { + let bounds = rating.getBoundingClientRect(); + currentRating = Math.round(((event.clientX - bounds.left) / bounds.width) * 5); + console.log(currentRating); + rating.value = currentRating; + } + +}); + +rating.addEventListener("mouseleave", () => { + rating.value = dataset.rating; +}); + +rating.addEventListener("click", () => { + if (!isRated) { + fetch(`${currentLocation.origin}/api/v1/datasets/id/` + dataset.id + "/stars?stars=" + currentRating, { + method: 'PUT' + }).then(resp => { + if (resp.ok) { + dataset.storageSetKey("is-rated", true); + isRated = true; + fetch(`${currentLocation.origin}/api/v1/datasets/id/` + dataset.id) + .then(resp => resp.json()) + .then((data) => { + dataset = new Dataset(data); + ratingText.innerText = parseFloat(dataset.rating).toFixed(1); + rating.value = dataset.rating; + }); + } + }); + } +}) diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 46da537..88979c4 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -45,8 +45,8 @@ - - + + diff --git a/src/main/resources/static/main.js b/src/main/resources/static/main.js index ad98c3b..d58c423 100644 --- a/src/main/resources/static/main.js +++ b/src/main/resources/static/main.js @@ -1,4 +1,4 @@ -import { DATASET_ENDPOINT, getBaseURL } from "./constants.js"; +import { DATASET_ENDPOINT, getBaseURL } from "./constants.js" import { fetchQuery } from "./contentUtility.js"; import Dataset from "./dataset.js"; diff --git a/src/test/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetControllerTests.java b/src/test/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetControllerTests.java index 8a3c7d9..87bb850 100644 --- a/src/test/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetControllerTests.java +++ b/src/test/java/de/uni_passau/fim/PADAS/group3/DataDash/Dataset/DatasetControllerTests.java @@ -185,17 +185,6 @@ public class DatasetControllerTests { .andExpect(status().isBadRequest()); } - @Test - void postMethodName_whenInvalidStars2() throws Exception { - UUID id = UUID.randomUUID(); - Dataset dataset = new Dataset("Title", "abst", "desc", "auth", null, null, Type.API, "MIT"); - - given(datasetService.getDatasetById(id)).willReturn(dataset); - - mockMvc.perform(put("/api/v1/datasets/id/" + id + "/stars?stars=0")) - .andExpect(status().isBadRequest()); - } - @Test void postMethodName_whenInvalidStars3() throws Exception { UUID id = UUID.randomUUID();