diff --git a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/controller/ProductController.java b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/controller/ProductController.java index 1f0eeb35..cf7076cc 100644 --- a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/controller/ProductController.java +++ b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/controller/ProductController.java @@ -1,10 +1,7 @@ package com.devd.spring.bookstorecatalogservice.controller; import com.devd.spring.bookstorecatalogservice.service.ProductService; -import com.devd.spring.bookstorecatalogservice.web.CreateProductRequest; -import com.devd.spring.bookstorecatalogservice.web.ProductResponse; -import com.devd.spring.bookstorecatalogservice.web.ProductsPagedResponse; -import com.devd.spring.bookstorecatalogservice.web.UpdateProductRequest; +import com.devd.spring.bookstorecatalogservice.web.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.web.PagedResourcesAssembler; @@ -25,6 +22,7 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.validation.Valid; +import java.math.BigDecimal; import java.net.URI; /** @@ -82,9 +80,11 @@ public ResponseEntity updateProduct(@RequestBody @Valid UpdateProductRequest public ResponseEntity getAllProducts(@RequestParam(value = "sort", required = false) String sort, @RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "size", required = false) Integer size, + @RequestParam(value = "searchText", required = false) String searchText, + ProductFiltersRequest filters, PagedResourcesAssembler assembler) { - Page list = productService.getAllProducts(sort, page, size); + Page list = productService.getAllProducts(sort, page, size, searchText, filters); Link link = new Link(ServletUriComponentsBuilder.fromCurrentRequest().build() .toUriString()); diff --git a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/repository/ProductRepository.java b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/repository/ProductRepository.java index ee64474e..c559e280 100644 --- a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/repository/ProductRepository.java +++ b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/repository/ProductRepository.java @@ -2,6 +2,7 @@ import com.devd.spring.bookstorecatalogservice.repository.dao.Product; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; /** @@ -9,5 +10,6 @@ * Date : 2019-06-06 */ @Repository -public interface ProductRepository extends JpaRepository { +public interface ProductRepository extends JpaRepository, + JpaSpecificationExecutor { } diff --git a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/repository/dao/Product.java b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/repository/dao/Product.java index 54485612..3bec9213 100644 --- a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/repository/dao/Product.java +++ b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/repository/dao/Product.java @@ -18,6 +18,7 @@ import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; +import java.math.BigDecimal; /** * @author: Devaraj Reddy, @@ -55,6 +56,12 @@ public class Product extends DateAudit { @Column(name = "AVAILABLE_ITEM_COUNT") private int availableItemCount; + @Column(name = "AVERAGE_RATING") + private BigDecimal averageRating; + + @Column(name = "NO_OF_RATINGS") + private Integer noOfRatings; + public String getProductCategory() { return productCategory.getProductCategoryName(); } diff --git a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/service/ProductService.java b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/service/ProductService.java index ac80a0f9..ac62f8d0 100644 --- a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/service/ProductService.java +++ b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/service/ProductService.java @@ -2,6 +2,7 @@ import com.devd.spring.bookstorecatalogservice.repository.dao.Product; import com.devd.spring.bookstorecatalogservice.web.CreateProductRequest; +import com.devd.spring.bookstorecatalogservice.web.ProductFiltersRequest; import com.devd.spring.bookstorecatalogservice.web.ProductResponse; import com.devd.spring.bookstorecatalogservice.web.UpdateProductRequest; import javax.validation.Valid; @@ -23,5 +24,6 @@ public interface ProductService { Page findAllProducts(Pageable pageable); - Page getAllProducts(String sort, Integer page, Integer size); + Page getAllProducts(String sort, Integer page, Integer size, String searchText, ProductFiltersRequest filters); + } diff --git a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/service/impl/ProductServiceImpl.java b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/service/impl/ProductServiceImpl.java index 8a7a108d..0393a3ec 100644 --- a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/service/impl/ProductServiceImpl.java +++ b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/service/impl/ProductServiceImpl.java @@ -9,6 +9,7 @@ import com.devd.spring.bookstorecatalogservice.service.ProductService; import com.devd.spring.bookstorecatalogservice.service.ReviewService; import com.devd.spring.bookstorecatalogservice.web.CreateProductRequest; +import com.devd.spring.bookstorecatalogservice.web.ProductFiltersRequest; import com.devd.spring.bookstorecatalogservice.web.ProductResponse; import com.devd.spring.bookstorecatalogservice.web.UpdateProductRequest; import com.fasterxml.jackson.databind.ObjectMapper; @@ -17,9 +18,13 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; +import javax.persistence.criteria.Predicate; import javax.validation.Valid; +import java.math.BigDecimal; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -84,7 +89,7 @@ private void populateRatingForProduct(String productId, ProductResponse productR if (reviewsForProduct.size() > 0) { double sum = reviewsForProduct.stream().mapToDouble(Review::getRatingValue).sum(); double rating = sum / reviewsForProduct.size(); - productResponse.setAverageRating(rating); + productResponse.setAverageRating(BigDecimal.valueOf(rating)); } productResponse.setNoOfRatings(Math.toIntExact(reviewRepository.countAllByProductId(productId))); @@ -148,7 +153,7 @@ public Page findAllProducts(Pageable pageable) { } @Override - public Page getAllProducts(String sort, Integer page, Integer size) { + public Page getAllProducts(String sort, Integer page, Integer size, String searchText, ProductFiltersRequest filters) { //set defaults if (size == null || size == 0) { @@ -162,7 +167,7 @@ public Page getAllProducts(String sort, Integer page, Integer s Pageable pageable; - if (sort == null) { + if (sort == null || sort.isEmpty()) { pageable = PageRequest.of(page, size); } else { Sort.Order order; @@ -179,10 +184,61 @@ public Page getAllProducts(String sort, Integer page, Integer s } } - Page allProducts = productRepository.findAll(pageable); - Page allProductsResponse = allProducts.map(Product::fromEntity); - allProductsResponse.forEach(productResponse -> populateRatingForProduct(productResponse.getProductId(), productResponse)); - return allProductsResponse; + Specification specification = Specification.where( + (root, criteriaQuery, criteriaBuilder) -> { + + List predicates = new ArrayList<>(); + + if (searchText != null) { + List predicateList = new ArrayList<>(); + predicateList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("productName")), "%" + searchText.toLowerCase() + "%")); + predicateList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("description")), "%" + searchText.toLowerCase() + "%")); + + Predicate[] array = new Predicate[predicateList.size()]; + predicates.add(criteriaBuilder.or(predicateList.toArray(array))); + } + + if (filters.getMinPrice() != null) { + predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("price"), filters.getMinPrice())); + } + + if (filters.getMaxPrice() != null) { + predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("price"), filters.getMaxPrice())); + } + + if (filters.getMinRating() != null) { + List predicateList = new ArrayList<>(); + predicateList.add(criteriaBuilder.greaterThanOrEqualTo(root.get("averageRating"), filters.getMinRating())); + if (filters.getMinRating().equals(BigDecimal.ZERO)) { + predicateList.add(criteriaBuilder.isNull(root.get("averageRating"))); // Include no rating products + } + + Predicate[] array = new Predicate[predicateList.size()]; + predicates.add(criteriaBuilder.or(predicateList.toArray(array))); + } + + if (filters.getMaxRating() != null) { + List predicateList = new ArrayList<>(); + predicateList.add(criteriaBuilder.lessThanOrEqualTo(root.get("averageRating"), filters.getMaxRating())); + predicateList.add(criteriaBuilder.isNull(root.get("averageRating"))); // Include no rating products + + Predicate[] array = new Predicate[predicateList.size()]; + predicates.add(criteriaBuilder.or(predicateList.toArray(array))); + } + + if (filters.getAvailability() != null && filters.getAvailability().equals(true)) { + predicates.add(criteriaBuilder.greaterThan(root.get("availableItemCount"), 0)); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[]{})); + + } + ); + + Page allProducts = productRepository.findAll(specification, pageable); + + return allProducts.map(Product::fromEntity); + } } diff --git a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/web/ProductFiltersRequest.java b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/web/ProductFiltersRequest.java new file mode 100644 index 00000000..4266d7d4 --- /dev/null +++ b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/web/ProductFiltersRequest.java @@ -0,0 +1,31 @@ +package com.devd.spring.bookstorecatalogservice.web; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * @author: Daniel Chungara, + * Date : 2021-09-16 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ProductFiltersRequest { + + private BigDecimal minPrice; + + private BigDecimal maxPrice; + + private BigDecimal minRating; + + private BigDecimal maxRating; + + private Boolean availability; + +} diff --git a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/web/ProductResponse.java b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/web/ProductResponse.java index ab5ceb5b..f1b07567 100644 --- a/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/web/ProductResponse.java +++ b/bookstore-catalog-service/src/main/java/com/devd/spring/bookstorecatalogservice/web/ProductResponse.java @@ -4,6 +4,8 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.math.BigDecimal; + /** * @author Devaraj Reddy, Date : 08-Nov-2020 */ @@ -18,7 +20,7 @@ public class ProductResponse { private double price; private String productCategory; private int availableItemCount; - private Double averageRating; + private BigDecimal averageRating; private int noOfRatings; private String imageId; diff --git a/bookstore-catalog-service/src/main/resources/application.yml b/bookstore-catalog-service/src/main/resources/application.yml index 5e542a7e..fba9e0d7 100644 --- a/bookstore-catalog-service/src/main/resources/application.yml +++ b/bookstore-catalog-service/src/main/resources/application.yml @@ -39,11 +39,11 @@ logging: spring: profiles: local jpa: - database-platform: org.hibernate.dialect.H2Dialect - database: h2 + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + database: mysql open-in-view: true hibernate: - ddl-auto: none + ddl-auto: validate naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl properties: @@ -52,10 +52,10 @@ spring: use_sql_comments: true format_sql: true datasource: - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:bookstore_db;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;DB_CLOSE_ON_EXIT=FALSE - username: sa - password: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/bookstore_db?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC + username: bookstoreDBA + password: PaSSworD h2: console: enabled: true diff --git a/bookstore-catalog-service/src/main/resources/db/migration/V3__average_rating_product_schema.sql b/bookstore-catalog-service/src/main/resources/db/migration/V3__average_rating_product_schema.sql new file mode 100644 index 00000000..a78b1146 --- /dev/null +++ b/bookstore-catalog-service/src/main/resources/db/migration/V3__average_rating_product_schema.sql @@ -0,0 +1,108 @@ +-- Adding average rating column into product. +alter table PRODUCT + add column average_rating decimal(2,1) default null; + +-- Function that returns the average rating of a product. +DELIMITER $$ +CREATE FUNCTION averageRating(product_id varchar(255)) +RETURNS DECIMAL(2,1) READS SQL DATA DETERMINISTIC +BEGIN + DECLARE average DECIMAL(2,1) DEFAULT NULL; + + SELECT avg (rating_value) + INTO average + FROM PRODUCT p + INNER JOIN REVIEW r + ON p.product_id = r.product_id + WHERE p.product_id = product_id; + + RETURN average; +END +$$ + +-- Procedure that updates the average rating of a product. +DELIMITER $$ +CREATE PROCEDURE updateAverageRatingByProductId(productID varchar(255)) +BEGIN + UPDATE PRODUCT p + SET p.average_rating = averageRating(productID) + WHERE p.product_id = productID ; +END $$ +DELIMITER ; + +-- Procedure that update the average rating of all products +DELIMITER $$ +CREATE PROCEDURE updateAllAverageRating() +BEGIN + DECLARE finished INTEGER DEFAULT 0; + DECLARE productID varchar(255) DEFAULT NULL; + + -- declare cursor + DEClARE curProduct + CURSOR FOR + SELECT product_id FROM PRODUCT; + + -- declare NOT FOUND handler + DECLARE CONTINUE HANDLER + FOR NOT FOUND SET finished = 1; + + OPEN curProduct; + + readLoop: LOOP + FETCH curProduct INTO productID; + IF finished = 1 THEN + LEAVE readLoop; + END IF; + CALL updateAverageRatingByProductId(productID); + END LOOP; + + CLOSE curProduct; + +END +$$ + +-- Call procedure for sets the average rating of all products +DELIMITER $$ +CALL updateAllAverageRating() +$$ + +-- Trigger that updates the average rating after insert a new review. +DELIMITER $$ +CREATE TRIGGER updateAverageRatingAfterInsertReviewTrigger + AFTER INSERT ON REVIEW + FOR EACH ROW + +BEGIN + CALL updateAverageRatingByProductId(NEW.product_id); +END +$$ + +-- Trigger that updates the average rating after update a review. +DELIMITER $$ +CREATE TRIGGER updateAverageRatingAfterUpdateReviewTrigger + AFTER UPDATE ON REVIEW + FOR EACH ROW + +BEGIN + CALL updateAverageRatingByProductId(NEW.product_id); + + -- In case product_id changes then update old + IF OLD.product_id <> NEW.product_id THEN + CALL updateAverageRatingByProductId(OLD.product_id); + END IF; + +END +$$ + +-- Trigger that updates the average rating after delete a review. +DELIMITER $$ +CREATE TRIGGER updateAverageRatingAfterDeleteReviewTrigger + AFTER DELETE ON REVIEW + FOR EACH ROW + +BEGIN + CALL updateAverageRatingByProductId(OLD.product_id); + +END +$$ +DELIMITER ; diff --git a/bookstore-catalog-service/src/main/resources/db/migration/V4__no_of_ratings_product_schema.sql b/bookstore-catalog-service/src/main/resources/db/migration/V4__no_of_ratings_product_schema.sql new file mode 100644 index 00000000..36e93e43 --- /dev/null +++ b/bookstore-catalog-service/src/main/resources/db/migration/V4__no_of_ratings_product_schema.sql @@ -0,0 +1,108 @@ +-- Adding no of rating column into product. +alter table PRODUCT + add column no_of_ratings int default null; + +-- Function that returns the no of ratings of a product. +DELIMITER $$ +CREATE FUNCTION noOfRatingByProductId(product_id varchar(255)) +RETURNS INT READS SQL DATA DETERMINISTIC +BEGIN + DECLARE total INT DEFAULT NULL; + + SELECT count(rating_value) + INTO total + FROM PRODUCT p + INNER JOIN REVIEW r + ON p.product_id = r.product_id + WHERE p.product_id = product_id; + + RETURN total; +END +$$ + +-- Procedure that updates the no of rating of a product. +DELIMITER $$ +CREATE PROCEDURE updateNoOfRatingByProductId(productID varchar(255)) +BEGIN + UPDATE PRODUCT p + SET p.no_of_ratings = noOfRatingByProductId(productID) + WHERE p.product_id = productID ; +END $$ +DELIMITER ; + +-- Procedure that update the no of rating of all products +DELIMITER $$ +CREATE PROCEDURE updateAllNoOfRating() +BEGIN + DECLARE finished INTEGER DEFAULT 0; + DECLARE productID varchar(255) DEFAULT NULL; + + -- declare cursor + DEClARE curProduct + CURSOR FOR + SELECT product_id FROM PRODUCT; + + -- declare NOT FOUND handler + DECLARE CONTINUE HANDLER + FOR NOT FOUND SET finished = 1; + + OPEN curProduct; + + readLoop: LOOP + FETCH curProduct INTO productID; + IF finished = 1 THEN + LEAVE readLoop; + END IF; + CALL updateNoOfRatingByProductId(productID); + END LOOP; + + CLOSE curProduct; + +END +$$ + +-- Call procedure for sets the no of rating of all products +DELIMITER $$ +CALL updateAllNoOfRating() +$$ + +-- Trigger that updates the no of rating after insert a new review. +DELIMITER $$ +CREATE TRIGGER updateNoOfRatingAfterInsertReviewTrigger + AFTER INSERT ON REVIEW + FOR EACH ROW + +BEGIN + CALL updateNoOfRatingByProductId(NEW.product_id); +END +$$ + +-- Trigger that updates the no of rating after update a review. +DELIMITER $$ +CREATE TRIGGER updateNoOfRatingAfterUpdateReviewTrigger + AFTER UPDATE ON REVIEW + FOR EACH ROW + +BEGIN + CALL updateNoOfRatingByProductId(NEW.product_id); + + -- In case product_id changes then update old + IF OLD.product_id <> NEW.product_id THEN + CALL updateNoOfRatingByProductId(OLD.product_id); + END IF; + +END +$$ + +-- Trigger that updates the no of rating after delete a review. +DELIMITER $$ +CREATE TRIGGER updateNoOfRatingAfterDeleteReviewTrigger + AFTER DELETE ON REVIEW + FOR EACH ROW + +BEGIN + CALL updateNoOfRatingByProductId(OLD.product_id); + +END +$$ +DELIMITER ; diff --git a/bookstore-frontend-react-app/package-lock.json b/bookstore-frontend-react-app/package-lock.json index f4570e04..8c2e4fbf 100644 --- a/bookstore-frontend-react-app/package-lock.json +++ b/bookstore-frontend-react-app/package-lock.json @@ -2135,6 +2135,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", @@ -2229,6 +2238,17 @@ "csstype": "^3.0.2" } }, + "@types/react-redux": { + "version": "7.1.18", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz", + "integrity": "sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/react-transition-group": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", @@ -2983,6 +3003,21 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.0.2.tgz", "integrity": "sha512-arU1h31OGFu+LPrOLGZ7nB45v940NMDMEJeNmbutu57P+UFDVnkZg3e+J1I2HJRZ9hT7gO8J91dn/PMrAiKakA==" }, + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + }, + "dependencies": { + "follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" + } + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -7003,6 +7038,19 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -7013,6 +7061,14 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -8791,6 +8847,11 @@ "object.assign": "^4.1.1" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -9219,6 +9280,15 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" }, + "mini-create-react-context": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + } + }, "mini-css-extract-plugin": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz", @@ -11503,9 +11573,39 @@ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "requires": { + "side-channel": "^1.0.4" + }, + "dependencies": { + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + } + } }, "query-string": { "version": "4.3.4", @@ -11580,9 +11680,9 @@ } }, "react": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", - "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -11747,11 +11847,120 @@ "prop-types": "^15.6.1" } }, + "react-paypal-button-v2": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-paypal-button-v2/-/react-paypal-button-v2-2.6.3.tgz", + "integrity": "sha512-Kt0QxWjZn+RnIbbFPHDfH8iHwMqe/jG5+SkKElEJ0Fg+CM9yeK37y8zQW97l5C9P9D1yPacW5YNAnuxmI9uS9w==", + "requires": { + "prop-types": "^15.7.2", + "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "react-redux": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.5.tgz", + "integrity": "sha512-Dt29bNyBsbQaysp6s/dN0gUodcq+dVKKER8Qv82UrpeygwYeX1raTtil7O/fftw/rFqzaf6gJhDZRkkZnn6bjg==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/react-redux": "^7.1.16", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.13.1" + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==" }, + "react-router": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", + "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==", + "requires": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "react-router-bootstrap": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/react-router-bootstrap/-/react-router-bootstrap-0.25.0.tgz", + "integrity": "sha512-/22eqxjn6Zv5fvY2rZHn57SKmjmJfK7xzJ6/G1OgxAjLtKVfWgV5sn41W2yiqzbtV5eE4/i4LeDLBGYTqx7jbA==", + "requires": { + "prop-types": "^15.5.10" + } + }, + "react-router-dom": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", + "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", + "requires": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.2.1", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, + "react-router-redux": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/react-router-redux/-/react-router-redux-4.0.8.tgz", + "integrity": "sha1-InQDWWtRUeGCN32rg1tdRfD4BU4=" + }, "react-scripts": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.0.tgz", @@ -11959,6 +12168,24 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-devtools-extension": { + "version": "2.13.9", + "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz", + "integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==" + }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -12150,6 +12377,11 @@ "uuid": "^3.3.2" }, "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -12245,6 +12477,11 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -14030,6 +14267,16 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -14490,6 +14737,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/bookstore-frontend-react-app/src/App.css b/bookstore-frontend-react-app/src/App.css index 2eb99bad..e7e08289 100644 --- a/bookstore-frontend-react-app/src/App.css +++ b/bookstore-frontend-react-app/src/App.css @@ -1,80 +1,135 @@ @import url('https://fonts.googleapis.com/css2?family=Kaushan+Script&display=swap'); +@import url('./themes.css'); + +#root { + background-color: var(--bg); /* background color variable */ + color: var(--color-text); + height: 100vh; +} + +#root main { + background-color: var(--bg)!important; +} body { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } button { - border-radius: 5px !important; + border-radius: 5px !important; } a { - border-radius: 5px !important; + border-radius: 5px !important; } p { - font-family: sans-serif !important; + font-family: sans-serif !important; } .bookstore-brand { - font-family: 'Kaushan Script', cursive; - font-size: 2em !important; - font-weight: 800; - clip-path: polygon(0 30%, 100% 11%, 100% 75%, 0 92%); + font-family: 'Kaushan Script', cursive; + font-size: 2em !important; + font-weight: 800; + clip-path: polygon(0 30%, 100% 11%, 100% 75%, 0 92%); - background: rgb(2, 0, 36); - background: linear-gradient(84deg, rgba(2, 0, 36, 1) 21%, rgba(0, 0, 179, 1) 63%, rgba(191, 0, 76, 1) 92%); - padding: 5px; + background: rgb(2, 0, 36); + background: linear-gradient(84deg, rgba(2, 0, 36, 1) 21%, rgba(0, 0, 179, 1) 63%, rgba(191, 0, 76, 1) 92%); + padding: 5px; } .bookstore-brand:hover { - color: aquamarine !important; + color: aquamarine !important; } .fp-container { - position: fixed; - width: 100%; - height: 100%; - top: 0; - left: 0; - background: #111c1fad; + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: #111c1fad; } .fp-container .fp-loader { - top: 45%; - left: 48%; - z-index: 10000; - position: absolute; + top: 45%; + left: 48%; + z-index: 10000; + position: absolute; } .pagination { - display: flex; - padding-left: 0; - list-style: none; - border-radius: 0.25rem; + display: flex; + padding-left: 0; + list-style: none; + border-radius: 0.25rem; } .page-item.active { - z-index: 3; - color: #fff; - /* background-color: #d9230f; */ - /* border-color: #d9230f; */ + z-index: 3; + color: #fff; + /* background-color: #d9230f; */ + /* border-color: #d9230f; */ } .page-item:first-child .page-link { - margin-left: 0; - border-top-left-radius: 0.25rem; - border-bottom-left-radius: 0.25rem; + margin-left: 0; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; } .page-link { - position: relative; - display: block; - padding: 0.5rem 0.75rem; - margin-left: -1px; - line-height: 1.25; - color: #d9230f; - background-color: #fff; - border: 1px solid #eee; + position: relative; + display: block; + padding: 0.5rem 0.75rem; + margin-left: -1px; + line-height: 1.25; + color: #d9230f; + background-color: #fff; + border: 1px solid #eee; +} + +.row-container-fully-centered { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.col-container-fully-centered { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.card-body, +.list-group-item +{ + background-color: var(--bg-panel)!important; } + +.table { + color: var(--color-text)!important; +} + +.table-hover tbody tr:hover { + color: var(--color-text)!important; + background-color: var(--color-hover-row)!important; +} + +.link-container a{ + color: var(--color-link)!important; +} + +.link-container a:hover, +.nav-link:hover{ + color: var(--color-hover)!important; + text-decoration-color: var(--color-hover); + -webkit-text-decoration-color: var(--color-hover); +} + +.pagination .page-link { + border-radius: 5px; +} \ No newline at end of file diff --git a/bookstore-frontend-react-app/src/actions/darkModeActions.js b/bookstore-frontend-react-app/src/actions/darkModeActions.js new file mode 100644 index 00000000..c839d3d2 --- /dev/null +++ b/bookstore-frontend-react-app/src/actions/darkModeActions.js @@ -0,0 +1,9 @@ +import { DARK_MODE_ENABLE, DARK_MODE_DISABLE } from '../constants/darkModeConstants'; + +export const ENABLE_DARK_MODE = () => (dispatch) => { + dispatch({ type: DARK_MODE_ENABLE }); +}; + +export const DISABLE_DARK_MODE = () => (dispatch) => { + dispatch({ type: DARK_MODE_DISABLE }); +}; \ No newline at end of file diff --git a/bookstore-frontend-react-app/src/actions/productActions.js b/bookstore-frontend-react-app/src/actions/productActions.js index 882dc3e2..0dbfabab 100644 --- a/bookstore-frontend-react-app/src/actions/productActions.js +++ b/bookstore-frontend-react-app/src/actions/productActions.js @@ -39,15 +39,17 @@ import { getImageApi } from '../service/RestApiCalls'; -export const listProductsAction = (pageNumber) => async (dispatch) => { +export const listProductsAction = (pageNumber, searchText= '', filters= {}) => async (dispatch) => { try { dispatch({ type: PRODUCT_LIST_REQUEST }); //Get All Products Detail - const allProductsDetail = await getAllProductsDetailApi(pageNumber || 0); + const allProductsDetail = await getAllProductsDetailApi(pageNumber || 0, searchText, filters); dispatch({ type: PRODUCT_LIST_SUCCESS, payload: allProductsDetail.page.content, - pageResponse: allProductsDetail.page + pageResponse: allProductsDetail.page, + searchText, + filters }); } catch (error) { dispatch({ diff --git a/bookstore-frontend-react-app/src/components/CartItem.js b/bookstore-frontend-react-app/src/components/CartItem.js index 8a35e6f2..3542d4e1 100644 --- a/bookstore-frontend-react-app/src/components/CartItem.js +++ b/bookstore-frontend-react-app/src/components/CartItem.js @@ -40,7 +40,7 @@ const CartItem = ({ item, addToCart }) => { {item.productName} - + {item.productName} diff --git a/bookstore-frontend-react-app/src/components/CheckoutSteps.js b/bookstore-frontend-react-app/src/components/CheckoutSteps.js index 67c00446..a80ac78b 100644 --- a/bookstore-frontend-react-app/src/components/CheckoutSteps.js +++ b/bookstore-frontend-react-app/src/components/CheckoutSteps.js @@ -8,7 +8,9 @@ const CheckoutSteps = ({ step1, step2, step3, step4 }) => { {step1 ? ( - Sign In         > +
+ Sign In         > +
) : ( Sign In @@ -18,7 +20,9 @@ const CheckoutSteps = ({ step1, step2, step3, step4 }) => { {step2 ? ( - Shipping         > +
+ Shipping         > +
) : ( Shipping @@ -28,7 +32,9 @@ const CheckoutSteps = ({ step1, step2, step3, step4 }) => { {step3 ? ( - Payment         > +
+ Payment         > +
) : ( Payment @@ -38,7 +44,9 @@ const CheckoutSteps = ({ step1, step2, step3, step4 }) => { {step4 ? ( - Place Order +
+ Place Order +
) : ( Place Order diff --git a/bookstore-frontend-react-app/src/components/DarkModeToggle.js b/bookstore-frontend-react-app/src/components/DarkModeToggle.js new file mode 100644 index 00000000..ac0efd5e --- /dev/null +++ b/bookstore-frontend-react-app/src/components/DarkModeToggle.js @@ -0,0 +1,54 @@ +import React, { useEffect } from 'react'; +import Form from 'react-bootstrap/Form'; +import { useDispatch, useSelector } from 'react-redux'; +import { ENABLE_DARK_MODE, DISABLE_DARK_MODE } from '../actions/darkModeActions'; + +const DarkModeToggle = () => { + + const { isDark } = useSelector((state) => state.darkMode); + const checked = isDark; + + const dispatch = useDispatch(); + + const changeThemeToDark = () => { + document.getElementById('root').setAttribute('data-theme', 'dark'); + }; + + const changeThemeToLight = () => { + document.getElementById('root').setAttribute('data-theme', 'light'); + }; + + const handleChangeToggle = (e) => { + if (e.target.checked) { + dispatch(ENABLE_DARK_MODE()); + } else { + dispatch(DISABLE_DARK_MODE()); + } + }; + + useEffect(() => { + if (checked) { + changeThemeToDark(); + localStorage.setItem('isDark', JSON.stringify(true)); + } else { + changeThemeToLight(); + localStorage.setItem('isDark', JSON.stringify(false)); + } + } + , [checked] + ); + + return ( +
+ handleChangeToggle(event)} + defaultChecked={checked} + /> + + ); +}; + +export default DarkModeToggle; diff --git a/bookstore-frontend-react-app/src/components/Header.js b/bookstore-frontend-react-app/src/components/Header.js index 3ec8e7f7..7b7111ab 100644 --- a/bookstore-frontend-react-app/src/components/Header.js +++ b/bookstore-frontend-react-app/src/components/Header.js @@ -4,6 +4,8 @@ import { LinkContainer } from 'react-router-bootstrap'; import { useDispatch, useSelector } from 'react-redux'; import { isAdmin } from '../service/CommonUtils'; import { logout } from '../actions/userActions'; +import DarkModeToggle from './DarkModeToggle'; +import SearchBar from './SearchBar'; const Header = (props) => { const userLogin = useSelector((state) => state.userLogin); const { userInfo } = userLogin; @@ -29,16 +31,19 @@ const Header = (props) => { BookStore + diff --git a/bookstore-frontend-react-app/src/components/OrderItem.js b/bookstore-frontend-react-app/src/components/OrderItem.js index 547c3adb..c66bb65b 100644 --- a/bookstore-frontend-react-app/src/components/OrderItem.js +++ b/bookstore-frontend-react-app/src/components/OrderItem.js @@ -34,7 +34,7 @@ const OrderItem = ({ item }) => { {item.productName} - + {product.productName} diff --git a/bookstore-frontend-react-app/src/components/PriceFilter.js b/bookstore-frontend-react-app/src/components/PriceFilter.js new file mode 100644 index 00000000..f8e4481b --- /dev/null +++ b/bookstore-frontend-react-app/src/components/PriceFilter.js @@ -0,0 +1,63 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Form } from 'react-bootstrap'; +import { useDispatch, useSelector } from 'react-redux'; +import { listProductsAction } from '../actions/productActions'; +import { useForm } from '../hooks/useForm'; + +const PriceFilter = () => { + + const filters = useSelector((state) => state.productList.filters); + const searchText = useSelector((state) => state.productList.searchText); + + const initialState = { + minPrice: '', + maxPrice: '', + }; + const [form, handleInputChange, resetForm, setForm] = useForm(initialState); + const {minPrice, maxPrice} = form; + + const dispatch = useDispatch(); + + const handleSubmit = (e) => { + e.preventDefault(); + dispatch(listProductsAction(0, searchText, {...filters, minPrice, maxPrice})); + }; + + useEffect(() => { + setForm({ + minPrice: filters.minPrice, + maxPrice: filters.maxPrice + }); + }, [filters]) + + return ( + <> +
+
Price
+
+ + + +
+
+ + ); +}; + +export default PriceFilter; diff --git a/bookstore-frontend-react-app/src/components/Product.js b/bookstore-frontend-react-app/src/components/Product.js index 82c3b561..4370afcd 100644 --- a/bookstore-frontend-react-app/src/components/Product.js +++ b/bookstore-frontend-react-app/src/components/Product.js @@ -8,7 +8,7 @@ const Product = (props) => { const product = props.product; return ( <> - + { = 3 ? 'fas fa-star' : value >= 2.5 ? 'fas fa-star-half-alt' : 'far fa-star'}> - = 4 ? 'fas fa-star' : value >= 4.5 ? 'fas fa-star-half-alt' : 'far fa-star'}> + = 4 ? 'fas fa-star' : value >= 3.5 ? 'fas fa-star-half-alt' : 'far fa-star'}> - = 5 ? 'fas fa-star' : value >= 3.5 ? 'fas fa-star-half-alt' : 'far fa-star'}> + = 5 ? 'fas fa-star' : value >= 4.5 ? 'fas fa-star-half-alt' : 'far fa-star'}> {text && [{text && text}]} diff --git a/bookstore-frontend-react-app/src/components/RatingFilter.js b/bookstore-frontend-react-app/src/components/RatingFilter.js new file mode 100644 index 00000000..64a66db4 --- /dev/null +++ b/bookstore-frontend-react-app/src/components/RatingFilter.js @@ -0,0 +1,72 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { listProductsAction } from '../actions/productActions'; +import useEffectDidMount from '../hooks/useEffectDidMount'; +import { useForm } from '../hooks/useForm'; + +const RatingFilter = () => { + + + const dispatch = useDispatch(); + const formEl = useRef(); + + const filters = useSelector((state) => state.productList.filters); + const searchText = useSelector((state) => state.productList.searchText); + + const initialStateForm = { + maxRating: '5' + } + const [form , handleInputChange, resetForm, setForm] = useForm(initialStateForm) + const {maxRating} = form; + + const [instantRating, setInstantRating] = useState(maxRating); + const handleChangeInstantRating = (event) => { + setInstantRating(event.target.value); + } + + const handleSubmit = (e) => { + e.preventDefault(); + dispatch(listProductsAction(0, searchText, { ...filters, minRating: '0', maxRating})); + }; + + useEffect(() => { + setForm({ + maxRating: filters.maxRating + }); + setInstantRating( + filters.maxRating + ); + }, [filters]) + + useEffectDidMount(()=> { + formEl.current.click(); + }, [maxRating]) + + return ( + <> +
+
Average Rating
+
+ +
0 - {instantRating}
+ +
+
+ + ); +}; + +export default RatingFilter; diff --git a/bookstore-frontend-react-app/src/components/SearchBar.js b/bookstore-frontend-react-app/src/components/SearchBar.js new file mode 100644 index 00000000..2e2439d0 --- /dev/null +++ b/bookstore-frontend-react-app/src/components/SearchBar.js @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; +import { Form } from 'react-bootstrap'; +import { listProductsAction, resetFiltersProductsAction } from '../actions/productActions'; +import { useDispatch, useSelector } from 'react-redux'; +import { useForm } from '../hooks/useForm'; +import { initialStateFilters } from '../reducers/productReducers'; + +const SearchBar = () => { + + const initialState = { + searchText:'' + }; + const [form, handleInputChange] = useForm(initialState); + const {searchText} = form; + + const dispatch = useDispatch(); + + const handleSubmit = (e) => { + e.preventDefault(); + dispatch(listProductsAction(0, searchText, initialStateFilters)); + }; + + return ( +
+
+
+
+ +
+ +
+
+
+ +
+ ); +}; + +export default React.memo(SearchBar); diff --git a/bookstore-frontend-react-app/src/components/SortingCatalog.js b/bookstore-frontend-react-app/src/components/SortingCatalog.js new file mode 100644 index 00000000..8d835ed5 --- /dev/null +++ b/bookstore-frontend-react-app/src/components/SortingCatalog.js @@ -0,0 +1,62 @@ +import React, { useRef, useState } from 'react'; +import { Dropdown, DropdownButton } from 'react-bootstrap'; +import { useDispatch, useSelector } from 'react-redux'; +import { listProductsAction } from '../actions/productActions'; +import useEffectDidMount from '../hooks/useEffectDidMount'; +import { initialStateSorting } from '../reducers/productReducers'; + +export const SortingCatalog = () => { + + const elRef = useRef(); + const dispatch = useDispatch(); + const searchText = useSelector(state => state.productList.searchText); + const filters = useSelector(state => state.productList.filters); + + const sortCriteria = { + relevance: 'description,ASC', + lowestPriceFirst: 'price,ASC', + highestPriceFirst: 'price,DESC' + }; + const [criteriaSelected, setCriteriaSelected] = useState(filters.sort) + + const handleSubmit = (e) => { + e.preventDefault(); + dispatch(listProductsAction(0, searchText, {...filters, sort: criteriaSelected})); + }; + + const handleSelectChange = (e) => { + setCriteriaSelected(e.target.value); + }; + + useEffectDidMount(() => { + elRef.current.click(); + }, [criteriaSelected]); + + return ( + <> +
+
Sorting
+ + + + +
+ + ) +} diff --git a/bookstore-frontend-react-app/src/components/StockFilter.js b/bookstore-frontend-react-app/src/components/StockFilter.js new file mode 100644 index 00000000..12691620 --- /dev/null +++ b/bookstore-frontend-react-app/src/components/StockFilter.js @@ -0,0 +1,56 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { Button, Form } from 'react-bootstrap'; +import { useDispatch, useSelector } from 'react-redux'; +import { listProductsAction } from '../actions/productActions'; +import useEffectDidMount from '../hooks/useEffectDidMount'; +import { useForm } from '../hooks/useForm'; + +const StockFilter = () => { + + const dispatch = useDispatch(); + const formEl = useRef(); + + const filters = useSelector((state) => state.productList.filters); + const searchText = useSelector((state) => state.productList.searchText); + + const initialStateForm = { + availability: '' + } + const [form , handleInputChange, resetForm, setForm, handleCheckedChange] = useForm(initialStateForm) + const {availability} = form; + + const handleSubmit = (e) => { + e.preventDefault(); + dispatch(listProductsAction(0, searchText, { ...filters, availability})); + }; + + useEffect(() => { + setForm({ + availability: filters.availability + }); + }, [filters]) + + useEffectDidMount(()=> { + formEl.current.click(); + }, [availability]) + + return ( + <> +
+
Availability
+ + + + + ); +}; + +export default StockFilter; diff --git a/bookstore-frontend-react-app/src/constants/darkModeConstants.js b/bookstore-frontend-react-app/src/constants/darkModeConstants.js new file mode 100644 index 00000000..5e0becc3 --- /dev/null +++ b/bookstore-frontend-react-app/src/constants/darkModeConstants.js @@ -0,0 +1,2 @@ +export const DARK_MODE_ENABLE = 'DARK_MODE_ENABLE'; +export const DARK_MODE_DISABLE = 'DARK_MODE_DISABLE'; diff --git a/bookstore-frontend-react-app/src/hooks/useEffectDidMount.js b/bookstore-frontend-react-app/src/hooks/useEffectDidMount.js new file mode 100644 index 00000000..d636c860 --- /dev/null +++ b/bookstore-frontend-react-app/src/hooks/useEffectDidMount.js @@ -0,0 +1,13 @@ +import React, { useEffect, useRef } from 'react'; + +// Not run effect in the first render of componnent. +const useEffectDidMount = (func, deps) => { + const didMount = useRef(false); + + useEffect(() => { + if (didMount.current) func(); + else didMount.current = true; + }, deps); +} + +export default useEffectDidMount; \ No newline at end of file diff --git a/bookstore-frontend-react-app/src/hooks/useForm.js b/bookstore-frontend-react-app/src/hooks/useForm.js new file mode 100644 index 00000000..4216f257 --- /dev/null +++ b/bookstore-frontend-react-app/src/hooks/useForm.js @@ -0,0 +1,27 @@ +import { useState } from "react" + +export const useForm = (initialState) => { + + const [form, setForm] = useState(initialState); + + const handleInputChange = ({target}) => { + setForm({ + ...form, + [target.name] : target.value + }); + }; + + const handleCheckedChange = ({target}) => { + setForm({ + ...form, + [target.name] : target.checked + }); + }; + + const resetForm = () => { + setForm(initialState); + } + + return [form , handleInputChange, resetForm, setForm, handleCheckedChange]; + +} diff --git a/bookstore-frontend-react-app/src/reducers/darkModeReducer.js b/bookstore-frontend-react-app/src/reducers/darkModeReducer.js new file mode 100644 index 00000000..b05c9afd --- /dev/null +++ b/bookstore-frontend-react-app/src/reducers/darkModeReducer.js @@ -0,0 +1,15 @@ +import { + DARK_MODE_ENABLE, + DARK_MODE_DISABLE +} from '../constants/darkModeConstants'; + +export const darkModeReducer = (state = { isDark: false }, action) => { + switch (action.type) { + case DARK_MODE_ENABLE: + return { isDark: true }; + case DARK_MODE_DISABLE: + return { isDark: false }; + default: + return state; + } +}; diff --git a/bookstore-frontend-react-app/src/reducers/productReducers.js b/bookstore-frontend-react-app/src/reducers/productReducers.js index 1d3a0000..d3bf3fe4 100644 --- a/bookstore-frontend-react-app/src/reducers/productReducers.js +++ b/bookstore-frontend-react-app/src/reducers/productReducers.js @@ -32,14 +32,36 @@ import { PRODUCT_IMAGE_RESET } from '../constants/productConstants'; -export const productListReducer = (state = { products: [] }, action) => { +export const initialStateFilters = { + minPrice:'', + maxPrice:'', + minRating:'0', + maxRating:'5', + availability:'', + sort:'description,ASC' +} +const initialStateProductList = { + loading: false, + products: [], + error: '', + searchText: '', + filters: initialStateFilters +}; +export const productListReducer = (state = initialStateProductList, action) => { switch (action.type) { case PRODUCT_LIST_REQUEST: - return { loading: true, products: [] }; + return { ...state, loading: true }; case PRODUCT_LIST_SUCCESS: - return { loading: false, products: action.payload, pageResponse: action.pageResponse }; + return { + ...state, + loading: false, + products: action.payload, + pageResponse: action.pageResponse, + searchText: action.searchText, + filters: action.filters + }; case PRODUCT_LIST_FAIL: - return { loading: false, error: action.payload }; + return { ...state, loading: false, error: action.payload }; default: return state; } diff --git a/bookstore-frontend-react-app/src/screens/CartScreen.js b/bookstore-frontend-react-app/src/screens/CartScreen.js index b552d2f4..65524162 100644 --- a/bookstore-frontend-react-app/src/screens/CartScreen.js +++ b/bookstore-frontend-react-app/src/screens/CartScreen.js @@ -70,9 +70,9 @@ const CartScreen = (props) => { ))} )} - + - Add more books + Add more books diff --git a/bookstore-frontend-react-app/src/screens/HomeScreen.js b/bookstore-frontend-react-app/src/screens/HomeScreen.js index 3486406d..4bb6a50f 100644 --- a/bookstore-frontend-react-app/src/screens/HomeScreen.js +++ b/bookstore-frontend-react-app/src/screens/HomeScreen.js @@ -7,21 +7,26 @@ import { Col, Row } from 'react-bootstrap'; import { listProductsAction } from '../actions/productActions'; import FullPageLoader from '../components/FullPageLoader'; import ReactPaginate from 'react-paginate'; +import PriceFilter from '../components/PriceFilter'; +import RatingFilter from '../components/RatingFilter'; +import StockFilter from '../components/StockFilter'; +import { SortingCatalog } from '../components/SortingCatalog'; const HomeScreen = () => { const dispatch = useDispatch(); const productList = useSelector((state) => state.productList); - const { loading, error, products, pageResponse } = productList; + const { loading, error, products, pageResponse, searchText, filters } = productList; + const currentPage = pageResponse?.number ? pageResponse.number : 0; useEffect(() => { - dispatch(listProductsAction(0)); + dispatch(listProductsAction(0, searchText, filters)); }, [dispatch]); const handlePageClick = (data) => { let selected = data.selected; - dispatch(listProductsAction(selected)); + dispatch(listProductsAction(selected, searchText, filters)); }; - + return ( <>

Latest Products

@@ -30,29 +35,40 @@ const HomeScreen = () => { ) : ( <> - {products.map((product) => ( - - - - ))} - - {/* pageResponse?.pageable?.pageNumber */} - - handlePageClick(e)} - containerClassName={'pagination'} - activeClassName={'page-item active'} - pageLinkClassName={'page-link'} - previousClassName={'page-link'} - nextClassName={'page-link'} - /> + + + + + + + + + {products.map((product) => ( + + + + ))} + + {/* pageResponse?.pageable?.pageNumber */} + + handlePageClick(e)} + containerClassName={'pagination'} + activeClassName={'page-item active'} + pageLinkClassName={'page-link'} + previousClassName={'page-link'} + nextClassName={'page-link'} + forcePage={currentPage} + /> + + )} diff --git a/bookstore-frontend-react-app/src/screens/OrderScreen.js b/bookstore-frontend-react-app/src/screens/OrderScreen.js index 4fd072f2..e21a3700 100644 --- a/bookstore-frontend-react-app/src/screens/OrderScreen.js +++ b/bookstore-frontend-react-app/src/screens/OrderScreen.js @@ -69,7 +69,7 @@ const OrderScreen = ({ match, history }) => {

Name: {userInfo.userName}

-

+

Email: {userInfo.email}

@@ -96,7 +96,7 @@ const OrderScreen = ({ match, history }) => { Not Paid )} -

+

Payment Receipt : {order.paymentReceiptUrl} diff --git a/bookstore-frontend-react-app/src/screens/PaymentScreen.js b/bookstore-frontend-react-app/src/screens/PaymentScreen.js index e6a21f3d..3a642b80 100644 --- a/bookstore-frontend-react-app/src/screens/PaymentScreen.js +++ b/bookstore-frontend-react-app/src/screens/PaymentScreen.js @@ -89,7 +89,8 @@ const PaymentScreen = ({ history }) => { className='p-2' style={{ whiteSpace: 'pre-wrap', - backgroundColor: '#eeeeee' + backgroundColor: '#eeeeee', + color: '#0e0e0e' }} onClick={(e) => { console.log(a.paymentMethodId); diff --git a/bookstore-frontend-react-app/src/screens/ProductScreen.js b/bookstore-frontend-react-app/src/screens/ProductScreen.js index 0023efbc..e340da83 100644 --- a/bookstore-frontend-react-app/src/screens/ProductScreen.js +++ b/bookstore-frontend-react-app/src/screens/ProductScreen.js @@ -30,6 +30,8 @@ const ProductScreen = (props) => { const productReviewCreate = useSelector((state) => state.productReviewCreate); const { success: successProductReview, loading: loadingProductReview, error: errorProductReview } = productReviewCreate; + const isDarkMode = useSelector((state) => state.darkMode?.isDark); + useEffect(async () => { // setProductimageBase64(null); // dispatch(listProductDetailsAction(props.match.params.id)); @@ -61,7 +63,7 @@ const ProductScreen = (props) => { return ( <> - + Go Back diff --git a/bookstore-frontend-react-app/src/screens/ShippingScreen.js b/bookstore-frontend-react-app/src/screens/ShippingScreen.js index de21c53e..5ca14601 100644 --- a/bookstore-frontend-react-app/src/screens/ShippingScreen.js +++ b/bookstore-frontend-react-app/src/screens/ShippingScreen.js @@ -124,7 +124,8 @@ const ShippingScreen = ({ history }) => { className='p-2' style={{ whiteSpace: 'pre-wrap', - backgroundColor: '#eeeeee' + backgroundColor: '#eeeeee', + color: '#0e0e0e' }} onClick={() => { if (shippingCheckbox) { diff --git a/bookstore-frontend-react-app/src/service/EncodeUtil.js b/bookstore-frontend-react-app/src/service/EncodeUtil.js new file mode 100644 index 00000000..3c98adfa --- /dev/null +++ b/bookstore-frontend-react-app/src/service/EncodeUtil.js @@ -0,0 +1,174 @@ +export const toURLEncode = (text = "") => { + + const mapURLEncode = new Map(Object.entries(urlEncodes)); + + let textEncoded = ""; + + // Iterate over search text + for (let i = 0; i < text.length; i++) { + if ( mapURLEncode.has(text[i])) { + textEncoded = textEncoded + mapURLEncode.get(text[i]); + } else { + textEncoded = textEncoded + text[i]; + } + } + + return textEncoded; + +} + +const urlEncodes = { + "!" : "%21", + "\"" : "%22", + "#" : "%23", + "$" : "%24", + "%" : "%25", + "&" : "%26", + "'" : "%27", + "(" : "%28", + ")" : "%29", + "*" : "%2A", + "+" : "%2B", + "," : "%2C", + "-" : "%2D", + "." : "%2E", + "/" : "%2F", + ":" : "%3A", + ";" : "%3B", + "<" : "%3C", + "=" : "%3D", + ">" : "%3E", + "?" : "%3F", + "@" : "%40", + "[" : "%5B", + "\\" : "%5C", + "]" : "%5D", + "^" : "%5E", + "_" : "%5F", + "\`" : "%60", + "{" : "%7B", + "|" : "%7C", + "}" : "%7D", + "~" : "%7E", + "‚" : "%E2%80%9A", + "ƒ" : "%C6%92", + "„" : "%E2%80%9E", + "…" : "%E2%80%A6", + "†" : "%E2%80%A0", + "‡" : "%E2%80%A1", + "ˆ" : "%CB%86", + "‰" : "%E2%80%B0", + "Š" : "%C5%A0", + "‹" : "%E2%80%B9", + "Œ" : "%C5%92", + "Ž" : "%C5%BD", + "‘" : "%E2%80%98", + "’" : "%E2%80%99", + "“" : "%E2%80%9C", + "”" : "%E2%80%9D", + "•" : "%E2%80%A2", + "–" : "%E2%80%93", + "—" : "%E2%80%94", + "˜" : "%CB%9C", + "™" : "%E2%84", + "š" : "%C5%A1", + "›" : "%E2%80", + "œ" : "%C5%93", + "ž" : "%C5%BE", + "Ÿ" : "%C5%B8", + "¡" : "%C2%A1", + "¢" : "%C2%A2", + "£" : "%C2%A3", + "¤" : "%C2%A4", + "¥" : "%C2%A5", + "¦" : "%C2%A6", + "§" : "%C2%A7", + "¨" : "%C2%A8", + "©" : "%C2%A9", + "ª" : "%C2%AA", + "«" : "%C2%AB", + "¬" : "%C2%AC", + "®" : "%C2%AE", + "¯" : "%C2%AF", + "°" : "%C2%B0", + "±" : "%C2%B1", + "²" : "%C2%B2", + "³" : "%C2%B3", + "´" : "%C2%B4", + "µ" : "%C2%B5", + "¶" : "%C2%B6", + "·" : "%C2%B7", + "¸" : "%C2%B8", + "¹" : "%C2%B9", + "º" : "%C2%BA", + "»" : "%C2%BB", + "¼" : "%C2%BC", + "½" : "%C2%BD", + "¾" : "%C2%BE", + "¿" : "%C2%BF", + "À" : "%C3%80", + "Á" : "%C3%81", + "Â" : "%C3%82", + "Ã" : "%C3%83", + "Ä" : "%C3%84", + "Å" : "%C3%85", + "Æ" : "%C3%86", + "Ç" : "%C3%87", + "È" : "%C3%88", + "É" : "%C3%89", + "Ê" : "%C3%8A", + "Ë" : "%C3%8B", + "Ì" : "%C3%8C", + "Í" : "%C3%8D", + "Î" : "%C3%8E", + "Ï" : "%C3%8F", + "Ð" : "%C3%90", + "Ñ" : "%C3%91", + "Ò" : "%C3%92", + "Ó" : "%C3%93", + "Ô" : "%C3%94", + "Õ" : "%C3%95", + "Ö" : "%C3%96", + "×" : "%C3%97", + "Ø" : "%C3%98", + "Ù" : "%C3%99", + "Ú" : "%C3%9A", + "Û" : "%C3%9B", + "Ü" : "%C3%9C", + "Ý" : "%C3%9D", + "Þ" : "%C3%9E", + "ß" : "%C3%9F", + "à" : "%C3%A0", + "á" : "%C3%A1", + "â" : "%C3%A2", + "ã" : "%C3%A3", + "ä" : "%C3%A4", + "å" : "%C3%A5", + "æ" : "%C3%A6", + "ç" : "%C3%A7", + "è" : "%C3%A8", + "é" : "%C3%A9", + "ê" : "%C3%AA", + "ë" : "%C3%AB", + "ì" : "%C3%AC", + "í" : "%C3%AD", + "î" : "%C3%AE", + "ï" : "%C3%AF", + "ð" : "%C3%B0", + "ñ" : "%C3%B1", + "ò" : "%C3%B2", + "ó" : "%C3%B3", + "ô" : "%C3%B4", + "õ" : "%C3%B5", + "ö" : "%C3%B6", + "÷" : "%C3%B7", + "ø" : "%C3%B8", + "ù" : "%C3%B9", + "ú" : "%C3%BA", + "û" : "%C3%BB", + "ü" : "%C3%BC", + "ý" : "%C3%BD", + "þ" : "%C3%BE", + "ÿ" : "%C3%BF" + +} \ No newline at end of file diff --git a/bookstore-frontend-react-app/src/service/RestApiCalls.js b/bookstore-frontend-react-app/src/service/RestApiCalls.js index 8d58d536..7e2f7087 100644 --- a/bookstore-frontend-react-app/src/service/RestApiCalls.js +++ b/bookstore-frontend-react-app/src/service/RestApiCalls.js @@ -3,6 +3,7 @@ import axios from 'axios'; import qs from 'qs'; import store from '../store'; import { USER_LOGOUT } from '../constants/userConstants'; +import { toURLEncode } from './EncodeUtil'; axios.interceptors.response.use( (response) => response, @@ -235,8 +236,27 @@ export const getImageApi = async (imageId) => { return responseData; }; -export const getAllProductsDetailApi = async (pageNumber) => { - const responseData = axios.get(`${BACKEND_API_GATEWAY_URL}/api/catalog/products?page=${pageNumber}&size=8`).then((response) => { +export const getAllProductsDetailApi = async (pageNumber, searchText= "", filters= {}) => { + + // Params must have format {key: value} where value is a primitive. + // Value must not be object + const toQueryParams = (params = {}) => { + let queryParams = ""; + // For every key into params add => &key=value + const arrayParams = Object.entries(params); + for (let i = 0; i < arrayParams.length; i++) { + queryParams = queryParams + `&${arrayParams[i][0]}=${arrayParams[i][1]}` + } + return queryParams; + } + + const pageParam = `page=${pageNumber}` + const sizePageParam = `&size=8`; + const searchTextParam = `&searchText=${toURLEncode(searchText)}`; + const filtersParams = toQueryParams(filters); + + const url = `${BACKEND_API_GATEWAY_URL}/api/catalog/products?${pageParam}${sizePageParam}${searchTextParam}${filtersParams}`; + const responseData = axios.get(url).then((response) => { return response.data; }); return responseData; diff --git a/bookstore-frontend-react-app/src/store.js b/bookstore-frontend-react-app/src/store.js index 1adc5090..a6ccf0b5 100644 --- a/bookstore-frontend-react-app/src/store.js +++ b/bookstore-frontend-react-app/src/store.js @@ -31,6 +31,7 @@ import { import { cartAddReducer, cartDetailReducer, cartRemoveReducer } from './reducers/cartReducers'; import { addressDeleteReducer, addressListMyReducer, addressSaveReducer } from './reducers/addressReducer'; import { paymentMethodListMyReducer, paymentMethodSaveReducer } from './reducers/paymentReducers'; +import { darkModeReducer } from './reducers/darkModeReducer'; const appReducer = combineReducers({ productList: productListReducer, @@ -61,13 +62,15 @@ const appReducer = combineReducers({ addressListMy: addressListMyReducer, addressDelete: addressDeleteReducer, paymentMethodSave: paymentMethodSaveReducer, - paymentMethodListMy: paymentMethodListMyReducer + paymentMethodListMy: paymentMethodListMyReducer, + darkMode: darkModeReducer }); const userInfoFromStorage = localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')) : null; const billingAddressId = localStorage.getItem('billingAddressId') ? localStorage.getItem('billingAddressId') : null; const shippingAddressId = localStorage.getItem('shippingAddressId') ? localStorage.getItem('shippingAddressId') : null; const paymentMethodId = localStorage.getItem('paymentMethodId') ? localStorage.getItem('paymentMethodId') : null; +const isDark = localStorage.getItem('isDark') ? JSON.parse(localStorage.getItem('isDark')) : false; const initialState = { userLogin: { userInfo: userInfoFromStorage }, @@ -75,6 +78,9 @@ const initialState = { billingAddressId, shippingAddressId, paymentMethodId + }, + darkMode: { + isDark: isDark } }; diff --git a/bookstore-frontend-react-app/src/themes.css b/bookstore-frontend-react-app/src/themes.css new file mode 100644 index 00000000..64d8b04b --- /dev/null +++ b/bookstore-frontend-react-app/src/themes.css @@ -0,0 +1,21 @@ +/* default styling variables - making background color as white */ +#root{ + --bg: #ffffff; + --bg-panel: #ffffff; + --color-heading: rgb(27, 168, 14); + --color-text: #333333; + --color-link: #d9230f; + --color-hover: #91170a; + --color-hover-row: rgba(0,0,0,.075); +} + +/* dark theme styling - Here, we set data-them as "dark"*/ +#root[data-theme='dark'] { + --bg: #333333; + --bg-panel: #434343; + --color-heading: #0077ff; + --color-text: #eae9e9; + --color-link: #ececec; + --color-hover: #ffffff; + --color-hover-row: rgba(0,0,0,.35); +} \ No newline at end of file diff --git a/docker-run-microservices.sh b/docker-run-microservices.sh new file mode 100644 index 00000000..445cfbe6 --- /dev/null +++ b/docker-run-microservices.sh @@ -0,0 +1,3 @@ +#!/bin/bash +mvn clean install +docker-compose up --build