feat: implement responsive tab plugin and new hover card v3 component, and add admin controllers for price and recruiting tables.

This commit is contained in:
2026-08-04 21:44:13 +07:00
parent 89c18f20fd
commit 373ee63716
42 changed files with 6148 additions and 1008 deletions
@@ -0,0 +1,160 @@
<style>
/* Bento Layout 02 (Featured 1 + 2x2 Grid) */
.news-section-v3 {
max-width: 1200px;
width: 100%;
background-color: #ffffff;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 24px;
margin: 0 auto;
}
.news-section-v3 .badge-news {
display: inline-block;
padding: 4px 8px;
background-color: #f1f5f9;
color: #475569;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
border-radius: 4px;
letter-spacing: 0.5px;
}
.news-section-v3 .post-date {
font-size: 12px;
color: #94a3b8;
}
.news-section-v3 .featured-post {
grid-column: span 6;
display: flex;
flex-direction: column;
gap: 16px;
}
.news-section-v3 .featured-img-box {
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 8px;
overflow: hidden;
}
.news-section-v3 .featured-img-box img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.news-section-v3 .featured-post:hover .featured-img-box img {
transform: scale(1.03);
}
.news-section-v3 .meta-row {
display: flex;
align-items: center;
gap: 12px;
}
.news-section-v3 .featured-title {
font-size: 22px;
font-weight: 700;
color: #0284c7;
line-height: 1.3;
cursor: pointer;
text-decoration: none;
}
.news-section-v3 .featured-title:hover {
color: #0369a1;
}
.news-section-v3 .featured-excerpt {
font-size: 14px;
color: #475569;
line-height: 1.6;
}
.news-section-v3 .side-news-grid {
grid-column: span 6;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.news-section-v3 .small-post {
display: flex;
flex-direction: column;
gap: 8px;
text-decoration: none;
color: inherit;
}
.news-section-v3 .small-img-box {
width: 100%;
aspect-ratio: 16 / 10;
border-radius: 8px;
overflow: hidden;
}
.news-section-v3 .small-img-box img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.news-section-v3 .small-post:hover .small-img-box img {
transform: scale(1.04);
}
.news-section-v3 .small-title {
font-size: 14px;
font-weight: 700;
color: #1e293b;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
}
.news-section-v3 .small-post:hover .small-title {
color: #0284c7;
}
@media (max-width: 992px) {
.news-section-v3 .featured-post,
.news-section-v3 .side-news-grid {
grid-column: span 12;
}
}
@media (max-width: 576px) {
.news-section-v3 .side-news-grid {
grid-template-columns: 1fr;
}
.news-section-v3 .featured-title {
font-size: 18px;
}
}
</style>
<div class="news-section-v3" th:if="${posts != null}">
<th:block th:with="filteredPosts=${posts}">
<article class="featured-post">
<div class="featured-img-box" th:if="${(block.data['showImage'] == null or block.data['showImage'] == true) and firstPost.featuredImage != null and !#strings.isEmpty(firstPost.featuredImage)}">
<img th:src="${firstPost.featuredImage}" th:alt="${firstPost.title}" />
</div>
<div class="meta-row">
<span class="badge-news" th:if="${firstPost.category != null}" th:text="${firstPost.category.name}">Tin tức</span>
<span class="post-date" th:if="${(block.data['showDate'] == null or block.data['showDate'] == true) and firstPost.createdDate != null}" th:text="${#temporals.format(firstPost.createdDate, 'dd/MM/yyyy')}">06/07/2026</span>
</div>
<a th:href="@{'/posts/' + ${firstPost.slug}}" class="featured-title" th:utext="${firstPost.title}">Featured Post Title</a>
<p class="featured-excerpt" th:if="${(block.data['showExcerpt'] == null or block.data['showExcerpt'] == true) and firstPost.excerpt != null and !#strings.isEmpty(firstPost.excerpt)}" th:utext="${firstPost.excerpt}">Featured Excerpt...</p>
</article>
</th:block>
<!-- CỘT PHẢI: Lưới 4 bài viết nhỏ (2x2) -->
<div class="side-news-grid">
<th:block th:each="p, iterStat : ${posts}" th:if="${iterStat.index > 0 and iterStat.index <= 4}">
<a th:href="@{'/posts/' + ${p.slug}}" class="small-post">
<div class="small-img-box" th:if="${(block.data['showImage'] == null or block.data['showImage'] == true) and p.featuredImage != null and !#strings.isEmpty(p.featuredImage)}">
<img th:src="${p.featuredImage}" th:alt="${p.title}" />
</div>
<div class="meta-row">
<span class="badge-news" th:if="${p.category != null}" th:text="${p.category.name}">Category</span>
</div>
<h3 class="small-title" th:utext="${p.title}">Small Post Title</h3>
<span class="post-date" th:if="${(block.data['showDate'] == null or block.data['showDate'] == true) and p.createdDate != null}" th:text="${#temporals.format(p.createdDate, 'dd/MM/yyyy')}">04/08/2026</span>
</a>
</th:block>
</div>
</div>