QPAT commited on
Commit
1f513e3
·
1 Parent(s): 7c67e34

fix perf article

Browse files
src/main/java/com/darkfantasy/api/ArticleAPIController.java ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.darkfantasy.api;
2
+
3
+ import java.util.HashMap;
4
+ import java.util.Map;
5
+
6
+ import org.springframework.data.domain.PageRequest;
7
+ import org.springframework.data.domain.Pageable;
8
+ import org.springframework.data.domain.Slice;
9
+ import org.springframework.web.bind.annotation.GetMapping;
10
+ import org.springframework.web.bind.annotation.RequestMapping;
11
+ import org.springframework.web.bind.annotation.RequestParam;
12
+ import org.springframework.web.bind.annotation.ResponseBody;
13
+ import org.springframework.web.bind.annotation.RestController;
14
+
15
+ import com.darkfantasy.dto.article.ArticleCardResponse;
16
+ import com.darkfantasy.service.ArticleService;
17
+
18
+ import lombok.RequiredArgsConstructor;
19
+
20
+ @RequestMapping("/api/article")
21
+ @RequiredArgsConstructor
22
+ @RestController
23
+ public class ArticleAPIController {
24
+ private final ArticleService articleService;
25
+
26
+ @GetMapping("/more")
27
+ @ResponseBody
28
+ public Map<String, Object> loadMoreNews(
29
+ @RequestParam(defaultValue = "1") int page,
30
+ @RequestParam(defaultValue = "6") int size) {
31
+
32
+ Pageable pageable = PageRequest.of(page, size);
33
+ Slice<ArticleCardResponse> slice = articleService.getArticlesDeletedFalse(pageable);
34
+ Map<String, Object> response = new HashMap<>();
35
+ response.put("articles", slice.getContent());
36
+ response.put("hasMore", slice.hasNext());
37
+
38
+ return response;
39
+ }
40
+ }
src/main/java/com/darkfantasy/dto/article/ArticleListResponse.java ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.darkfantasy.dto.article;
2
+
3
+ import com.darkfantasy.repository.projection.article.ArticleList;
4
+ import com.darkfantasy.util.TimeUtil;
5
+
6
+ import lombok.AllArgsConstructor;
7
+ import lombok.Builder;
8
+ import lombok.Data;
9
+ import lombok.NoArgsConstructor;
10
+
11
+ @Data
12
+ @NoArgsConstructor
13
+ @AllArgsConstructor
14
+ @Builder
15
+ public class ArticleListResponse {
16
+ private Long id;
17
+ private String thumbnailUrl;
18
+ private String title;
19
+ private String authorUsername;
20
+ private String type;
21
+ private String createdAt;
22
+ private boolean deleted;
23
+
24
+ public static ArticleListResponse fromProjection(ArticleList article) {
25
+ return ArticleListResponse.builder()
26
+ .id(article.getId())
27
+ .thumbnailUrl(article.getThumbnailUrl())
28
+ .title(article.getTitle())
29
+ .authorUsername(article.getAuthorUsername())
30
+ .type(article.getType())
31
+ .createdAt(TimeUtil.formatInstant(article.getCreatedAt()))
32
+ .deleted(article.isDeleted())
33
+ .build();
34
+ }
35
+ }
src/main/java/com/darkfantasy/repository/ArticleRepository.java CHANGED
@@ -14,6 +14,7 @@ import org.springframework.stereotype.Repository;
14
  import com.darkfantasy.entity.Article;
15
  import com.darkfantasy.entity.enums.ArticleType;
16
  import com.darkfantasy.repository.projection.article.ArticleDetail;
 
17
  import com.darkfantasy.repository.projection.article.ArticleCardBar;
18
  import com.darkfantasy.repository.projection.article.ArticleCard;
19
 
@@ -46,4 +47,6 @@ public interface ArticleRepository extends JpaRepository<Article, Long> {
46
  Page<ArticleCard> findPublicArticleList(Pageable pageable);
47
 
48
  List<ArticleCardBar> findByIdNotAndDeletedFalseOrderByCreatedAtDesc(Long id, Pageable pageable);
 
 
49
  }
 
14
  import com.darkfantasy.entity.Article;
15
  import com.darkfantasy.entity.enums.ArticleType;
16
  import com.darkfantasy.repository.projection.article.ArticleDetail;
17
+ import com.darkfantasy.repository.projection.article.ArticleList;
18
  import com.darkfantasy.repository.projection.article.ArticleCardBar;
19
  import com.darkfantasy.repository.projection.article.ArticleCard;
20
 
 
47
  Page<ArticleCard> findPublicArticleList(Pageable pageable);
48
 
49
  List<ArticleCardBar> findByIdNotAndDeletedFalseOrderByCreatedAtDesc(Long id, Pageable pageable);
50
+
51
+ Page<ArticleList> findAllProjectedBy(Pageable pageable);
52
  }
src/main/java/com/darkfantasy/repository/projection/article/ArticleList.java ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.darkfantasy.repository.projection.article;
2
+
3
+ import java.time.Instant;
4
+
5
+ import org.springframework.beans.factory.annotation.Value;
6
+
7
+ public interface ArticleList {
8
+ Long getId();
9
+ String getThumbnailUrl();
10
+ String getTitle();
11
+ @Value("#{target.createdBy.username}")
12
+ String getAuthorUsername();
13
+ String getType();
14
+ Instant getCreatedAt();
15
+ boolean isDeleted();
16
+ }
src/main/java/com/darkfantasy/service/ArticleService.java CHANGED
@@ -9,6 +9,7 @@ import org.springframework.data.domain.Slice;
9
  import com.darkfantasy.dto.article.ArticleCardBarResponse;
10
  import com.darkfantasy.dto.article.ArticleCardResponse;
11
  import com.darkfantasy.dto.article.ArticleDetailResponse;
 
12
  import com.darkfantasy.dto.article.ArticleResponse;
13
  import com.darkfantasy.dto.article.CreateArticleRequest;
14
  import com.darkfantasy.dto.article.UpdateArticleRequest;
@@ -26,7 +27,7 @@ public interface ArticleService {
26
 
27
  void restoreArticle(Long articleId);
28
 
29
- Page<ArticleResponse> getArticles(Pageable pageable);
30
 
31
  Slice<ArticleCardResponse> getArticlesDeletedFalse(Pageable pageable);
32
 
 
9
  import com.darkfantasy.dto.article.ArticleCardBarResponse;
10
  import com.darkfantasy.dto.article.ArticleCardResponse;
11
  import com.darkfantasy.dto.article.ArticleDetailResponse;
12
+ import com.darkfantasy.dto.article.ArticleListResponse;
13
  import com.darkfantasy.dto.article.ArticleResponse;
14
  import com.darkfantasy.dto.article.CreateArticleRequest;
15
  import com.darkfantasy.dto.article.UpdateArticleRequest;
 
27
 
28
  void restoreArticle(Long articleId);
29
 
30
+ Page<ArticleListResponse> getArticles(Pageable pageable);
31
 
32
  Slice<ArticleCardResponse> getArticlesDeletedFalse(Pageable pageable);
33
 
src/main/java/com/darkfantasy/service/impl/ArticleServiceImpl.java CHANGED
@@ -12,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional;
12
  import com.darkfantasy.dto.article.ArticleCardBarResponse;
13
  import com.darkfantasy.dto.article.ArticleCardResponse;
14
  import com.darkfantasy.dto.article.ArticleDetailResponse;
 
15
  import com.darkfantasy.dto.article.ArticleResponse;
16
  import com.darkfantasy.dto.article.CreateArticleRequest;
17
  import com.darkfantasy.dto.article.UpdateArticleRequest;
@@ -134,11 +135,11 @@ public class ArticleServiceImpl implements ArticleService {
134
  }
135
 
136
  @Override
137
- public Page<ArticleResponse> getArticles(Pageable pageable) {
138
  if (pageable == null) {
139
  throw new IllegalArgumentException("Pageable không được null");
140
  }
141
- return articleRepository.findAll(pageable).map(ArticleResponse::fromEntity);
142
  }
143
 
144
  @Transactional
 
12
  import com.darkfantasy.dto.article.ArticleCardBarResponse;
13
  import com.darkfantasy.dto.article.ArticleCardResponse;
14
  import com.darkfantasy.dto.article.ArticleDetailResponse;
15
+ import com.darkfantasy.dto.article.ArticleListResponse;
16
  import com.darkfantasy.dto.article.ArticleResponse;
17
  import com.darkfantasy.dto.article.CreateArticleRequest;
18
  import com.darkfantasy.dto.article.UpdateArticleRequest;
 
135
  }
136
 
137
  @Override
138
+ public Page<ArticleListResponse> getArticles(Pageable pageable) {
139
  if (pageable == null) {
140
  throw new IllegalArgumentException("Pageable không được null");
141
  }
142
+ return articleRepository.findAllProjectedBy(pageable).map(ArticleListResponse::fromProjection);
143
  }
144
 
145
  @Transactional
src/main/resources/application.properties CHANGED
@@ -18,9 +18,9 @@ spring.thymeleaf.mode=HTML
18
  # spring.security.oauth2.client.registration.google.scope=email,profile
19
 
20
  # DEVTOOLS & DEVELOPMENT MODE (Dành cho Production)
21
- spring.devtools.restart.enabled=false
22
- spring.devtools.livereload.enabled=false
23
- spring.devtools.add-properties=false
24
  # JSP:
25
 
26
  spring.mvc.view.cache=true
@@ -49,10 +49,10 @@ spring.jpa.hibernate.ddl-auto=update
49
 
50
  # Bật tính năng in câu lệnh SQL ra console
51
  #Nên bật khi code, khi deploy thì tắt
52
- spring.jpa.show-sql=false
53
 
54
  # Format SQL cho dễ đọc (xuống dòng, thụt lề), nếu không nó sẽ in thành 1 hàng ngang rất rối
55
- spring.jpa.properties.hibernate.format_sql=false
56
 
57
  # Làm nổi bật các từ khóa SQL (SELECT, FROM, JOIN) bằng màu sắc
58
  spring.jpa.properties.hibernate.highlight_sql=false
 
18
  # spring.security.oauth2.client.registration.google.scope=email,profile
19
 
20
  # DEVTOOLS & DEVELOPMENT MODE (Dành cho Production)
21
+ spring.devtools.restart.enabled=${DEV_TOOL:false}
22
+ spring.devtools.livereload.enabled=${DEV_TOOL:false}
23
+ spring.devtools.add-properties=${DEV_TOOL:false}
24
  # JSP:
25
 
26
  spring.mvc.view.cache=true
 
49
 
50
  # Bật tính năng in câu lệnh SQL ra console
51
  #Nên bật khi code, khi deploy thì tắt
52
+ spring.jpa.show-sql=${SHOW_SQL:false}
53
 
54
  # Format SQL cho dễ đọc (xuống dòng, thụt lề), nếu không nó sẽ in thành 1 hàng ngang rất rối
55
+ spring.jpa.properties.hibernate.format_sql=true
56
 
57
  # Làm nổi bật các từ khóa SQL (SELECT, FROM, JOIN) bằng màu sắc
58
  spring.jpa.properties.hibernate.highlight_sql=false
src/main/resources/static/js/loadmore.js ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", function () {
2
+ const loadMoreBtn = document.getElementById("load-more-btn");
3
+ const newsGrid = document.querySelector(".grid-3");
4
+
5
+ if (loadMoreBtn) {
6
+ loadMoreBtn.addEventListener("click", async function () {
7
+ // Lấy thông số trang hiện tại
8
+ let currentPage = parseInt(loadMoreBtn.getAttribute("data-page"));
9
+ let size = parseInt(loadMoreBtn.getAttribute("data-size"));
10
+
11
+ // Đổi text thành Đang tải để tăng UX
12
+ const originalText = loadMoreBtn.innerText;
13
+ loadMoreBtn.innerText = "Đang tải...";
14
+ loadMoreBtn.disabled = true;
15
+
16
+ try {
17
+ // Gọi API backend
18
+ const response = await fetch(
19
+ `/api/article/more?page=${currentPage}&size=${size}`,
20
+ );
21
+ if (!response.ok) throw new Error("Network response was not ok");
22
+
23
+ const data = await response.json();
24
+
25
+ const template = document.getElementById("news-card-template");
26
+
27
+ data.articles.forEach((article) => {
28
+ // 1. Nhân bản (clone) cái khung HTML trống
29
+ const clone = template.content.cloneNode(true);
30
+
31
+ // 2. Trỏ tới từng element và nhét dữ liệu (textContent tự động chống lỗi XSS)
32
+ const img = clone.querySelector(".card-img");
33
+ img.src = article.thumbnailUrl;
34
+ img.alt = article.title;
35
+
36
+ clone.querySelector(".card-tag").textContent = article.type;
37
+ clone.querySelector(".card-title").textContent = article.title;
38
+ clone.querySelector(".card-date").textContent = article.createdAt;
39
+ clone.querySelector(".card-link").href = `/news/${article.id}`;
40
+
41
+ // 3. Nhét cục DOM đã hoàn thiện vào danh sách
42
+ newsGrid.appendChild(clone);
43
+ });
44
+ // Xử lý trạng thái nút bấm
45
+ if (data.hasMore) {
46
+ // Tăng số trang lên 1 cho lần bấm tiếp theo
47
+ loadMoreBtn.setAttribute("data-page", currentPage + 1);
48
+ loadMoreBtn.innerText = originalText;
49
+ loadMoreBtn.disabled = false;
50
+ } else {
51
+ // Ẩn nút đi nếu đã hết dữ liệu
52
+ loadMoreBtn.style.display = "none";
53
+ }
54
+ } catch (error) {
55
+ console.error("Lỗi khi tải thêm bài viết:", error);
56
+ loadMoreBtn.innerText = "Lỗi kết nối. Thử lại!";
57
+ loadMoreBtn.disabled = false;
58
+ }
59
+ });
60
+ }
61
+ });
src/main/webapp/WEB-INF/views/layout/footer.jsp CHANGED
@@ -182,6 +182,7 @@
182
  });
183
  </script> -->
184
  <script src="${pageContext.request.contextPath}/js/index2.js"></script>
 
185
  </body>
186
 
187
  </html>
 
182
  });
183
  </script> -->
184
  <script src="${pageContext.request.contextPath}/js/index2.js"></script>
185
+ <script src="${pageContext.request.contextPath}/js/loadmore.js"></script>
186
  </body>
187
 
188
  </html>
src/main/webapp/WEB-INF/views/news/news.jsp CHANGED
@@ -36,7 +36,7 @@
36
  class="news-featured-img"
37
  src="${featuredArticle.thumbnailUrl}"
38
  alt="${featuredArticle.title}"
39
- loading="lazy" />
40
 
41
  <div
42
  style="
@@ -66,16 +66,6 @@
66
  ${featuredArticle.title}
67
  </h2>
68
 
69
- <p
70
- class="font-crimson"
71
- style="
72
- color: var(--mid-grey);
73
- font-size: 1rem;
74
- line-height: 1.7;
75
- margin-bottom: 1.5rem;
76
- ">
77
- </p>
78
-
79
  <div style="display: flex; gap: 1rem">
80
 
81
  <a href="/news/${featuredArticle.id}"
@@ -183,15 +173,29 @@
183
  </div>
184
  <c:if test="${hasMore}">
185
  <div class="text-center mt-4">
186
- <a href="?limit=${nextLimit}#news-list"
187
- class="btn btn-secondary">
188
- Hiện thêm
189
- </a>
 
 
190
  </div>
191
  </c:if>
192
  </div>
193
  </section>
194
-
 
 
 
 
 
 
 
 
 
 
 
 
195
  <!-- NEWSLETTER
196
  <section
197
  style="
 
36
  class="news-featured-img"
37
  src="${featuredArticle.thumbnailUrl}"
38
  alt="${featuredArticle.title}"
39
+ />
40
 
41
  <div
42
  style="
 
66
  ${featuredArticle.title}
67
  </h2>
68
 
 
 
 
 
 
 
 
 
 
 
69
  <div style="display: flex; gap: 1rem">
70
 
71
  <a href="/news/${featuredArticle.id}"
 
173
  </div>
174
  <c:if test="${hasMore}">
175
  <div class="text-center mt-4">
176
+ <button id="load-more-btn"
177
+ class="btn btn-secondary"
178
+ data-page="1"
179
+ data-size="6">
180
+ Hiện thêm
181
+ </button>
182
  </div>
183
  </c:if>
184
  </div>
185
  </section>
186
+ <template id="news-card-template">
187
+ <div class="card">
188
+ <img class="card-img" src="" alt="" loading="lazy">
189
+ <div class="card-body">
190
+ <span class="card-tag"></span>
191
+ <h3 class="card-title"></h3>
192
+ </div>
193
+ <div class="card-footer">
194
+ <span class="font-Cormorant Garamond text-grey card-date"></span>
195
+ <a href="" class="text-crimson font-Lora card-link">Đọc thêm →</a>
196
+ </div>
197
+ </div>
198
+ </template>
199
  <!-- NEWSLETTER
200
  <section
201
  style="