Merge branch '22-integrate-api-and-frontend' into 46-add-missing-fields-to-the-add-page

This commit is contained in:
Elias Schriefer 2024-07-06 16:23:35 +02:00
commit 9215148eff
35 changed files with 1300 additions and 453 deletions

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,6 @@
package de.uni_passau.fim.PADAS.group3.DataDash.Dataset;
public enum Type {
DATASET,
API
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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";
}
}

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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());};
}
}

View File

@ -1,6 +0,0 @@
package de.uni_passau.fim.PADAS.group3.DataDash.model;
public enum Type {
DATASET,
API
}

View File

@ -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

View 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');

View 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;

View File

@ -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;

View File

@ -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();
}
});

View File

@ -0,0 +1,2 @@
export const DATASET_ENDPOINT = "/api/v1/datasets";
export const getBaseURL = () => location.origin;

View File

@ -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());
}
}
}

View File

@ -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;
}
}

View 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);
}

View 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>

View 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");
}

View File

@ -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>

View File

@ -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;

View File

@ -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);
}
}

View 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