feat: implement page/post duplication, add event layout, auto-extract featured images, and include HTML escaping for templates.
This commit is contained in:
+135
@@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
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-4.20.xsd">
|
||||
|
||||
<changeSet id="20260714080000-1" author="antigravity">
|
||||
<insert tableName="sis_component_template">
|
||||
<column name="id" valueComputed="(select coalesce(max(id), 0) + 1 from sis_component_template)"/>
|
||||
<column name="slug" value="news-grid"/>
|
||||
<column name="name" value="News Grid"/>
|
||||
<column name="description" value="Dynamic grid layout for news."/>
|
||||
<column name="active" valueBoolean="true"/>
|
||||
<column name="html_template"><![CDATA[{{#each news_posts}}
|
||||
<div class="featured-grid__item">
|
||||
<article class="news-card news-card--featured">
|
||||
<div class="news-card__image">
|
||||
<div>
|
||||
<img loading="lazy" src="{{featuredImageUrl}}" alt="{{title}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="news-card__content">
|
||||
<div class="featured-grid__category">
|
||||
<a href="#">TIN TỨC</a>
|
||||
</div>
|
||||
<h3 class="news-card__title">
|
||||
<a href="/post/{{slug}}" rel="bookmark">
|
||||
<span>{{title}}</span>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="news-card__date">{{date}}</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{{/each}}]]></column>
|
||||
<column name="created_date" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
</insert>
|
||||
|
||||
<insert tableName="sis_component_template">
|
||||
<column name="id" valueComputed="(select coalesce(max(id), 0) + 1 from sis_component_template)"/>
|
||||
<column name="slug" value="events-list"/>
|
||||
<column name="name" value="Events List"/>
|
||||
<column name="description" value="List layout for events."/>
|
||||
<column name="active" valueBoolean="true"/>
|
||||
<column name="html_template"><![CDATA[<style>
|
||||
.campus-events-widget { background: transparent; padding: 0; font-family: inherit; }
|
||||
.campus-events-widget .events-list { list-style: none; margin: 0; padding: 0; }
|
||||
.campus-events-widget .teaser-item { padding: 20px 0; border-bottom: 1px solid #e5e5e5; }
|
||||
.campus-events-widget .teaser-item:last-child { border-bottom: none; }
|
||||
.campus-events-widget .event-wrapper { display: flex; gap: 20px; align-items: flex-start; }
|
||||
.campus-events-widget .event-date { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 0.125rem; padding: 0.25rem; background: #881C1C; width: 2.875rem; height: 3.125rem; box-sizing: border-box; }
|
||||
.campus-events-widget .event-date__month { font-size: 1rem; font-weight: 800; line-height: 1.25rem; text-transform: uppercase; text-align: center; color: #fff; }
|
||||
.campus-events-widget .event-date__day { font-size: 1rem; font-weight: 800; line-height: 1.25rem; text-transform: uppercase; text-align: center; color: #fff; }
|
||||
.campus-events-widget .event-details { flex: 1; position: relative; }
|
||||
.campus-events-widget .event-title { font-size: 1rem; font-weight: 800; line-height: 1.25rem; text-transform: uppercase; color: #000; }
|
||||
.campus-events-widget .event-title a { color: inherit; text-decoration: none; }
|
||||
.campus-events-widget .event-title a:hover { text-decoration: underline; }
|
||||
.campus-events-widget .event-time { font-size: 16px; color: #000; margin-bottom: 4px; }
|
||||
.campus-events-widget .event-location { font-size: 1rem; font-weight: 400; line-height: 1.5rem; font-style: normal; color: #000; }
|
||||
@media (max-width: 768px) {
|
||||
.campus-events-widget .event-title { font-size: 0.75rem; }
|
||||
.campus-events-widget .event-time { font-size: 0.75rem; }
|
||||
.campus-events-widget .event-location { font-size: 0.75rem; }
|
||||
.campus-events-widget .event-wrapper { display: flex; flex: 1; padding-right: 10px; align-items: stretch; gap: 12px; border: 1px solid #CCC; }
|
||||
.campus-events-widget .event-date { display: flex; padding: 4px 8px; flex-direction: column; justify-content: center; align-items: center; align-self: stretch; background: #881C1C; min-width: 48px; height: auto; }
|
||||
.campus-events-widget .event-details { padding: 8px 10px 8px 0; }
|
||||
.campus-events-widget .teaser-item { flex: 0 0 279px; display: flex; border: 0px solid #CCC; padding-bottom: 0; }
|
||||
.campus-events-widget .events-list { display: flex; gap: 1rem; overflow-x: auto; padding-bottom: 1rem; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.campus-events-widget .event-wrapper { gap: 10px; }
|
||||
.campus-events-widget .event-date__day { font-size: 18px; }
|
||||
}
|
||||
</style>
|
||||
<div class="campus-events-widget">
|
||||
<div class="events-list">
|
||||
{{#each event_posts}}
|
||||
<div class="teaser-item">
|
||||
<div class="event-wrapper">
|
||||
<div class="event-date">
|
||||
<div class="event-date__month">{{eventMonth}}</div>
|
||||
<div class="event-date__day">{{eventDay}}</div>
|
||||
</div>
|
||||
<div class="event-details">
|
||||
<div class="event-title">
|
||||
<a href="/post/{{slug}}">{{title}}</a>
|
||||
</div>
|
||||
<div class="event-time">{{eventTimeStr}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>]]></column>
|
||||
<column name="created_date" valueComputed="CURRENT_TIMESTAMP"/>
|
||||
</insert>
|
||||
<changeSet id="20260714080000-3" author="antigravity" runAlways="true">
|
||||
<update tableName="sis_component_template">
|
||||
<column name="html_template"><![CDATA[{{#each news_posts}}
|
||||
<div class="featured-grid__item">
|
||||
<article class="news-card news-card--featured">
|
||||
<div class="news-card__image">
|
||||
<div>
|
||||
<img loading="lazy" src="{{featuredImageUrl}}" alt="{{title}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="news-card__content">
|
||||
<div class="featured-grid__category">
|
||||
<a href="#">TIN TỨC</a>
|
||||
</div>
|
||||
<h3 class="news-card__title">
|
||||
<a href="/post/{{slug}}" rel="bookmark">
|
||||
<span>{{title}}</span>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="news-card__date">{{date}}</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{{/each}}]]></column>
|
||||
<where>slug = 'news-grid'</where>
|
||||
</update>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
|
||||
|
||||
<changeSet id="20260714080000-2" author="antigravity" runAlways="true">
|
||||
<update tableName="sis_component_template">
|
||||
<column name="html_template" valueComputed="REPLACE(html_template, '\n', '')"/>
|
||||
<where>slug IN ('news-grid', 'events-list')</where>
|
||||
</update>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@@ -36,4 +36,5 @@
|
||||
<!-- jhipster-needle-liquibase-add-changelog - JHipster will add liquibase changelogs here -->
|
||||
<!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here -->
|
||||
<!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here -->
|
||||
<include file="config/liquibase/changelog/20260714080000_seed_news_events_components.xml" relativeToChangelogFile="false"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
@@ -67,16 +67,23 @@
|
||||
<td th:text="${page.displayOrder}"></td>
|
||||
<td th:text="${page.lastModifiedDate != null} ? ${#dates.format(page.lastModifiedDateAsDate, 'yyyy-MM-dd HH:mm')} : '-'"></td>
|
||||
<td>
|
||||
<a th:href="@{/manage/pages/{id}/edit(id=${page.id})}" class="btn btn-sm btn-info" title="Edit">
|
||||
<a th:href="@{/manage/pages/{id}/edit(id=${page.id})}" class="btn btn-sm btn-info mb-1" title="Edit">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
<form th:action="@{/manage/pages/{id}/delete(id=${page.id})}" method="post" style="display:inline;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this page?');">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-danger" title="Delete">
|
||||
<button type="submit" class="btn btn-sm btn-danger mb-1" title="Delete">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
<form th:action="@{/manage/pages/{id}/duplicate(id=${page.id})}" method="post" style="display:inline;"
|
||||
onsubmit="return confirm('Are you sure you want to duplicate this page?');">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-secondary mb-1" title="Duplicate">
|
||||
<i class="fas fa-copy"></i> Duplicate
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(pages)}">
|
||||
|
||||
@@ -267,6 +267,14 @@
|
||||
</select>
|
||||
<small class="form-text text-muted">Controls the visual appearance on the frontend.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-3" id="eventTimeGroup" style="display: none;">
|
||||
<label for="postEventTime" class="font-weight-bold text-danger">
|
||||
<i class="far fa-calendar-alt"></i> Event Time
|
||||
</label>
|
||||
<input type="datetime-local" class="form-control" id="postEventTime" th:field="*{eventTimeLocal}">
|
||||
<small class="form-text text-muted">Specify the date and time for this event.</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a th:href="@{/manage/posts}" class="btn btn-secondary btn-sm">
|
||||
@@ -469,6 +477,19 @@
|
||||
slugPreview.textContent = slug || 'auto-generated';
|
||||
}
|
||||
});
|
||||
|
||||
// --- Event Time Layout Toggle ---
|
||||
var layoutSelect = document.getElementById('postLayout');
|
||||
var eventTimeGroup = document.getElementById('eventTimeGroup');
|
||||
function toggleEventTime() {
|
||||
if (layoutSelect.value === 'EVENT') {
|
||||
eventTimeGroup.style.display = 'block';
|
||||
} else {
|
||||
eventTimeGroup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
layoutSelect.addEventListener('change', toggleEventTime);
|
||||
toggleEventTime(); // Initial check
|
||||
});
|
||||
|
||||
// --- Tag click-to-add helper ---
|
||||
|
||||
@@ -130,10 +130,17 @@
|
||||
<form th:action="@{/manage/posts/{id}/delete(id=${post.id})}" method="post" style="display:inline;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this post?');">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger mr-1" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form th:action="@{/manage/posts/{id}/duplicate(id=${post.id})}" method="post" style="display:inline;"
|
||||
onsubmit="return confirm('Are you sure you want to duplicate this post?');">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary" title="Duplicate">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(posts)}">
|
||||
|
||||
Reference in New Issue
Block a user