feat: implement automated Swiper slider seeding and integrate slider component into training details page
This commit is contained in:
@@ -157,3 +157,4 @@ coverage/
|
||||
######################
|
||||
src/main/docker/oracle-data/
|
||||
|
||||
.gitnexus/
|
||||
|
||||
Generated
+3409
-51
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,8 @@
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"gitnexus:analyze": "gitnexus analyze",
|
||||
"gitnexus:setup": "gitnexus setup",
|
||||
"app:start": "./gradlew",
|
||||
"app:up": "docker compose -f src/main/docker/app.yml up --wait",
|
||||
"backend:build-cache": "npm run backend:info && npm run backend:nohttp:test && npm run ci:e2e:package -- -x webapp -x webapp_test",
|
||||
@@ -42,6 +44,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"generator-jhipster": "9.1.0",
|
||||
"gitnexus": "^1.6.9",
|
||||
"prettier": "3.8.3",
|
||||
"prettier-plugin-java": "2.9.6",
|
||||
"prettier-plugin-packagejson": "3.0.2"
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import oracledb
|
||||
import json
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Connect to the Oracle database
|
||||
try:
|
||||
connection = oracledb.connect(
|
||||
user="sisvietnam",
|
||||
password="sisvietnam",
|
||||
dsn="localhost:1521/sisvietnam"
|
||||
)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
# Get the ID of the 'education' tag
|
||||
sql_tag = "SELECT id, name FROM sis_tag WHERE name LIKE '%education%' FETCH FIRST 1 ROWS ONLY"
|
||||
cursor.execute(sql_tag)
|
||||
tag = cursor.fetchone()
|
||||
|
||||
if not tag:
|
||||
logging.error("Tag 'education' not found.")
|
||||
else:
|
||||
tag_id = tag[0]
|
||||
tag_name = tag[1]
|
||||
logging.info(f"Found tag '{tag_name}' with ID {tag_id}")
|
||||
|
||||
# Fetch the latest 10 published posts with this tag
|
||||
sql_posts = """
|
||||
SELECT p.title, p.featured_image, p.slug, p.excerpt, p.created_date
|
||||
FROM sis_post p
|
||||
JOIN sis_post_tag pt ON p.id = pt.post_id
|
||||
WHERE pt.tag_id = :tag_id AND p.status = 'PUBLISHED'
|
||||
ORDER BY p.created_date DESC
|
||||
FETCH FIRST 10 ROWS ONLY
|
||||
"""
|
||||
cursor.execute(sql_posts, [tag_id])
|
||||
posts = cursor.fetchall()
|
||||
|
||||
import random
|
||||
prices = ["42.400.000 VNĐ", "23.000.000 VNĐ", "32.500.000 VNĐ", "15.000.000 VNĐ", "Liên hệ"]
|
||||
badges = ["SAT", "SUN", "MON", "TUE", "WED", "THU", "FRI"]
|
||||
|
||||
items = []
|
||||
for post in posts:
|
||||
img = post[1] if post[1] else ""
|
||||
desc = post[3] if post[3] else ""
|
||||
date_val = post[4]
|
||||
date_str = date_val.strftime("%d/%m/%Y") if hasattr(date_val, 'strftime') else str(date_val) if date_val else "12/09/2026"
|
||||
|
||||
items.append({
|
||||
"title": post[0],
|
||||
"imageUrl": img,
|
||||
"linkUrl": f"/dao-tao/{post[2]}",
|
||||
"description": desc,
|
||||
"badge": random.choice(badges),
|
||||
"price": random.choice(prices),
|
||||
"dateStr": date_str
|
||||
})
|
||||
|
||||
new_group = {
|
||||
"slug": "chuong-trinh-khac",
|
||||
"name": "Chương trình khác",
|
||||
"layoutType": "course-card",
|
||||
"description": "",
|
||||
"className": "",
|
||||
"notes": f"Tự động tạo bằng Python từ thẻ {tag_name}",
|
||||
"items": items
|
||||
}
|
||||
|
||||
# Get existing settings
|
||||
sql_setting = "SELECT setting_value FROM sis_setting WHERE setting_key = 'plugin_swiper_slider_data'"
|
||||
cursor.execute(sql_setting)
|
||||
setting = cursor.fetchone()
|
||||
|
||||
if setting and setting[0]:
|
||||
try:
|
||||
# In Oracle CLOB is sometimes returned as LOB object, read it if so
|
||||
val = setting[0]
|
||||
if hasattr(val, 'read'):
|
||||
val = val.read()
|
||||
groups = json.loads(val)
|
||||
except Exception as e:
|
||||
logging.error(f"Error parsing JSON: {e}")
|
||||
groups = []
|
||||
else:
|
||||
groups = []
|
||||
|
||||
# Remove existing 'chuong-trinh-khac' if present
|
||||
groups = [g for g in groups if g.get('slug') != 'chuong-trinh-khac']
|
||||
|
||||
# Add the new group
|
||||
groups.append(new_group)
|
||||
|
||||
new_setting_value = json.dumps(groups, ensure_ascii=False)
|
||||
|
||||
# Update the database
|
||||
if setting:
|
||||
sql_update = "UPDATE sis_setting SET setting_value = :val WHERE setting_key = 'plugin_swiper_slider_data'"
|
||||
cursor.execute(sql_update, [new_setting_value])
|
||||
else:
|
||||
sql_insert = "INSERT INTO sis_setting (setting_key, setting_value) VALUES ('plugin_swiper_slider_data', :val)"
|
||||
cursor.execute(sql_insert, [new_setting_value])
|
||||
|
||||
connection.commit()
|
||||
logging.info("Successfully updated sis_setting with the new slider group.")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"An error occurred: {e}")
|
||||
finally:
|
||||
if 'connection' in locals():
|
||||
connection.close()
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.sisvietnamvn.web.config;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sisvietnamvn.web.domain.Post;
|
||||
import com.sisvietnamvn.web.domain.Tag;
|
||||
import com.sisvietnamvn.web.plugins.swiperslider.SwiperSliderAdminController.SliderGroup;
|
||||
import com.sisvietnamvn.web.plugins.swiperslider.SwiperSliderAdminController.SliderItem;
|
||||
import com.sisvietnamvn.web.repository.PostRepository;
|
||||
import com.sisvietnamvn.web.repository.TagRepository;
|
||||
import com.sisvietnamvn.web.service.SettingService;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class SliderSeeder implements CommandLineRunner {
|
||||
|
||||
private final PostRepository postRepository;
|
||||
private final TagRepository tagRepository;
|
||||
private final SettingService settingService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public SliderSeeder(PostRepository postRepository, TagRepository tagRepository, SettingService settingService, ObjectMapper objectMapper) {
|
||||
this.postRepository = postRepository;
|
||||
this.tagRepository = tagRepository;
|
||||
this.settingService = settingService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
System.out.println("=== STARTING SWIPER SLIDER SEEDER ===");
|
||||
|
||||
Optional<Tag> tagOpt = tagRepository.findByName("Đào tạo");
|
||||
if (tagOpt.isEmpty()) {
|
||||
List<Tag> possible = tagRepository.findByNameContainingIgnoreCase("Đào tạo");
|
||||
if (!possible.isEmpty()) {
|
||||
tagOpt = Optional.of(possible.get(0));
|
||||
System.out.println("Using fallback tag: " + tagOpt.get().getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (tagOpt.isEmpty()) {
|
||||
System.out.println("Tag 'Đào tạo' not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Post> posts = postRepository.findByTagId(tagOpt.get().getId(), PageRequest.of(0, 10));
|
||||
System.out.println("Found " + posts.size() + " posts with tag " + tagOpt.get().getName());
|
||||
|
||||
List<SliderItem> items = posts.stream().map(post -> {
|
||||
String imageUrl = post.getFeaturedImage() != null ? post.getFeaturedImage() : "";
|
||||
String linkUrl = "/dao-tao/" + post.getSlug();
|
||||
return new SliderItem(post.getTitle(), imageUrl, linkUrl, "", null, null, null);
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
SliderGroup newGroup = new SliderGroup(
|
||||
"chuong-trinh-khac",
|
||||
"Chương trình khác",
|
||||
"card",
|
||||
"",
|
||||
"",
|
||||
"Tự động tạo từ thẻ " + tagOpt.get().getName(),
|
||||
items
|
||||
);
|
||||
|
||||
String json = settingService.getValue("plugin_swiper_slider_data", "[]");
|
||||
List<SliderGroup> groups = new ArrayList<>();
|
||||
try {
|
||||
groups = objectMapper.readValue(json, new TypeReference<List<SliderGroup>>() {});
|
||||
} catch (Exception e) {}
|
||||
|
||||
groups.removeIf(g -> "chuong-trinh-khac".equals(g.slug()));
|
||||
groups.add(newGroup);
|
||||
|
||||
String newJson = objectMapper.writeValueAsString(groups);
|
||||
settingService.setValue("plugin_swiper_slider_data", newJson);
|
||||
System.out.println("=== SWIPER SLIDER SEEDED SUCCESSFULLY ===");
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,48 @@ public class PageController {
|
||||
@GetMapping("/specialty")
|
||||
public String getSpecialty(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.SPECIALTY), model); }
|
||||
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private com.sisvietnamvn.web.repository.TagRepository tagRepository;
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private com.sisvietnamvn.web.plugins.swiperslider.SwiperSliderPlugin swiperSliderPlugin;
|
||||
|
||||
@GetMapping("/run-seeder")
|
||||
@org.springframework.web.bind.annotation.ResponseBody
|
||||
public String runSeeder() {
|
||||
java.util.Optional<com.sisvietnamvn.web.domain.Tag> tagOpt = tagRepository.findByName("Đào tạo");
|
||||
if (tagOpt.isEmpty()) {
|
||||
java.util.List<com.sisvietnamvn.web.domain.Tag> possible = tagRepository.findByNameContainingIgnoreCase("Đào tạo");
|
||||
if (!possible.isEmpty()) {
|
||||
tagOpt = java.util.Optional.of(possible.get(0));
|
||||
}
|
||||
}
|
||||
if (tagOpt.isEmpty()) return "Tag not found";
|
||||
|
||||
java.util.List<com.sisvietnamvn.web.domain.Post> posts = postRepository.findByTagId(tagOpt.get().getId(), org.springframework.data.domain.PageRequest.of(0, 10));
|
||||
|
||||
java.util.List<com.sisvietnamvn.web.plugins.swiperslider.SwiperSliderAdminController.SliderItem> items = posts.stream().map(post -> {
|
||||
String imageUrl = post.getFeaturedImage() != null ? post.getFeaturedImage() : "";
|
||||
return new com.sisvietnamvn.web.plugins.swiperslider.SwiperSliderAdminController.SliderItem(post.getTitle(), imageUrl, "/dao-tao/" + post.getSlug(), "", null, null, null);
|
||||
}).collect(java.util.stream.Collectors.toList());
|
||||
|
||||
com.sisvietnamvn.web.plugins.swiperslider.SwiperSliderAdminController.SliderGroup newGroup = new com.sisvietnamvn.web.plugins.swiperslider.SwiperSliderAdminController.SliderGroup(
|
||||
"chuong-trinh-khac", "Chương trình khác", "card", "", "", "Tự động tạo từ thẻ " + tagOpt.get().getName(), items
|
||||
);
|
||||
|
||||
String json = settingService.getValue("plugin_swiper_slider_data", "[]");
|
||||
java.util.List<com.sisvietnamvn.web.plugins.swiperslider.SwiperSliderAdminController.SliderGroup> groups = new java.util.ArrayList<>();
|
||||
try {
|
||||
groups = objectMapper.readValue(json, new com.fasterxml.jackson.core.type.TypeReference<>() {});
|
||||
} catch (Exception e) {}
|
||||
groups.removeIf(g -> "chuong-trinh-khac".equals(g.slug()));
|
||||
groups.add(newGroup);
|
||||
try {
|
||||
settingService.setValue("plugin_swiper_slider_data", objectMapper.writeValueAsString(groups));
|
||||
} catch (Exception e) {}
|
||||
|
||||
return "Seeded " + items.size() + " posts!";
|
||||
}
|
||||
|
||||
@GetMapping("/doctor")
|
||||
@org.springframework.transaction.annotation.Transactional(readOnly = true)
|
||||
public String getDoctor(Model model) {
|
||||
|
||||
+3
-2
@@ -151,11 +151,12 @@ public class SwiperSliderAdminController {
|
||||
return "redirect:/manage/plugins/swiper-slider";
|
||||
}
|
||||
|
||||
public record SliderGroup(String slug, String name, String layoutType, String outerTemplate, String itemTemplate, List<SliderItem> items) {
|
||||
public record SliderGroup(String slug, String name, String layoutType, String outerTemplate, String itemTemplate, String note, List<SliderItem> items) {
|
||||
public String getSlug() { return slug; }
|
||||
public String getName() { return name; }
|
||||
public String getLayoutType() { return layoutType; }
|
||||
public String getNote() { return note; }
|
||||
public List<SliderItem> getItems() { return items; }
|
||||
}
|
||||
public record SliderItem(String title, String imageUrl, String linkUrl, String description) {}
|
||||
public record SliderItem(String title, String imageUrl, String linkUrl, String description, String badge, String price, String dateStr) {}
|
||||
}
|
||||
|
||||
+13
-2
@@ -134,13 +134,16 @@ public class SwiperSliderPlugin {
|
||||
Optional<SwiperSliderAdminController.SliderGroup> groupOpt = groups.stream()
|
||||
.filter(g -> slug.equals(g.slug()))
|
||||
.findFirst();
|
||||
if (groupOpt.isEmpty() || groupOpt.get().items() == null || groupOpt.get().items().isEmpty() || json.contains("UMC_files")) {
|
||||
|
||||
// If UMC_files is present, we replace all data with defaults (only happens once during migration)
|
||||
if (json.contains("UMC_files")) {
|
||||
json = forceSeedAllData();
|
||||
groups = parseGroups(json);
|
||||
groupOpt = groups.stream()
|
||||
.filter(g -> slug.equals(g.slug()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
return groupOpt;
|
||||
}
|
||||
|
||||
@@ -224,6 +227,8 @@ public class SwiperSliderPlugin {
|
||||
slideTemplate = getSlideTemplateForSlug("chung-chi");
|
||||
} else if ("large-image".equals(layoutType)) {
|
||||
slideTemplate = getSlideTemplateForSlug("hinh-anh-thuc-hanh");
|
||||
} else if ("course-card".equals(layoutType)) {
|
||||
slideTemplate = getSlideTemplateForSlug("course-card");
|
||||
} else if ("custom".equals(layoutType) && group.itemTemplate() != null && !group.itemTemplate().trim().isEmpty()) {
|
||||
slideTemplate = group.itemTemplate();
|
||||
} else {
|
||||
@@ -236,7 +241,10 @@ public class SwiperSliderPlugin {
|
||||
.replace("{{title}}", item.title() != null ? item.title() : "")
|
||||
.replace("{{imageUrl}}", item.imageUrl() != null ? item.imageUrl() : "")
|
||||
.replace("{{linkUrl}}", item.linkUrl() != null ? item.linkUrl() : "")
|
||||
.replace("{{description}}", item.description() != null ? item.description() : "");
|
||||
.replace("{{description}}", item.description() != null ? item.description() : "")
|
||||
.replace("{{date}}", item.dateStr() != null ? item.dateStr() : "")
|
||||
.replace("{{badge}}", item.badge() != null ? item.badge() : "")
|
||||
.replace("{{price}}", item.price() != null ? item.price() : "");
|
||||
slidesHtml.append(slide);
|
||||
}
|
||||
return slidesHtml.toString();
|
||||
@@ -252,6 +260,9 @@ public class SwiperSliderPlugin {
|
||||
if ("hinh-anh-thuc-hanh".equals(slug)) {
|
||||
return "<div class=\"swiper-slide max-w-[789px] !mr-2\" style=\"width: 789px; max-width: 100%;\"><div class=\"relative rounded-lg aspect-[789/460] overflow-hidden\"><img src=\"{{imageUrl}}\" alt=\"{{title}}\" class=\"absolute inset-0 w-full h-full object-cover\"></div></div>";
|
||||
}
|
||||
if ("course-card".equals(slug)) {
|
||||
return "<div class=\"swiper-slide\" style=\"width: 261px; max-width: 100%;\"><article class=\"relative p-3 lg:p-4 bg-white rounded-lg border border-gray-100 group\" style=\"display: flex; flex-direction: column; gap: 14px;\"><a class=\"relative overflow-hidden rounded block\" style=\"aspect-ratio: 9/5;\" href=\"{{linkUrl}}\"><img alt=\"{{title}}\" class=\"object-cover h-full w-full\" src=\"{{imageUrl}}\"/><div class=\"absolute inset-0 w-full h-full bg-black/40 opacity-0 lg:group-hover:opacity-100 duration-300 ease-in-out\"></div></a><div class=\"px-1.5 w-full\" style=\"display: flex; flex-direction: column; gap: 14px;\"><div class=\"w-full space-y-1\"><a class=\"title-1 font-bold line-clamp-2 lg:group-hover:text-primary-600 duration-300 ease-in-out block\" style=\"font-size: 18px; height: 54px; line-height: 1.5;\" href=\"{{linkUrl}}\">{{title}}</a><div class=\"body-3 text-gray-700 line-clamp-3\" style=\"font-size: 14px; height: 64px; line-height: 1.5;\">{{description}}</div></div><div class=\"w-full h-px bg-gray-100\"></div><div class=\"flex justify-between items-center w-full\"><div class=\"flex gap-4 items-center self-stretch my-auto\"><div class=\"flex gap-1.5 items-center self-stretch my-auto\"><svg class=\"lucide lucide-calendar size-3.5\" fill=\"none\" height=\"14\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" viewBox=\"0 0 24 24\" width=\"14\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M8 2v4\"></path><path d=\"M16 2v4\"></path><rect height=\"18\" rx=\"2\" width=\"18\" x=\"3\" y=\"4\"></rect><path d=\"M3 10h18\"></path></svg><time class=\"self-stretch body-3 my-auto\" style=\"font-size: 14px;\">{{date}}</time></div><span class=\"text-primary-600 font-bold border-b-2 border-primary-600\" style=\"font-size: 14px;\">{{badge}}</span></div></div></div><a class=\"p-2 lg:p-3 w-full text-white bg-primary-600 rounded lg:hover:bg-primary-300 duration-300 ease-in-out block mt-2\" href=\"{{linkUrl}}\"><div class=\"flex items-center gap-0.5 justify-center w-full font-bold\" style=\"font-size: 18px;\"><span>{{price}}</span></div></a></article></div>";
|
||||
}
|
||||
return "<div class=\"swiper-slide\"><div class=\"relative rounded-lg aspect-[3/2] overflow-hidden\"><img src=\"{{imageUrl}}\" alt=\"{{title}}\" class=\"absolute inset-0 w-full h-full object-cover\"></div></div>";
|
||||
}
|
||||
|
||||
|
||||
@@ -1212,3 +1212,37 @@ figure.table table tr:hover {
|
||||
}
|
||||
}
|
||||
|
||||
.education {
|
||||
.flex {
|
||||
display: flex !important;
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.rounded-full {
|
||||
justify-content: center;
|
||||
background-color: var(--color-white);
|
||||
border-radius: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
.training-sidebar {
|
||||
.gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.text-white {
|
||||
margin-bottom: 4px;
|
||||
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 32px;
|
||||
background-color: var(--color-background) !important;
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
a {
|
||||
text-decoration: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{themes/__${activeTheme}__/layout(forceFullWidth=true, bodyClass='education path-frontpage')}">
|
||||
<!doctype html>
|
||||
<html
|
||||
lang="vi"
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{themes/__${activeTheme}__/layout(forceFullWidth=true, bodyClass='education path-frontpage')}"
|
||||
>
|
||||
<head>
|
||||
<title th:text="${post.title}">Chi tiết đào tạo</title>
|
||||
<style>
|
||||
.overview-wrapper-shadow {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||
0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
@@ -138,7 +145,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<section class="bg-gray-50 training-section" style="padding-top: 1.5rem; padding-bottom: 1.5rem;">
|
||||
<section class="bg-gray-50 training-section" style="padding-top: 1.5rem; padding-bottom: 1.5rem">
|
||||
<div class="container mx-auto px-4 training-container">
|
||||
<div class="training-layout relative">
|
||||
<div class="overview-wrapper-shadow training-main">
|
||||
@@ -146,12 +153,12 @@
|
||||
<h1 class="display-7 text-primary-600 xl:mb-4 md:mb-3 mb-2" th:text="${post.title}">Tiêu đề Khóa học</h1>
|
||||
<!-- Ảnh đại diện -->
|
||||
<div class="img-container" th:if="${post.featuredImage != null and !#strings.isEmpty(post.featuredImage)}">
|
||||
<img th:src="${post.featuredImage}" th:alt="${post.title}" loading="lazy" decoding="async">
|
||||
<img th:src="${post.featuredImage}" th:alt="${post.title}" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
<div class="prose prose-blog prose-table prose-content max-w-none" id="postContentParsed">
|
||||
<!-- Nội dung chi tiết sẽ được render tại đây -->
|
||||
</div>
|
||||
<div id="postContentRaw" style="display:none;" th:text="${post.content}"></div>
|
||||
<div id="postContentRaw" style="display: none" th:text="${post.content}"></div>
|
||||
</div>
|
||||
<!-- Sidebar info -->
|
||||
<div id="info" class="training-sidebar">
|
||||
@@ -167,136 +174,189 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Swiper CSS (Must be inside layout fragment to not be stripped by Thymeleaf) -->
|
||||
<link rel="stylesheet" th:href="@{/css/swiper-bundle.min.css}" />
|
||||
<style>
|
||||
.swiper-button-lock {
|
||||
display: none !important;
|
||||
}
|
||||
.swiper-button-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
[class*='btn-navigation-']:hover:not([disabled]):not(.swiper-button-disabled):not(.swiper-button-lock) {
|
||||
background-color: var(--color-primary-600);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Related Courses (Chương trình khác) -->
|
||||
<section class="related-courses-section" style="background-color: #f0f9ff; padding: 3rem 0; margin-top: 2rem;">
|
||||
<section class="py-6 xl:py-12 md:py-8 bg-white">
|
||||
<div class="container mx-auto px-4">
|
||||
<h2 class="text-center mb-8 font-bold" style="font-size: 1.5rem; color: var(--color-primary-600);">Chương trình khác</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<!-- Card 1 -->
|
||||
<div style="background-color: white; border-radius: 0.5rem; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid #e5e7eb;">
|
||||
<div style="aspect-ratio: 16/9; background-color: #e2e8f0;"></div>
|
||||
<div style="padding: 1rem;">
|
||||
<h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; line-height: 1.4;">Chương trình thực hành 12 tháng Bác sĩ y khoa</h3>
|
||||
<p style="color: #6b7280; font-size: 0.875rem; margin-bottom: 1rem;">12/09/2026</p>
|
||||
<a href="#" style="display: block; width: 100%; text-align: center; background-color: var(--color-primary-600, #a82024); color: white; padding: 0.5rem; border-radius: 0.25rem; font-weight: 600; text-decoration: none;">42.400.000 VNĐ</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card 2 -->
|
||||
<div style="background-color: white; border-radius: 0.5rem; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid #e5e7eb;">
|
||||
<div style="aspect-ratio: 16/9; background-color: #e2e8f0;"></div>
|
||||
<div style="padding: 1rem;">
|
||||
<h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; line-height: 1.4;">Lớp Nghiệp vụ hộ lý trợ giúp chăm sóc</h3>
|
||||
<p style="color: #6b7280; font-size: 0.875rem; margin-bottom: 1rem;">01/04/2026</p>
|
||||
<a href="#" style="display: block; width: 100%; text-align: center; background-color: var(--color-primary-600, #a82024); color: white; padding: 0.5rem; border-radius: 0.25rem; font-weight: 600; text-decoration: none;">23.000.000 VNĐ</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card 3 -->
|
||||
<div style="background-color: white; border-radius: 0.5rem; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid #e5e7eb;">
|
||||
<div style="aspect-ratio: 16/9; background-color: #e2e8f0;"></div>
|
||||
<div style="padding: 1rem;">
|
||||
<h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; line-height: 1.4;">Thực hành cấp cứu và điều trị đột quỵ</h3>
|
||||
<p style="color: #6b7280; font-size: 0.875rem; margin-bottom: 1rem;">29/12/2025</p>
|
||||
<a href="#" style="display: block; width: 100%; text-align: center; background-color: var(--color-primary-600, #a82024); color: white; padding: 0.5rem; border-radius: 0.25rem; font-weight: 600; text-decoration: none;">32.500.000 VNĐ</a>
|
||||
<h2 class="display-7 text-primary-600 text-center xl:mb-8 md:mb-6 mb-4">Chương trình khác</h2>
|
||||
<div class="relative md:flex md:items-center">
|
||||
<button
|
||||
class="btn-navigation flex-shrink-0 lg:mr-4 mr-2 !relative z-10 md:size-[42px] size-[32px] items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group btn-navigation-chuong-trinh-khac-prev md:!flex hidden"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-chevron-up size-4 -rotate-90"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m18 15-6-6-6 6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
class="swiper swiper-chuong-trinh-khac [&>.swiper-pagination]:!static [&>.swiper-pagination]:mt-1 lg:[&>.swiper-pagination]:!hidden w-full"
|
||||
>
|
||||
<div class="swiper-wrapper">
|
||||
<th:block th:utext="${hookManager.applyFilters('swiper_slider_items', '', 'chuong-trinh-khac')}"></th:block>
|
||||
</div>
|
||||
<div class="swiper-pagination"></div>
|
||||
</div>
|
||||
<button
|
||||
class="btn-navigation flex-shrink-0 lg:ml-4 ml-2 !relative z-10 md:size-[42px] size-[32px] items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group btn-navigation-chuong-trinh-khac-next md:!flex hidden"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-chevron-down size-4 -rotate-90"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Content rendering and dynamic tabs parsing -->
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
var rawData = document.getElementById("postContentRaw").textContent;
|
||||
var parsedContainer = document.getElementById("postContentParsed");
|
||||
|
||||
// Content is now HTML
|
||||
parsedContainer.innerHTML = rawData;
|
||||
|
||||
// Parse dynamic tabs from HTML (migrated from Editor.js)
|
||||
var tabStarts = Array.from(parsedContainer.querySelectorAll('h3.dynamic-tab-start'));
|
||||
if (tabStarts.length > 0) {
|
||||
var tabsGroup = [];
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'custom-tabs-wrapper training-course-tabs-wrapper';
|
||||
|
||||
var navContainer = document.createElement('div');
|
||||
navContainer.className = 'custom-tabs-nav-container';
|
||||
var tabsContainer = document.createElement('div');
|
||||
tabsContainer.className = 'tabs-container';
|
||||
navContainer.appendChild(tabsContainer);
|
||||
wrapper.appendChild(navContainer);
|
||||
|
||||
var firstTabStart = tabStarts[0];
|
||||
var parentNode = firstTabStart.parentNode;
|
||||
|
||||
// Collect all elements into tabs
|
||||
tabStarts.forEach(function(startEl, index) {
|
||||
var tabTitle = startEl.textContent;
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'dao-tao-tab-btn custom-tab-btn' + (index === 0 ? ' active' : '');
|
||||
btn.setAttribute('data-tab-idx', index);
|
||||
btn.textContent = tabTitle;
|
||||
tabsContainer.appendChild(btn);
|
||||
|
||||
var panel = document.createElement('div');
|
||||
panel.className = 'dao-tao-tab-panel custom-tab-panel';
|
||||
panel.setAttribute('data-tab-idx', index);
|
||||
panel.style.display = (index === 0) ? 'block' : 'none';
|
||||
|
||||
var currentEl = startEl.nextElementSibling;
|
||||
while (currentEl && !currentEl.classList.contains('dynamic-tab-start') && !currentEl.classList.contains('dynamic-tab-end')) {
|
||||
var nextEl = currentEl.nextElementSibling;
|
||||
panel.appendChild(currentEl);
|
||||
currentEl = nextEl;
|
||||
}
|
||||
|
||||
// Remove the dynamic-tab-end if present
|
||||
if (currentEl && currentEl.classList.contains('dynamic-tab-end')) {
|
||||
currentEl.parentNode.removeChild(currentEl);
|
||||
}
|
||||
|
||||
wrapper.appendChild(panel);
|
||||
startEl.parentNode.removeChild(startEl);
|
||||
});
|
||||
|
||||
parentNode.appendChild(wrapper);
|
||||
|
||||
// Tab click listener
|
||||
wrapper.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('custom-tab-btn')) {
|
||||
var btn = e.target;
|
||||
var idx = btn.getAttribute('data-tab-idx');
|
||||
|
||||
wrapper.querySelectorAll('.custom-tab-btn').forEach(function(b) {
|
||||
b.classList.remove('active');
|
||||
});
|
||||
btn.classList.add('active');
|
||||
|
||||
wrapper.querySelectorAll('.custom-tab-panel').forEach(function(p) {
|
||||
p.style.display = 'none';
|
||||
});
|
||||
var activePanel = wrapper.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
|
||||
if (activePanel) activePanel.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
<!-- Add Swiper JS -->
|
||||
<script th:src="@{/js/swiper-bundle.min.js}"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const el = document.querySelector('.swiper-chuong-trinh-khac');
|
||||
if (el && typeof Swiper !== 'undefined' && !el.classList.contains('swiper-initialized')) {
|
||||
new Swiper('.swiper-chuong-trinh-khac', {
|
||||
slidesPerView: 'auto',
|
||||
spaceBetween: 16,
|
||||
navigation: {
|
||||
nextEl: '.btn-navigation-chuong-trinh-khac-next',
|
||||
prevEl: '.btn-navigation-chuong-trinh-khac-prev',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<!-- Content rendering and dynamic tabs parsing -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var rawData = document.getElementById('postContentRaw').textContent;
|
||||
var parsedContainer = document.getElementById('postContentParsed');
|
||||
|
||||
// Content is now HTML
|
||||
parsedContainer.innerHTML = rawData;
|
||||
|
||||
// Parse dynamic tabs from HTML (migrated from Editor.js)
|
||||
var tabStarts = Array.from(parsedContainer.querySelectorAll('h3.dynamic-tab-start'));
|
||||
if (tabStarts.length > 0) {
|
||||
var tabsGroup = [];
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'custom-tabs-wrapper training-course-tabs-wrapper';
|
||||
|
||||
var navContainer = document.createElement('div');
|
||||
navContainer.className = 'custom-tabs-nav-container';
|
||||
var tabsContainer = document.createElement('div');
|
||||
tabsContainer.className = 'tabs-container';
|
||||
navContainer.appendChild(tabsContainer);
|
||||
wrapper.appendChild(navContainer);
|
||||
|
||||
var firstTabStart = tabStarts[0];
|
||||
var parentNode = firstTabStart.parentNode;
|
||||
|
||||
// Collect all elements into tabs
|
||||
tabStarts.forEach(function (startEl, index) {
|
||||
var tabTitle = startEl.textContent;
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'dao-tao-tab-btn custom-tab-btn' + (index === 0 ? ' active' : '');
|
||||
btn.setAttribute('data-tab-idx', index);
|
||||
btn.textContent = tabTitle;
|
||||
tabsContainer.appendChild(btn);
|
||||
|
||||
var panel = document.createElement('div');
|
||||
panel.className = 'dao-tao-tab-panel custom-tab-panel';
|
||||
panel.setAttribute('data-tab-idx', index);
|
||||
panel.style.display = index === 0 ? 'block' : 'none';
|
||||
|
||||
var currentEl = startEl.nextElementSibling;
|
||||
while (currentEl && !currentEl.classList.contains('dynamic-tab-start') && !currentEl.classList.contains('dynamic-tab-end')) {
|
||||
var nextEl = currentEl.nextElementSibling;
|
||||
panel.appendChild(currentEl);
|
||||
currentEl = nextEl;
|
||||
}
|
||||
|
||||
// Remove the dynamic-tab-end if present
|
||||
if (currentEl && currentEl.classList.contains('dynamic-tab-end')) {
|
||||
currentEl.parentNode.removeChild(currentEl);
|
||||
}
|
||||
|
||||
wrapper.appendChild(panel);
|
||||
startEl.parentNode.removeChild(startEl);
|
||||
});
|
||||
|
||||
parentNode.appendChild(wrapper);
|
||||
|
||||
// Tab click listener
|
||||
wrapper.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('custom-tab-btn')) {
|
||||
var btn = e.target;
|
||||
var idx = btn.getAttribute('data-tab-idx');
|
||||
|
||||
wrapper.querySelectorAll('.custom-tab-btn').forEach(function (b) {
|
||||
b.classList.remove('active');
|
||||
});
|
||||
btn.classList.add('active');
|
||||
|
||||
wrapper.querySelectorAll('.custom-tab-panel').forEach(function (p) {
|
||||
p.style.display = 'none';
|
||||
});
|
||||
var activePanel = wrapper.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
|
||||
if (activePanel) activePanel.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
function switchTrainingTab(btn, tabId) {
|
||||
var container = btn.closest('.training-course-tabs-wrapper');
|
||||
if(!container) return;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
var btns = container.querySelectorAll('.dao-tao-tab-btn');
|
||||
for(var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
|
||||
for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
|
||||
btn.classList.add('active');
|
||||
|
||||
|
||||
var panels = container.querySelectorAll('.dao-tao-tab-panel');
|
||||
for(var j = 0; j < panels.length; j++) panels[j].classList.remove('active');
|
||||
for (var j = 0; j < panels.length; j++) panels[j].classList.remove('active');
|
||||
var panel = container.querySelector('.dao-tao-tab-panel[data-tab-content="' + tabId + '"]');
|
||||
if(panel) panel.classList.add('active');
|
||||
if (panel) panel.classList.add('active');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,45 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<!-- Widget hiển thị thông tin động của khóa học (Ngày khai giảng, địa điểm, hình thức...) -->
|
||||
<div th:fragment="course-info" class="bg-primary-600 rounded-lg p-4 flex flex-col justify-center mb-4">
|
||||
<div class="xl:space-y-8 md:space-y-6 space-y-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<body>
|
||||
<!-- Widget hiển thị thông tin động của khóa học (Ngày khai giảng, địa điểm, hình thức...) -->
|
||||
<div th:fragment="course-info" class="bg-primary-600 rounded-lg p-4 flex flex-col justify-center mb-4" style="height: 100%">
|
||||
<div class="flex flex-col gap-3 xl:space-y-8 md:space-y-6 space-y-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-8 h-8 bg-white rounded-full flex items-center justify-center shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar size-3.5 text-primary-600"><path d="M8 2v4"></path><path d="M16 2v4"></path><rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-calendar size-3.5 text-primary-600"
|
||||
>
|
||||
<path d="M8 2v4"></path>
|
||||
<path d="M16 2v4"></path>
|
||||
<rect width="18" height="18" x="3" y="4" rx="2"></rect>
|
||||
<path d="M3 10h18"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white/80 text-sm">Ngày khai giảng</p>
|
||||
<p class="text-white font-semibold" th:text="${post.eventTime != null ? #temporals.format(post.eventTime, 'dd/MM/yyyy') : 'Đang cập nhật'}">Đang cập nhật</p>
|
||||
<p class="text-white text-sm">Ngày khai giảng</p>
|
||||
<p
|
||||
class="text-white font-semibold"
|
||||
th:text="${post.eventTime != null ? #temporals.format(post.eventTime, 'dd/MM/yyyy') : 'Đang cập nhật'}"
|
||||
>
|
||||
Đang cập nhật
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-8 h-8 bg-white rounded-full flex items-center justify-center shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin size-3.5 text-primary-600"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"></path><circle cx="12" cy="10" r="3"></circle></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-map-pin size-3.5 text-primary-600"
|
||||
>
|
||||
<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"></path>
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white/80 text-sm">Địa điểm</p>
|
||||
<p class="text-white font-semibold" th:text="${post.location != null ? post.location : 'SIS Cần Thơ'}">SIS Cần Thơ</p>
|
||||
<p class="text-white text-sm">Địa điểm</p>
|
||||
<p class="text-white font-semibold" th:text="${post.location != null ? post.location : 'SIS Cần Thơ'}">SIS Cần Thơ</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-8 h-8 bg-white rounded-full flex items-center justify-center shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users size-3.5 text-primary-600"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-users size-3.5 text-primary-600"
|
||||
>
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white/80 text-sm">Hình thức</p>
|
||||
<p class="text-white font-semibold">Trực tiếp / Tập trung</p>
|
||||
<p class="text-white text-sm">Hình thức</p>
|
||||
<p class="text-white font-semibold">Trực tiếp / Tập trung</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 mt-4 border-t border-white/20">
|
||||
<a href="#" class="block w-full py-3 px-4 bg-white text-primary-600 text-center font-bold rounded-lg hover:bg-gray-100 transition-colors">
|
||||
Đăng ký ngay
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 mt-4 border-t border-white/20">
|
||||
<div
|
||||
class="btn btn-primary bg-white block w-full py-3 px-4 bg-white text-primary-600 text-center font-bold rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<a href="#"> Đăng ký ngay </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+6
-1
@@ -37,7 +37,11 @@
|
||||
<label>Slug (Dùng cho Shortcode)</label>
|
||||
<input type="text" class="form-control" onchange="updateGroup('slug', this.value)" id="groupSlug" placeholder="Ví dụ: chung-chi" required />
|
||||
</div>
|
||||
<div class="col-md-12 tab-field mt-3">
|
||||
<div class="col-md-6 tab-field mt-3">
|
||||
<label>Ghi chú (Hiển thị ở danh sách)</label>
|
||||
<input type="text" class="form-control" onchange="updateGroup('note', this.value)" id="groupNote" placeholder="Ví dụ: Dùng cho trang chủ" />
|
||||
</div>
|
||||
<div class="col-md-6 tab-field mt-3">
|
||||
<label>Mẫu Giao Diện (Layout Type)</label>
|
||||
<select class="form-control" onchange="updateGroup('layoutType', this.value); toggleCustomTemplates(this.value)" id="groupLayoutType">
|
||||
<option value="">Mặc định (Dựa theo Slug, hỗ trợ tương thích ngược)</option>
|
||||
@@ -85,6 +89,7 @@
|
||||
function renderGroup() {
|
||||
document.getElementById('groupName').value = group.name || '';
|
||||
document.getElementById('groupSlug').value = group.slug || '';
|
||||
document.getElementById('groupNote').value = group.note || '';
|
||||
document.getElementById('groupLayoutType').value = group.layoutType || '';
|
||||
document.getElementById('groupOuterTemplate').value = group.outerTemplate || '';
|
||||
document.getElementById('groupItemTemplate').value = group.itemTemplate || '';
|
||||
|
||||
+3
-1
@@ -26,6 +26,7 @@
|
||||
<tr>
|
||||
<th>Tên Nhóm</th>
|
||||
<th>Slug (Shortcode)</th>
|
||||
<th>Ghi chú</th>
|
||||
<th>Mẫu Giao Diện</th>
|
||||
<th>Số lượng Slide</th>
|
||||
<th>Hành động</th>
|
||||
@@ -35,6 +36,7 @@
|
||||
<tr th:each="group : ${groups}">
|
||||
<td th:text="${group.name}"></td>
|
||||
<td><code th:text="${group.slug}"></code></td>
|
||||
<td><em th:text="${group.note ?: ''}"></em></td>
|
||||
<td th:text="${group.layoutType == 'gallery' ? 'Thư viện ảnh' : (group.layoutType == 'card' ? 'Dạng thẻ' : (group.layoutType == 'large-image' ? 'Hình ảnh lớn' : (group.layoutType == 'custom' ? 'Tùy chỉnh' : 'Mặc định')))}"></td>
|
||||
<td th:text="${group.items != null ? group.items.size() : 0}"></td>
|
||||
<td>
|
||||
@@ -46,7 +48,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${groups.empty}">
|
||||
<td colspan="5" class="text-center">Chưa có nhóm slider nào. Hãy tạo mới!</td>
|
||||
<td colspan="6" class="text-center">Chưa có nhóm slider nào. Hãy tạo mới!</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.sisvietnamvn.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import com.sisvietnamvn.web.repository.PostRepository;
|
||||
import com.sisvietnamvn.web.repository.TagRepository;
|
||||
import com.sisvietnamvn.web.service.SettingService;
|
||||
import com.sisvietnamvn.web.plugins.swiperslider.SwiperSliderAdminController.SliderGroup;
|
||||
import com.sisvietnamvn.web.plugins.swiperslider.SwiperSliderAdminController.SliderItem;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
@SpringBootTest
|
||||
public class SliderSeederTest {
|
||||
|
||||
@Autowired
|
||||
private PostRepository postRepository;
|
||||
@Autowired
|
||||
private TagRepository tagRepository;
|
||||
@Autowired
|
||||
private SettingService settingService;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Test
|
||||
public void testSeedSlider() throws Exception {
|
||||
java.util.Optional<com.sisvietnamvn.web.domain.Tag> tagOpt = tagRepository.findByName("Đào tạo");
|
||||
if (tagOpt.isEmpty()) {
|
||||
java.util.List<com.sisvietnamvn.web.domain.Tag> possible = tagRepository.findByNameContainingIgnoreCase("Đào tạo");
|
||||
if (!possible.isEmpty()) {
|
||||
tagOpt = java.util.Optional.of(possible.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
if (tagOpt.isEmpty()) {
|
||||
System.out.println("Tag 'Đào tạo' not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
java.util.List<com.sisvietnamvn.web.domain.Post> posts = postRepository.findByTagId(tagOpt.get().getId(), org.springframework.data.domain.PageRequest.of(0, 10));
|
||||
|
||||
java.util.List<SliderItem> items = posts.stream().map(post -> {
|
||||
String imageUrl = post.getFeaturedImage() != null ? post.getFeaturedImage() : "";
|
||||
String linkUrl = "/dao-tao/" + post.getSlug();
|
||||
return new SliderItem(post.getTitle(), imageUrl, linkUrl, "");
|
||||
}).collect(java.util.stream.Collectors.toList());
|
||||
|
||||
SliderGroup newGroup = new SliderGroup(
|
||||
"chuong-trinh-khac",
|
||||
"Chương trình khác",
|
||||
"card",
|
||||
"",
|
||||
"",
|
||||
"Tự động tạo từ thẻ " + tagOpt.get().getName(),
|
||||
items
|
||||
);
|
||||
|
||||
String json = settingService.getValue("plugin_swiper_slider_data", "[]");
|
||||
java.util.List<SliderGroup> groups = new java.util.ArrayList<>();
|
||||
try {
|
||||
groups = objectMapper.readValue(json, new TypeReference<>() {});
|
||||
} catch (Exception e) {}
|
||||
|
||||
groups.removeIf(g -> "chuong-trinh-khac".equals(g.slug()));
|
||||
groups.add(newGroup);
|
||||
|
||||
String newJson = objectMapper.writeValueAsString(groups);
|
||||
settingService.setValue("plugin_swiper_slider_data", newJson);
|
||||
System.out.println("=== SWIPER SLIDER SEEDED SUCCESSFULLY ===");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user