Merge branch '22-integrate-api-and-frontend' into 46-add-missing-fields-to-the-add-page
This commit is contained in:
commit
9215148eff
4
pom.xml
4
pom.xml
@ -52,8 +52,8 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
@ -1,16 +1,23 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.model;
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.Dataset;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.uni_passau.fim.PADAS.group3.DataDash.category.Category;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
|
||||
@Entity
|
||||
public class Dataset {
|
||||
@ -39,10 +46,18 @@ public class Dataset {
|
||||
private int upvotes;
|
||||
|
||||
private URL url;
|
||||
@Enumerated(EnumType.STRING)
|
||||
|
||||
@Column(name = "terms_of_use")
|
||||
private URL termsOfUse;
|
||||
|
||||
private String licence;
|
||||
|
||||
private static final List<String> sortable = Arrays.asList("author", "title", "upvotes", "date");
|
||||
|
||||
@ManyToOne
|
||||
private Category categorie;
|
||||
|
||||
public Dataset(String title, String abst, String description, String author, URL url, Category categories, Type type) {
|
||||
public Dataset(String title, String abst, String description, String author, URL url, Category categories, Type type, String licence) {
|
||||
|
||||
this.raiting = 0;
|
||||
this.votes = 0;
|
||||
@ -55,6 +70,7 @@ public class Dataset {
|
||||
setCategorie(categories);
|
||||
setType(type);
|
||||
setUrl(url);
|
||||
setLicence(licence);
|
||||
}
|
||||
|
||||
public Dataset() {
|
||||
@ -109,6 +125,18 @@ public class Dataset {
|
||||
return url;
|
||||
}
|
||||
|
||||
public URL getTermsOfUse() {
|
||||
return termsOfUse;
|
||||
}
|
||||
|
||||
public String getLicence() {
|
||||
return licence;
|
||||
}
|
||||
|
||||
public static List<String> getSort() {
|
||||
return sortable;
|
||||
}
|
||||
|
||||
public void setAbst(String abst) {
|
||||
this.abst = abst.substring(0, Math.min(abst.length(), 100));
|
||||
}
|
||||
@ -132,6 +160,10 @@ public class Dataset {
|
||||
public void setUrl(URL url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public void setTermsOfUse(URL termsOfUse) {
|
||||
this.termsOfUse = termsOfUse;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title.substring(0, Math.min(title.length(), 50));
|
||||
@ -153,4 +185,8 @@ public class Dataset {
|
||||
upvotes--;
|
||||
}
|
||||
|
||||
public void setLicence(String licence) {
|
||||
this.licence = licence;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.Dataset;
|
||||
|
||||
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 org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.config.EnableSpringDataWebSupport;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/datasets")
|
||||
@EnableSpringDataWebSupport
|
||||
public class DatasetController {
|
||||
@Autowired
|
||||
private DatasetService datasetService;
|
||||
|
||||
@GetMapping("/id/{id}")
|
||||
public ResponseEntity<Dataset> getDatasetById(@PathVariable("id") UUID id) {
|
||||
Dataset d = datasetService.getDatasetById(id);
|
||||
if (d == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ResponseEntity<>(d, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@PostMapping
|
||||
public Dataset createDataset(@RequestBody Dataset dataset) {
|
||||
return datasetService.addDataset(dataset);
|
||||
}
|
||||
|
||||
@DeleteMapping("/id/{id}")
|
||||
public ResponseEntity<?> deleteDataset(@PathVariable("id") UUID id) {
|
||||
if (datasetService.getDatasetById(id) == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
datasetService.deleteDataset(id);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PutMapping("/id/{id}/upvote")
|
||||
public ResponseEntity<Dataset> upvote(@PathVariable("id") UUID id) {
|
||||
if (datasetService.getDatasetById(id) == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
datasetService.upvoteDataset(id);
|
||||
return new ResponseEntity<>(datasetService.getDatasetById(id), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PutMapping("/id/{id}/downvote")
|
||||
public ResponseEntity<Dataset> downvote(@PathVariable("id") UUID id) {
|
||||
if (datasetService.getDatasetById(id) == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
datasetService.downvoteDataset(id);
|
||||
return new ResponseEntity<>(datasetService.getDatasetById(id), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PutMapping("/id/{id}/stars")
|
||||
public ResponseEntity<Dataset> postMethodName(@PathVariable("id") UUID id,
|
||||
@RequestParam("stars") int stars) {
|
||||
if (datasetService.getDatasetById(id) == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (!(stars > 0 && stars < 6)) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
datasetService.voteDataset(id, stars);
|
||||
return new ResponseEntity<>(datasetService.getDatasetById(id), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public ResponseEntity<Page<Dataset>> search(
|
||||
@RequestParam(value = "search", required = false, defaultValue = "%") String search,
|
||||
@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 = "category", required = false, defaultValue = "%") String category,
|
||||
@RequestParam(value = "type", required = false, defaultValue = "%") String type) {
|
||||
Pageable pageable = null;
|
||||
if (!Dataset.getSort().contains(sort))
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
try {
|
||||
pageable = PageRequest.of(page, size,
|
||||
Sort.by(Sort.Direction.fromString(direction), sort));
|
||||
} catch (Exception e) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
return new ResponseEntity<>(datasetService.searchByOptionalCriteria(search, category, type, pageable),
|
||||
HttpStatus.OK);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.Dataset;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import de.uni_passau.fim.PADAS.group3.DataDash.category.Category;
|
||||
import de.uni_passau.fim.PADAS.group3.DataDash.category.CategoryRepository;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
@Service
|
||||
public class DatasetService {
|
||||
private dataRepository datasetRepository;
|
||||
private CategoryRepository categoryRepository;
|
||||
|
||||
public DatasetService(dataRepository datasetRepository, CategoryRepository categoryRepository) {
|
||||
this.datasetRepository = datasetRepository;
|
||||
this.categoryRepository = categoryRepository;
|
||||
}
|
||||
|
||||
public Dataset getDatasetById(UUID id) {
|
||||
return datasetRepository.getDatasetById(id);
|
||||
}
|
||||
|
||||
public Dataset addDataset(Dataset dataset) {
|
||||
dataset.setDate(LocalDate.now());
|
||||
return datasetRepository.save(dataset);
|
||||
}
|
||||
|
||||
public void voteDataset(UUID id, int vote) {
|
||||
Dataset dataset = datasetRepository.getDatasetById(id);
|
||||
dataset.vote(vote);
|
||||
datasetRepository.save(dataset);
|
||||
}
|
||||
|
||||
public void deleteDataset(UUID id) {
|
||||
Dataset dataset = datasetRepository.getDatasetById(id);
|
||||
datasetRepository.delete(dataset);
|
||||
}
|
||||
|
||||
public void upvoteDataset(UUID id) {
|
||||
Dataset dataset = datasetRepository.getDatasetById(id);
|
||||
dataset.upvote();
|
||||
datasetRepository.save(dataset);
|
||||
}
|
||||
|
||||
public void downvoteDataset(UUID id) {
|
||||
Dataset dataset = datasetRepository.getDatasetById(id);
|
||||
dataset.downvote();
|
||||
datasetRepository.save(dataset);
|
||||
}
|
||||
|
||||
public Page<Dataset> 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);
|
||||
|
||||
if (category == null) {
|
||||
return datasetRepository.searchByOptionalCriteria(Optional.ofNullable(search), Optional.ofNullable(t),
|
||||
pageable);
|
||||
}
|
||||
return datasetRepository.searchByOptionalCriteriaWithCategory(Optional.ofNullable(search), category,
|
||||
Optional.ofNullable(t), pageable);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.Dataset;
|
||||
|
||||
public enum Type {
|
||||
DATASET,
|
||||
API
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.model;
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.Dataset;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@ -11,6 +11,8 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import de.uni_passau.fim.PADAS.group3.DataDash.category.Category;
|
||||
|
||||
public interface dataRepository extends JpaRepository<Dataset, UUID> {
|
||||
|
||||
Dataset getDatasetById(UUID id);
|
||||
@ -65,10 +67,19 @@ public interface dataRepository extends JpaRepository<Dataset, UUID> {
|
||||
"((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" +
|
||||
"(d.categorie = :categorie) AND" +
|
||||
"(:type IS NULL OR d.type = :type)")
|
||||
Page<Dataset> searchByOptionalCriteria(@Param("search") Optional<String> search,
|
||||
@Param("categorie") Optional<Category> categories,
|
||||
Page<Dataset> searchByOptionalCriteriaWithCategory(@Param("search") Optional<String> search,
|
||||
@Param("categorie") Category categories,
|
||||
@Param("type") Optional<Type> type,
|
||||
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" +
|
||||
"(:type IS NULL OR d.type = :type)")
|
||||
Page<Dataset> searchByOptionalCriteria(@Param("search") Optional<String> search,
|
||||
@Param("type") Optional<Type> type,
|
||||
Pageable pageable);
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.category;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import de.uni_passau.fim.PADAS.group3.DataDash.Dataset.Dataset;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.OneToMany;
|
||||
|
||||
@Entity
|
||||
public class Category {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private UUID id;
|
||||
|
||||
private String name;
|
||||
|
||||
@Lazy
|
||||
@OneToMany(mappedBy = "categorie")
|
||||
private List<Dataset> datasets;
|
||||
|
||||
public Category(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Category() {
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.category;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/categories")
|
||||
public class CategoryController {
|
||||
@Autowired
|
||||
private CategoryService categoryService;
|
||||
|
||||
@GetMapping
|
||||
public List<CategoryDto> getMethodName() {
|
||||
return categoryService.getAllCategories() ;
|
||||
}
|
||||
|
||||
@GetMapping("/id/{id}")
|
||||
public ResponseEntity<?> fetchCategoryById(@PathVariable("id") UUID id) {
|
||||
CategoryDto category = categoryService.getCategoryById(id);
|
||||
if(category == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ResponseEntity<>(category, HttpStatus.OK);
|
||||
}
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@PostMapping
|
||||
public void createCategory(@RequestBody CategoryDto dto) {
|
||||
categoryService.addCategory(dto);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.category;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class CategoryDto {
|
||||
|
||||
private String name;
|
||||
private UUID id;
|
||||
|
||||
public CategoryDto() {
|
||||
}
|
||||
|
||||
CategoryDto(String name, UUID id) {
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.category;
|
||||
|
||||
public class CategoryDtoMapper {
|
||||
|
||||
public static CategoryDto toDto(Category category) {
|
||||
CategoryDto dto = new CategoryDto(category.getName(), category.getId());
|
||||
return dto;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.category;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
|
||||
public interface CategoryRepository extends JpaRepository<Category, UUID>{
|
||||
|
||||
Category getCategoryById(UUID id);
|
||||
|
||||
@SuppressWarnings("null")
|
||||
List<Category> findAll();
|
||||
List<Category> findByName(String name);
|
||||
@SuppressWarnings("null")
|
||||
Optional<Category> findById(UUID id);
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.category;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class CategoryService {
|
||||
private CategoryRepository categoryRepository;
|
||||
|
||||
public CategoryService(CategoryRepository categoryRepository) {
|
||||
this.categoryRepository = categoryRepository;
|
||||
}
|
||||
|
||||
public void addCategory(CategoryDto category) {
|
||||
Category cat = new Category(category.getName());
|
||||
categoryRepository.save(cat);
|
||||
}
|
||||
|
||||
public List<CategoryDto> getAllCategories() {
|
||||
List<Category> tmp = categoryRepository.findAll();
|
||||
List<CategoryDto> s = tmp.stream().map(CategoryDtoMapper::toDto).toList();
|
||||
return s;
|
||||
}
|
||||
|
||||
public CategoryDto getCategoryById(UUID id) {
|
||||
Category c = categoryRepository.getCategoryById(id);
|
||||
if (c == null) {
|
||||
return null;
|
||||
}
|
||||
return CategoryDtoMapper.toDto(c);
|
||||
}
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.controler;
|
||||
|
||||
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;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.config.EnableSpringDataWebSupport;
|
||||
import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/datasets")
|
||||
public class DatasetController {
|
||||
@Autowired
|
||||
private DatasetService datasetService;
|
||||
|
||||
// @GetMapping
|
||||
// public List<Dataset> getAllDatasets() {
|
||||
// return datasetService.getAllDatasets();
|
||||
// }
|
||||
|
||||
@GetMapping("/id/{id}")
|
||||
public Dataset getDatasetById(@PathVariable("id") UUID id) {
|
||||
return datasetService.getDatasetById(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Dataset createDataset(@RequestBody Dataset dataset) {
|
||||
datasetService.addDataset(dataset);
|
||||
// TODO: figure out what the fuck i need to do here
|
||||
return null;
|
||||
}
|
||||
|
||||
// @PutMapping("/{id}")
|
||||
// public Dataset updateDataset(@PathVariable("id") Long id, @RequestBody
|
||||
// Dataset dataset) {
|
||||
// return datasetService.updateDataset(id, dataset);
|
||||
// }
|
||||
//
|
||||
|
||||
@DeleteMapping("/id/{id}")
|
||||
public void deleteDataset(@PathVariable("id") UUID id) {
|
||||
datasetService.deleteDataset(id);
|
||||
}
|
||||
|
||||
@PutMapping("/id/{id}/upvote")
|
||||
public Dataset upvote(@PathVariable("id") UUID id) {
|
||||
datasetService.upvoteDataset(id);
|
||||
return datasetService.getDatasetById(id);
|
||||
}
|
||||
|
||||
@PutMapping("/id/{id}/downvote")
|
||||
public Dataset downvote(@PathVariable("id") UUID id) {
|
||||
datasetService.downvoteDataset(id);
|
||||
return getDatasetById(id); // new ResponseEntity<>(null, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@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 datasetService.getDatasetById(id);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Page<Dataset> 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-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 = "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, rating, category,pageable);
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public Page<Dataset> search(@RequestParam(value = "search", required = false, defaultValue = "%") String search,
|
||||
@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 = "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, category, type, pageable);
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.model;
|
||||
|
||||
public enum Category {
|
||||
HEALTH,
|
||||
ENVIRONMENT,
|
||||
ECONOMY,
|
||||
POLITICS,
|
||||
TECHNOLOGY,
|
||||
SPORTS,
|
||||
SCIENCE,
|
||||
CULTURE,
|
||||
EDUCATION,
|
||||
OTHER
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
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;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
|
||||
@Service
|
||||
public class DatasetService {
|
||||
private dataRepository datasetRepository;
|
||||
|
||||
public DatasetService(dataRepository datasetRepository) {
|
||||
this.datasetRepository = datasetRepository;
|
||||
}
|
||||
|
||||
public List<Dataset> getAllDatasets() {
|
||||
return datasetRepository.findAll();
|
||||
}
|
||||
|
||||
public Dataset getDatasetById(UUID id) {
|
||||
return datasetRepository.getDatasetById(id);
|
||||
}
|
||||
|
||||
public void addDataset(Dataset dataset) {
|
||||
dataset.setDate(LocalDate.now());
|
||||
datasetRepository.save(dataset);
|
||||
}
|
||||
|
||||
public void updateDatasetTitle(UUID id, String title) {
|
||||
datasetRepository.getDatasetById(id).setTitle(title);
|
||||
}
|
||||
|
||||
public void voteDataset(UUID id, int vote) {
|
||||
Dataset dataset = datasetRepository.getDatasetById(id);
|
||||
dataset.vote(vote);
|
||||
datasetRepository.save(dataset);
|
||||
}
|
||||
|
||||
public void deleteDataset(UUID id) {
|
||||
Dataset dataset = datasetRepository.getDatasetById(id);
|
||||
datasetRepository.delete(dataset);
|
||||
}
|
||||
|
||||
public List<Dataset> getDatasetsByTitle(String title) {
|
||||
return datasetRepository.findByTitle(title);
|
||||
}
|
||||
|
||||
public List<Dataset> getDatasetsByTitleLike(String title) {
|
||||
return datasetRepository.findByTitleLike(title);
|
||||
}
|
||||
|
||||
public List<Dataset> findByDescriptionLike(String description) {
|
||||
return datasetRepository.findByDescriptionLike(description);
|
||||
}
|
||||
|
||||
public List<Dataset> getDatasetsByAuthorLike(String author) {
|
||||
return datasetRepository.findByAuthorLike(author);
|
||||
}
|
||||
|
||||
public List<Dataset> getDatasetsByType(Type type) {
|
||||
return datasetRepository.findByType(type);
|
||||
}
|
||||
|
||||
public List<Dataset> getDatasetsByAbstLike(String abst) {
|
||||
return datasetRepository.findByAbstLike(abst);
|
||||
}
|
||||
|
||||
public List<Dataset> getDatasetsByRaitingGreaterThan(float raiting) {
|
||||
return datasetRepository.findByRaitingGreaterThan(raiting);
|
||||
}
|
||||
|
||||
public void upvoteDataset(UUID id) {
|
||||
Dataset dataset = datasetRepository.getDatasetById(id);
|
||||
dataset.upvote();
|
||||
datasetRepository.save(dataset);
|
||||
}
|
||||
|
||||
public void downvoteDataset(UUID id) {
|
||||
Dataset dataset = datasetRepository.getDatasetById(id);
|
||||
dataset.downvote();
|
||||
datasetRepository.save(dataset);
|
||||
}
|
||||
|
||||
public Page<Dataset> getDatasetsByOptionalCriteria(String title, String description, String author, String abst,
|
||||
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(category),
|
||||
Optional.ofNullable(raiting), pageable);
|
||||
}
|
||||
|
||||
public Page<Dataset> 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);
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
|
||||
@Configuration
|
||||
public class LoadDummyDatabase {
|
||||
|
||||
private static final org.slf4j.Logger log = LoggerFactory.getLogger(LoadDummyDatabase.class);
|
||||
|
||||
@Bean
|
||||
CommandLineRunner initDatabase(dataRepository repository) {
|
||||
|
||||
|
||||
|
||||
return args -> {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
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<Dataset> s = repository.findByTitleLike("%Title%");
|
||||
log.info("Found Entry with ID: " + s.get(1).getId());};
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package de.uni_passau.fim.PADAS.group3.DataDash.model;
|
||||
|
||||
public enum Type {
|
||||
DATASET,
|
||||
API
|
||||
}
|
@ -1 +1,14 @@
|
||||
spring.application.name=DataDash
|
||||
|
||||
# Datasource configuration
|
||||
spring.datasource.url=jdbc:h2:mem:studentcoursedb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
spring.sql.init.mode=always
|
||||
spring.jpa.hibernate.ddl-auto=none
|
||||
|
||||
# Uncomment for web console
|
||||
#spring.h2.console.enabled=true
|
||||
#spring.h2.console.path=/h2-console
|
||||
#spring.datasource.username=sa
|
||||
#spring.datasource.password=pwd
|
30
src/main/resources/data.sql
Normal file
30
src/main/resources/data.sql
Normal file
@ -0,0 +1,30 @@
|
||||
-- Insert sample data into category
|
||||
INSERT INTO category (id, name) VALUES
|
||||
('123e4567-e89b-12d3-a456-426614174003', 'Business'),
|
||||
('123e4567-e89b-12d3-a456-426614174004', 'Education'),
|
||||
('123e4567-e89b-12d3-a456-426614174005', 'Sports'),
|
||||
('123e4567-e89b-12d3-a456-426614174006', 'Entertainment'),
|
||||
('123e4567-e89b-12d3-a456-426614174007', 'Art'),
|
||||
('123e4567-e89b-12d3-a456-426614174000', 'Science'),
|
||||
('123e4567-e89b-12d3-a456-426614174001', 'Technology'),
|
||||
('123e4567-e89b-12d3-a456-426614174002', 'Health');
|
||||
|
||||
-- Insert sample data into dataset
|
||||
INSERT INTO dataset (date, raiting, upvotes, votes, categorie_id, id, abst, author, description, title, url, type, licence, terms_of_use) VALUES
|
||||
('2023-01-01', 4.5, 100, 120, '123e4567-e89b-12d3-a456-426614174000', '123e4567-e89b-12d3-a456-426614174100', 'Abstract 1', 'Author 1', 'Description 1', 'Title 1', 'http://example.com/1', 'API', 'MIT', 'http://url.de'),
|
||||
('2023-01-02', 4.7, 150, 170, '123e4567-e89b-12d3-a456-426614174001', '123e4567-e89b-12d3-a456-426614174101', 'Abstract 2', 'Author 2', 'Description 2', 'Title 2', 'http://example.com/2', 'DATASET', 'MIT', 'http://url.de'),
|
||||
('2023-01-03', 4.9, 200, 220, '123e4567-e89b-12d3-a456-426614174002', '123e4567-e89b-12d3-a456-426614174102', 'Abstract 3', 'Author 3', 'Description 3', 'Title 3', 'http://example.com/3', 'API', 'MIT', 'http://url.de'),
|
||||
('2023-01-04', 4.2, 80, 100, '123e4567-e89b-12d3-a456-426614174003', '123e4567-e89b-12d3-a456-426614174103', 'Abstract 4', 'Author 4', 'Description 4', 'Title 4', 'http://example.com/4', 'DATASET', 'MIT', 'http://url.de'),
|
||||
('2023-01-05', 4.6, 120, 140, '123e4567-e89b-12d3-a456-426614174004', '123e4567-e89b-12d3-a456-426614174104', 'Abstract 5', 'Author 5', 'Description 5', 'Title 5', 'http://example.com/5', 'API', 'MIT', 'http://url.de');
|
||||
-- Insert 10 more sample data into dataset
|
||||
INSERT INTO dataset (date, raiting, upvotes, votes, categorie_id, id, abst, author, description, title, url, type, licence, terms_of_use) VALUES
|
||||
('2023-01-06', 4.8, 180, 200, '123e4567-e89b-12d3-a456-426614174005', '123e4567-e89b-12d3-a456-426614174105', 'Abstract 6', 'Author 6', 'Description 6', 'Title 6', 'http://example.com/6', 'API', 'MIT', 'http://zip.com'),
|
||||
('2023-01-07', 4.3, 90, 110, '123e4567-e89b-12d3-a456-426614174006', '123e4567-e89b-12d3-a456-426614174106', 'Abstract 7', 'Author 7', 'Description 7', 'Title 7', 'http://example.com/7', 'DATASET', 'MIT', 'http://zip.com'),
|
||||
('2023-01-08', 4.7, 150, 170, '123e4567-e89b-12d3-a456-426614174007', '123e4567-e89b-12d3-a456-426614174107', 'Abstract 8', 'Author 8', 'Description 8', 'Title 8', 'http://example.com/8', 'API', 'MIT', 'http://zip.com'),
|
||||
('2023-01-09', 4.9, 200, 220, '123e4567-e89b-12d3-a456-426614174000', '123e4567-e89b-12d3-a456-426614174108', 'Abstract 9', 'Author 9', 'Description 9', 'Title 9', 'http://example.com/9', 'DATASET', 'MIT', 'http://zip.com'),
|
||||
('2023-01-10', 4.2, 80, 100, '123e4567-e89b-12d3-a456-426614174001', '123e4567-e89b-12d3-a456-426614174109', 'Abstract 10', 'Author 10', 'Description 10', 'Title 10', 'http://example.com/10', 'API', 'MIT', 'http://zip.com'),
|
||||
('2023-11-11', 4.6, 120, 140, '123e4567-e89b-12d3-a456-426614174002', '123e4567-e89b-12d3-a456-426614174110', 'Abstract 11', 'Author 11', 'Description 11', 'Title 11', 'http://example.com/11', 'DATASET', 'MIT', 'http://zip.com'),
|
||||
('2023-09-12', 4.8, 180, 200, '123e4567-e89b-12d3-a456-426614174003', '123e4567-e89b-12d3-a456-426614174111', 'Abstract 12', 'Author 12', 'Description 12', 'Title 12', 'http://example.com/12', 'API', 'MIT', 'http://zip.com'),
|
||||
('2023-03-13', 4.3, 90, 110, '123e4567-e89b-12d3-a456-426614174004', '123e4567-e89b-12d3-a456-426614174112', 'Abstract 13', 'Author 13', 'Description 13', 'Title 13', 'http://example.com/13', 'DATASET', 'MIT', 'http://zip.com'),
|
||||
('2021-01-14', 4.7, 150, 170, '123e4567-e89b-12d3-a456-426614174005', '123e4567-e89b-12d3-a456-426614174113', 'Abstract 14', 'Author 14', 'Description 14', 'Title 14', 'http://example.com/14', 'API', 'MIT', 'http://zip.com'),
|
||||
('2024-01-15', 4.9, 200, 220, '123e4567-e89b-12d3-a456-426614174006', '123e4567-e89b-12d3-a456-426614174114', 'Abstract 15', 'Author 15', 'Description 15', 'Title 15', 'http://example.com/15', 'DATASET', 'MIT', 'http://zip.com');
|
7
src/main/resources/schema.sql
Normal file
7
src/main/resources/schema.sql
Normal file
@ -0,0 +1,7 @@
|
||||
DROP TABLE IF EXISTS dataset;
|
||||
DROP TABLE IF EXISTS category;
|
||||
|
||||
|
||||
create table category (id uuid not null, name varchar(255), primary key (id));
|
||||
create table dataset (date date, raiting float(24) not null, upvotes integer not null, votes integer not null, categorie_id uuid, id uuid not null, abst varchar(255), author varchar(255), description varchar(200000), title varchar(255), url varchar(2048), terms_of_use varchar(2048), type enum ('API','DATASET'), licence varchar(255), primary key (id));
|
||||
alter table if exists dataset add constraint FKq6qwq6u473f89h71s7rf97ruy foreign key (categorie_id) references category;
|
@ -1,9 +1,5 @@
|
||||
@import url("main.css");
|
||||
|
||||
:root {
|
||||
--accent-color: oklch(65.33% 0.158 247.76);
|
||||
}
|
||||
|
||||
form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import Dataset from "./dataset.js";
|
||||
|
||||
const form = document.forms[0];
|
||||
const {
|
||||
title: titleEntry,
|
||||
@ -23,7 +25,11 @@ const validationListener = () => {
|
||||
fullDescriptionEntry,
|
||||
].forEach(input => input.addEventListener("input", validationListener));
|
||||
|
||||
form.addEventListener("submit", e => {
|
||||
cancelBtn.addEventListener("click", () => {
|
||||
window.location.href = location.origin;
|
||||
})
|
||||
|
||||
form.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
if (!form.reportValidity()) return;
|
||||
|
||||
@ -43,17 +49,20 @@ form.addEventListener("submit", e => {
|
||||
// Don't allow several requests to be sent at the same time
|
||||
addBtn.disabled = true;
|
||||
|
||||
fetch("/api/v1/datasets", {
|
||||
let response = await 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();
|
||||
}
|
||||
|
||||
});
|
||||
let data = await response.json();
|
||||
let dataset = new Dataset(data);
|
||||
dataset.storageSetKey("created-locally", true);
|
||||
if (response.ok) {
|
||||
location.assign("/");
|
||||
} else {
|
||||
addBtn.disabled = !form.checkValidity();
|
||||
}
|
||||
});
|
||||
|
2
src/main/resources/static/constants.js
Normal file
2
src/main/resources/static/constants.js
Normal file
@ -0,0 +1,2 @@
|
||||
export const DATASET_ENDPOINT = "/api/v1/datasets";
|
||||
export const getBaseURL = () => location.origin;
|
@ -1,25 +1,38 @@
|
||||
import {searchBarTimeout, searchSection} from "./main.js"
|
||||
// TODO consider renaming this to "searchUtility.js"
|
||||
|
||||
import { searchBarTimeout, searchSection, lastQuery } from "./main.js"
|
||||
import Dataset from "./dataset.js"
|
||||
|
||||
export function fetchQuery(fetchString) {
|
||||
export async function fetchQuery(fetchString, clearResults) {
|
||||
clearTimeout(searchBarTimeout);
|
||||
fetch(fetchString)
|
||||
.then(resp => resp.json())
|
||||
.then((data) => {
|
||||
parseContent(data.content);
|
||||
});
|
||||
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) {
|
||||
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");
|
||||
searchSection.querySelector(".datasets").classList.add("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());
|
||||
nothingFoundElement.classList.add("hidden");
|
||||
|
||||
const datasets = content.map(dataset => Dataset.get(dataset.id) ?? new Dataset(dataset));
|
||||
searchSection.querySelector(".datasets").classList.remove("hidden");
|
||||
if (clearResults) {
|
||||
Array.from(searchSection.querySelectorAll(".datasets .dataset")).forEach(e => e.remove());
|
||||
}
|
||||
|
||||
for (const dataset of datasets) {
|
||||
searchSection.querySelector(".datasets").appendChild(dataset.createDatasetHTMLElement());
|
||||
searchSection.querySelector(".datasets")
|
||||
.appendChild(dataset.createDatasetHTMLElement());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { vote } from "./main.js";
|
||||
import { DATASET_ENDPOINT, getBaseURL } from "./constants.js";
|
||||
|
||||
export default class Dataset {
|
||||
#abstract;
|
||||
static #datasets = new Map();
|
||||
|
||||
#shortDescription;
|
||||
#author;
|
||||
#categories;
|
||||
#category;
|
||||
#date;
|
||||
#description;
|
||||
#fullDescription;
|
||||
#id;
|
||||
#rating;
|
||||
#title;
|
||||
@ -13,39 +15,222 @@ export default class Dataset {
|
||||
#upvotes;
|
||||
#url;
|
||||
#votes;
|
||||
#license;
|
||||
#termsOfUse;
|
||||
#elements = [];
|
||||
|
||||
constructor({abst: shortDescription, author, categories, date, description, id, rating, title, type, upvotes, url, votes}) {
|
||||
this.#abstract = shortDescription;
|
||||
static get(id) {
|
||||
return this.#datasets.get(id);
|
||||
}
|
||||
|
||||
constructor({ abst: shortDescription, author, categorie, date, description, id, raiting, title, type, upvotes, url, votes, license, termsOfUse }) {
|
||||
this.#shortDescription = shortDescription;
|
||||
this.#author = author;
|
||||
this.#categories = categories;
|
||||
this.#category = categorie;
|
||||
this.#date = date;
|
||||
this.#description = description;
|
||||
this.#fullDescription = description;
|
||||
this.#id = id;
|
||||
this.#rating = rating;
|
||||
this.#rating = raiting;
|
||||
this.#title = title;
|
||||
this.#type = type;
|
||||
this.#upvotes = upvotes;
|
||||
this.#url = url;
|
||||
this.#votes = votes;
|
||||
this.#license = license;
|
||||
this.#termsOfUse = termsOfUse;
|
||||
|
||||
Dataset.#datasets.set(id, this);
|
||||
}
|
||||
|
||||
// Getters
|
||||
get shortDescription() {
|
||||
return this.#shortDescription;
|
||||
}
|
||||
|
||||
get author() {
|
||||
return this.#author;
|
||||
}
|
||||
|
||||
get category() {
|
||||
return this.#category;
|
||||
}
|
||||
|
||||
get date() {
|
||||
return this.#date;
|
||||
}
|
||||
|
||||
get fullDescription() {
|
||||
return this.#fullDescription;
|
||||
}
|
||||
|
||||
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 license() {
|
||||
return this.#license;
|
||||
}
|
||||
|
||||
get termsOfUse() {
|
||||
return this.#termsOfUse;
|
||||
}
|
||||
|
||||
get mainElement() {
|
||||
return this.#elements[0];
|
||||
}
|
||||
|
||||
get elements() {
|
||||
return this.#elements;
|
||||
}
|
||||
|
||||
parseDate() {
|
||||
return new Date(this.#date);
|
||||
}
|
||||
|
||||
// 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;
|
||||
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(".downvote-btn").addEventListener("click", () => {
|
||||
vote(this.#id, false);
|
||||
let datasetContainer = clone.querySelector(".dataset");
|
||||
datasetContainer.dataset.id = this.#id;
|
||||
datasetContainer.addEventListener("click", event => {
|
||||
if (!event.target.classList.contains("btn")) {
|
||||
let detailsPage = new URL("/details.html", location.origin);
|
||||
detailsPage.searchParams.append("id", this.#id);
|
||||
window.location.href = detailsPage.toString();
|
||||
}
|
||||
})
|
||||
clone.querySelector(".dataset-title").innerText = this.#title;
|
||||
clone.querySelector(".dataset-description").innerText = this.#shortDescription;
|
||||
clone.querySelector(".upvote-count").innerText = this.#upvotes;
|
||||
|
||||
this.#elements.push(clone.children[0]);
|
||||
this.#createUpvoteButtons(clone);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
213
src/main/resources/static/details.css
Normal file
213
src/main/resources/static/details.css
Normal file
@ -0,0 +1,213 @@
|
||||
@import url("./main.css");
|
||||
@import url('https://fonts.googleapis.com/css2?family=Flow+Circular&display=swap');
|
||||
|
||||
:root {
|
||||
--min-card-size: min(60ch, calc(100vw - var(--pad-main)));
|
||||
--rating-color: gold;
|
||||
}
|
||||
|
||||
main {
|
||||
& > :is(header, section, footer) {
|
||||
background-color: var(--fg-color);
|
||||
padding: var(--pad-datasets-block) var(--pad-datasets-inline);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& > :first-child {
|
||||
border-top-left-radius: var(--corner-radius);
|
||||
border-top-right-radius: var(--corner-radius);
|
||||
margin-top: var(--pad-main);
|
||||
}
|
||||
|
||||
& > :last-child {
|
||||
border-bottom-left-radius: var(--corner-radius);
|
||||
border-bottom-right-radius: var(--corner-radius);
|
||||
margin-bottom: var(--pad-main);
|
||||
}
|
||||
|
||||
& > :not(:last-child):is(header, section)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: calc(var(--pad-datasets-inline) - var(--gap-small));
|
||||
bottom: 0;
|
||||
border-bottom: 3px solid var(--bg-color);
|
||||
opacity: .25;
|
||||
transform: translateY(50%);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
margin-inline: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr max-content;
|
||||
align-items: center;
|
||||
grid-gap: var(--gap-medium);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-block: var(--gap-medium) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#title {
|
||||
grid-column: 1 / 3;
|
||||
|
||||
&::after {
|
||||
content: attr(data-type);
|
||||
background-color: var(--accent-color);
|
||||
font-size: .5em;
|
||||
font-weight: initial;
|
||||
position: relative;
|
||||
bottom: .25lh;
|
||||
margin-inline: var(--gap-small);
|
||||
padding: 2pt 4pt;
|
||||
border-radius: 4pt;
|
||||
}
|
||||
|
||||
&[data-type="dataset"]::after {
|
||||
content: "Dataset";
|
||||
}
|
||||
|
||||
&[data-type="api"]::after {
|
||||
content: "API";
|
||||
}
|
||||
}
|
||||
|
||||
#details summary, #url {
|
||||
text-align: start;
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
#rating {
|
||||
color: color-mix(in oklab, var(--text-color) 80%, black);
|
||||
color: transparent;
|
||||
width: 5lh;
|
||||
height: 1lh;
|
||||
margin-inline: .5ch;
|
||||
mask-image: url("stars.svg");
|
||||
-webkit-mask-image: url("stars.svg");
|
||||
mask-size: contain;
|
||||
mask-mode: alpha;
|
||||
--rating-percent: calc((var(--rating, 0) / 5) * 100%);
|
||||
background: var(--bg-color);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
#rating::-webkit-meter-bar {
|
||||
background: var(--bg-color);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
grid-row: 1 / -1;
|
||||
}
|
||||
|
||||
#rating::-webkit-meter-optimum-value,
|
||||
#rating::-webkit-meter-suboptimum-value,
|
||||
#rating::-webkit-meter-even-less-good-value {
|
||||
background-color: var(--rating-color);
|
||||
}
|
||||
|
||||
#rating::-moz-meter-bar {
|
||||
background: var(--rating-color);
|
||||
}
|
||||
|
||||
a {
|
||||
--text-color: var(--accent-color);
|
||||
|
||||
/*
|
||||
Why doesn't it do this automatically? It is inherited from body,
|
||||
so I should be able to just change --text-color, right?
|
||||
*/
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.upvote {
|
||||
margin: var(--gap-medium) 0;
|
||||
align-self: self-start;
|
||||
grid-column: 3;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
#metadata {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-large);
|
||||
}
|
||||
|
||||
#full-description {
|
||||
text-wrap: balance;
|
||||
text-wrap: pretty;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:is(#full-description, #not-found) br {
|
||||
margin-bottom: .5lh;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
font-family: "Flow Circular";
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
user-select: none;
|
||||
--rating-color: var(--text-color);
|
||||
|
||||
a, .btn {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
& > * {
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
@media screen and not (prefers-reduced-motion) {
|
||||
:is(header, section) > :is(p, span, h1, a) {
|
||||
animation: infinite 2s linear skeleton-loading;
|
||||
background: radial-gradient(
|
||||
circle farthest-side,
|
||||
var(--highlight-color, white) 20%,
|
||||
var(--text-color) 40%
|
||||
) no-repeat, var(--text-color);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
|
||||
&:is(a) {
|
||||
--highlight-color: color-mix(in oklab, white, var(--text-color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#title::after {
|
||||
color: color-mix(in oklab, var(--accent-color) 50%, currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
from {
|
||||
background-position-x: calc(-1.4 * var(--min-card-size)), 0;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position-x: calc(1.4 * var(--min-card-size)), 0;
|
||||
}
|
||||
}
|
||||
|
||||
#not-found:not(.hidden) {
|
||||
min-height: calc(100vh - 2 * var(--pad-main));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-large);
|
||||
justify-content: space-evenly;
|
||||
container: nothing-found / inline-size;
|
||||
|
||||
p, h1 {
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
}
|
||||
|
||||
#nothing-found-bg {
|
||||
background-position-x: calc(50% + 3cqh);
|
||||
}
|
76
src/main/resources/static/details.html
Normal file
76
src/main/resources/static/details.html
Normal file
@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dataset details</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Flow+Circular&display=swap" crossorigin>
|
||||
<link rel="stylesheet" href="details.css">
|
||||
<script defer type="module" src="details.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<template id="voting-template">
|
||||
<aside class="upvote">
|
||||
<button class="upvote-btn btn flat">Upvote</button>
|
||||
<span class="upvote-count">0</span>
|
||||
<button class="downvote-btn btn flat">Downvote</button>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<main id="details" class="skeleton">
|
||||
<header data-id="dataset-id">
|
||||
<h1 id="title" data-type="api">Title</h1>
|
||||
<summary>
|
||||
<span id="rating-text">4</span><meter id="rating" value="4" max="5"></meter>
|
||||
<span id="short-description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Perspiciatis recusandae laborum odio corrupti voluptas quisquam dicta, quibusdam ipsum qui exercitationem.</span>
|
||||
</summary>
|
||||
<a id="url">https://example.com/dataset</a>
|
||||
<aside class="upvote">
|
||||
<button disabled class="upvote-btn btn flat">Upvote</button>
|
||||
<span class="upvote-count">0</span>
|
||||
<button disabled class="downvote-btn btn flat">Downvote</button>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<section id="metadata">
|
||||
<span>Added on: <time id="date" datetime="0">1. January 1970</time></span>
|
||||
<span>Category: <span id="category">Something</span></span>
|
||||
<span>License: <span id="license">MIT</span></span>
|
||||
<a id="terms-of-use">Terms of Use</a>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p id="full-description">Full description<br>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Beatae
|
||||
nihil saepe et numquam quo id voluptatum recusandae assumenda
|
||||
doloremque pariatur consequatur molestias delectus dolore
|
||||
corrupti, odio modi vitae repellat tempora sed eos voluptates
|
||||
temporibus veritatis repudiandae. Cum eveniet molestias, in
|
||||
beatae non reiciendis quia voluptatem id, facere architecto
|
||||
vitae harum ipsum earum deleniti dolor atque recusandae odit
|
||||
corporis error dolorum blanditiis vel maxime pariatur quibusdam!
|
||||
<br>Saepe debitis ab possimus, dolorem neque ad voluptatibus ex
|
||||
quisquam itaque. Nihil et non consequuntur error ipsa.
|
||||
Necessitatibus voluptatibus aliquid itaque id ipsum, pariatur
|
||||
odio explicabo, dolores ex, incidunt tenetur dolore. Assumenda
|
||||
ipsam nobis quis.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<main id="not-found" class="hidden">
|
||||
<h1>This is not the page you're looking for.</h1>
|
||||
<div id="nothing-found-bg"></div>
|
||||
<p>
|
||||
The dataset or API you were sent to is not in our database.
|
||||
Either it has been deleted by its author or we didn't know of it
|
||||
to begin with.
|
||||
<br>
|
||||
You can try browsing for other datasets and APIs on our
|
||||
<a href="/">homepage</a>.
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
59
src/main/resources/static/details.js
Normal file
59
src/main/resources/static/details.js
Normal file
@ -0,0 +1,59 @@
|
||||
import Dataset from "./dataset.js";
|
||||
|
||||
const mainPage = document.getElementById("details");
|
||||
const notFoundPage = document.getElementById("not-found");
|
||||
|
||||
const title = document.getElementById("title");
|
||||
const rating = document.getElementById("rating");
|
||||
const ratingText = document.getElementById("rating-text");
|
||||
const shortDescription = document.getElementById("short-description");
|
||||
const url = document.getElementById("url");
|
||||
const date = document.getElementById("date");
|
||||
const category = document.getElementById("category");
|
||||
const license = document.getElementById("license");
|
||||
const termsOfUse = document.getElementById("terms-of-use");
|
||||
const fullDescription = document.getElementById("full-description");
|
||||
|
||||
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);
|
||||
const upvoteComponent = dataset.createUpvoteComponent();
|
||||
|
||||
title.innerText = dataset.title;
|
||||
title.dataset.type = dataset.type.toLowerCase();
|
||||
rating.value = dataset.rating;
|
||||
ratingText.innerText = dataset.rating;
|
||||
shortDescription.innerText = dataset.shortDescription;
|
||||
url.href = dataset.url;
|
||||
url.innerText = dataset.url;
|
||||
mainPage.querySelector(".upvote").replaceWith(upvoteComponent);
|
||||
|
||||
date.datetime = dataset.date;
|
||||
date.innerText = dataset.parseDate().toLocaleDateString(undefined, {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
category.innerText = dataset.category.name;
|
||||
category.dataset.id = dataset.category.id;
|
||||
license.innerText = dataset.license;
|
||||
termsOfUse.href = dataset.termsOfUse;
|
||||
|
||||
fullDescription.innerText = dataset.fullDescription;
|
||||
|
||||
mainPage.classList.remove("skeleton");
|
||||
} else {
|
||||
mainPage.classList.add("hidden");
|
||||
notFoundPage.classList.remove("hidden");
|
||||
}
|
||||
} else {
|
||||
mainPage.classList.add("hidden");
|
||||
notFoundPage.classList.remove("hidden");
|
||||
}
|
@ -4,8 +4,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DataDash</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="prefetch" href="https://fonts.googleapis.com/css2?family=Flow+Circular&display=swap">
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<script type="module" src="main.js" defer></script>
|
||||
<link rel="prefetch" href="details.html">
|
||||
<link rel="prefetch" href="details.css">
|
||||
<script type="module" src="main.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="add-btn" title="Add a new API entry"></div>
|
||||
@ -19,8 +24,8 @@
|
||||
<li class="dataset" data-id="id">
|
||||
<div class="dataset-info">
|
||||
<div class="details">
|
||||
<h3>title</h3>
|
||||
<p>Simply daily accountability phone call, powered by AI</p>
|
||||
<h3 class="dataset-title">title</h3>
|
||||
<p class="dataset-description">Simply daily accountability phone call, powered by AI</p>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="upvote">
|
||||
@ -78,6 +83,7 @@
|
||||
</div>
|
||||
<ul class="datasets">
|
||||
</ul>
|
||||
<div id="observable" style="height: 2rem"></div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
@ -2,7 +2,12 @@
|
||||
--bg-color: #222;
|
||||
--fg-color: #555;
|
||||
--text-color: #dbdbdb;
|
||||
--pad-datasets: 1rem;
|
||||
--accent-color: oklch(65.33% 0.158 247.76);
|
||||
--pad-datasets-block: 1rem;
|
||||
--pad-datasets-inline: 2rem;
|
||||
--gap-large: 1.5rem;
|
||||
--gap-medium: 1rem;
|
||||
--gap-small: .5rem;
|
||||
--pad-main: 2rem;
|
||||
--min-card-size: min(60ch, 33vw);
|
||||
--corner-radius: 1rem;
|
||||
@ -17,7 +22,7 @@ body {
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: calc(2 * var(--min-card-size) + var(--pad-main) + var(--pad-datasets));
|
||||
max-width: calc(2 * var(--min-card-size) + var(--pad-main) + 2 * var(--pad-datasets-inline));
|
||||
padding-inline: var(--pad-main);
|
||||
margin-inline: auto;
|
||||
container-type: inline-size;
|
||||
@ -47,12 +52,12 @@ header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
float: right;
|
||||
gap: .5rem;
|
||||
gap: var(--gap-small);
|
||||
background-color: var(--fg-color, darkgrey);
|
||||
padding: .5rem 1rem;
|
||||
margin-bottom: var(--pad-datasets);
|
||||
margin-left: var(--pad-datasets);
|
||||
margin-right: var(--pad-datasets);
|
||||
margin-bottom: var(--pad-datasets-block);
|
||||
margin-left: var(--pad-datasets-inline);
|
||||
margin-right: var(--pad-datasets-inline);
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
@ -78,20 +83,22 @@ header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
#search-entry:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.datasets {
|
||||
padding-inline: var(--pad-datasets);
|
||||
padding-inline: var(--pad-datasets-inline);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(var(--min-card-size), 1fr));
|
||||
gap: 1rem;
|
||||
gap: var(--gap-medium);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@container (width < 60ch) {
|
||||
.datasets {
|
||||
grid-template-columns: 1fr;
|
||||
@ -99,7 +106,7 @@ header {
|
||||
}
|
||||
|
||||
.dataset {
|
||||
padding: var(--pad-datasets) 2rem;
|
||||
padding: var(--pad-datasets-block) var(--pad-datasets-inline);
|
||||
background-color: var(--fg-color, darkgrey);
|
||||
border-radius: var(--corner-radius);
|
||||
list-style: none;
|
||||
@ -107,6 +114,11 @@ header {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dataset:hover {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
.upvote {
|
||||
@ -115,8 +127,9 @@ header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: .5em;
|
||||
gap: var(--gap-small);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.upvote-btn, .downvote-btn, #search-btn, #filter-btn, #sort-btn, #reset-tools-btn {
|
||||
background: var(--icon-url) no-repeat;
|
||||
@ -143,6 +156,7 @@ header {
|
||||
--icon-url: url(looking-glass.svg);
|
||||
--icon-size: 1rem;
|
||||
}
|
||||
|
||||
#filter-btn {
|
||||
--icon-url: url(filter.svg);
|
||||
--icon-size: 1rem;
|
||||
@ -158,6 +172,10 @@ header {
|
||||
--icon-size: 1rem;
|
||||
}
|
||||
|
||||
.isVoted {
|
||||
filter: brightness(1.75);
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: .05rem;
|
||||
height: 1rem;
|
||||
|
@ -1,14 +1,13 @@
|
||||
import {fetchQuery} from "./contentUtility.js";
|
||||
import { DATASET_ENDPOINT, getBaseURL } from "./constants.js";
|
||||
import { fetchQuery } from "./contentUtility.js";
|
||||
import Dataset from "./dataset.js";
|
||||
|
||||
const apiEndpoint = "/api/v1/datasets";
|
||||
const baseURL = location.origin;
|
||||
const defaultPagingValue = 20;
|
||||
const lastQuery = {
|
||||
url: "",
|
||||
export const lastQuery = {
|
||||
totalPages: 0,
|
||||
currentPage: 0
|
||||
currentPage: 0,
|
||||
};
|
||||
const loadedCategories = new Set;
|
||||
|
||||
// definition of all buttons & sections
|
||||
const addButton = document.getElementById("add-btn");
|
||||
@ -33,33 +32,34 @@ addButton.addEventListener("click", () => {
|
||||
});
|
||||
|
||||
filterButton.addEventListener("change", () => {
|
||||
const filterString = filterButton.value;
|
||||
if (filterString !== filterButton.querySelector("#default-filter").value) {
|
||||
fetchQuery(createQuery());
|
||||
}
|
||||
fetchQuery(createQuery(), true);
|
||||
});
|
||||
|
||||
filterButton.addEventListener("click", () => {
|
||||
fetchCategories();
|
||||
})
|
||||
|
||||
searchButton.addEventListener("click", () => {
|
||||
fetchQuery(createQuery());
|
||||
fetchQuery(createQuery(), true);
|
||||
|
||||
});
|
||||
|
||||
searchBar.addEventListener("input", () => {
|
||||
updateSections();
|
||||
clearTimeout(searchBarTimeout);
|
||||
searchBarTimeout = setTimeout(() => {
|
||||
fetchQuery(createQuery());
|
||||
fetchQuery(createQuery(), true);
|
||||
updateSections();
|
||||
}, searchDelay);
|
||||
});
|
||||
|
||||
searchBar.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
fetchQuery(createQuery());
|
||||
fetchQuery(createQuery(), true);
|
||||
}
|
||||
});
|
||||
|
||||
sortButton.addEventListener("change", () => {
|
||||
fetchQuery(createQuery());
|
||||
fetchQuery(createQuery(), true);
|
||||
});
|
||||
|
||||
resetButton.addEventListener("click", () => {
|
||||
@ -69,37 +69,19 @@ resetButton.addEventListener("click", () => {
|
||||
updateSections();
|
||||
});
|
||||
|
||||
// Consider moving this to datasets.js completely
|
||||
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
|
||||
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";
|
||||
window.location.href = "/add.html"; //TODO: move to EventListener?
|
||||
}
|
||||
|
||||
function getFilterQuery() {
|
||||
let filterString= filterButton.value.toUpperCase();
|
||||
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 {
|
||||
return ["category", filterString];
|
||||
return ["category", filterButton.options[filterButton.selectedIndex].value]
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,39 +103,20 @@ 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 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
|
||||
@ -172,44 +135,47 @@ function updateSections() {
|
||||
|
||||
// fetches the further categories used in the filter function
|
||||
function fetchCategories() {
|
||||
const fetchURL = new URL(
|
||||
"api/v1/categories" , baseURL);
|
||||
const fetchURL = new URL("api/v1/categories", getBaseURL());
|
||||
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));
|
||||
let categoryName = data[i].name.toLowerCase();
|
||||
categoryName = categoryName.charAt(0).toUpperCase() + categoryName.slice(1);
|
||||
if (!loadedCategories.has(categoryName)) {
|
||||
let newCategory = new Option(categoryName, data[i].id);
|
||||
document.getElementById("other-categories").appendChild(newCategory);
|
||||
loadedCategories.add(categoryName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
document.querySelector("#top .datasets")
|
||||
.appendChild(new Dataset(data.content[0]).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 () {
|
||||
@ -217,6 +183,21 @@ window.onload = function () {
|
||||
fetchInitialEntries();
|
||||
updateSections();
|
||||
if (searchBar.value !== "") {
|
||||
fetchQuery(createQuery());
|
||||
fetchQuery(createQuery(), true);
|
||||
}
|
||||
let observer = new IntersectionObserver(() => {
|
||||
if (!searchSection.classList.contains("hidden")) {
|
||||
fetchPagingResults();
|
||||
}
|
||||
}, {root: null, rootMargin: "0px", threshold: .9});
|
||||
observer.observe(document.getElementById("observable"));
|
||||
}
|
||||
|
||||
function fetchPagingResults() {
|
||||
lastQuery.currentPage++
|
||||
if (lastQuery.currentPage < lastQuery.totalPages) {
|
||||
let pagingQuery = new URL(createQuery());
|
||||
pagingQuery.searchParams.append("page", lastQuery.currentPage.toString(10));
|
||||
fetchQuery(pagingQuery, false);
|
||||
}
|
||||
}
|
||||
|
127
src/main/resources/static/stars.svg
Normal file
127
src/main/resources/static/stars.svg
Normal file
@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="4.52333in" height="0.853333in"
|
||||
viewBox="0 0 1357 256">
|
||||
<path id="Selection"
|
||||
fill="black" stroke="black" stroke-width="1"
|
||||
d="M 145.00,19.00
|
||||
C 148.26,23.95 151.83,34.12 154.20,40.00
|
||||
154.20,40.00 167.00,71.00 167.00,71.00
|
||||
168.58,74.95 174.23,90.23 176.65,92.40
|
||||
178.81,94.36 189.75,94.96 193.00,95.00
|
||||
193.00,95.00 256.00,98.00 256.00,98.00
|
||||
252.97,103.56 245.88,108.30 241.00,112.42
|
||||
241.00,112.42 212.00,137.08 212.00,137.08
|
||||
209.10,139.50 197.48,148.71 196.46,151.43
|
||||
195.50,153.97 199.97,168.40 200.87,172.00
|
||||
200.87,172.00 216.00,229.00 216.00,229.00
|
||||
216.00,229.00 168.00,199.95 168.00,199.95
|
||||
164.08,197.50 150.78,188.29 147.00,188.29
|
||||
143.53,188.30 130.58,197.29 127.00,199.67
|
||||
127.00,199.67 79.00,230.00 79.00,230.00
|
||||
79.00,230.00 92.79,173.00 92.79,173.00
|
||||
92.79,173.00 96.68,152.17 96.68,152.17
|
||||
96.68,152.17 80.00,137.39 80.00,137.39
|
||||
80.00,137.39 52.00,114.42 52.00,114.42
|
||||
52.00,114.42 36.00,100.00 36.00,100.00
|
||||
36.00,100.00 92.00,96.09 92.00,96.09
|
||||
96.36,95.79 111.98,95.53 114.78,93.40
|
||||
118.14,90.85 122.99,76.57 124.68,72.00
|
||||
124.68,72.00 145.00,19.00 145.00,19.00 Z
|
||||
M 411.00,19.00
|
||||
C 414.26,23.95 417.83,34.12 420.20,40.00
|
||||
420.20,40.00 433.00,71.00 433.00,71.00
|
||||
434.58,74.95 440.23,90.23 442.65,92.40
|
||||
444.81,94.36 455.75,94.96 459.00,95.00
|
||||
459.00,95.00 522.00,98.00 522.00,98.00
|
||||
518.97,103.56 511.88,108.30 507.00,112.42
|
||||
507.00,112.42 478.00,137.08 478.00,137.08
|
||||
475.10,139.50 463.48,148.71 462.46,151.43
|
||||
461.50,153.97 465.97,168.40 466.87,172.00
|
||||
466.87,172.00 482.00,229.00 482.00,229.00
|
||||
482.00,229.00 434.00,199.95 434.00,199.95
|
||||
430.08,197.50 416.78,188.29 413.00,188.29
|
||||
409.53,188.30 396.58,197.29 393.00,199.67
|
||||
393.00,199.67 345.00,230.00 345.00,230.00
|
||||
345.00,230.00 358.79,173.00 358.79,173.00
|
||||
358.79,173.00 362.68,152.17 362.68,152.17
|
||||
362.68,152.17 346.00,137.39 346.00,137.39
|
||||
346.00,137.39 318.00,114.42 318.00,114.42
|
||||
318.00,114.42 302.00,100.00 302.00,100.00
|
||||
302.00,100.00 358.00,96.09 358.00,96.09
|
||||
362.36,95.79 377.98,95.53 380.78,93.40
|
||||
384.14,90.85 388.99,76.57 390.68,72.00
|
||||
390.68,72.00 411.00,19.00 411.00,19.00 Z
|
||||
M 678.00,19.00
|
||||
C 681.26,23.95 684.83,34.12 687.20,40.00
|
||||
687.20,40.00 700.00,71.00 700.00,71.00
|
||||
701.58,74.95 707.23,90.23 709.65,92.40
|
||||
711.81,94.36 722.75,94.96 726.00,95.00
|
||||
726.00,95.00 789.00,98.00 789.00,98.00
|
||||
785.97,103.56 778.88,108.30 774.00,112.42
|
||||
774.00,112.42 745.00,137.08 745.00,137.08
|
||||
742.10,139.50 730.48,148.71 729.46,151.43
|
||||
728.50,153.97 732.97,168.40 733.87,172.00
|
||||
733.87,172.00 749.00,229.00 749.00,229.00
|
||||
749.00,229.00 701.00,199.95 701.00,199.95
|
||||
697.08,197.50 683.78,188.29 680.00,188.29
|
||||
676.53,188.30 663.58,197.29 660.00,199.67
|
||||
660.00,199.67 612.00,230.00 612.00,230.00
|
||||
612.00,230.00 625.79,173.00 625.79,173.00
|
||||
625.79,173.00 629.68,152.17 629.68,152.17
|
||||
629.68,152.17 613.00,137.39 613.00,137.39
|
||||
613.00,137.39 585.00,114.42 585.00,114.42
|
||||
585.00,114.42 569.00,100.00 569.00,100.00
|
||||
569.00,100.00 625.00,96.09 625.00,96.09
|
||||
629.36,95.79 644.98,95.53 647.78,93.40
|
||||
651.14,90.85 655.99,76.57 657.68,72.00
|
||||
657.68,72.00 678.00,19.00 678.00,19.00 Z
|
||||
M 944.00,19.00
|
||||
C 947.26,23.95 950.83,34.12 953.20,40.00
|
||||
953.20,40.00 966.00,71.00 966.00,71.00
|
||||
967.58,74.95 973.23,90.23 975.65,92.40
|
||||
977.81,94.36 988.75,94.96 992.00,95.00
|
||||
992.00,95.00 1055.00,98.00 1055.00,98.00
|
||||
1051.97,103.56 1044.88,108.30 1040.00,112.42
|
||||
1040.00,112.42 1011.00,137.08 1011.00,137.08
|
||||
1008.10,139.50 996.48,148.71 995.46,151.43
|
||||
994.50,153.97 998.97,168.40 999.87,172.00
|
||||
999.87,172.00 1015.00,229.00 1015.00,229.00
|
||||
1015.00,229.00 967.00,199.95 967.00,199.95
|
||||
963.08,197.50 949.78,188.29 946.00,188.29
|
||||
942.53,188.30 929.58,197.29 926.00,199.67
|
||||
926.00,199.67 878.00,230.00 878.00,230.00
|
||||
878.00,230.00 891.79,173.00 891.79,173.00
|
||||
891.79,173.00 895.68,152.17 895.68,152.17
|
||||
895.68,152.17 879.00,137.39 879.00,137.39
|
||||
879.00,137.39 851.00,114.42 851.00,114.42
|
||||
851.00,114.42 835.00,100.00 835.00,100.00
|
||||
835.00,100.00 891.00,96.09 891.00,96.09
|
||||
895.36,95.79 910.98,95.53 913.78,93.40
|
||||
917.14,90.85 921.99,76.57 923.68,72.00
|
||||
923.68,72.00 944.00,19.00 944.00,19.00 Z
|
||||
M 1211.00,19.00
|
||||
C 1214.26,23.95 1217.83,34.12 1220.20,40.00
|
||||
1220.20,40.00 1233.00,71.00 1233.00,71.00
|
||||
1234.58,74.95 1240.23,90.23 1242.65,92.40
|
||||
1244.81,94.36 1255.75,94.96 1259.00,95.00
|
||||
1259.00,95.00 1322.00,98.00 1322.00,98.00
|
||||
1318.97,103.56 1311.88,108.30 1307.00,112.42
|
||||
1307.00,112.42 1278.00,137.08 1278.00,137.08
|
||||
1275.10,139.50 1263.48,148.71 1262.46,151.43
|
||||
1261.50,153.97 1265.97,168.40 1266.87,172.00
|
||||
1266.87,172.00 1282.00,229.00 1282.00,229.00
|
||||
1282.00,229.00 1234.00,199.95 1234.00,199.95
|
||||
1230.08,197.50 1216.78,188.29 1213.00,188.29
|
||||
1209.53,188.30 1196.58,197.29 1193.00,199.67
|
||||
1193.00,199.67 1145.00,230.00 1145.00,230.00
|
||||
1145.00,230.00 1158.79,173.00 1158.79,173.00
|
||||
1158.79,173.00 1162.68,152.17 1162.68,152.17
|
||||
1162.68,152.17 1146.00,137.39 1146.00,137.39
|
||||
1146.00,137.39 1118.00,114.42 1118.00,114.42
|
||||
1118.00,114.42 1102.00,100.00 1102.00,100.00
|
||||
1102.00,100.00 1158.00,96.09 1158.00,96.09
|
||||
1162.36,95.79 1177.98,95.53 1180.78,93.40
|
||||
1184.14,90.85 1188.99,76.57 1190.68,72.00
|
||||
1190.68,72.00 1211.00,19.00 1211.00,19.00 Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 6.5 KiB |
Loading…
Reference in New Issue
Block a user