feat: implement widget and menu shortcode rendering in HtmlSnippetService and update section title

This commit is contained in:
2026-07-13 19:09:44 +07:00
parent 157d7eba7b
commit 849c79cc0f
5 changed files with 182 additions and 12 deletions
@@ -3,7 +3,7 @@
<div class="container">
<div class="text-container">
<div data-component-id="umass_base:section-title" class="f--section-title">
<h2>TÌM HIỂU<span class="highlight">CHUYÊN KHOA</span>
<h2>TÌM HIỂU<span class="highlight">KHOA ĐIỀU TRỊ</span>
</h2>
</div>
</div>
@@ -1,7 +1,10 @@
package com.sisvietnamvn.web.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sisvietnamvn.web.domain.HtmlSnippet;
import com.sisvietnamvn.web.repository.HtmlSnippetRepository;
import com.sisvietnamvn.web.service.dto.WidgetDto;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -16,9 +19,15 @@ import java.util.Optional;
public class HtmlSnippetService {
private final HtmlSnippetRepository snippetRepository;
private final SettingService settingService;
private final ObjectMapper objectMapper;
private final com.sisvietnamvn.web.service.MenuService menuService;
public HtmlSnippetService(HtmlSnippetRepository snippetRepository) {
public HtmlSnippetService(HtmlSnippetRepository snippetRepository, SettingService settingService, ObjectMapper objectMapper, com.sisvietnamvn.web.service.MenuService menuService) {
this.snippetRepository = snippetRepository;
this.settingService = settingService;
this.objectMapper = objectMapper;
this.menuService = menuService;
}
public List<HtmlSnippet> findAll() {
@@ -50,9 +59,107 @@ public class HtmlSnippetService {
if (slug == null || slug.isEmpty()) {
return "";
}
return snippetRepository.findBySlug(slug)
String content = snippetRepository.findBySlug(slug)
.filter(HtmlSnippet::isActive)
.map(HtmlSnippet::getContent)
.orElse("");
if (content.isEmpty()) return content;
if (content.contains("[widgets:sidebar]")) {
content = content.replace("[widgets:sidebar]", renderWidgets("sidebar"));
}
if (content.contains("[widgets:footer]")) {
content = content.replace("[widgets:footer]", renderWidgets("footer"));
}
if (content.contains("[widgets:stats]")) {
content = content.replace("[widgets:stats]", renderWidgets("stats"));
}
// Process shortcodes for menus, e.g., [menu:INFO] or [menu:INFO:dropdown]
// Support HTML entities &#91; and &#93; in case the WYSIWYG editor escaped them
java.util.regex.Matcher m = java.util.regex.Pattern.compile("(?:\\[|&#91;)menu:([a-zA-Z0-9_-]+)(?::([a-zA-Z0-9_-]+))?(?:\\]|&#93;)").matcher(content);
StringBuffer sb = new StringBuffer();
while (m.find()) {
String location = m.group(1);
String style = m.group(2) != null ? m.group(2) : "button";
m.appendReplacement(sb, renderMenu(location, style));
}
m.appendTail(sb);
content = sb.toString();
return content;
}
private String renderMenu(String location, String style) {
return menuService.findByLocation(location).map(menu -> {
StringBuilder sb = new StringBuilder();
if ("dropdown".equalsIgnoreCase(style)) {
sb.append("<nav class=\"mc--menu mc--info-menu\">\n");
sb.append(" <ul class=\"menu m--menu m--info-menu\">\n");
sb.append(" <li class=\"menu-item menu-item--expanded\">\n");
sb.append(" <details class=\"utility-button-wrapper mc--info-menu-container\">\n");
sb.append(" <summary type=\"button\" aria-label=\"Display submenu for Information menu\" aria-expanded=\"false\" aria-haspopup=\"true\" class=\"utility-button arrow-toggle information-for-toggle\">\n");
sb.append(" <span>").append(menu.getName() != null ? menu.getName() : "Info For").append("</span>\n");
sb.append(" <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" viewBox=\"0 0 16 7\" enable-background=\"new 0 0 16 7\" xml:space=\"preserve\" width=\"16\" height=\"7\" class=\"arrow\">\n");
sb.append(" <polygon fill-rule=\"evenodd\" clip-rule=\"evenodd\" fill=\"#ffffff\" points=\"7,7 0,0 16,0 \"></polygon>\n");
sb.append(" </svg>\n");
sb.append(" </summary>\n");
sb.append(" <div class=\"submenu-wrapper\">\n");
sb.append(" <ul class=\"submenu\">\n");
for (com.sisvietnamvn.web.domain.MenuItem item : menu.getItems()) {
if (item.getParent() == null) {
sb.append(" <li class=\"menu-item\"><a href=\"").append(item.getUrl() != null ? item.getUrl() : "#").append("\">").append(item.getLabel()).append("</a></li>\n");
}
}
sb.append(" </ul>\n");
sb.append(" </div>\n");
sb.append(" </details>\n");
sb.append(" <div class=\"submenu-wrapper-desktop\">\n");
sb.append(" <ul class=\"submenu\">\n");
for (com.sisvietnamvn.web.domain.MenuItem item : menu.getItems()) {
if (item.getParent() == null) {
sb.append(" <li class=\"menu-item\"><a href=\"").append(item.getUrl() != null ? item.getUrl() : "#").append("\">").append(item.getLabel()).append("</a></li>\n");
}
}
sb.append(" </ul>\n");
sb.append(" </div>\n");
sb.append(" </li>\n");
sb.append(" </ul>\n");
sb.append("</nav>\n");
} else {
// Default button style
for (com.sisvietnamvn.web.domain.MenuItem item : menu.getItems()) {
if (item.getParent() == null) {
sb.append("<div class=\"f--field f--button\">\n");
sb.append(" <a href=\"").append(item.getUrl() != null ? item.getUrl() : "#").append("\" class=\"button-text-link button-context-light\" aria-label=\"").append(item.getLabel()).append("\" target=\"_self\">\n");
sb.append(" <span class=\"button-text\"> ").append(item.getLabel()).append(" </span>\n");
sb.append(" </a>\n");
sb.append("</div>\n");
}
}
}
return sb.toString();
}).orElse("");
}
private String renderWidgets(String area) {
String json = settingService.getValue("theme_widgets_" + area, "[]");
try {
List<WidgetDto> widgets = objectMapper.readValue(json, new TypeReference<List<WidgetDto>>() {});
StringBuilder sb = new StringBuilder();
for (WidgetDto w : widgets) {
sb.append("<div class=\"card shadow-sm mb-4\">");
if (w.getTitle() != null && !w.getTitle().isEmpty()) {
sb.append("<div class=\"card-header bg-white font-weight-bold\">").append(w.getTitle()).append("</div>");
}
sb.append("<div class=\"card-body\">").append(w.getContent()).append("</div>");
sb.append("</div>");
}
return sb.toString();
} catch (Exception e) {
return "";
}
}
}
@@ -0,0 +1,51 @@
<div class="content-top">
<div class="lc--layout-container lc--full">
<div class="l--layout l--full">
<div class="lr--layout-region lr--main">
<div class="cc--component-container cc--homepage-hero ">
<div class="c--component c--homepage-hero">
<div class="slides-container">
<div class="image-video-container has-video">
<img src="https://www.umass.edu/sites/default/files/styles/1_1_1920x1920/public/2025-09/250616_UMASS_4515.jpg" alt="Students collaborate using a driving simulator in the UMass Center for Transportation." srcset="https://www.umass.edu/sites/default/files/styles/1_1_1920x1920/public/2025-09/250616_UMASS_4515.jpg?h=9855f42d&amp;itok=d27r8GaT 1920w">
<div class="f--ambient-video">
<video role="presentation" tabindex="-1" loop="" autoplay="" playsinline="" muted="">
<source src="http://localhost:8080/uploads/2026/07/9375f45b-027a-4fb4-bd18-66889309280d.mp4" type="video/mp4">
</video>
</div>
<div class="video-controls" data-once="ambientVideo">
<div class="video-controls-inner">
<button aria-labelledby="pauseBtn" class="video-button video-pause-button">
<svg height="14" viewBox="0 0 10 14" width="10" xmlns="http://www.w3.org/2000/svg">
<title id="pauseBtn">Pause Background Video</title>
<path d="m1143 711v14h-3v-14zm7 0v14h-3v-14z" fill="#fff" fill-rule="evenodd" transform="translate(-1140 -711)"></path>
</svg>
</button>
<button aria-labelledby="playBtn" class="video-button video-play-button">
<svg height="29" viewBox="0 0 29 29" width="29" xmlns="http://www.w3.org/2000/svg">
<title id="playBtn">Play Background Video</title>
<path d="m17.5 3c-7.99789474 0-14.5 6.50210526-14.5 14.5 0 7.9978947 6.50210526 14.5 14.5 14.5 7.9978947 0 14.5-6.5021053 14.5-14.5 0-7.99789474-6.5021053-14.5-14.5-14.5zm5.6763936 15.2939067-7.3503366 4.5939604c-.6635721.408352-1.5313202-.051044-1.5313202-.8422261v-9.1879207c0-.7911821.8677481-1.2761001 1.5313202-.8422261l7.3503366 4.5939604c.612528.38283.612528 1.3016221 0 1.6844521z" fill="#fff" fill-rule="evenodd" transform="translate(-3 -3)"></path>
</svg>
</button>
</div>
</div>
</div>
<div class="text-container centered">
<div class="slide-text-container-inner">
<h3>
<a href="https://www.umass.edu/gateway/why-umass">Stroke International School 2026.</a>
</h3>
<div class="f--field f--button">
<a href="https://www.umass.edu/gateway/why-umass" class="button-secondary button-context-dark " aria-label="Learn More Learn more about UMass Amherst." data-component-id="umass_base:button">
<span class="button-text"> Learn More </span>
</a>
</div>
[menu:INFO:dropdown]
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div> chỉnh
@@ -641,6 +641,26 @@
media: '<i class="fa fa-newspaper-o fa-2x"></i>'
});
// ── DYNAMIC WIDGETS ──
bm.add('widget-sidebar', {
label: 'Sidebar Widgets',
category: 'Widgets',
content: '<div class="widget-area" style="padding: 10px; border: 2px dashed #6366f1; text-align: center; color: #6366f1; background: #f0f0f5; border-radius: 8px;">[widgets:sidebar]</div>',
media: '<i class="fa fa-th-list fa-2x"></i>'
});
bm.add('widget-footer', {
label: 'Footer Widgets',
category: 'Widgets',
content: '<div class="widget-area" style="padding: 10px; border: 2px dashed #6366f1; text-align: center; color: #6366f1; background: #f0f0f5; border-radius: 8px;">[widgets:footer]</div>',
media: '<i class="fa fa-th-large fa-2x"></i>'
});
bm.add('widget-stats', {
label: 'Stats Widgets',
category: 'Widgets',
content: '<div class="widget-area" style="padding: 10px; border: 2px dashed #6366f1; text-align: center; color: #6366f1; background: #f0f0f5; border-radius: 8px;">[widgets:stats]</div>',
media: '<i class="fa fa-bar-chart fa-2x"></i>'
});
// ── MEDIA ──
bm.add('image-gallery', {
label: 'Image Gallery',
@@ -26,15 +26,7 @@
<!-- 1. HTML Snippet Block (Custom) -->
<th:block th:if="${block.type == 'snippet'}">
<th:block th:if="${block.data.id == 'top-banner' or block.data.id == 'umass_banner_snippet' or block.data.id == 'content_top_snippet' or block.data.id == 'top_banner' or block.data.id == 'content-top'}">
<div th:replace="~{themes/__${activeTheme}__/content-top}"></div>
</th:block>
<th:block th:if="${block.data.id == 'block_1_block-multiple-ctas' or block.data.id == 'block-multiple-ctas'}">
<div th:replace="~{themes/__${activeTheme}__/content-main-blocks-cta}"></div>
</th:block>
<th:block th:unless="${block.data.id == 'top-banner' or block.data.id == 'umass_banner_snippet' or block.data.id == 'content_top_snippet' or block.data.id == 'top_banner' or block.data.id == 'content-top' or block.data.id == 'block_1_block-multiple-ctas' or block.data.id == 'block-multiple-ctas'}">
<div th:utext="${block.data.htmlContent}" th:remove="tag"></div>
</th:block>
<div th:utext="${block.data.htmlContent}" th:remove="tag"></div>
</th:block>
<!-- 2. Header Block -->