Trong bài trước, chúng ta đã liên kết các mô đun cần thiết, trong bài này chúng ta sẽ cùng nhau tạo giao diện cho trang chủ.

Mục tiêu

  • Giúp bạn lập trình được backend cho trang chủ.
  • Giúp bạn lập trình được front-end cho trang chủ.

Lập trình trang chủ phía backend

Đầu tiên chúng ta sẽ cần tạo một lớp chứa thông tin về một cuốn sách có tên WebBookResponse như sau:

package org.youngmonkeys.bookstore.web.response;

// các import

@Getter
@Builder
public class WebBookResponse {
    private long id;
    private String code;
    private String name;
    private String authorName;
    private String authorUuid;
    private MediaNameModel bannerImage;
    private String shortedDescription;
    private String publisher;
}

Ở đây chúng đang lấy một số thông tin cực kỳ cơ bản của một cuốn sách như id, mã, tên, tác giả, ảnh banner, ... Trong tương lai khi làm các tính năng phức tạp hơn, chúng ta sẽ bổ sung thêm các trường khác vào sau.

Tiếp theo chúng ta sẽ tạo một lớp để tổng hợp dữ liệu có tên WebBookModelDecorator như sau:

package org.youngmonkeys.bookstore.web.controller.decorator;

// các import

@EzySingleton
@AllArgsConstructor
public class WebBookModelDecorator {

    private final WebMediaService mediaService;
    private final WebPostService postService;
    private final WebProductBookService productBookService;
    private final WebProductDescriptionService productDescriptionService;
    private final WebUserService userService;
    private final WebBookStoreModelToResponseConverter
        modelToResponseConverter;

    public List<WebBookResponse> decorateToBookResponse(
        List<ProductModel> models
    ) {
        List<Long> productIds = newArrayList(
            models,
            ProductModel::getId
        );
        Map<Long, ProductBookModel> bookById = productBookService
            .getProductBookMapByIds(productIds);
        Set<Long> userIds = bookById
            .values()
            .stream()
            .map(ProductBookModel::getAuthorUserId)
            .filter(it -> it > 0)
            .collect(Collectors.toSet());
        Set<Long> mediaIds = models
            .stream()
            .map(ProductModel::getBannerImageId)
            .filter(it -> it > 0)
            .collect(Collectors.toSet());
        Map<Long, Long> descriptionPostIdByProductId = productDescriptionService
            .getProductDescriptionPostIdMapByIds(
                productIds
            );
        return Reactive.multiple()
            .register("userById", () ->
                userService.getUserMapByIds(userIds)
            )
            .register("mediaById", () ->
                mediaService.getMediaNameMapByIds(mediaIds)
            )
            .register("descriptionById", () ->
                postService.getPostMapByIds(
                    descriptionPostIdByProductId.values()
                )
            )
            .blockingGet(map -> {
                Map<Long, UserModel> userById = map.get("userById");
                Map<Long, MediaNameModel> mediaById = map.get("mediaById");
                Map<Long, PostModel> descriptionById = map.get("descriptionById");
                return newArrayList(models, it -> {
                    ProductBookModel book = bookById.getOrDefault(
                        it.getId(),
                        ProductBookModel.builder().build()
                    );
                    return modelToResponseConverter.toBookResponse(
                        it,
                        book,
                        userById.getOrDefault(
                            book.getAuthorUserId(),
                            UserModel.builder().build()
                        ),
                        mediaById.get(it.getBannerImageId()),
                        descriptionById.getOrDefault(
                            descriptionPostIdByProductId.getOrDefault(
                                it.getId(),
                                ZERO_LONG
                            ),
                            PostModel.builder().build()
                        )
                    );
                });
            });
    }
}

Lớp này sẽ giúp chúng ta tổng hợp các dữ liệu thành phần để tạo nên một danh sách thông tin các cuốn sách WebBookResponse.

Tiếp theo chúng ta sẽ cần tạo lớp WebBookControllerService để lấy danh sách các cuốn sách nổi bật:

package org.youngmonkeys.bookstore.web.controller.service;

// các import

@Service
@AllArgsConstructor
public class WebBookControllerService {

    private final WebProductService productService;
    private final WebBookModelDecorator bookModelDecorator;

    public List<WebBookResponse> getTopBooksByShopId(
        long shopId
    ) {
        List<ProductModel> models = productService
            .getProductsByShopIdAndProductTypeInAndStatusInSortByByDisplayOrderDescIdDesc(
                shopId,
                Collections.singletonList(BookStoreProductType.BOOK.toString()),
                Collections.singletonList(ProductStatus.PUBLISHED.toString()),
                0,
                1
            );
        return bookModelDecorator.decorateToBookResponse(models);
    }
}

Ở đây chúng ta đang lấy ra các sản phẩm có kiểu là sách BOOK, có trạng thái đã được xuất bản PUBLISHED, với thứ hiển thị từ cao xuống thấp và tạm thời chúng ta chỉ lấy một cuốn sách duy nhất, trong tương lai chúng ta sẽ lấy ra nhiều cuốn sách hơn.

Tiếp theo chúng ta sẽ tạo ra một lớp có tên ViewFactory:

package org.youngmonkeys.bookstore.web.view;

// các import 

@EzySingleton
@AllArgsConstructor
public class ViewFactory {

    private final WebPageFragmentManager pageFragmentManager;
    private final WebShopService shopService;
    private final WebBookControllerService bookControllerService;

    public View.Builder newHomeViewBuilder(String language) {
        long shopId = shopService.getDefaultShopId();
        return View.builder()
            .template("home")
            .addVariable("pageTitle", "home")
            .addVariable(
                "topBooks",
                bookControllerService.getTopBooksByShopId(shopId)
            )
            .addVariable(
                "fragments",
                pageFragmentManager.getPageFragmentMap(
                    "home",
                    language
                )
            );
    }
}

Lớp này đóng gói việc khởi tạo ra trang chủ, bởi vì ở trang chủ còn có các trạng thái như mở modal đăng nhập, đăng ký nên nó sẽ dùng ở nhiều chỗ nên chúng ta cần đóng gói thông qua lớp factory này.

Tiếp theo, ở book-store-web-plugin chúng ta sẽ tạo ra lớp HomeController:

package org.youngmonkeys.bookstore.web.controller.view;

// các import

@Setter
public class HomeController {

    @EzyAutoBind
    private ViewFactory viewFactory;

    @EzyAutoBind
    private WebLanguageControllerService languageControllerService;

    @DoGet("/")
    public View homeGet(
        HttpServletRequest request
    ) {
        String language = languageControllerService
            .getLanguageCodeOrDefault(request);
        return viewFactory
            .newHomeViewBuilder(language)
            .build();
    }
}

Ở đây chúng ta đang khai báo controller cho uri / chính là trang chủ, lớp này sẽ chỉ đơn giản là gọi đến ViewFactory để tạo view mà thôi.

Tiếp theo ở book-store-theme chúng ta sẽ tạo ra lớp WebBookStoreHomeController:

package org.youngmonkeys.bookstore.web.controller.view;

// các import

@Controller
public class WebBookStoreHomeController extends HomeController {}

Lớp này chỉ đơn giản là thừa kế lại HomeController.

Tại sao lại phải phức tạp như vậy? Như chúng ta đã biết thì EzyPlatform sẽ chỉ cho một theme được cài tại một thời điểm nhưng plugin thì lại có thể nhiều. Khi một ai đó cài theme book-store họ có thể tự code ra một giao diện mới thay vì dùng giao diện mặc định do chúng ta tạo ra, lúc này họ có có thể thừa kế HomeController hoặc tự cài đặt một cái mới.

Bây giờ, bạn hãy mở admin bằng cách chạy BookStoreAdminPluginStartupTest và truy cập vào các sản phẩm. Sau đó tạo một sản phẩm với thông tin kiểu thế này:

Screenshot 2025-11-12 at 11.01.39.png Screenshot 2025-11-12 at 11.02.04.png

Như vậy là xong, bây giờ chúng ta có thể chạy lại BookStoreThemeStartupTest và kết quả chúng ta nhận được sẽ là:

Screenshot 2025-11-12 at 11.21.05.png

Lập trình trang chủ phía frontend

Như chúng ta thấy, giao diện trang chủ hiện tại hơi thô kệch, chúng ta hãy thay đổi html của home.html một chút như sau:

<div layout:fragment="content">
    <div class="common-spacing-1"></div>
    <div class="container">
        <div th:each="topBook : ${topBooks}" class="main-book">
            <div class="description-item">
                <h1 class="display-2 text-dark">
                    [[${topBook.name}]]
                </h1>
                <div class="lead" th:utext="${topBook.shortedDescription}"></div>
                <p class="author text-muted mb-0" th:text="${topBook.authorName}"></p>
            </div>
            <div class="image-item">
                <img th:if="${topBook.bannerImage != null}"
                     th:src="${topBook.bannerImage.getUrlOrNull()}">
            </div>
        </div>
    </div>
</div>

Ở đây chúng ta thêm một khoảng trắng ở trên đầu, bổ sung thêm tiên tác giả.

Chúng ta cũng bổ sung thêm vào home.css:

.main-book .description-item .lead {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 5;
    overflow: hidden;
}

.main-book .description-item .author {
    margin-top: 12px;
    font-size: 2rem;
}

.main-book .image-item {
    width: 50%;
    display: flex;
    justify-content: end;
}

Để giới hạn số dòng mô tả sách và cho tên tác giả nổi bật hơn. Bây giờ refresh lại trang và kết quả chúng ta nhận được là:

Screenshot 2025-11-12 at 11.58.49.png

Giao diện đã trông đẹp và rõ ràng hơn rất nhiều.

Tài liệu tham khảo