feat: implement specialty content module with expanded Page entity fields, database migrations, and data import utilities

This commit is contained in:
2026-07-28 19:20:56 +07:00
parent 01a3b83672
commit 186f70ec32
429 changed files with 52283 additions and 35 deletions
@@ -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>
@@ -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;
}
}
}
File diff suppressed because one or more lines are too long
@@ -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"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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);})();
@@ -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");
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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>