Files
sisvietnamvn_01/sisvietnamvn_main/src/main/resources/templates/page.html
T

747 lines
46 KiB
HTML

<!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}"
>
<head>
<title th:text="${page.title}">Bệnh Viện S.I.S Cần Thơ</title>
<!-- Add Meta Description for SEO -->
<meta name="description" th:if="${page.metaDescription != null}" th:content="${page.metaDescription}" />
<!-- Load lightweight Bootstrap Grid CSS to support grid/flex block layouts without overriding theme styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap-grid.min.css" />
<!-- Customizer CSS overrides -->
<style th:inline="css">
body {
font-family: /*[[${themeModFontFamily}]]*/ sans-serif !important;
}
/* Inject Customizer Primary Color dynamically */
a,
.text-primary {
color: /*[[${themeModPrimaryColor}]]*/ #007bff !important;
}
/*[[${themeModCustomCss}]]*/
</style>
</head>
<body>
<!-- Render Editor.js Blocks -->
<div layout:fragment="content">
<!-- Dedicated Page Custom CSS -->
<style th:if="${page != null and page.customCss != null and !page.customCss.empty}" th:utext="${page.customCss}"></style>
<th:block th:each="block : ${blocks}">
<div
th:class="${(block.data != null and block.data['stretched'] == true) or (block.data != null and block.data['stretched'] == null and block['stretched'] == true) ? 'sis-fullwidth-breakout' : ''}"
>
<!-- 1. HTML Snippet Block (Custom) -->
<th:block th:if="${block['type'] == 'snippet'}">
<div th:utext="${block.data['htmlContent']}" th:remove="tag"></div>
</th:block>
<!-- 2. Header Block -->
<th:block th:if="${block['type'] == 'header'}">
<th:block th:switch="${block.data['level']}">
<h1 th:case="1" th:utext="${block.data['text']}"></h1>
<h2 th:case="2" th:utext="${block.data['text']}"></h2>
<h3 th:case="3" th:utext="${block.data['text']}"></h3>
<h4 th:case="4" th:utext="${block.data['text']}"></h4>
<h5 th:case="5" th:utext="${block.data['text']}"></h5>
<h6 th:case="6" th:utext="${block.data['text']}"></h6>
<h2 th:case="*" th:utext="${block.data['text']}"></h2>
</th:block>
</th:block>
<!-- 3. Paragraph Block -->
<th:block th:if="${block['type'] == 'paragraph'}">
<p th:utext="${block.data['text']}"></p>
</th:block>
<!-- 4. List Block -->
<th:block th:if="${block['type'] == 'list'}">
<ul th:if="${block.data['style'] == 'unordered'}">
<li th:each="item : ${block.data['items']}" th:utext="${item}"></li>
</ul>
<ol th:if="${block.data['style'] == 'ordered'}">
<li th:each="item : ${block.data['items']}" th:utext="${item}"></li>
</ol>
</th:block>
<!-- 5. Image Block -->
<th:block th:if="${block['type'] == 'image'}">
<img th:if="${block.data['file'] != null}" th:src="${block.data['file']['url']}" th:alt="${block.data['caption']}" />
<p th:if="${block.data['caption']}" th:utext="${block.data['caption']}"></p>
</th:block>
<!-- 6. Quote Block -->
<th:block th:if="${block['type'] == 'quote'}">
<blockquote>
<p th:utext="${block.data['text']}"></p>
<footer th:if="${block.data['caption']}" th:utext="${block.data['caption']}"></footer>
</blockquote>
</th:block>
<!-- 7. Delimiter Block -->
<th:block th:if="${block['type'] == 'delimiter'}">
<hr />
</th:block>
<!-- 8. Table Block -->
<th:block th:if="${block['type'] == 'table'}">
<table>
<tbody>
<tr th:each="row, rowStat : ${block.data['content']}">
<th:block th:if="${block.data['withHeadings'] == true and rowStat.index == 0}">
<th th:each="cell : ${row}" th:utext="${cell}"></th>
</th:block>
<th:block th:unless="${block.data['withHeadings'] == true and rowStat.index == 0}">
<td th:each="cell : ${row}" th:utext="${cell}"></td>
</th:block>
</tr>
</tbody>
</table>
</th:block>
<!-- 9. Raw HTML Block (Fallback/WordPress Imports) -->
<th:block th:if="${block['type'] == 'raw'}">
<div th:utext="${block.data['html']}" th:remove="tag"></div>
</th:block>
<!-- 10. Accordion Block -->
<th:block th:if="${block['type'] == 'accordion'}">
<details class="sis-accordion-item mb-3" th:open="${block.data['isOpen'] == true}">
<summary class="sis-accordion-title font-weight-bold p-3 bg-light border rounded" style="cursor: pointer; user-select: none">
<span th:utext="${block.data['title']}">Accordion Title</span>
</summary>
<div class="sis-accordion-body p-3 border border-top-0 rounded-bottom">
<div th:utext="${block.data['content']}">Accordion Content</div>
</div>
</details>
</th:block>
<!-- 11. YouTube Video Block -->
<th:block th:if="${block['type'] == 'youtube'}">
<div class="sis-youtube-container my-4 text-center">
<div
class="sis-yt-thumb-wrapper"
th:data-embed-url="${block.data['url']}"
th:data-custom-thumb="${block.data['thumbnailUrl'] != null ? block.data['thumbnailUrl'] : ''}"
style="
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
border-radius: 8px;
background-color: #111;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
margin: 0 auto;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
"
th:styleappend="${(block.data['thumbnailUrl'] != null and !#strings.isEmpty(block.data['thumbnailUrl'])) ? 'background-image: url(' + block.data['thumbnailUrl'] + ');' : ((block.data['url'] != null and #strings.contains(block.data['url'], 'youtu.be/')) ? 'background-image: url(https://img.youtube.com/vi/' + #strings.substringAfter(block.data['url'], 'youtu.be/') + '/hqdefault.jpg);' : ((block.data['url'] != null and #strings.contains(block.data['url'], 'v=')) ? 'background-image: url(https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(block.data['url'], 'v='), '&') + '/hqdefault.jpg);' : ((block.data['url'] != null and #strings.contains(block.data['url'], 'embed/')) ? 'background-image: url(https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(block.data['url'], 'embed/'), '?') + '/hqdefault.jpg);' : '')))}"
>
<div
class="sis-yt-play-btn"
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); transition: transform 0.2s ease"
>
<svg width="68" height="48" viewBox="0 0 68 48">
<path
d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z"
fill="#ff0000"
/>
<path d="M45 24L27 14v20z" fill="#ffffff" />
</svg>
</div>
</div>
<p
class="small text-muted mt-2"
th:if="${block.data['caption'] != null and !#strings.isEmpty(block.data['caption'])}"
th:utext="${block.data['caption']}"
></p>
</div>
</th:block>
<!-- 12. Hero Banner Block -->
<th:block th:if="${block['type'] == 'hero'}">
<div
class="sis-hero-banner text-white py-5 px-3 rounded shadow-sm text-center"
style="
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-size: cover;
background-position: center;
background-color: #002554;
"
th:styleappend="${'min-height: ' + (block.data['height'] != null and !#strings.isEmpty(block.data['height']) ? block.data['height'] : '350px') + ';' + ((block.data['bgImage'] != null and !#strings.isEmpty(block.data['bgImage'])) ? 'background-image: url(' + block.data['bgImage'] + ');' : 'background: linear-gradient(135deg, #002554 0%, #881C1C 100%);')}"
>
<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.4); z-index: 1"></div>
<div class="container" style="position: relative; z-index: 2">
<h1
class="display-4 font-weight-bold text-white mb-3"
th:if="${block.data['title'] != null and !#strings.isEmpty(block.data['title'])}"
th:utext="${block.data['title']}"
>
Hero Banner Title
</h1>
<p
class="lead text-light mb-4"
th:if="${block.data['subtitle'] != null and !#strings.isEmpty(block.data['subtitle'])}"
th:utext="${block.data['subtitle']}"
></p>
<a
th:if="${block.data['btnText'] != null and !#strings.isEmpty(block.data['btnText'])}"
th:href="${(block.data['btnLink'] != null and !#strings.isEmpty(block.data['btnLink'])) ? block.data['btnLink'] : '#'}"
class="btn btn-danger btn-lg font-weight-bold px-4 py-2 shadow"
th:utext="${block.data['btnText']}"
>
Click Here
</a>
</div>
</div>
</th:block>
<!-- 13. Sticky Nav / Sub-menu Block (UMass Theme mc--info-menu style) -->
<th:block th:if="${block['type'] == 'stickyNav' or block['type'] == 'nav'}">
<nav
th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
class="mc--menu mc--info-menu sis-sticky-nav my-3"
th:classappend="${(block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? (block.data['globalClass'] + ' ') : '') + (block.data['stretched'] == true ? 'sis-fullwidth-breakout ' : '') + (block.data['sticky'] == true or block.data['sticky'] == null ? 'is-sticky ' : '') + (block.data['bgColor'] == '#ffffff' ? 'sis-nav-light ' : '')}"
th:style="${'background-color: ' + (block.data['bgColor'] != null ? block.data['bgColor'] : '#002554') + ' !important;'}"
th:data-custom-attrs="${block.data['globalAttributes'] != null and !#strings.isEmpty(block.data['globalAttributes']) ? block.data['globalAttributes'] : (block.data['attributes'] != null and !#strings.isEmpty(block.data['attributes']) ? block.data['attributes'] : null)}"
>
<ul class="menu m--menu m--info-menu">
<li class="menu-item menu-item--expanded">
<details class="utility-button-wrapper mc--info-menu-container mobile-only">
<summary
type="button"
class="utility-button arrow-toggle information-for-toggle"
aria-label="Display submenu"
aria-expanded="false"
aria-haspopup="true"
>
<span
th:text="${block.data['title'] != null and !#strings.isEmpty(block.data['title']) ? block.data['title'] : 'Điều hướng nhanh'}"
>Điều hướng nhanh</span
>
<svg
version="1.1"
class="arrow"
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"
>
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#ffffff" points="7,7 0,0 16,0 "></polygon>
</svg>
</summary>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item" th:each="item : ${block.data['items']}">
<a th:href="${item['link']}" th:utext="${item['label']}">Nav Link</a>
</li>
</ul>
</div>
</details>
<div class="submenu-wrapper-desktop">
<ul class="submenu">
<li class="menu-item" th:each="item : ${block.data['items']}">
<a th:href="${item['link']}" th:utext="${item['label']}">Nav Link</a>
</li>
</ul>
</div>
</li>
</ul>
</nav>
</th:block>
<!-- 14. Timeline Block (Group) -->
<th:block th:if="${block['type'] == 'timeline' and block.data['items'] != null}">
<section class="sis-timeline" th:id="'timeline-' + ${stat != null ? stat.index : 0}">
<div class="sis-timeline__grid">
<!-- Text Column (Left) -->
<div class="sis-timeline__text-col">
<div class="sis-timeline__line"></div>
<th:block th:each="item : ${block.data['items']}">
<div class="sis-timeline__entry">
<div class="sis-timeline__dot"></div>
<div class="sis-timeline__entry-content">
<div class="sis-timeline__date" th:utext="${item['date']}"></div>
<div class="sis-timeline__desc" th:utext="${item['description']}"></div>
<!-- Mobile image -->
<div class="sis-timeline__mobile-img" th:if="${item['imageUrl'] != null and !#strings.isEmpty(item['imageUrl'])}">
<img th:src="${item['imageUrl']}" alt="Timeline Image" loading="lazy" />
</div>
</div>
</div>
</th:block>
</div>
<!-- Image Column (Right) -->
<div class="sis-timeline__img-col">
<th:block th:each="item : ${block.data['items']}">
<div class="sis-timeline__img-slot" th:if="${item['imageUrl'] != null and !#strings.isEmpty(item['imageUrl'])}">
<div class="sis-timeline__img-wrapper">
<img th:src="${item['imageUrl']}" alt="Timeline Image" loading="lazy" />
</div>
</div>
<div
class="sis-timeline__img-slot sis-timeline__img-slot-empty"
th:unless="${item['imageUrl'] != null and !#strings.isEmpty(item['imageUrl'])}"
></div>
</th:block>
</div>
</div>
</section>
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
const syncTimelineHeights = () => {
if (window.innerWidth < 1024) {
document.querySelectorAll('.sis-timeline__entry, .sis-timeline__img-slot').forEach(el => {
el.style.minHeight = '';
el.style.marginBottom = '';
});
return;
}
// Use the specific timeline section ID in case there are multiple
const section = document.querySelector('#timeline-[[${stat != null ? stat.index : 0}]]');
if (!section) return;
const texts = section.querySelectorAll('.sis-timeline__entry');
const imgs = section.querySelectorAll('.sis-timeline__img-slot');
for (let i = 0; i < texts.length; i++) {
if (imgs[i]) {
// reset inline styles first to measure natural height
texts[i].style.minHeight = '';
texts[i].style.marginBottom = '';
imgs[i].style.minHeight = '';
imgs[i].style.marginBottom = '';
const textH = texts[i].getBoundingClientRect().height;
const imgH = imgs[i].getBoundingClientRect().height;
const maxH = Math.max(textH, imgH);
texts[i].style.minHeight = maxH + 'px';
imgs[i].style.minHeight = maxH + 'px';
// sync bottom margin so they space out identically
texts[i].style.marginBottom = '4rem';
imgs[i].style.marginBottom = '4rem';
}
}
};
syncTimelineHeights();
window.addEventListener('resize', syncTimelineHeights);
// Run once more after images load
window.addEventListener('load', syncTimelineHeights);
});
</script>
</th:block>
<!-- 15. Grid / Columns Block -->
<th:block th:if="${block['type'] == 'grid'}">
<div th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
th:class="${block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? block.data['globalClass'] : null}"
th:data-custom-attrs="${block.data['globalAttributes'] != null and !#strings.isEmpty(block.data['globalAttributes']) ? block.data['globalAttributes'] : (block.data['attributes'] != null and !#strings.isEmpty(block.data['attributes']) ? block.data['attributes'] : null)}">
<div class="container py-4">
<div class="row">
<th:block th:each="i : ${#numbers.sequence(1, block.data['cols'])}">
<div
th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i]) ? block.data['id' + i] : null}"
th:class="${(block.data['width' + i] != null and block.data['width' + i] gt 0) ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (block.data['cols'] == 1 ? 'col-12 mb-4' : (block.data['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (block.data['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (block.data['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i]) ? block.data['class' + i] : ''}"
th:data-custom-attrs="${block.data['attr' + i] != null and !#strings.isEmpty(block.data['attr' + i]) ? block.data['attr' + i] : null}"
>
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, blockData=${block.data})"></div>
</div>
</th:block>
</div>
</div>
</div>
</th:block>
<!-- 16. Flex Layout Block -->
<th:block th:if="${block['type'] == 'flex'}">
<div th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
th:class="${block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? block.data['globalClass'] : null}"
th:data-custom-attrs="${block.data['globalAttributes'] != null and !#strings.isEmpty(block.data['globalAttributes']) ? block.data['globalAttributes'] : (block.data['attributes'] != null and !#strings.isEmpty(block.data['attributes']) ? block.data['attributes'] : null)}">
<div class="container py-4">
<div
style="display: flex; flex-wrap: wrap;"
th:styleappend="${'flex-direction: ' + (block.data['direction'] != null ? block.data['direction'] : 'row') + '; justify-content: ' + (block.data['justify'] == 'start' ? 'flex-start' : (block.data['justify'] == 'end' ? 'flex-end' : (block.data['justify'] == 'between' ? 'space-between' : (block.data['justify'] == 'around' ? 'space-around' : (block.data['justify'] == 'evenly' ? 'space-evenly' : 'center'))))) + '; align-items: ' + (block.data['align'] == 'start' ? 'flex-start' : (block.data['align'] == 'end' ? 'flex-end' : (block.data['align'] != null ? block.data['align'] : 'stretch'))) + '; gap: ' + (block.data['gap'] == '0' ? '0' : (block.data['gap'] == '1' ? '0.25rem' : (block.data['gap'] == '2' ? '0.5rem' : (block.data['gap'] == '3' ? '1rem' : (block.data['gap'] == '4' ? '1.5rem' : (block.data['gap'] == '5' ? '2rem' : '1rem')))))) + ';'}"
>
<th:block th:each="i : ${#numbers.sequence(1, block.data['cols'])}">
<div
th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i]) ? block.data['id' + i] : null}"
th:class="${(block.data['width' + i] != null and block.data['width' + i] gt 0) ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (block.data['cols'] == 1 ? 'col-12 mb-4' : (block.data['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (block.data['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (block.data['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i]) ? block.data['class' + i] : ''}"
th:data-custom-attrs="${block.data['attr' + i] != null and !#strings.isEmpty(block.data['attr' + i]) ? block.data['attr' + i] : null}"
>
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, blockData=${block.data})"></div>
</div>
</th:block>
</div>
</div>
</div>
</th:block>
<!-- 17. Dedicated Hover Cards Block -->
<th:block th:if="${block['type'] == 'hovercard' or block['type'] == 'hover-card' or block['type'] == 'hoverCard'}">
<div th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
th:class="${(block.data['styleVariant'] == 'v2' or (block.data['globalClass'] != null and block.data['globalClass'].contains('v2'))) ? ('sis-hover-card-v2-section ' + (block.data['globalClass'] != null ? block.data['globalClass'] : '')) : ('sis-hover-card-section ' + (block.data['globalClass'] != null ? block.data['globalClass'] : ''))}"
th:data-custom-attrs="${block.data['globalAttributes'] != null and !#strings.isEmpty(block.data['globalAttributes']) ? block.data['globalAttributes'] : (block.data['attributes'] != null and !#strings.isEmpty(block.data['attributes']) ? block.data['attributes'] : null)}">
<div class="container py-4">
<div class="row align-items-center">
<th:block th:each="item, iterStat : ${block.data['items']}">
<div th:class="${block.data['cols'] == 1 ? 'col-12 mb-4' : (block.data['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (block.data['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (block.data['cols'] == 6 ? 'col-lg-2 col-md-4 col-6 mb-4' : 'col-lg-3 col-md-6 col-12 mb-4')))}">
<div th:class="${(block.data['styleVariant'] == 'v2' or (block.data['globalClass'] != null and block.data['globalClass'].contains('v2'))) ? ('sis-hover-card sis-hover-card-v2 ' + ((item['active'] == true or (item['active'] == null and iterStat.first)) ? 'active' : '')) : ('sis-hover-card ' + ((item['active'] == true or (item['active'] == null and iterStat.first)) ? 'active' : ''))}"
th:data-custom-attrs="${item['attributes'] != null and !#strings.isEmpty(item['attributes']) ? item['attributes'] : null}">
<div class="w-full flex flex-col items-center justify-center transition-all duration-500">
<div class="sis-hover-card__icon" th:if="${item['imageUrl'] != null and !#strings.isEmpty(item['imageUrl'])}">
<img th:src="${item['imageUrl']}" class="sis-hover-card__img-main" th:alt="${item['title']}" />
<img th:if="${item['hoverImageUrl'] != null and !#strings.isEmpty(item['hoverImageUrl'])}" th:src="${item['hoverImageUrl']}" class="sis-hover-card__img-hover" th:alt="${item['title']}" />
</div>
<h3 class="sis-hover-card__title" th:utext="${item['title']}">Title</h3>
</div>
<p class="sis-hover-card__desc" th:utext="${item['description']}">Description</p>
</div>
</div>
</th:block>
</div>
</div>
</div>
</th:block>
<!-- 18. Custom HTML Tag Block (Opening or Closing Tag) -->
<th:block th:if="${block['type'] == 'tag'}">
<th:block th:if="${block.data['rawHtmlTag'] != null and !#strings.isEmpty(block.data['rawHtmlTag'])}"
th:utext="${block.data['rawHtmlTag']}"></th:block>
</th:block>
<!-- Shared Cell Content Renderer Fragment -->
<th:block th:fragment="renderCell(type, i, blockData)">
<!-- Type 1: HTML / Text -->
<th:block th:if="${type == null or type == 'html'}" th:utext="${blockData != null ? blockData['col' + i] : ''}"></th:block>
<!-- Type 2: Image -->
<th:block th:if="${type == 'image'}">
<div class="sis-grid-image-wrapper text-center">
<!-- Native Image Plugin structure -->
<th:block th:if="${blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['file'] != null}">
<img th:src="${blockData['col' + i]['file']['url']}" class="img-fluid rounded shadow-sm" th:alt="${blockData['col' + i]['caption']}" />
</th:block>
<!-- Custom imgUrl fallback -->
<th:block th:unless="${blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['file'] != null}">
<img th:if="${blockData != null and blockData['imgUrl' + i] != null and !#strings.isEmpty(blockData['imgUrl' + i])}" th:src="${blockData['imgUrl' + i]}" class="img-fluid rounded shadow-sm" alt="Grid Image" />
</th:block>
</div>
</th:block>
<!-- Type 3: YouTube Video -->
<th:block th:if="${type == 'youtube'}">
<div class="sis-youtube-container text-center">
<div
class="sis-yt-thumb-wrapper"
th:data-embed-url="${blockData != null ? blockData['ytUrl' + i] : ''}"
style="
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
border-radius: 8px;
background-color: #111;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
margin: 0 auto;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
"
th:styleappend="${blockData != null and blockData['ytUrl' + i] != null ? ((#strings.contains(blockData['ytUrl' + i], 'youtu.be/')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringAfter(blockData['ytUrl' + i], 'youtu.be/') + '/hqdefault.jpg' + ');' : ((#strings.contains(blockData['ytUrl' + i], 'v=')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(blockData['ytUrl' + i], 'v='), '&amp;') + '/hqdefault.jpg' + ');' : ((#strings.contains(blockData['ytUrl' + i], 'embed/')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(blockData['ytUrl' + i], 'embed/'), '?') + '/hqdefault.jpg' + ');' : ''))) : ''}"
>
<div class="sis-yt-play-btn" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); transition: transform 0.2s ease">
<svg width="68" height="48" viewBox="0 0 68 48">
<path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="#f00"></path>
<polygon points="26,12 26,36 48,24" fill="#fff"></polygon>
</svg>
</div>
</div>
</div>
</th:block>
<!-- Type 4: Accordion -->
<th:block th:if="${type == 'accordion'}">
<details class="sis-accordion-item mb-3">
<summary class="sis-accordion-title font-weight-bold p-3 bg-light border rounded" style="cursor: pointer; user-select: none">
<span th:utext="${blockData != null ? blockData['accTitle' + i] : ''}">Accordion Title</span>
</summary>
<div class="sis-accordion-body p-3 border border-top-0 rounded-bottom" th:utext="${blockData != null ? blockData['accContent' + i] : ''}"></div>
</details>
</th:block>
<!-- Nested Plugin: html-snippet -->
<th:block th:if="${type == 'html-snippet' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['id'] != null}">
<div th:replace="~{fragments/snippets :: snippet(id=${blockData['col' + i]['id']})}"></div>
</th:block>
<!-- Nested Plugin: timeline -->
<th:block th:if="${type == 'timeline' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['items'] != null}">
<section class="sis-timeline">
<div class="sis-timeline__grid">
<div class="sis-timeline__text-col" style="width: 100%;">
<div class="sis-timeline__line"></div>
<th:block th:each="item : ${blockData['col' + i]['items']}">
<div class="sis-timeline__entry">
<div class="sis-timeline__dot"></div>
<div class="sis-timeline__entry-content">
<div class="sis-timeline__date" th:utext="${item['date']}"></div>
<div class="sis-timeline__desc" th:utext="${item['description']}"></div>
<div class="sis-timeline__mobile-img" th:if="${item['imageUrl'] != null and !#strings.isEmpty(item['imageUrl'])}">
<img th:src="${item['imageUrl']}" alt="Timeline Image" loading="lazy" />
</div>
</div>
</div>
</th:block>
</div>
</div>
</section>
</th:block>
<!-- Nested Plugin: hovercard -->
<th:block th:if="${(type == 'hovercard' or type == 'hover-card' or type == 'hoverCard') and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['items'] != null}">
<div th:id="${blockData['col' + i]['globalId'] != null and !#strings.isEmpty(blockData['col' + i]['globalId']) ? blockData['col' + i]['globalId'] : null}"
th:class="${(blockData['col' + i]['styleVariant'] == 'v2' or (blockData['col' + i]['globalClass'] != null and blockData['col' + i]['globalClass'].contains('v2'))) ? ('sis-hover-card-v2-section ' + (blockData['col' + i]['globalClass'] != null ? blockData['col' + i]['globalClass'] : '')) : ('sis-hover-card-section ' + (blockData['col' + i]['globalClass'] != null ? blockData['col' + i]['globalClass'] : ''))}">
<div class="container-fluid p-0">
<div class="row align-items-center">
<th:block th:each="item, iterStat : ${blockData['col' + i]['items']}">
<div th:class="${blockData['col' + i]['cols'] == 1 ? 'col-12 mb-4' : (blockData['col' + i]['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 6 ? 'col-lg-2 col-md-4 col-6 mb-4' : 'col-lg-3 col-md-6 col-12 mb-4')))}">
<div th:class="${(blockData['col' + i]['styleVariant'] == 'v2' or (blockData['col' + i]['globalClass'] != null and blockData['col' + i]['globalClass'].contains('v2'))) ? ('sis-hover-card sis-hover-card-v2 ' + ((item['active'] == true or (item['active'] == null and iterStat.first)) ? 'active' : '')) : ('sis-hover-card ' + ((item['active'] == true or (item['active'] == null and iterStat.first)) ? 'active' : ''))}">
<div class="w-full flex flex-col items-center justify-center transition-all duration-500">
<div class="sis-hover-card__icon" th:if="${item['imageUrl'] != null and !#strings.isEmpty(item['imageUrl'])}">
<img th:src="${item['imageUrl']}" class="sis-hover-card__img-main" th:alt="${item['title']}" />
<img th:if="${item['hoverImageUrl'] != null and !#strings.isEmpty(item['hoverImageUrl'])}" th:src="${item['hoverImageUrl']}" class="sis-hover-card__img-hover" th:alt="${item['title']}" />
</div>
<h3 class="sis-hover-card__title" th:utext="${item['title']}">Title</h3>
</div>
<p class="sis-hover-card__desc" th:utext="${item['description']}">Description</p>
</div>
</div>
</th:block>
</div>
</div>
</div>
</th:block>
<!-- Nested Plugin: tag -->
<th:block th:if="${type == 'tag' and blockData != null and blockData['col' + i] != null}">
<th:block th:if="${blockData['col' + i]['rawHtmlTag'] != null and !#strings.isEmpty(blockData['col' + i]['rawHtmlTag'])}"
th:utext="${blockData['col' + i]['rawHtmlTag']}"></th:block>
</th:block>
<!-- Nested Plugin: grid (Recursive) -->
<th:block th:if="${type == 'grid' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['cols'] != null}">
<div th:id="${blockData['col' + i]['globalId'] != null and !#strings.isEmpty(blockData['col' + i]['globalId']) ? blockData['col' + i]['globalId'] : null}"
th:class="${blockData['col' + i]['globalClass'] != null and !#strings.isEmpty(blockData['col' + i]['globalClass']) ? blockData['col' + i]['globalClass'] : null}">
<div class="container-fluid p-0">
<div class="row">
<th:block th:each="nestedI : ${#numbers.sequence(1, blockData['col' + i]['cols'])}">
<div
th:id="${blockData['col' + i]['id' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['id' + nestedI]) ? blockData['col' + i]['id' + nestedI] : null}"
th:class="${(blockData['col' + i]['width' + nestedI] != null and blockData['col' + i]['width' + nestedI] gt 0) ? ('col-lg-' + blockData['col' + i]['width' + nestedI] + ' col-md-6 col-12 mb-4') : (blockData['col' + i]['cols'] == 1 ? 'col-12 mb-4' : (blockData['col' + i]['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
th:classappend="${blockData['col' + i]['class' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['class' + nestedI]) ? blockData['col' + i]['class' + nestedI] : ''}"
>
<div th:replace=":: renderCell(type=${blockData['col' + i]['type' + nestedI]}, i=${nestedI}, blockData=${blockData['col' + i]})"></div>
</div>
</th:block>
</div>
</div>
</div>
</th:block>
<!-- Nested Plugin: flex (Recursive) -->
<th:block th:if="${type == 'flex' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['cols'] != null}">
<div th:id="${blockData['col' + i]['globalId'] != null and !#strings.isEmpty(blockData['col' + i]['globalId']) ? blockData['col' + i]['globalId'] : null}"
th:class="${blockData['col' + i]['globalClass'] != null and !#strings.isEmpty(blockData['col' + i]['globalClass']) ? blockData['col' + i]['globalClass'] : null}">
<div class="container-fluid p-0">
<div
style="display: flex; flex-wrap: wrap;"
th:styleappend="${'flex-direction: ' + (blockData['col' + i]['direction'] != null ? blockData['col' + i]['direction'] : 'row') + '; justify-content: ' + (blockData['col' + i]['justify'] == 'start' ? 'flex-start' : (blockData['col' + i]['justify'] == 'end' ? 'flex-end' : (blockData['col' + i]['justify'] == 'between' ? 'space-between' : (blockData['col' + i]['justify'] == 'around' ? 'space-around' : (blockData['col' + i]['justify'] == 'evenly' ? 'space-evenly' : 'center'))))) + '; align-items: ' + (blockData['col' + i]['align'] == 'start' ? 'flex-start' : (blockData['col' + i]['align'] == 'end' ? 'flex-end' : (blockData['col' + i]['align'] != null ? blockData['col' + i]['align'] : 'stretch'))) + '; gap: ' + (blockData['col' + i]['gap'] == '0' ? '0' : (blockData['col' + i]['gap'] == '1' ? '0.25rem' : (blockData['col' + i]['gap'] == '2' ? '0.5rem' : (blockData['col' + i]['gap'] == '3' ? '1rem' : (blockData['col' + i]['gap'] == '4' ? '1.5rem' : (blockData['col' + i]['gap'] == '5' ? '2rem' : '1rem')))))) + ';'}"
>
<th:block th:each="nestedI : ${#numbers.sequence(1, blockData['col' + i]['cols'])}">
<div
th:id="${blockData['col' + i]['id' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['id' + nestedI]) ? blockData['col' + i]['id' + nestedI] : null}"
th:class="${(blockData['col' + i]['width' + nestedI] != null and blockData['col' + i]['width' + nestedI] gt 0) ? ('col-lg-' + blockData['col' + i]['width' + nestedI] + ' col-md-6 col-12 mb-4') : (blockData['col' + i]['cols'] == 1 ? 'col-12 mb-4' : (blockData['col' + i]['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
th:classappend="${blockData['col' + i]['class' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['class' + nestedI]) ? blockData['col' + i]['class' + nestedI] : ''}"
>
<div th:replace=":: renderCell(type=${blockData['col' + i]['type' + nestedI]}, i=${nestedI}, blockData=${blockData['col' + i]})"></div>
</div>
</th:block>
</div>
</div>
</div>
</th:block>
</th:block>
<style>
html {
scroll-behavior: smooth;
}
[id] {
scroll-margin-top: 80px;
}
.sis-sticky-nav .menu-item--expanded,
.sis-sticky-nav .submenu-wrapper-desktop,
.sis-sticky-nav .submenu-wrapper {
background-color: inherit !important;
}
.sis-sticky-nav.sis-nav-light a,
.sis-sticky-nav.sis-nav-light summary span {
color: #002554 !important;
}
.sis-sticky-nav.sis-nav-light summary polygon {
fill: #002554 !important;
}
.sis-fullwidth-breakout {
width: 100vw !important;
position: relative !important;
left: 50% !important;
right: 50% !important;
margin-left: -50vw !important;
margin-right: -50vw !important;
max-width: 100vw !important;
box-sizing: border-box !important;
}
.ckeditor-accordion dt {
cursor: pointer;
font-weight: 600;
padding: 12px 16px;
background: #ffffff;
border: 1px solid #e3e6f0;
border-radius: 6px;
margin-top: 10px;
margin-bottom: 0;
user-select: none;
}
.ckeditor-accordion dt:hover {
background: #eaecf4;
}
.ckeditor-accordion dd {
padding: 16px;
border: 1px solid #e3e6f0;
border-top: none;
border-radius: 0 0 6px 6px;
background: #fff;
margin-bottom: 15px;
}
details.mc--info-menu-container {
border: unset;
}
nav.sis-sticky-nav ul.menu {
max-width: 1280px;
margin: auto !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.ckeditor-accordion dt').forEach(function (dt) {
dt.addEventListener('click', function () {
var dd = this.nextElementSibling;
if (dd && dd.tagName.toLowerCase() === 'dd') {
if (dd.style.display === 'none' || getComputedStyle(dd).display === 'none') {
dd.style.display = 'block';
} else {
dd.style.display = 'none';
}
}
});
});
// YouTube Thumbnail Preview Click-to-Play
document.querySelectorAll('.sis-yt-thumb-wrapper').forEach(function (wrapper) {
var embedUrl = wrapper.getAttribute('data-embed-url');
var customThumb = wrapper.getAttribute('data-custom-thumb');
if (embedUrl) {
var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
var match = embedUrl.match(regExp);
var videoId = match && match[2].length === 11 ? match[2] : embedUrl.length === 11 ? embedUrl : '';
var thumbUrl =
customThumb && customThumb.trim() !== ''
? customThumb.trim()
: videoId
? 'https://img.youtube.com/vi/' + videoId + '/hqdefault.jpg'
: '';
if (thumbUrl && !wrapper.style.backgroundImage) {
wrapper.style.backgroundImage = 'url("' + thumbUrl + '")';
}
var isLoaded = false;
wrapper.addEventListener('click', function () {
if (isLoaded) return;
isLoaded = true;
var baseUrl = videoId ? 'https://www.youtube.com/embed/' + videoId : embedUrl;
var finalSrc = baseUrl + (baseUrl.includes('?') ? '&' : '?') + 'autoplay=1';
wrapper.innerHTML =
'<iframe src="' +
finalSrc +
'" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>';
});
}
});
// Interactive Hover Cards — Mouseover event delegation to apply active class dynamically
document.addEventListener('mouseover', function (e) {
var card = e.target.closest('.sis-hover-card, .sis-hover-card-v2');
if (card && !card.classList.contains('active')) {
var parentRow = card.closest('.row') || card.parentElement;
if (parentRow) {
parentRow.querySelectorAll('.sis-hover-card, .sis-hover-card-v2').forEach(function (c) {
c.classList.remove('active');
});
}
card.classList.add('active');
}
});
// Apply Custom Attributes (e.g. data-aos="fade-up" style="background:#fff") dynamically
document.querySelectorAll('[data-custom-attrs]').forEach(function (el) {
var attrStr = el.getAttribute('data-custom-attrs');
if (attrStr && attrStr.trim() !== '') {
var tempDiv = document.createElement('div');
tempDiv.innerHTML = '<div ' + attrStr + '></div>';
var tempEl = tempDiv.firstElementChild;
if (tempEl) {
Array.from(tempEl.attributes).forEach(function (attr) {
if (attr.name === 'class') {
attr.value.split(/\s+/).filter(Boolean).forEach(function (c) {
el.classList.add(c);
});
} else if (attr.name === 'style') {
el.style.cssText += ';' + attr.value;
} else {
el.setAttribute(attr.name, attr.value);
}
});
}
el.removeAttribute('data-custom-attrs');
}
});
});
</script>
<!-- Dedicated Page Custom JS -->
<script th:if="${page != null and page.customJs != null and !page.customJs.empty}" th:utext="${page.customJs}"></script>
</div>
</body>
</html>