diff --git a/book-store/book-store-theme/src/main/resources/static/css/home.css b/book-store/book-store-theme/src/main/resources/static/css/home.css index 69e1915..13ec9de 100644 --- a/book-store/book-store-theme/src/main/resources/static/css/home.css +++ b/book-store/book-store-theme/src/main/resources/static/css/home.css @@ -29,6 +29,7 @@ overflow: hidden; text-decoration: none; } + .main-book .book-title:hover { color: #d0011b; } @@ -109,7 +110,7 @@ } .btn-view-detail:focus { - outline: 3px solid rgba(208,1,27,0.2); + outline: 3px solid rgba(208, 1, 27, 0.2); outline-offset: 2px; } @@ -120,7 +121,7 @@ object-fit: contain; border-radius: 8px; - box-shadow: 0 10px 30px rgba(0,0,0,0.15); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); transition: transform 0.3s ease; } @@ -160,8 +161,8 @@ width: 44px; height: 44px; border-radius: 50%; - background: rgba(255,255,255,0.9); - box-shadow: 0 2px 10px rgba(0,0,0,0.12); + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); } .highlight-swiper:hover .swiper-button-prev, @@ -183,6 +184,7 @@ } @media (hover: none) { + .highlight-swiper .swiper-button-prev, .highlight-swiper .swiper-button-next { opacity: 1; @@ -305,3 +307,286 @@ white-space: nowrap !important; } +.flashsale-section { + margin: 12px 0 48px; + padding: 28px 28px 24px; + background: #fff; + border-radius: 22px; + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.05); +} + +.flashsale-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; + margin-bottom: 24px; +} + +.flashsale-intro { + flex: 1; + min-width: 0; +} + +.flashsale-topline { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 10px; +} + +.flashsale-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 12px; + border-radius: 999px; + background: #ef7376; + color: #fff; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.flashsale-countdown { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.flashsale-time-box { + min-width: 34px; + height: 34px; + padding: 0 8px; + border-radius: 8px; + background: #16213e; + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 700; + line-height: 1; +} + +.flashsale-time-sep { + color: #16213e; + font-size: 16px; + font-weight: 700; + line-height: 1; +} + +.flashsale-title { + margin: 0 0 8px; + color: #4a5974; + font-size: 22px; + font-weight: 500; + line-height: 1.25; +} + +.flashsale-subtitle { + margin: 0; + color: #8791a5; + font-size: 12px; + line-height: 1.6; +} + +.flashsale-actions { + display: flex; + align-items: center; + gap: 10px; + padding-top: 4px; +} + +.flashsale-nav { + width: 40px; + height: 40px; + border: 1px solid #dce2ec; + border-radius: 50%; + background: #fff; + color: #5a6780; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; +} + +.flashsale-nav:hover { + background: #16213e; + color: #fff; + border-color: #16213e; +} + +.flashsale-swiper { + overflow: hidden; +} + +.flashsale-swiper .swiper-slide { + height: auto; +} + +.flashsale-swiper .book { + height: 100%; + background: #fff; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.05); + display: flex; + flex-direction: column; +} + +.flashsale-swiper .book-image { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 340px; + background: #f6f7fb; + overflow: hidden; + padding: 10px; +} + +.flashsale-swiper .book-image img { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + object-fit: contain; + display: block; +} + +.flashsale-swiper .book-content { + padding: 14px 16px 16px; + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; +} + +.flashsale-swiper .book-tags { + min-height: 24px; +} + +.flashsale-swiper .discount-percent { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 5px 10px; + border-radius: 999px; + background: #f4cf57; + color: #111; + font-size: 12px; + font-weight: 700; + line-height: 1; +} + +.flashsale-swiper .book-title { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + min-height: 44px; + color: #16213e; + font-size: 16px; + font-weight: 600; + line-height: 1.4; + text-decoration: none; +} + +.flashsale-swiper .book-title:hover { + color: #ea5b5b; + text-decoration: none; +} + +.flashsale-swiper .book-author { + display: flex; + align-items: center; + gap: 6px; + min-height: 20px; + color: #6e7b93; + font-size: 13px; + line-height: 1.4; +} + +.flashsale-swiper .book-author-icon { + font-size: 12px; + color: #8c95a8; +} + +.flashsale-swiper .author-names { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.flashsale-swiper .author-name { + color: #6e7b93; + text-decoration: none; +} + +.flashsale-swiper .author-name:hover { + color: #ea5b5b; + text-decoration: none; +} + +.flashsale-swiper .prices { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 8px; + margin-top: auto; +} + +.flashsale-swiper .price { + color: #16213e; + font-size: 17px; + font-weight: 700; + line-height: 1.2; +} + +.flashsale-swiper .original-price { + color: #9aa3b5; + font-size: 13px; + text-decoration: line-through; + line-height: 1.2; +} + +/* --- RESPONSIVE FLASH SALE --- */ +@media (max-width: 991px) { + .flashsale-section { + padding: 22px 18px 20px; + } + + .flashsale-header { + flex-direction: column; + align-items: flex-start; + } +} + +@media (max-width: 767px) { + .flashsale-title { + font-size: 20px; + } + + .flashsale-time-box { + min-width: 30px; + height: 30px; + font-size: 15px; + } + + .flashsale-time-sep { + font-size: 14px; + } + + .flashsale-nav { + width: 36px; + height: 36px; + } + + .flashsale-swiper .book-title { + font-size: 15px; + } +} \ No newline at end of file diff --git a/book-store/book-store-theme/src/main/resources/templates/home.html b/book-store/book-store-theme/src/main/resources/templates/home.html index 5fe09b4..24717df 100644 --- a/book-store/book-store-theme/src/main/resources/templates/home.html +++ b/book-store/book-store-theme/src/main/resources/templates/home.html @@ -1,153 +1,272 @@ - + -
-
+
+
- -
-
-
+ +
+
+
-
-
-
- - - [[${book.name}]] - +
+
+
+ + + [[${book.name}]] + - -
+ +
- -
-
[[#{author}]]
+ +
+
[[#{author}]]
-
-
+ + +
+ + [[${book.formattedPriceIncludeSymbol}]] + + + + [[${book.formattedOriginalPriceIncludeSymbol}]] + + + + -[[${book.discountPercent}]]% + +
+ + + + [[#{view_detail}]] +
- -
- - [[${book.formattedPriceIncludeSymbol}]] - - - - [[${book.formattedOriginalPriceIncludeSymbol}]] - - - - -[[${book.discountPercent}]]% - + + +
+
- - - [[#{view_detail}]] - +
+ + +
+
+
+
+
+ +
+ +
+
+
+
+
+ [[#{flash_sale}]] +
+ 00 + : + 00 + : + 00 +
-
- - - +

[[#{flash_sale}]]

+
+ +
+ + +
+
+ +
+
+
+
+ +
+
+
-
+
- -
-
-
+ +
+

[[#{bestselling_books}]]

+
+ + + +
-
-
+
- -
-

[[#{bestselling_books}]]

-
- - - + +
+

[[#{new_book_s}]]

+
+ + + +
-
+ + + - + + $(function () { + var $cd = $('#flashsale-countdown'); + if (!$cd.length) return; + + var endMs = parseInt($cd.attr('data-end-ms'), 10); + if (!endMs || isNaN(endMs)) return; + + function pad2(n) { + return String(n).padStart(2, '0'); + } + + function renderBoxes(h, m, s) { + $cd.html( + '' + pad2(h) + '' + + ':' + + '' + pad2(m) + '' + + ':' + + '' + pad2(s) + '' + ); + } + + function tick() { + var diff = endMs - Date.now(); + if (diff < 0) diff = 0; + + var totalSeconds = Math.floor(diff / 1000); + var seconds = totalSeconds % 60; + var totalMinutes = Math.floor(totalSeconds / 60); + var minutes = totalMinutes % 60; + var totalHours = Math.floor(totalMinutes / 60); + var hours = totalHours; + + renderBoxes(hours, minutes, seconds); + + if (diff === 0) { + clearInterval(timer); + } + } + + tick(); + var timer = setInterval(tick, 1000); + }); + + - + \ No newline at end of file diff --git a/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/controller/service/WebBookFlashSaleControllerService.java b/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/controller/service/WebBookFlashSaleControllerService.java new file mode 100644 index 0000000..b96edc2 --- /dev/null +++ b/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/controller/service/WebBookFlashSaleControllerService.java @@ -0,0 +1,94 @@ +package org.youngmonkeys.bookstore.web.controller.service; + +import com.tvd12.ezyhttp.server.core.annotation.Service; +import lombok.AllArgsConstructor; +import org.youngmonkeys.bookstore.web.controller.decorator.WebBookModelDecorator; +import org.youngmonkeys.bookstore.web.response.WebBookFlashSaleResponse; +import org.youngmonkeys.bookstore.web.response.WebBookResponse; +import org.youngmonkeys.bookstore.web.service.WebProductPriceListProductService; +import org.youngmonkeys.bookstore.web.service.WebProductPriceListService; +import org.youngmonkeys.ecommerce.entity.ShopProductPriceList; +import org.youngmonkeys.ecommerce.model.ProductCurrencyModel; +import org.youngmonkeys.ecommerce.model.ProductModel; +import org.youngmonkeys.ecommerce.web.service.WebProductService; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Collections; +import java.util.List; + +@Service +@AllArgsConstructor +public class WebBookFlashSaleControllerService { + private final WebProductService productService; + private final WebProductPriceListService productPriceListService; + private final WebProductPriceListProductService productPriceListProductService; + private final WebBookModelDecorator bookModelDecorator; + + public WebBookFlashSaleResponse getFlashSale( + ProductCurrencyModel currency, + int limit + ) { + ShopProductPriceList flashSale = + productPriceListService.getActiveFlashSalePriceList(); + if (flashSale == null) { + return null; + } + + List books = getFlashSaleBooks( + flashSale.getId(), + currency, + limit + ); + + return toFlashSaleResponse( + flashSale, + books + ); + } + + private List getFlashSaleBooks( + long priceListId, + ProductCurrencyModel currency, + int limit + ) { + List productIds = + productPriceListProductService.getProductIdsByPriceListId( + priceListId, + limit + ); + + if (productIds.isEmpty()) { + return Collections.emptyList(); + } + + List products = productService.getProductsByIds(productIds); + return bookModelDecorator.decorateToBookResponses( + products, + currency + ); + } + + private WebBookFlashSaleResponse toFlashSaleResponse( + ShopProductPriceList flashSale, + List books + ) { + return WebBookFlashSaleResponse.builder() + .id(flashSale.getId()) + .displayName(flashSale.getDisplayName()) + .startApplyAtMs(toEpochMs(flashSale.getStartApplyAt())) + .finishApplyAtMs(toEpochMs(flashSale.getFinishApplyAt())) + .serverNowMs(System.currentTimeMillis()) + .books(books) + .build(); + } + + private long toEpochMs(LocalDateTime time) { + if (time == null) { + return 0L; + } + return time.atZone(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + } +} diff --git a/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/response/WebBookFlashSaleResponse.java b/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/response/WebBookFlashSaleResponse.java new file mode 100644 index 0000000..f510d8b --- /dev/null +++ b/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/response/WebBookFlashSaleResponse.java @@ -0,0 +1,17 @@ +package org.youngmonkeys.bookstore.web.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class WebBookFlashSaleResponse { + private long id; + private String displayName; + private long startApplyAtMs; + private long finishApplyAtMs; + private long serverNowMs; + private List books; +} diff --git a/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/service/WebProductPriceListProductService.java b/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/service/WebProductPriceListProductService.java new file mode 100644 index 0000000..0474c47 --- /dev/null +++ b/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/service/WebProductPriceListProductService.java @@ -0,0 +1,31 @@ +package org.youngmonkeys.bookstore.web.service; + +import com.tvd12.ezyfox.io.EzyLists; +import com.tvd12.ezyfox.util.Next; +import com.tvd12.ezyhttp.server.core.annotation.Service; +import lombok.AllArgsConstructor; +import org.youngmonkeys.ecommerce.entity.ShopProductPriceListProduct; +import org.youngmonkeys.ecommerce.repo.ShopProductPriceListProductRepository; + +import java.util.List; + +@Service +@AllArgsConstructor +public class WebProductPriceListProductService { + + private final ShopProductPriceListProductRepository priceListProductRepository; + + public List getProductIdsByPriceListId( + long priceListId, + int limit + ) { + return EzyLists.newArrayList( + priceListProductRepository.findByPriceListIdAndIdGtOrderByIdAsc( + priceListId, + 0L, + Next.limit(limit) + ), + ShopProductPriceListProduct::getProductId + ); + } +} diff --git a/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/service/WebProductPriceListService.java b/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/service/WebProductPriceListService.java new file mode 100644 index 0000000..ebe117d --- /dev/null +++ b/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/service/WebProductPriceListService.java @@ -0,0 +1,43 @@ +package org.youngmonkeys.bookstore.web.service; + +import com.tvd12.ezyfox.util.Next; +import com.tvd12.ezyhttp.server.core.annotation.Service; +import lombok.AllArgsConstructor; +import org.youngmonkeys.ecommerce.entity.ProductPriceListStatus; +import org.youngmonkeys.ecommerce.entity.ProductPriceListType; +import org.youngmonkeys.ecommerce.entity.ShopProductPriceList; +import org.youngmonkeys.ecommerce.repo.ShopProductPriceListRepository; +import org.youngmonkeys.ezyplatform.time.ClockProxy; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@AllArgsConstructor +public class WebProductPriceListService { + + private final ShopProductPriceListRepository shopProductPriceListRepository; + private final ClockProxy clock; + + public ShopProductPriceList getActiveFlashSalePriceList() { + LocalDateTime now = clock.nowDateTime(); + int limit = 100; + List priceLists = + shopProductPriceListRepository + .findByTypeAndStatusAndStartApplyAtLteOrderByStartApplyAtAscIdAsc( + ProductPriceListType.SCHEDULED_APPLY.toString(), + ProductPriceListStatus.APPLIED.toString(), + now, + Next.limit(limit) + ); + + for (ShopProductPriceList priceList : priceLists) { + LocalDateTime finishApplyAt = priceList.getFinishApplyAt(); + if (finishApplyAt == null || finishApplyAt.isAfter(now)) { + return priceList; + } + } + return null; + } + +} diff --git a/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/view/ViewFactory.java b/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/view/ViewFactory.java index 24a4adc..65e2695 100644 --- a/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/view/ViewFactory.java +++ b/book-store/book-store-web-plugin/src/main/java/org/youngmonkeys/bookstore/web/view/ViewFactory.java @@ -4,6 +4,7 @@ import com.tvd12.ezyhttp.server.core.view.View; import lombok.AllArgsConstructor; import org.youngmonkeys.bookstore.web.controller.service.WebBookControllerService; +import org.youngmonkeys.bookstore.web.controller.service.WebBookFlashSaleControllerService; import org.youngmonkeys.ecommerce.model.ProductCurrencyModel; import org.youngmonkeys.ecommerce.web.service.WebProductCurrencyService; import org.youngmonkeys.ezyarticle.web.manager.WebPageFragmentManager; @@ -17,6 +18,7 @@ public class ViewFactory { private final WebPageFragmentManager pageFragmentManager; private final WebProductCurrencyService currencyService; private final WebBookControllerService bookControllerService; + private final WebBookFlashSaleControllerService flashSaleControllerService; public View.Builder newHomeViewBuilder( long currencyId, @@ -48,6 +50,12 @@ public View.Builder newHomeViewBuilder( DEFAULT_BOOKS_LIMIT ) ) + .addVariable("flashSale", + flashSaleControllerService.getFlashSale( + currency, + DEFAULT_BOOKS_LIMIT + ) + ) .addVariable( "fragments", pageFragmentManager.getPageFragmentMap( diff --git a/book-store/pom.xml b/book-store/pom.xml index f056a35..d30b72c 100644 --- a/book-store/pom.xml +++ b/book-store/pom.xml @@ -13,7 +13,9 @@ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. ---> +--> + 4.0.0 org.youngmonkeys @@ -31,21 +33,21 @@ ${env.EZYPLATFORM_HOME} - 0.9.1 - 1.1.0 - 0.2.2 - 0.1.4 + 0.9.5 + 1.1.5 + 0.2.4 + 0.1.6 2022.3.1 v2-rev157-1.25.0 1.33.2 1.6.2 - 0.1.5 - 0.3.0 + 0.1.6 + 0.3.3 0.1.8 0.3.9 1.18.3 3.5.3 - 0.1.7 + 0.2.1 0.1.3 0.0.8 @@ -80,26 +82,28 @@ ecommerce-sdk ${ecommerce.version} system - ${ezyplatform.home}/web/plugins/ecommerce/lib/ecommerce-sdk-${ecommerce.version}.jar + ${ezyplatform.home}/web/plugins/ecommerce/lib/ecommerce-sdk-${ecommerce.version}.jar + - + com.google.zxing javase ${zxing.version} - provided + provided org.jsoup jsoup ${jsoup.version} - provided + provided org.youngmonkeys ezyarticle-sdk ${ezyarticle.version} system - ${ezyplatform.home}/admin/plugins/ezyarticle/lib/ezyarticle-sdk-${ezyarticle.version}.jar + ${ezyplatform.home}/admin/plugins/ezyarticle/lib/ezyarticle-sdk-${ezyarticle.version}.jar + org.youngmonkeys @@ -109,17 +113,24 @@ ${ezyplatform.home}/lib/ezyplatform-common-${ezy.platform.version}.jar - org.youngmonkeys - ezyplatform-test-sdk - ${ezy.platform.version} - test - - org.youngmonkeys ezyplatform-socket-sdk ${ezy.platform.version} provided + + com.tvd12 + ezyhttp-server-thymeleaf + ${ezy.http.version} + provided + + + javax.servlet + javax.servlet-api + ${javax.servlet.version} + true + provided +