feat: implement specialty content module with expanded Page entity fields, database migrations, and data import utilities
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package com.sisvietnamvn.web.config;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.sisvietnamvn.web.repository.PageRepository;
|
||||
|
||||
@Component
|
||||
public class DebugRunner implements CommandLineRunner {
|
||||
@Autowired
|
||||
private PageRepository pageRepository;
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
long count = pageRepository.findAll().stream().filter(p -> "SPECIALTY_DETAIL".equals(p.getLayout().name())).count();
|
||||
System.out.println("TOTAL_SPECIALTIES_IN_DB: " + count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.sisvietnamvn.web.config;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Menu;
|
||||
import com.sisvietnamvn.web.domain.MenuItem;
|
||||
import com.sisvietnamvn.web.domain.Page;
|
||||
import com.sisvietnamvn.web.domain.PageLayout;
|
||||
import com.sisvietnamvn.web.repository.MenuItemRepository;
|
||||
import com.sisvietnamvn.web.repository.MenuRepository;
|
||||
import com.sisvietnamvn.web.service.PageService;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class MenuSeeder implements CommandLineRunner {
|
||||
|
||||
private final MenuRepository menuRepository;
|
||||
private final MenuItemRepository menuItemRepository;
|
||||
private final PageService pageService;
|
||||
|
||||
public MenuSeeder(MenuRepository menuRepository, MenuItemRepository menuItemRepository, PageService pageService) {
|
||||
this.menuRepository = menuRepository;
|
||||
this.menuItemRepository = menuItemRepository;
|
||||
this.pageService = pageService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void run(String... args) {
|
||||
Menu primaryMenu = menuRepository.findAll().stream()
|
||||
.filter(m -> m.getName() != null && m.getName().trim().equals("primaryMenu"))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if (primaryMenu == null) return;
|
||||
|
||||
MenuItem chuyenKhoa = menuItemRepository.findByMenu_Id(primaryMenu.getId()).stream()
|
||||
.filter(i -> i.getLabel() != null && "CHUYÊN KHOA".equalsIgnoreCase(i.getLabel().trim()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if (chuyenKhoa == null) return;
|
||||
|
||||
List<Page> specialties = pageService.findAll().stream()
|
||||
.filter(p -> PageLayout.SPECIALTY_DETAIL.equals(p.getLayout()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
int maxOrder = menuItemRepository.findByMenu_Id(primaryMenu.getId()).stream()
|
||||
.filter(i -> chuyenKhoa.equals(i.getParent()))
|
||||
.mapToInt(i -> i.getDisplayOrder() != null ? i.getDisplayOrder() : 0)
|
||||
.max().orElse(0);
|
||||
|
||||
for (Page spec : specialties) {
|
||||
boolean exists = menuItemRepository.findByMenu_Id(primaryMenu.getId()).stream()
|
||||
.filter(i -> chuyenKhoa.equals(i.getParent()))
|
||||
.anyMatch(i -> (i.getLabel() != null && i.getLabel().equalsIgnoreCase(spec.getTitle())) ||
|
||||
(i.getTitle() != null && i.getTitle().equalsIgnoreCase(spec.getTitle())));
|
||||
|
||||
if (!exists) {
|
||||
maxOrder++;
|
||||
MenuItem newItem = new MenuItem();
|
||||
newItem.setMenu(primaryMenu);
|
||||
newItem.setParent(chuyenKhoa);
|
||||
newItem.setLabel(spec.getTitle()); // Set label
|
||||
newItem.setTitle(spec.getTitle()); // Set title as well
|
||||
newItem.setUrl("/chuyen-khoa/" + spec.getSlug());
|
||||
newItem.setDisplayOrder(maxOrder);
|
||||
menuItemRepository.save(newItem);
|
||||
System.out.println("ADDED MENU ITEM: " + spec.getTitle());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -55,7 +55,7 @@ public class SecurityConfiguration {
|
||||
.requestMatchers(HttpMethod.GET, "/", "/about", "/flex-finish", "/tin-tuc", "/tin-tuc/**", "/lien-he",
|
||||
"/manage/login", "/css/**", "/images/**", "/js/**", "/vendor/**", "/fonts/**", "/login-assets/**", "/UMass*/**", "/Undergraduate*/**",
|
||||
"/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**", "/uploads/**", "/upload/**", "/api/manage/snippets/**", "/page/**", "/news/article/**", "/post/**", "/error",
|
||||
"/about-us", "/specialty", "/doctor", "/bac-si/**", "/service", "/health-library", "/news-and-events", "/patient-support", "/medical-expert", "/umcers", "/bidding", "/contact-us", "/theme-assets/**", "/dao-tao/**",
|
||||
"/about-us", "/specialty", "/doctor", "/bac-si/**", "/service", "/health-library", "/news-and-events", "/patient-support", "/medical-expert", "/umcers", "/bidding", "/contact-us", "/theme-assets/**", "/dao-tao/**", "/chuyen-khoa", "/chuyen-khoa/**",
|
||||
"/Đào tạo tại UMC_files/**")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/manage/media/upload").permitAll()
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.sisvietnamvn.web.controller;
|
||||
import com.sisvietnamvn.web.repository.PageRepository;
|
||||
import com.sisvietnamvn.web.domain.Page;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
@RestController
|
||||
public class DebugController {
|
||||
@Autowired
|
||||
private PageRepository pageRepository;
|
||||
|
||||
@GetMapping("/api/debug-page")
|
||||
public String debugPage(@RequestParam String slug) {
|
||||
Page page = pageRepository.findBySlug(slug).orElse(null);
|
||||
if (page == null) return "PAGE NOT FOUND";
|
||||
String content = page.getContent();
|
||||
return "CONTENT_LENGTH: " + (content == null ? "NULL" : content.length()) + "\nCONTENT_PREVIEW: " + (content == null ? "NULL" : content);
|
||||
}
|
||||
}
|
||||
+121
-13
@@ -132,6 +132,44 @@ public class PageController {
|
||||
@GetMapping("/specialty")
|
||||
public String getSpecialty(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.SPECIALTY), model); }
|
||||
|
||||
@GetMapping("/chuyen-khoa")
|
||||
public String getSpecialtyList(@org.springframework.web.bind.annotation.RequestParam(required = false) String category, Model model) {
|
||||
List<Page> allSpecialties = pageService.findAll().stream()
|
||||
.filter(p -> com.sisvietnamvn.web.domain.PageLayout.SPECIALTY_DETAIL.equals(p.getLayout()))
|
||||
.filter(p -> com.sisvietnamvn.web.domain.PageStatus.PUBLISHED.equals(p.getStatus()) || com.sisvietnamvn.web.domain.PageStatus.DRAFT.equals(p.getStatus()))
|
||||
.sorted((a, b) -> {
|
||||
String ta = a.getTitle() != null ? a.getTitle() : "";
|
||||
String tb = b.getTitle() != null ? b.getTitle() : "";
|
||||
return ta.compareToIgnoreCase(tb);
|
||||
})
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
java.util.function.Predicate<Page> catFilter = p -> true;
|
||||
if ("khoa-lam-sang".equals(category)) {
|
||||
catFilter = p -> "KHOA_LAM_SANG".equals(p.getSpecialtyCategory());
|
||||
} else if ("khoa-can-lam-sang".equals(category)) {
|
||||
catFilter = p -> "KHOA_CAN_LAM_SANG".equals(p.getSpecialtyCategory());
|
||||
} else if ("khoa-ho-tro-lam-sang".equals(category)) {
|
||||
catFilter = p -> "KHOA_HO_TRO_LAM_SANG".equals(p.getSpecialtyCategory());
|
||||
}
|
||||
|
||||
List<Page> filtered = allSpecialties.stream().filter(catFilter).collect(java.util.stream.Collectors.toList());
|
||||
|
||||
model.addAttribute("specialties", filtered);
|
||||
model.addAttribute("currentCategory", category != null ? category : "all");
|
||||
model.addAttribute("countAll", allSpecialties.size());
|
||||
model.addAttribute("countLamSang", allSpecialties.stream().filter(p -> "KHOA_LAM_SANG".equals(p.getSpecialtyCategory())).count());
|
||||
model.addAttribute("countCanLamSang", allSpecialties.stream().filter(p -> "KHOA_CAN_LAM_SANG".equals(p.getSpecialtyCategory())).count());
|
||||
model.addAttribute("countHoTro", allSpecialties.stream().filter(p -> "KHOA_HO_TRO_LAM_SANG".equals(p.getSpecialtyCategory())).count());
|
||||
return "pages/specialty-list";
|
||||
}
|
||||
|
||||
@GetMapping("/chuyen-khoa/{slug}")
|
||||
public String getSpecialtyDetail(@PathVariable String slug, Model model) {
|
||||
model.addAttribute("activeTheme", "umass"); // Consistent with other routes
|
||||
return renderPage(pageService.findBySlug(slug), model);
|
||||
}
|
||||
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private com.sisvietnamvn.web.repository.TagRepository tagRepository;
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
@@ -174,6 +212,54 @@ public class PageController {
|
||||
return "Seeded " + items.size() + " posts!";
|
||||
}
|
||||
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private com.sisvietnamvn.web.repository.MenuItemRepository menuItemRepository;
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private com.sisvietnamvn.web.repository.MenuRepository menuRepository;
|
||||
|
||||
@GetMapping("/run-add-specialties")
|
||||
@org.springframework.web.bind.annotation.ResponseBody
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public String addSpecialtiesToMenu() {
|
||||
com.sisvietnamvn.web.domain.Menu primaryMenu = menuRepository.findByName("primaryMenu").orElse(null);
|
||||
if (primaryMenu == null) return "Primary menu not found";
|
||||
|
||||
com.sisvietnamvn.web.domain.MenuItem chuyenKhoa = menuItemRepository.findByMenu_Id(primaryMenu.getId()).stream()
|
||||
.filter(i -> "CHUYÊN KHOA".equalsIgnoreCase(i.getTitle()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if (chuyenKhoa == null) return "CHUYÊN KHOA menu item not found";
|
||||
|
||||
List<com.sisvietnamvn.web.domain.Page> specialties = pageService.findAll().stream()
|
||||
.filter(p -> com.sisvietnamvn.web.domain.PageLayout.SPECIALTY_DETAIL.equals(p.getLayout()))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
int maxOrder = menuItemRepository.findByMenu_Id(primaryMenu.getId()).stream()
|
||||
.filter(i -> chuyenKhoa.equals(i.getParent()))
|
||||
.mapToInt(i -> i.getDisplayOrder() != null ? i.getDisplayOrder() : 0)
|
||||
.max().orElse(0);
|
||||
|
||||
int count = 0;
|
||||
for (com.sisvietnamvn.web.domain.Page spec : specialties) {
|
||||
boolean exists = menuItemRepository.findByMenu_Id(primaryMenu.getId()).stream()
|
||||
.filter(i -> chuyenKhoa.equals(i.getParent()))
|
||||
.anyMatch(i -> i.getTitle().equalsIgnoreCase(spec.getTitle()));
|
||||
|
||||
if (!exists) {
|
||||
maxOrder++;
|
||||
com.sisvietnamvn.web.domain.MenuItem newItem = new com.sisvietnamvn.web.domain.MenuItem();
|
||||
newItem.setMenu(primaryMenu);
|
||||
newItem.setParent(chuyenKhoa);
|
||||
newItem.setTitle(spec.getTitle());
|
||||
newItem.setUrl("/chuyen-khoa/" + spec.getSlug());
|
||||
newItem.setDisplayOrder(maxOrder);
|
||||
menuItemRepository.save(newItem);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return "Added " + count + " specialties to CHUYÊN KHOA menu.";
|
||||
}
|
||||
|
||||
@GetMapping("/doctor")
|
||||
@org.springframework.transaction.annotation.Transactional(readOnly = true)
|
||||
public String getDoctor(Model model) {
|
||||
@@ -273,24 +359,42 @@ public class PageController {
|
||||
}
|
||||
|
||||
// Parse Editor.js content JSON to extract blocks
|
||||
List<Map<String, Object>> blocks = Collections.emptyList();
|
||||
List<Map<String, Object>> blocks = new java.util.ArrayList<>();
|
||||
if (page.getContent() != null && !page.getContent().trim().isEmpty()) {
|
||||
try {
|
||||
Map<String, Object> editorData = objectMapper.readValue(page.getContent(), new TypeReference<>() {});
|
||||
if (editorData.containsKey("blocks")) {
|
||||
blocks = (List<Map<String, Object>>) editorData.get("blocks");
|
||||
for (Map<String, Object> block : blocks) {
|
||||
if ("snippet".equals(block.get("type"))) {
|
||||
Map<String, Object> data = (Map<String, Object>) block.get("data");
|
||||
if (data != null && data.containsKey("id")) {
|
||||
String snippetId = (String) data.get("id");
|
||||
data.put("htmlContent", snippetService.getSnippetContent(snippetId));
|
||||
String contentTrimmed = page.getContent().trim();
|
||||
if (contentTrimmed.startsWith("{")) {
|
||||
try {
|
||||
Map<String, Object> editorData = objectMapper.readValue(contentTrimmed, new TypeReference<>() {});
|
||||
if (editorData.containsKey("blocks")) {
|
||||
blocks = (List<Map<String, Object>>) editorData.get("blocks");
|
||||
for (Map<String, Object> block : blocks) {
|
||||
if ("snippet".equals(block.get("type"))) {
|
||||
Map<String, Object> data = (Map<String, Object>) block.get("data");
|
||||
if (data != null && data.containsKey("id")) {
|
||||
String snippetId = (String) data.get("id");
|
||||
data.put("htmlContent", snippetService.getSnippetContent(snippetId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
LOG.error("Failed to parse Editor.js JSON for page ID: {}", page.getId(), e);
|
||||
// Fallback to raw HTML on error
|
||||
Map<String, Object> rawBlock = new java.util.HashMap<>();
|
||||
rawBlock.put("type", "raw");
|
||||
Map<String, Object> data = new java.util.HashMap<>();
|
||||
data.put("html", contentTrimmed);
|
||||
rawBlock.put("data", data);
|
||||
blocks.add(rawBlock);
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
LOG.error("Failed to parse Editor.js JSON for page ID: {}", page.getId(), e);
|
||||
} else {
|
||||
// Fallback to raw HTML
|
||||
Map<String, Object> rawBlock = new java.util.HashMap<>();
|
||||
rawBlock.put("type", "raw");
|
||||
Map<String, Object> data = new java.util.HashMap<>();
|
||||
data.put("html", contentTrimmed);
|
||||
rawBlock.put("data", data);
|
||||
blocks.add(rawBlock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +405,10 @@ public class PageController {
|
||||
return "pages/contact-us";
|
||||
}
|
||||
|
||||
if (com.sisvietnamvn.web.domain.PageLayout.SPECIALTY_DETAIL.equals(page.getLayout())) {
|
||||
return "pages/specialty-detail";
|
||||
}
|
||||
|
||||
return "page";
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,21 @@ public class Page extends AbstractAuditingEntity<Long> {
|
||||
@Column(name = "layout", length = 20, nullable = false)
|
||||
private PageLayout layout = PageLayout.STANDARD;
|
||||
|
||||
@Column(name = "contact_email", length = 255)
|
||||
private String contactEmail;
|
||||
|
||||
@Column(name = "contact_address", length = 500)
|
||||
private String contactAddress;
|
||||
|
||||
@Column(name = "contact_phone", length = 100)
|
||||
private String contactPhone;
|
||||
|
||||
@Column(name = "hero_image", length = 1000)
|
||||
private String heroImage;
|
||||
|
||||
@Column(name = "specialty_category", length = 50)
|
||||
private String specialtyCategory;
|
||||
|
||||
// --- Getters and Setters ---
|
||||
|
||||
@Override
|
||||
@@ -133,6 +148,46 @@ public class Page extends AbstractAuditingEntity<Long> {
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
public String getContactEmail() {
|
||||
return contactEmail;
|
||||
}
|
||||
|
||||
public void setContactEmail(String contactEmail) {
|
||||
this.contactEmail = contactEmail;
|
||||
}
|
||||
|
||||
public String getContactAddress() {
|
||||
return contactAddress;
|
||||
}
|
||||
|
||||
public void setContactAddress(String contactAddress) {
|
||||
this.contactAddress = contactAddress;
|
||||
}
|
||||
|
||||
public String getContactPhone() {
|
||||
return contactPhone;
|
||||
}
|
||||
|
||||
public void setContactPhone(String contactPhone) {
|
||||
this.contactPhone = contactPhone;
|
||||
}
|
||||
|
||||
public String getHeroImage() {
|
||||
return heroImage;
|
||||
}
|
||||
|
||||
public void setHeroImage(String heroImage) {
|
||||
this.heroImage = heroImage;
|
||||
}
|
||||
|
||||
public String getSpecialtyCategory() {
|
||||
return specialtyCategory;
|
||||
}
|
||||
|
||||
public void setSpecialtyCategory(String specialtyCategory) {
|
||||
this.specialtyCategory = specialtyCategory;
|
||||
}
|
||||
|
||||
// --- equals, hashCode, toString ---
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,5 +6,6 @@ package com.sisvietnamvn.web.domain;
|
||||
public enum PageLayout {
|
||||
STANDARD,
|
||||
SIDEBAR,
|
||||
FULL_WIDTH
|
||||
FULL_WIDTH,
|
||||
SPECIALTY_DETAIL
|
||||
}
|
||||
|
||||
+1
-1
@@ -261,7 +261,7 @@ public class SwiperSliderPlugin {
|
||||
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\" 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; text-align: left;\"><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: 20px; 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; overflow: hidden;\">{{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><div class=\"btn\"><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></div></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>";
|
||||
}
|
||||
|
||||
+1
@@ -9,4 +9,5 @@ import org.springframework.stereotype.Repository;
|
||||
*/
|
||||
@Repository
|
||||
public interface MenuItemRepository extends JpaRepository<MenuItem, Long> {
|
||||
java.util.List<MenuItem> findByMenu_Id(Long menuId);
|
||||
}
|
||||
|
||||
@@ -21,4 +21,7 @@ public interface MenuRepository extends JpaRepository<Menu, Long> {
|
||||
|
||||
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"items", "items.children"})
|
||||
Optional<Menu> findByLocation(String location);
|
||||
|
||||
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"items", "items.children"})
|
||||
Optional<Menu> findByName(String name);
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="20260728140500-1" author="system">
|
||||
<addColumn tableName="sis_page">
|
||||
<column name="contact_email" type="varchar(255)"/>
|
||||
<column name="contact_address" type="varchar(500)"/>
|
||||
<column name="contact_phone" type="varchar(100)"/>
|
||||
<column name="hero_image" type="varchar(1000)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="20260728160000-1" author="system">
|
||||
<addColumn tableName="sis_page">
|
||||
<column name="specialty_category" type="varchar(50)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="20260728160000-2" author="system">
|
||||
<!-- Khoa lâm sàng -->
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_LAM_SANG"/><where>slug = 'khoa-cap-cuu'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_LAM_SANG"/><where>slug = 'khoa-than-kinh-dot-quy'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_LAM_SANG"/><where>slug = 'tim-mach'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_LAM_SANG"/><where>slug = 'khoa-ngoai-tong-hop'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_LAM_SANG"/><where>slug = 'khoa-kham-benh'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_LAM_SANG"/><where>slug = 'phau-thuat-gay-me-hoi-suc'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_LAM_SANG"/><where>slug = 'don-vi-can-thiep-mach-dsa'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_LAM_SANG"/><where>slug = 'don-vi-cap-cuu-ngoai-vien'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_LAM_SANG"/><where>slug = 'chuyen-khoa-tai-mui-hong'</where></update>
|
||||
|
||||
<!-- Khoa cận lâm sàng -->
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_CAN_LAM_SANG"/><where>slug = 'khoa-chan-doan-hinh-anh'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_CAN_LAM_SANG"/><where>slug = 'khoa-xet-nghiem'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_CAN_LAM_SANG"/><where>slug = 'khoa-vat-ly-tri-lieu-phuc-hoi-chuc-nang'</where></update>
|
||||
|
||||
<!-- Khoa hỗ trợ lâm sàng -->
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_HO_TRO_LAM_SANG"/><where>slug = 'khoa-duoc'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_HO_TRO_LAM_SANG"/><where>slug = 'khoa-dinh-duong-tiet-che'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_HO_TRO_LAM_SANG"/><where>slug = 'don-vi-kiem-soat-nhiem-khuan'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_HO_TRO_LAM_SANG"/><where>slug = 'don-vi-kham-suc-khoe-ngoai-vien'</where></update>
|
||||
<update tableName="sis_page"><column name="specialty_category" value="KHOA_HO_TRO_LAM_SANG"/><where>slug = 'phong-quan-ly-van-hanh'</where></update>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -50,4 +50,6 @@
|
||||
<include file="config/liquibase/changelog/20260723185500_add_doctor_schedule.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260724150000_alter_setting_value.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260724151500_alter_setting_value_to_clob.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260728140500_add_contact_fields_to_page.xml" relativeToChangelogFile="false"/>
|
||||
<include file="config/liquibase/changelog/20260728160000_add_specialty_category.xml" relativeToChangelogFile="false"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
@@ -1213,12 +1213,19 @@ figure.table table tr:hover {
|
||||
}
|
||||
|
||||
.education {
|
||||
.gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex !important;
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.rounded-full {
|
||||
justify-content: center;
|
||||
background-color: var(--color-white);
|
||||
@@ -1227,10 +1234,27 @@ figure.table table tr:hover {
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-navigation {
|
||||
border-radius: 24px;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-white);
|
||||
color: var(--color-black);
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.15);
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
svg {
|
||||
color: var(--color-old-brick);
|
||||
}
|
||||
svg:hover{
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.training-sidebar {
|
||||
.gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
margin-bottom: 4px;
|
||||
|
||||
@@ -1245,4 +1269,21 @@ figure.table table tr:hover {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-slide {
|
||||
border: solid 1px #efeff0;
|
||||
border-radius: 15px;
|
||||
padding: 1rem;
|
||||
.btn {
|
||||
padding: 0.75rem;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.flex.items-center {
|
||||
justify-content: center;
|
||||
}
|
||||
a {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
:root{--swiper-navigation-size:44px}.swiper-button-next,.swiper-button-prev{position:absolute;top:var(--swiper-navigation-top-offset,50%);width:calc(var(--swiper-navigation-size) / 44 * 27);height:var(--swiper-navigation-size);margin-top:calc(0px - (var(--swiper-navigation-size) / 2));z-index:10;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--swiper-navigation-color,var(--swiper-theme-color))}.swiper-button-next.swiper-button-disabled,.swiper-button-prev.swiper-button-disabled{opacity:.35;cursor:auto;pointer-events:none}.swiper-button-next.swiper-button-hidden,.swiper-button-prev.swiper-button-hidden{opacity:0;cursor:auto;pointer-events:none}.swiper-navigation-disabled .swiper-button-next,.swiper-navigation-disabled .swiper-button-prev{display:none!important}.swiper-button-next svg,.swiper-button-prev svg{width:100%;height:100%;object-fit:contain;transform-origin:center}.swiper-rtl .swiper-button-next svg,.swiper-rtl .swiper-button-prev svg{transform:rotate(180deg)}.swiper-button-prev,.swiper-rtl .swiper-button-next{left:var(--swiper-navigation-sides-offset,10px);right:auto}.swiper-button-lock{display:none}.swiper-button-next:after,.swiper-button-prev:after{font-family:swiper-icons;font-size:var(--swiper-navigation-size);text-transform:none!important;letter-spacing:0;font-variant:normal;line-height:1}.swiper-button-prev:after,.swiper-rtl .swiper-button-next:after{content:"prev"}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:var(--swiper-navigation-sides-offset,10px);left:auto}.swiper-button-next:after,.swiper-rtl .swiper-button-prev:after{content:"next"}
|
||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
/* PLEASE DO NOT COPY AND PASTE THIS CODE. */(function(){var w=window,C='___grecaptcha_cfg',cfg=w[C]=w[C]||{},N='grecaptcha';var gr=w[N]=w[N]||{};gr.ready=gr.ready||function(f){(cfg['fns']=cfg['fns']||[]).push(f);};w['__recaptcha_api']='https://www.google.com/recaptcha/api2/';(cfg['render']=cfg['render']||[]).push('onload');(cfg['anchor-ms']=cfg['anchor-ms']||[]).push(20000);(cfg['execute-ms']=cfg['execute-ms']||[]).push(30000);w['__google_recaptcha_client']=true;var d=document,po=d.createElement('script');po.type='text/javascript';po.async=true; po.charset='utf-8';po.src='https://www.gstatic.com/recaptcha/releases/A7KpaEASfhDcK0nXxgQEyyYv/recaptcha__en_gb.js';po.crossOrigin='anonymous';po.integrity='sha384-5OOK2erh/YOEG9kGHGnFP3+VW3JJ6xFMntvj2Hukmf0CErQumRfz6RUGAs6A/57r';var e=d.querySelector('script[nonce]'),n=e&&(e['nonce']||e.getAttribute('nonce'));if(n){po.setAttribute('nonce',n);}var s=d.getElementsByTagName('script')[0];s.parentNode.insertBefore(po, s);})();
|
||||
+1
@@ -0,0 +1 @@
|
||||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[7358],{24473:(e,s,n)=>{Promise.resolve().then(n.t.bind(n,14933,23)),Promise.resolve().then(n.t.bind(n,86695,23)),Promise.resolve().then(n.t.bind(n,54775,23)),Promise.resolve().then(n.t.bind(n,22908,23)),Promise.resolve().then(n.t.bind(n,23624,23)),Promise.resolve().then(n.t.bind(n,59440,23)),Promise.resolve().then(n.t.bind(n,62920,23)),Promise.resolve().then(n.t.bind(n,19710,23))}},e=>{var s=s=>e(e.s=s);e.O(0,[1032,4619],()=>(s(36596),s(24473))),_N_E=e.O()}]);
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
!function(r,i){"use strict";var e,o=r.location,s=r.document,t=s.querySelector('[src*="'+i+'"]'),l=t&&t.getAttribute("data-domain"),p=r.localStorage.plausible_ignore;function c(e){console.warn("Ignoring Event: "+e)}function a(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(o.hostname)||"file:"===o.protocol)return c("localhost");if(!(r.phantom||r._phantom||r.__nightmare||r.navigator.webdriver||r.Cypress)){if("true"==p)return c("localStorage flag");var a={};a.n=e,a.u=o.href,a.d=l,a.r=s.referrer||null,a.w=r.innerWidth,t&&t.meta&&(a.m=JSON.stringify(t.meta)),t&&t.props&&(a.p=JSON.stringify(t.props));var n=new XMLHttpRequest;n.open("POST",i+"/api/event",!0),n.setRequestHeader("Content-Type","text/plain"),n.send(JSON.stringify(a)),n.onreadystatechange=function(){4==n.readyState&&t&&t.callback&&t.callback()}}}function n(){e!==o.pathname&&(e=o.pathname,a("pageview"))}try{var u,h=r.history;h.pushState&&(u=h.pushState,h.pushState=function(){u.apply(this,arguments),n()},r.addEventListener("popstate",n));var g=r.plausible&&r.plausible.q||[];r.plausible=a;for(var f=0;f<g.length;f++)a.apply(this,g[f]);"prerender"===s.visibilityState?s.addEventListener("visibilitychange",function(){e||"visible"!==s.visibilityState||n()}):n()}catch(e){console.error(e),(new Image).src=i+"/api/error?message="+encodeURIComponent(e.message)}}(window,"https://analytics.jamstackvietnam.com");
|
||||
+1
File diff suppressed because one or more lines are too long
+1710
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -186,22 +186,20 @@
|
||||
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="py-6 xl:py-12 md:py-8 bg-white">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="container mx-auto px-4" style="text-align: center">
|
||||
<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">
|
||||
<div class="relative flex items-center gap-3">
|
||||
<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"
|
||||
class="btn-navigation flex-shrink-0 lg:mr-4 mr-2 z-10 flex items-center justify-center btn-navigation-chuong-trinh-khac-prev"
|
||||
style="width: 42px; height: 42px; min-width: 42px; flex-shrink: 0"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-chevron-up size-4 -rotate-90"
|
||||
class="lucide lucide-chevron-up size-4"
|
||||
style="transform: rotate(-90deg)"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
@@ -215,19 +213,19 @@
|
||||
<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 swiper-chuong-trinh-khac w-full" style="overflow: hidden">
|
||||
<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 class="swiper-pagination hidden"></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"
|
||||
class="btn-navigation flex-shrink-0 lg:ml-4 ml-2 z-10 flex items-center justify-center btn-navigation-chuong-trinh-khac-next"
|
||||
style="width: 42px; height: 42px; min-width: 42px; flex-shrink: 0"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-chevron-down size-4 -rotate-90"
|
||||
class="lucide lucide-chevron-down size-4"
|
||||
style="transform: rotate(-90deg)"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -180,6 +180,42 @@
|
||||
title.</small>
|
||||
</div>
|
||||
|
||||
<!-- Specialty/Contact Info -->
|
||||
<div class="card bg-light mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="font-weight-bold text-primary mb-3">Thông tin liên hệ & Banner (Dành riêng cho trang Chuyên khoa)</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="contactEmail" class="font-weight-bold">Email</label>
|
||||
<input type="text" class="form-control" id="contactEmail" th:field="*{contactEmail}" placeholder="vd: capcuu@umc.edu.vn">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="contactPhone" class="font-weight-bold">Điện thoại / SĐT</label>
|
||||
<input type="text" class="form-control" id="contactPhone" th:field="*{contactPhone}" placeholder="vd: 028 3952 5115">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="contactAddress" class="font-weight-bold">Địa chỉ</label>
|
||||
<input type="text" class="form-control" id="contactAddress" th:field="*{contactAddress}" placeholder="vd: Tầng trệt - Khu A">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="heroImage" class="font-weight-bold">Ảnh Banner (Hero Image URL)</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="heroImage" th:field="*{heroImage}" placeholder="URL ảnh">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="document.getElementById('mediaManagerBtn').click()">
|
||||
<i class="fas fa-image"></i> Chọn ảnh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Block Editor Content -->
|
||||
<div class="form-group">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
@@ -249,6 +285,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/underline@1.1.0/dist/bundle.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.9.0/dist/image.umd.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/attaches@1.3.0/dist/bundle.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/raw@2.4.3/dist/bundle.js"></script>
|
||||
|
||||
<!--
|
||||
============================================================
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,302 @@
|
||||
<!doctype html>
|
||||
<html
|
||||
lang="en"
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{themes/__${activeTheme}__/layout(forceFullWidth=true, bodyClass='specialty-list-page')}"
|
||||
>
|
||||
<head>
|
||||
<title>Chuyên khoa - Bệnh viện S.I.S Cần Thơ</title>
|
||||
<meta name="description" content="Danh sách các chuyên khoa tại Bệnh viện Đa khoa Quốc tế S.I.S Cần Thơ" />
|
||||
|
||||
<link rel="stylesheet" th:href="@{/theme-assets/bvdhyd/css/b6b2bf2d3af810a6.css}" />
|
||||
<link rel="stylesheet" th:href="@{/theme-assets/bvdhyd/css/3cd83cfe34ca397f.css}" />
|
||||
<link rel="stylesheet" th:href="@{/theme-assets/bvdhyd/css/45d4f6442d75f756.css}" />
|
||||
<link rel="stylesheet" th:href="@{/theme-assets/bvdhyd/css/c37340727ca4fe15.css}" />
|
||||
|
||||
<style>
|
||||
.specialty-card {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.1),
|
||||
0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.specialty-card:hover {
|
||||
background: var(--color-old-brick);
|
||||
color: white;
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 25px rgba(0, 84, 166, 0.25);
|
||||
}
|
||||
.specialty-card:hover .specialty-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.specialty-card:hover .specialty-icon svg {
|
||||
color: white;
|
||||
}
|
||||
.specialty-card:hover .specialty-name {
|
||||
color: white;
|
||||
}
|
||||
.specialty-icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: #f0f7ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.specialty-icon svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--color-old-brick);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.specialty-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
color: var(--color-old-brick);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: white;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.filter-btn:hover {
|
||||
border-color: var(--color-primary-600, #0054a6);
|
||||
color: var(--color-primary-600, #0054a6);
|
||||
}
|
||||
.filter-btn.active {
|
||||
background: var(--color-primary-600, #0054a6);
|
||||
color: white;
|
||||
border-color: var(--color-primary-600, #0054a6);
|
||||
}
|
||||
.filter-btn .count {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.filter-btn.active .count {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary-600, #0054a6);
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--color-primary-600, #0054a6);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.specialty-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
.specialty-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
.specialty-icon svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.specialty-name {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<section class="xl:py-12 md:py-8 py-6" style="background: #f6f6f6">
|
||||
<div class="container mx-auto px-4">
|
||||
<!-- Page Title -->
|
||||
<h1 class="display-4 text-primary-600 text-center xl:mb-8 md:mb-6 mb-4">Chuyên khoa</h1>
|
||||
|
||||
<!-- Filter Dropdown -->
|
||||
<div
|
||||
id="specialties_sel"
|
||||
class="flex justify-center xl:mb-10 md:mb-7 mb-5 relative w-full md:max-w-[320px] mx-auto xl:mb-10 md:mb-7 mb-5 title-4"
|
||||
>
|
||||
<select
|
||||
onchange="window.location.href = this.value"
|
||||
class="w-full group bg-white rounded-lg border lg:hover:border-primary-600 cursor-pointer xl:px-6 px-4 py-3 flex items-center justify-between lg:duration-150 shadow border-gray-200"
|
||||
>
|
||||
<option th:value="@{/chuyen-khoa}" th:selected="${currentCategory == 'all'}">Tất cả ([[${countAll}]])</option>
|
||||
<option th:value="@{/chuyen-khoa(category='khoa-lam-sang')}" th:selected="${currentCategory == 'khoa-lam-sang'}">
|
||||
Khoa lâm sàng ([[${countLamSang}]])
|
||||
</option>
|
||||
<option th:value="@{/chuyen-khoa(category='khoa-can-lam-sang')}" th:selected="${currentCategory == 'khoa-can-lam-sang'}">
|
||||
Khoa cận lâm sàng ([[${countCanLamSang}]])
|
||||
</option>
|
||||
<option
|
||||
th:value="@{/chuyen-khoa(category='khoa-ho-tro-lam-sang')}"
|
||||
th:selected="${currentCategory == 'khoa-ho-tro-lam-sang'}"
|
||||
>
|
||||
Khoa hỗ trợ lâm sàng ([[${countHoTro}]])
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Specialty Grid -->
|
||||
<div class="grid lg:grid-cols-4 md:grid-cols-3 grid-cols-2 xl:gap-8 md:gap-6 gap-4">
|
||||
<a th:each="spec : ${specialties}" th:href="@{/chuyen-khoa/{slug}(slug=${spec.slug})}" class="specialty-card">
|
||||
<div class="specialty-icon">
|
||||
<th:block th:switch="${spec.slug}">
|
||||
<!-- chuyen-khoa-tai-mui-hong -->
|
||||
<th:block th:case="'chuyen-khoa-tai-mui-hong'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/Taimuihong-blue.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- don-vi-kiem-soat-nhiem-khuan -->
|
||||
<th:block th:case="'don-vi-kiem-soat-nhiem-khuan'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/library-29.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- khoa-cap-cuu -->
|
||||
<th:block th:case="'khoa-cap-cuu'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/library-37.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- khoa-chan-doan-hinh-anh -->
|
||||
<th:block th:case="'khoa-chan-doan-hinh-anh'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/library-3.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- khoa-dinh-duong-tiet-che -->
|
||||
<th:block th:case="'khoa-dinh-duong-tiet-che'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/library-9.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- khoa-duoc -->
|
||||
<th:block th:case="'khoa-duoc'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/library-11.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- khoa-kham-benh -->
|
||||
<th:block th:case="'khoa-kham-benh'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/library-27.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- khoa-than-kinh-dot-quy -->
|
||||
<th:block th:case="'khoa-than-kinh-dot-quy'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/Thankinh-blue.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- khoa-vat-ly-tri-lieu-phuc-hoi-chuc-nang -->
|
||||
<th:block th:case="'khoa-vat-ly-tri-lieu-phuc-hoi-chuc-nang'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/Phuchoichucnang-blue.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- khoa-xet-nghiem -->
|
||||
<th:block th:case="'khoa-xet-nghiem'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/Xetnghiem-blue.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- phau-thuat-gay-me-hoi-suc -->
|
||||
<th:block th:case="'phau-thuat-gay-me-hoi-suc'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/library-13.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- tim-mach -->
|
||||
<th:block th:case="'tim-mach'">
|
||||
<img
|
||||
src="https://console.bvdaihoc.com.vn/uploads/chuyen-khoa/Timmachcanthiep-blue.png"
|
||||
alt="Icon"
|
||||
class="w-12 h-12 object-contain mx-auto"
|
||||
/>
|
||||
</th:block>
|
||||
<!-- Default -->
|
||||
<th:block th:case="*">
|
||||
<div class="w-12 h-12 rounded-full bg-primary-100 flex items-center justify-center text-primary-600 mx-auto">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path d="M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V6a2 2 0 012-2zM9 14h6M12 11v6" />
|
||||
</svg>
|
||||
</div>
|
||||
</th:block>
|
||||
</th:block>
|
||||
</div>
|
||||
<span class="specialty-name" th:text="${spec.title}">Tên chuyên khoa</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div th:if="${#lists.isEmpty(specialties)}" class="text-center py-12">
|
||||
<p class="text-gray-500 text-lg">Không tìm thấy chuyên khoa nào trong nhóm này.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user