feat: implement widget and menu shortcode rendering in HtmlSnippetService and update section title
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="text-container">
|
<div class="text-container">
|
||||||
<div data-component-id="umass_base:section-title" class="f--section-title">
|
<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>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+109
-2
@@ -1,7 +1,10 @@
|
|||||||
package com.sisvietnamvn.web.service;
|
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.domain.HtmlSnippet;
|
||||||
import com.sisvietnamvn.web.repository.HtmlSnippetRepository;
|
import com.sisvietnamvn.web.repository.HtmlSnippetRepository;
|
||||||
|
import com.sisvietnamvn.web.service.dto.WidgetDto;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -16,9 +19,15 @@ import java.util.Optional;
|
|||||||
public class HtmlSnippetService {
|
public class HtmlSnippetService {
|
||||||
|
|
||||||
private final HtmlSnippetRepository snippetRepository;
|
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.snippetRepository = snippetRepository;
|
||||||
|
this.settingService = settingService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.menuService = menuService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<HtmlSnippet> findAll() {
|
public List<HtmlSnippet> findAll() {
|
||||||
@@ -50,9 +59,107 @@ public class HtmlSnippetService {
|
|||||||
if (slug == null || slug.isEmpty()) {
|
if (slug == null || slug.isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return snippetRepository.findBySlug(slug)
|
String content = snippetRepository.findBySlug(slug)
|
||||||
.filter(HtmlSnippet::isActive)
|
.filter(HtmlSnippet::isActive)
|
||||||
.map(HtmlSnippet::getContent)
|
.map(HtmlSnippet::getContent)
|
||||||
.orElse("");
|
.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 [ and ] in case the WYSIWYG editor escaped them
|
||||||
|
java.util.regex.Matcher m = java.util.regex.Pattern.compile("(?:\\[|[)menu:([a-zA-Z0-9_-]+)(?::([a-zA-Z0-9_-]+))?(?:\\]|])").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&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>'
|
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 ──
|
// ── MEDIA ──
|
||||||
bm.add('image-gallery', {
|
bm.add('image-gallery', {
|
||||||
label: 'Image Gallery',
|
label: 'Image Gallery',
|
||||||
|
|||||||
@@ -26,15 +26,7 @@
|
|||||||
|
|
||||||
<!-- 1. HTML Snippet Block (Custom) -->
|
<!-- 1. HTML Snippet Block (Custom) -->
|
||||||
<th:block th:if="${block.type == 'snippet'}">
|
<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:utext="${block.data.htmlContent}" th:remove="tag"></div>
|
||||||
<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>
|
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<!-- 2. Header Block -->
|
<!-- 2. Header Block -->
|
||||||
|
|||||||
Reference in New Issue
Block a user