Phần Post đã thiết kế xong chỉ cần sửa lại tempalte nữa là được

This commit is contained in:
2026-06-28 16:25:51 +07:00
parent 38d6b8234a
commit 0326535eb6
64 changed files with 13477 additions and 1603 deletions
@@ -14,6 +14,8 @@
# ===================================================================
logging:
file:
name: application.log
level:
ROOT: DEBUG
tech.jhipster: DEBUG
@@ -24,7 +26,7 @@ spring:
devtools:
restart:
enabled: true
additional-exclude: static/**,.h2.server.properties
additional-exclude: static/**
livereload:
enabled: true # enabled for backend-only app (run ./gradlew classes -t)
jackson2:
@@ -32,16 +34,15 @@ spring:
indent-output: true
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:oracle:thin:@192.168.1.15:1521/sisvietnam
url: jdbc:oracle:thin:@localhost:1521/FREEPDB1
username: sisvietnam
password: "sisvietnam"
hikari:
poolName: Hikari
auto-commit: false
h2:
console:
# JHipster uses a custom h2-console initializer
enabled: false
jpa:
properties:
hibernate.dialect: org.hibernate.dialect.OracleDialect
liquibase:
# Remove 'faker' if you do not want the sample data to be loaded automatically
contexts: dev, faker
@@ -0,0 +1,126 @@
<?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">
<!-- ============================== -->
<!-- 1. Create sis_category table -->
<!-- ============================== -->
<changeSet id="20260628100000-1" author="sisvietnam">
<createTable tableName="sis_category">
<column name="id" type="bigint">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="slug" type="varchar(255)">
<constraints nullable="false" unique="true" uniqueConstraintName="ux_sis_category_slug"/>
</column>
<column name="description" type="varchar(1000)"/>
<column name="parent_id" type="bigint"/>
<column name="created_by" type="varchar(50)">
<constraints nullable="false"/>
</column>
<column name="created_date" type="timestamp"/>
<column name="last_modified_by" type="varchar(50)"/>
<column name="last_modified_date" type="timestamp"/>
</createTable>
<addForeignKeyConstraint baseTableName="sis_category"
baseColumnNames="parent_id"
constraintName="fk_category_parent"
referencedTableName="sis_category"
referencedColumnNames="id"/>
</changeSet>
<!-- ============================== -->
<!-- 2. Create sis_tag table -->
<!-- ============================== -->
<changeSet id="20260628100000-2" author="sisvietnam">
<createTable tableName="sis_tag">
<column name="id" type="bigint">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(255)">
<constraints nullable="false" unique="true" uniqueConstraintName="ux_sis_tag_name"/>
</column>
<column name="slug" type="varchar(255)">
<constraints nullable="false" unique="true" uniqueConstraintName="ux_sis_tag_slug"/>
</column>
<column name="created_by" type="varchar(50)">
<constraints nullable="false"/>
</column>
<column name="created_date" type="timestamp"/>
<column name="last_modified_by" type="varchar(50)"/>
<column name="last_modified_date" type="timestamp"/>
</createTable>
</changeSet>
<!-- ============================== -->
<!-- 3. Create sis_post table -->
<!-- ============================== -->
<changeSet id="20260628100000-3" author="sisvietnam">
<createTable tableName="sis_post">
<column name="id" type="bigint">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="title" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="slug" type="varchar(255)">
<constraints nullable="false" unique="true" uniqueConstraintName="ux_sis_post_slug"/>
</column>
<column name="content" type="${clobType}"/>
<column name="excerpt" type="varchar(1000)"/>
<column name="featured_image" type="varchar(500)"/>
<column name="status" type="varchar(20)" defaultValue="DRAFT">
<constraints nullable="false"/>
</column>
<column name="meta_description" type="varchar(500)"/>
<column name="category_id" type="bigint"/>
<column name="created_by" type="varchar(50)">
<constraints nullable="false"/>
</column>
<column name="created_date" type="timestamp"/>
<column name="last_modified_by" type="varchar(50)"/>
<column name="last_modified_date" type="timestamp"/>
</createTable>
<addForeignKeyConstraint baseTableName="sis_post"
baseColumnNames="category_id"
constraintName="fk_post_category"
referencedTableName="sis_category"
referencedColumnNames="id"/>
</changeSet>
<!-- ============================== -->
<!-- 4. Create sis_post_tag table -->
<!-- ============================== -->
<changeSet id="20260628100000-4" author="sisvietnam">
<createTable tableName="sis_post_tag">
<column name="post_id" type="bigint">
<constraints nullable="false"/>
</column>
<column name="tag_id" type="bigint">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey tableName="sis_post_tag" columnNames="post_id, tag_id"/>
<addForeignKeyConstraint baseTableName="sis_post_tag"
baseColumnNames="post_id"
constraintName="fk_post_tag_post"
referencedTableName="sis_post"
referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="sis_post_tag"
baseColumnNames="tag_id"
constraintName="fk_post_tag_tag"
referencedTableName="sis_tag"
referencedColumnNames="id"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,15 @@
<?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="20260628151700-1" author="antigravity">
<addColumn tableName="sis_post">
<column name="layout" type="varchar(20)" defaultValue="STANDARD">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -21,6 +21,8 @@
<include file="config/liquibase/changelog/00000000000000_initial_schema.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260625100000_add_page_entity.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260625120000_add_page_type.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260628100000_add_post_category_tag.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260628151700_add_post_layout.xml" relativeToChangelogFile="false"/>
<!-- 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 -->
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
<footer id="l--main-footer" class="site-footer" th:fragment="footer">
<h2 class="visually-hidden">Site Footer</h2>
<footer id="l--main-footer" class="site-footer">
<h2 class="visually-hidden">Site Footer</h2>
<div class="region region-footer r--region r--footer">
<div class="lc--layout-container lc--full">
<div class="l--layout l--full">
@@ -11,192 +11,270 @@
<div class="footer-top">
<div class="branding-container">
<div id="block-footerbranding"
class="block block-fixed-block-content block-fixed-block-contentfooter-branding cc--component-container cc--footer-branding">
<div class="c--component c--footer-branding">
<div id="block-footerbranding" class="block block-fixed-block-content block-fixed-block-contentfooter-branding cc--component-container cc--footer-branding">
<div class="c--component c--footer-branding">
<a class="logo" href="https://www.umass.edu/" aria-label="UMass Amherst">
<div><!--?xml version="1.0" encoding="utf-8"?--><!-- Generator: Adobe Illustrator 27.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg id="Layer_1" style="enable-background:new 0 0 117.41 54.06;" version="1.1" viewBox="0 0 117.41 54.06" x="0px" xml:space="preserve" y="0px" xmlns:xlink="http://www.w3.org/1999/xlink">
<style type="text/css">.st0{fill:#FFFFFF;} .st1{fill:#FFFFFF;stroke:#303030;stroke-width:0.7005;stroke-miterlimit:10;} .st2{fill:#891D1C;}
</style>
<g> <g> <path class="st0" d="M53.94,15.8c-0.65,0.04-0.91,0.2-0.96,0.64c-0.03,0.29-0.05,0.76-0.05,1.77v0.91c0,1.29-0.2,2.21-0.83,2.88
c-0.5,0.52-1.23,0.75-1.95,0.75c-0.63,0-1.27-0.15-1.75-0.52c-0.65-0.5-0.96-1.32-0.96-2.6V16.9c0-0.98-0.09-1.03-0.95-1.1v-0.31
h2.83v0.31c-0.86,0.05-0.95,0.12-0.95,1.1v2.49c0,1.76,0.7,2.76,2.05,2.76c1.57,0,2.06-1.31,2.06-3.11v-0.83
c0-1-0.03-1.46-0.08-1.8c-0.07-0.43-0.29-0.56-1.06-0.61v-0.31h2.6C53.94,15.49,53.94,15.8,53.94,15.8z"></path> </g> <g> <path class="st0" d="M56.83,22.59V22.3c0.64-0.07,0.72-0.15,0.72-0.91v-1.82c0-0.75-0.26-1.24-0.99-1.24
c-0.44,0-0.86,0.27-1.19,0.56v2.56c0,0.74,0.07,0.79,0.71,0.85v0.28h-2.37V22.3c0.74-0.08,0.8-0.13,0.8-0.85v-2.26
c0-0.7-0.07-0.73-0.65-0.84V18.1c0.52-0.1,1.04-0.23,1.52-0.45v0.87c0.22-0.16,0.46-0.33,0.73-0.51c0.31-0.2,0.59-0.33,0.91-0.33
c0.86,0,1.41,0.6,1.41,1.66v2.11c0,0.74,0.05,0.79,0.69,0.85v0.28L56.83,22.59L56.83,22.59z"></path> </g> <g> <path class="st0" d="M59.6,22.59V22.3c0.69-0.07,0.76-0.13,0.76-0.87v-2.25c0-0.68-0.03-0.72-0.69-0.83v-0.26
c0.56-0.09,1.07-0.23,1.55-0.43v3.76c0,0.74,0.08,0.81,0.77,0.87v0.28H59.6V22.59z M60.18,16.18c0-0.33,0.26-0.57,0.56-0.57
c0.31,0,0.55,0.24,0.55,0.57c0,0.29-0.24,0.56-0.56,0.56C60.44,16.74,60.18,16.48,60.18,16.18z"></path> </g> <g> <path class="st0" d="M67.08,18.09c-0.57,0.08-0.67,0.17-0.96,0.83c-0.36,0.82-0.94,2.16-1.54,3.79h-0.29
c-0.49-1.31-0.99-2.55-1.51-3.82c-0.25-0.63-0.34-0.71-0.93-0.8v-0.27h2.24v0.27c-0.59,0.08-0.61,0.16-0.44,0.63
c0.28,0.76,0.72,1.91,1.04,2.69c0.38-0.91,0.73-1.9,0.97-2.61c0.16-0.52,0.1-0.64-0.52-0.72v-0.27h1.94
C67.08,17.81,67.08,18.09,67.08,18.09z"></path> </g> <g> <path class="st0" d="M70.71,21.66c-0.69,0.86-1.44,1.06-1.78,1.06c-1.34,0-2.13-1.09-2.13-2.32c0-0.75,0.27-1.42,0.72-1.91
c0.45-0.5,1.04-0.8,1.6-0.8c0.91,0,1.64,0.81,1.64,1.66c0,0.22-0.05,0.31-0.25,0.35c-0.19,0.03-1.55,0.14-2.85,0.19
c0.01,1.48,0.86,2.09,1.63,2.09c0.44,0,0.85-0.17,1.24-0.55L70.71,21.66z M67.72,19.43c0.61,0,1.21-0.01,1.84-0.03
c0.2,0,0.26-0.07,0.26-0.22c0.01-0.57-0.36-1.09-0.96-1.09C68.36,18.09,67.88,18.55,67.72,19.43z"></path> </g> <g> <path class="st0" d="M72.92,18.84c0.33-0.55,0.8-1.16,1.32-1.16c0.37,0,0.63,0.28,0.63,0.55c0,0.22-0.14,0.44-0.35,0.53
c-0.13,0.05-0.23,0.04-0.29-0.02c-0.15-0.16-0.28-0.23-0.46-0.23c-0.28,0-0.63,0.33-0.87,0.94v2c0,0.72,0.05,0.79,0.89,0.85v0.28
h-2.51V22.3c0.68-0.07,0.75-0.13,0.75-0.85v-2.26c0-0.72-0.05-0.76-0.68-0.85V18.1c0.53-0.09,1.01-0.22,1.54-0.45v1.19H72.92z"></path> </g> <g> <path class="st0" d="M77.94,19.1c-0.21-0.64-0.59-1.08-1.12-1.08c-0.41,0-0.71,0.33-0.71,0.75c0,0.49,0.44,0.74,0.93,0.95
c0.82,0.34,1.37,0.73,1.37,1.48c0,0.99-0.93,1.51-1.8,1.51c-0.56,0-1.06-0.21-1.25-0.37c-0.08-0.15-0.17-0.89-0.17-1.29L75.47,21
c0.2,0.72,0.69,1.35,1.34,1.35c0.4,0,0.77-0.31,0.77-0.76c0-0.48-0.32-0.74-0.86-0.99c-0.65-0.31-1.36-0.67-1.36-1.51
c0-0.76,0.65-1.41,1.66-1.41c0.45,0,0.74,0.1,0.97,0.19c0.09,0.25,0.21,0.89,0.24,1.17L77.94,19.1z"></path> </g> <g> <path class="st0" d="M79.03,22.59V22.3c0.69-0.07,0.76-0.13,0.76-0.87v-2.25c0-0.68-0.03-0.72-0.69-0.83v-0.26
c0.56-0.09,1.07-0.23,1.55-0.43v3.76c0,0.74,0.08,0.81,0.77,0.87v0.28h-2.39V22.59z M79.61,16.18c0-0.33,0.26-0.57,0.56-0.57
c0.31,0,0.55,0.24,0.55,0.57c0,0.29-0.24,0.56-0.56,0.56C79.87,16.74,79.61,16.48,79.61,16.18z"></path> </g> <g> <path class="st0" d="M84.01,22.57c-0.19,0.1-0.36,0.14-0.46,0.14c-0.69,0-1.07-0.44-1.07-1.3v-3.17h-0.75l-0.04-0.11l0.29-0.32
h0.5v-0.8l0.71-0.72l0.15,0.02v1.49h1.23c0.1,0.11,0.07,0.35-0.07,0.43h-1.17v2.8c0,0.88,0.36,1.05,0.63,1.05s0.52-0.11,0.68-0.19
l0.1,0.28L84.01,22.57z"></path> </g> <g> <path class="st0" d="M90,18.09c-0.58,0.09-0.68,0.17-0.97,0.8c-0.27,0.58-0.61,1.48-1.36,3.28c-0.73,1.75-0.97,2.37-1.12,2.96
c-0.05,0.21-0.2,0.26-0.35,0.26c-0.31,0-0.62-0.26-0.62-0.52c0-0.17,0.1-0.27,0.28-0.39c0.34-0.22,0.56-0.49,0.81-0.97
c0.21-0.38,0.29-0.61,0.34-0.75c0.05-0.16,0.05-0.31-0.01-0.49c-0.44-1.24-0.93-2.56-1.25-3.34c-0.23-0.6-0.34-0.74-0.94-0.83
v-0.27h2.21v0.27c-0.5,0.09-0.56,0.2-0.41,0.6l0.99,2.74c0.31-0.74,0.77-2.03,1-2.72c0.13-0.41,0.05-0.53-0.57-0.62v-0.27H90
V18.09z"></path> </g> <g> <path class="st0" d="M52.65,31.38v-0.31c0.88-0.09,0.93-0.14,0.92-1.3l-0.03-4.45H53.5l-2.66,5.98h-0.22l-2.44-5.83h-0.02
L48,28.58c-0.04,0.89-0.04,1.37-0.03,1.81c0.02,0.52,0.28,0.6,1,0.68v0.31h-2.54v-0.31c0.63-0.08,0.85-0.2,0.93-0.65
c0.05-0.38,0.12-0.86,0.21-2l0.15-2.27c0.1-1.42,0.04-1.46-0.95-1.56v-0.31h1.79l2.49,5.46l2.52-5.46h1.85v0.31
c-0.96,0.09-1.04,0.11-1,1.23l0.11,3.95c0.03,1.16,0.07,1.21,1.01,1.3v0.31C55.54,31.38,52.65,31.38,52.65,31.38z"></path> </g> <g> <path class="st0" d="M59.34,31.51c-0.16,0-0.39-0.1-0.51-0.21c-0.14-0.15-0.21-0.31-0.26-0.49c-0.44,0.29-0.96,0.7-1.29,0.7
c-0.77,0-1.31-0.64-1.31-1.33c0-0.53,0.28-0.87,0.88-1.08c0.65-0.22,1.45-0.51,1.69-0.71v-0.2c0-0.79-0.37-1.23-0.92-1.23
c-0.22,0-0.38,0.09-0.49,0.22c-0.14,0.14-0.24,0.38-0.33,0.72c-0.04,0.17-0.16,0.25-0.32,0.25c-0.2,0-0.48-0.22-0.48-0.47
c0-0.16,0.14-0.28,0.35-0.44c0.31-0.22,1.01-0.64,1.64-0.76c0.33,0,0.65,0.1,0.89,0.29c0.38,0.32,0.49,0.76,0.49,1.33v2.03
c0,0.49,0.2,0.65,0.38,0.65c0.13,0,0.28-0.05,0.39-0.12l0.11,0.28L59.34,31.51z M58.55,28.76c-0.23,0.12-0.75,0.35-0.98,0.46
c-0.43,0.2-0.67,0.4-0.67,0.8c0,0.57,0.44,0.84,0.77,0.84c0.28-0.01,0.67-0.2,0.87-0.39L58.55,28.76L58.55,28.76z"></path> </g> <g> <path class="st0" d="M63.38,27.9c-0.21-0.64-0.59-1.08-1.12-1.08c-0.41,0-0.71,0.33-0.71,0.75c0,0.49,0.44,0.74,0.93,0.95
c0.82,0.34,1.37,0.73,1.37,1.48c0,0.99-0.93,1.51-1.8,1.51c-0.56,0-1.06-0.21-1.25-0.37c-0.08-0.15-0.17-0.89-0.17-1.29l0.28-0.05
c0.2,0.72,0.69,1.35,1.34,1.35c0.4,0,0.77-0.31,0.77-0.76c0-0.48-0.32-0.74-0.86-0.99c-0.65-0.31-1.36-0.67-1.36-1.51
c0-0.76,0.65-1.41,1.66-1.41c0.45,0,0.74,0.1,0.97,0.19c0.09,0.25,0.21,0.89,0.24,1.17L63.38,27.9z"></path> </g> <g> <path class="st0" d="M67.22,27.9c-0.21-0.64-0.59-1.08-1.12-1.08c-0.41,0-0.71,0.33-0.71,0.75c0,0.49,0.44,0.74,0.93,0.95
c0.82,0.34,1.37,0.73,1.37,1.48c0,0.99-0.93,1.51-1.8,1.51c-0.56,0-1.06-0.21-1.25-0.37c-0.08-0.15-0.17-0.89-0.17-1.29l0.28-0.05
c0.2,0.72,0.69,1.35,1.34,1.35c0.4,0,0.77-0.31,0.77-0.76c0-0.48-0.32-0.74-0.86-0.99c-0.65-0.31-1.36-0.67-1.36-1.51
c0-0.76,0.65-1.41,1.66-1.41c0.45,0,0.74,0.1,0.97,0.19c0.09,0.25,0.21,0.89,0.24,1.17L67.22,27.9z"></path> </g> <g> <path class="st0" d="M71.64,31.51c-0.16,0-0.39-0.1-0.51-0.21c-0.14-0.15-0.21-0.31-0.26-0.49c-0.44,0.29-0.96,0.7-1.29,0.7
c-0.77,0-1.31-0.64-1.31-1.33c0-0.53,0.28-0.87,0.88-1.08c0.65-0.22,1.45-0.51,1.69-0.71v-0.2c0-0.79-0.37-1.23-0.92-1.23
c-0.22,0-0.38,0.09-0.49,0.22c-0.14,0.14-0.24,0.38-0.33,0.72c-0.04,0.17-0.16,0.25-0.32,0.25c-0.2,0-0.48-0.22-0.48-0.47
c0-0.16,0.14-0.28,0.35-0.44c0.31-0.22,1.01-0.64,1.64-0.76c0.33,0,0.65,0.1,0.89,0.29c0.38,0.32,0.49,0.76,0.49,1.33v2.03
c0,0.49,0.2,0.65,0.38,0.65c0.13,0,0.28-0.05,0.39-0.12l0.11,0.28L71.64,31.51z M70.84,28.76c-0.23,0.12-0.75,0.35-0.98,0.46
c-0.43,0.2-0.67,0.4-0.67,0.8c0,0.57,0.44,0.84,0.77,0.84c0.28-0.01,0.67-0.2,0.87-0.39v-1.71H70.84z"></path> </g> <g> <path class="st0" d="M76.75,30.4c-0.48,0.63-1.21,1.11-1.85,1.11c-1.37,0-2.18-1.11-2.18-2.28c0-0.84,0.36-1.57,1.09-2.14
c0.57-0.44,1.22-0.61,1.65-0.61c0.44,0,0.8,0.14,0.97,0.28c0.14,0.12,0.17,0.22,0.17,0.34c0,0.23-0.24,0.47-0.36,0.47
c-0.05,0-0.11-0.03-0.21-0.12C75.71,27.14,75.32,27,74.94,27c-0.72,0-1.35,0.6-1.35,1.75c0,1.52,1.04,2.02,1.65,2.02
c0.47,0,0.83-0.13,1.33-0.6L76.75,30.4z"></path> </g> <g> <path class="st0" d="M79.86,31.38V31.1c0.63-0.07,0.72-0.13,0.72-0.84v-1.81c0-0.89-0.34-1.34-1.07-1.34
c-0.41,0-0.83,0.21-1.13,0.56v2.6c0,0.7,0.05,0.77,0.7,0.84v0.28h-2.31v-0.28c0.65-0.05,0.75-0.12,0.75-0.84v-5.2
c0-0.69-0.02-0.74-0.74-0.81v-0.25c0.52-0.08,1.18-0.25,1.6-0.38v3.67c0.38-0.39,0.97-0.8,1.56-0.8c0.88,0,1.51,0.56,1.51,1.87
v1.91c0,0.72,0.08,0.79,0.72,0.84v0.28h-2.31V31.38z"></path> </g> <g> <path class="st0" d="M87.55,31.12c-0.5,0.08-1.06,0.23-1.6,0.39l-0.07-0.07v-0.77c-0.25,0.19-0.5,0.38-0.81,0.57
c-0.32,0.19-0.55,0.27-0.84,0.27c-0.73,0-1.35-0.45-1.35-1.63v-2.14c0-0.6-0.04-0.64-0.67-0.76v-0.25c0.51-0.03,1.04-0.1,1.56-0.2
c-0.03,0.34-0.03,0.85-0.03,1.59v1.46c0,0.97,0.47,1.23,0.96,1.23c0.39,0,0.81-0.16,1.18-0.52v-2.54c0-0.62-0.07-0.67-0.85-0.77
v-0.25c0.57-0.03,1.13-0.08,1.71-0.2v3.64c0,0.55,0.09,0.62,0.5,0.64l0.31,0.02C87.55,30.83,87.55,31.12,87.55,31.12z"></path> <path class="st0" d="M90.8,27.9c-0.21-0.64-0.59-1.08-1.12-1.08c-0.41,0-0.71,0.33-0.71,0.75c0,0.49,0.44,0.74,0.93,0.95
c0.82,0.34,1.37,0.73,1.37,1.48c0,0.99-0.93,1.51-1.8,1.51c-0.56,0-1.06-0.21-1.25-0.37c-0.08-0.15-0.17-0.89-0.17-1.29l0.28-0.05
c0.2,0.72,0.69,1.35,1.34,1.35c0.4,0,0.77-0.31,0.77-0.76c0-0.48-0.32-0.74-0.86-0.99c-0.65-0.31-1.36-0.67-1.36-1.51
c0-0.76,0.65-1.41,1.66-1.41c0.45,0,0.74,0.1,0.97,0.19c0.09,0.25,0.21,0.89,0.24,1.17L90.8,27.9z"></path> </g> <g> <path class="st0" d="M95.79,30.45c-0.69,0.86-1.44,1.06-1.78,1.06c-1.34,0-2.13-1.09-2.13-2.32c0-0.75,0.27-1.42,0.72-1.91
c0.45-0.5,1.04-0.8,1.6-0.8c0.91,0,1.64,0.81,1.64,1.66c0,0.22-0.05,0.31-0.25,0.35c-0.19,0.03-1.55,0.14-2.85,0.19
c0.01,1.48,0.86,2.09,1.63,2.09c0.44,0,0.85-0.17,1.24-0.55L95.79,30.45z M92.8,28.22c0.61,0,1.21-0.01,1.84-0.03
c0.2,0,0.26-0.07,0.26-0.22c0.01-0.57-0.36-1.09-0.96-1.09C93.44,26.88,92.96,27.34,92.8,28.22z"></path> </g> <g> <path class="st0" d="M98.41,31.37c-0.19,0.1-0.36,0.14-0.46,0.14c-0.69,0-1.07-0.44-1.07-1.3v-3.17h-0.75l-0.04-0.11l0.29-0.32
h0.5v-0.8l0.71-0.72l0.15,0.02v1.49h1.23c0.1,0.11,0.07,0.35-0.07,0.43h-1.17v2.8c0,0.88,0.36,1.05,0.63,1.05s0.52-0.11,0.68-0.19
l0.1,0.28L98.41,31.37z"></path> </g> <g> <path class="st0" d="M101.54,31.37c-0.19,0.1-0.36,0.14-0.46,0.14c-0.69,0-1.07-0.44-1.07-1.3v-3.17h-0.75l-0.04-0.11l0.29-0.32
h0.5v-0.8l0.71-0.72l0.15,0.02v1.49h1.23c0.1,0.11,0.07,0.35-0.07,0.43h-1.17v2.8c0,0.88,0.36,1.05,0.63,1.05s0.52-0.11,0.68-0.19
l0.1,0.28L101.54,31.37z"></path> </g> <g> <path class="st0" d="M105.4,27.9c-0.21-0.64-0.59-1.08-1.12-1.08c-0.41,0-0.71,0.33-0.71,0.75c0,0.49,0.44,0.74,0.93,0.95
c0.82,0.34,1.37,0.73,1.37,1.48c0,0.99-0.93,1.51-1.8,1.51c-0.56,0-1.06-0.21-1.25-0.37c-0.08-0.15-0.17-0.89-0.17-1.29l0.28-0.05
c0.2,0.72,0.69,1.35,1.34,1.35c0.4,0,0.77-0.31,0.77-0.76c0-0.48-0.32-0.74-0.86-0.99c-0.65-0.31-1.36-0.67-1.36-1.51
c0-0.76,0.65-1.41,1.66-1.41c0.45,0,0.74,0.1,0.97,0.19c0.09,0.25,0.21,0.89,0.24,1.17L105.4,27.9z"></path> </g> <g> <path class="st0" d="M91.39,20.4c0-0.95,0.5-1.67,1.11-1.99c0.26-0.14,0.56-0.23,0.79-0.23c0.6,0,0.97,0.41,0.97,1.13
c0,0.63-0.27,1.34-0.78,1.76c-0.31,0.26-0.71,0.46-1.12,0.46C91.66,21.53,91.39,20.99,91.39,20.4z M92.84,21.2
c0.39-0.17,0.79-0.94,0.79-1.87c0-0.59-0.25-0.87-0.59-0.87c-0.11,0-0.2,0.03-0.28,0.07C92.49,18.65,92,19.24,92,20.36
c0,0.54,0.23,0.88,0.61,0.88C92.7,21.24,92.77,21.23,92.84,21.2z"></path> <path class="st0" d="M95.15,18.3c0.11-0.55,0.28-1.08,0.61-1.45s0.75-0.58,1.04-0.58c0.25,0,0.44,0.18,0.44,0.31
c0,0.11-0.1,0.24-0.18,0.3c-0.06,0.04-0.13,0.05-0.18-0.01c-0.15-0.16-0.31-0.25-0.46-0.25c-0.27,0-0.49,0.33-0.71,1.68h0.82
l-0.15,0.23l-0.73,0.07c-0.13,0.8-0.21,1.24-0.37,2.05c-0.25,1.25-0.52,1.8-0.92,2.21c-0.32,0.34-0.63,0.46-0.82,0.46
c-0.21,0-0.46-0.15-0.45-0.31c0-0.09,0.09-0.22,0.2-0.31c0.06-0.05,0.09-0.04,0.14,0c0.12,0.11,0.33,0.18,0.46,0.18
c0.12,0,0.24-0.06,0.31-0.15c0.09-0.11,0.27-0.41,0.51-1.83c0.17-1,0.27-1.61,0.39-2.33h-0.6l-0.02-0.08l0.22-0.2h0.45V18.3z"></path> </g> <g> <path class="st0" d="M49.89,39.11v-0.25c0.55-0.07,0.63-0.14,0.52-0.48s-0.29-0.82-0.48-1.35h-1.87c-0.14,0.4-0.27,0.76-0.39,1.13
c-0.19,0.55-0.11,0.64,0.54,0.7v0.25H46.3v-0.25c0.55-0.08,0.68-0.13,0.98-0.91l1.82-4.61l0.27-0.08c0.54,1.51,1.13,3.18,1.7,4.71
c0.28,0.76,0.4,0.83,0.97,0.89v0.25H49.89z M49.03,34.41h-0.02c-0.28,0.76-0.56,1.52-0.83,2.24h1.62L49.03,34.41z"></path> </g> <g> <path class="st0" d="M57.15,39.11v-0.23c0.52-0.05,0.62-0.09,0.62-0.71V36.7c0-0.62-0.22-1.02-0.77-1.02
c-0.32,0-0.65,0.18-0.98,0.46c0.02,0.11,0.03,0.2,0.03,0.37v1.71c0,0.55,0.08,0.61,0.56,0.66v0.23h-1.88v-0.23
c0.54-0.05,0.62-0.1,0.62-0.68v-1.51c0-0.65-0.23-1.01-0.76-1.01c-0.35,0-0.69,0.24-0.96,0.46v2.07c0,0.57,0.07,0.62,0.5,0.68
v0.23h-1.85v-0.23c0.58-0.05,0.66-0.11,0.66-0.68v-1.82c0-0.56-0.03-0.61-0.54-0.69v-0.2c0.39-0.07,0.83-0.18,1.22-0.36v0.71
c0.18-0.12,0.38-0.28,0.64-0.45c0.23-0.14,0.44-0.24,0.72-0.24c0.42,0,0.8,0.26,0.98,0.72c0.25-0.19,0.51-0.36,0.73-0.49
c0.18-0.12,0.41-0.23,0.66-0.23c0.69,0,1.12,0.48,1.12,1.34v1.7c0,0.59,0.07,0.62,0.57,0.68v0.23
C59.04,39.11,57.15,39.11,57.15,39.11z"></path> </g> <g> <path class="st0" d="M61.79,39.11v-0.23c0.51-0.05,0.58-0.11,0.58-0.68v-1.46c0-0.72-0.27-1.08-0.86-1.08
c-0.33,0-0.67,0.17-0.91,0.45v2.09c0,0.56,0.04,0.62,0.56,0.68v0.23H59.3v-0.23c0.53-0.04,0.61-0.1,0.61-0.68v-4.19
c0-0.55-0.02-0.6-0.6-0.65v-0.2c0.42-0.06,0.95-0.2,1.29-0.31v2.95c0.31-0.32,0.78-0.64,1.26-0.64c0.71,0,1.21,0.45,1.21,1.5v1.54
c0,0.58,0.06,0.63,0.58,0.68v0.23H61.79z"></path> </g> <g> <path class="st0" d="M67.08,38.36c-0.55,0.69-1.16,0.85-1.43,0.85c-1.08,0-1.71-0.88-1.71-1.87c0-0.61,0.22-1.14,0.58-1.54
s0.83-0.64,1.29-0.64c0.73,0,1.32,0.65,1.32,1.34c0,0.18-0.04,0.25-0.2,0.28c-0.15,0.03-1.25,0.11-2.29,0.15
c0.01,1.2,0.69,1.69,1.31,1.69c0.35,0,0.69-0.14,1-0.44L67.08,38.36z M64.67,36.57c0.49,0,0.98-0.01,1.49-0.03
c0.16,0,0.21-0.05,0.21-0.18c0.01-0.46-0.29-0.88-0.77-0.88C65.19,35.49,64.8,35.86,64.67,36.57z"></path> </g> <g> <path class="st0" d="M68.86,36.09c0.26-0.44,0.64-0.93,1.06-0.93c0.3,0,0.51,0.23,0.51,0.44c0,0.18-0.11,0.35-0.28,0.43
c-0.11,0.04-0.18,0.04-0.24-0.02c-0.12-0.13-0.23-0.18-0.37-0.18c-0.23,0-0.51,0.26-0.7,0.76v1.61c0,0.58,0.04,0.63,0.72,0.69
v0.23h-2.02v-0.23c0.54-0.05,0.61-0.11,0.61-0.69v-1.82c0-0.58-0.04-0.62-0.54-0.69V35.5c0.43-0.07,0.82-0.18,1.24-0.36
L68.86,36.09L68.86,36.09L68.86,36.09z"></path> </g> <g> <path class="st0" d="M72.86,36.3c-0.17-0.52-0.47-0.87-0.91-0.87c-0.33,0-0.57,0.26-0.57,0.61c0,0.4,0.35,0.6,0.75,0.76
c0.66,0.27,1.11,0.59,1.11,1.2c0,0.8-0.75,1.21-1.45,1.21c-0.45,0-0.85-0.17-1.01-0.3c-0.06-0.12-0.14-0.72-0.14-1.04l0.23-0.04
c0.16,0.58,0.55,1.09,1.08,1.09c0.33,0,0.62-0.25,0.62-0.62c0-0.39-0.25-0.6-0.69-0.8c-0.53-0.25-1.1-0.54-1.1-1.21
c0-0.62,0.53-1.13,1.34-1.13c0.36,0,0.6,0.08,0.78,0.15c0.07,0.2,0.17,0.72,0.19,0.94L72.86,36.3z"></path> </g> <g> <path class="st0" d="M75.31,39.1c-0.15,0.08-0.29,0.11-0.37,0.11c-0.55,0-0.86-0.35-0.86-1.05V35.6h-0.61l-0.04-0.09l0.24-0.25
h0.4v-0.64l0.57-0.58l0.12,0.02v1.2h0.99c0.08,0.09,0.05,0.28-0.05,0.34h-0.94v2.26c0,0.71,0.29,0.84,0.51,0.84
s0.42-0.09,0.54-0.15l0.08,0.23L75.31,39.1z"></path> </g> </g> <path class="st1" d="M42.32,38.93H33.5l0.08-9.78c-1.2,1.83-4.59,6.89-4.59,6.89l-0.63,0.01c0,0-3.41-5.07-4.61-6.89l0.08,9.78
h-8.82l-0.02-5.76h1.19l-0.09-11.43h-1.16l-0.03-5.97h8.19l5.57,9.03l5.57-9.03h8.19l-0.03,5.97h-1.16l-0.09,11.43h1.19L42.32,38.93
z"></path> <path class="st2" d="M34.85,37.59h6.13l0.01-3.08H39.8l0.11-14.11h1.15l0.02-3.28h-6.09l-6.32,10.25l-6.32-10.25h-6.09l0.02,3.28
h1.15l0.11,14.11h-1.19l0.01,3.08h6.13l-0.1-13l1.25,1.92c1.3,2,2.99,4.55,5.04,7.6c2.04-3.05,3.74-5.6,5.04-7.6l1.25-1.92
L34.85,37.59z"></path> </svg></div>
</a>
</div>
</div>
<a class="logo" href="#" aria-label="SIS">
<div>
<img src="https://sisvietnam.vn/wp-content/uploads/2026/06/LogoSIS-1.png" alt="Logo SIS" />
</div>
</a>
</div>
</div>
<div id="block-footersocial"
class="block block-fixed-block-content block-fixed-block-contentfooter-social cc--component-container cc--footer-social">
<div class="c--component c--footer-social">
<nav aria-label="UMass Social Media Links">
<div>
<nav aria-label="Social media links">
<ul class="social-media-links--platforms platforms">
<li>
<a href="#" class="ext" target="_blank" rel="noopener noreferrer"
aria-label="Facebook (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path
d="m19.9593333 5h-14.0853333c-.48269687 0-.874.39130313-.874.874v14.0853333c0 .4826969.39130313.874.874.874h7.5835333v-6.1230666h-2.0564333v-2.3965334h2.0564333v-1.7625666c0-2.04503337 1.2489334-3.15843337 3.0735667-3.15843337.6158056-.00219435 1.23127.02930829 1.8436333.09436667v2.13623333h-1.2616c-.9899 0-1.1811666.47119997-1.1811666 1.16343337v1.5263333h2.3731l-.3090667 2.3965333h-2.0640333v6.1237h4.0273666c.4826969 0 .874-.3913031.874-.874v-14.0853333c0-.48269687-.3913031-.874-.874-.874z"
fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">Facebook</span>
</a>
</li>
<li>
<a href="https://twitter.com/UMassAmherst" class="ext" target="_blank"
rel="noopener noreferrer" aria-label="X (formerly Twitter) (opens in new window)">
<svg style="fill:#FFFFFF;" width="24" height="24" y="0px" x="0px" viewBox="0 -2 50 50"
aria-hidden="true" focusable="false">
<path
d="M 5.9199219 6 L 20.582031 27.375 L 6.2304688 44 L 9.4101562 44 L 21.986328 29.421875 L 31.986328 44 L 44 44 L 28.681641 21.669922 L 42.199219 6 L 39.029297 6 L 27.275391 19.617188 L 17.933594 6 L 5.9199219 6 z M 9.7167969 8 L 16.880859 8 L 40.203125 42 L 33.039062 42 L 9.7167969 8 z">
</path>
</svg>
<span class="visually-hidden">X (formerly Twitter)</span>
</a>
</li>
<li>
<a href="https://www.youtube.com/user/UMass" class="ext" target="_blank"
rel="noopener noreferrer" aria-label="YouTube (opens in new window)">
<svg height="15" viewBox="0 0 23 15" width="23" aria-hidden="true" focusable="false">
<path
d="m22.935049 8.18627451s-.2389706-1.43382353-.8762255-2.07107843c-.7965686-.87622549-1.752451-.87622549-2.1507353-.87622549-2.9473039-.23897059-7.4877451-.23897059-7.4877451-.23897059s-4.54044114 0-7.48774506.23897059c-.47794118 0-1.35416667 0-2.23039216.87622549-.6372549.6372549-.79656863 2.07107843-.79656863 2.07107843s-.23897058 1.67279412-.23897058 3.42524509v1.5931373c0 1.6727941.23897058 3.4252451.23897058 3.4252451s.23897059 1.4338235.8762255 2.0710784c.79656862.8762255 1.9117647.7965686 2.38970588.8762255 1.75245098.1593137 7.32843137.2389706 7.32843137.2389706s4.5404412 0 7.567402-.2389706c.3982843-.0796569 1.3541666-.0796569 2.1507353-.8762255.6372549-.6372549.8762254-2.0710784.8762254-2.0710784s.2389706-1.6727942.2389706-3.4252451v-1.5931373c-.1593137-1.75245097-.3982843-3.42524509-.3982843-3.42524509zm-12.745098 7.16911769v-5.97426475l5.7352941 2.86764705z"
fill="#fff" fill-rule="evenodd" transform="translate(-1 -5)"></path>
</svg>
<span class="visually-hidden">YouTube</span>
</a>
</li>
<li>
<a href="#" class="ext" target="_blank" rel="noopener noreferrer"
aria-label="Instagram (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path
d="m12.9166667 5c-2.1666667 0-2.4166667 0-3.25000003.08333333-.83333334 0-1.41666667.16666667-1.91666667.33333334-.5.16666666-1 .5-1.41666667.91666666-.41666666.41666667-.75.91666667-.91666666 1.41666667-.16666667.5-.33333334 1.08333333-.33333334 1.91666667-.08333333.83333333-.08333333 1.08333333-.08333333 3.25000003 0 2.1666666 0 2.4166666.08333333 3.25 0 .8333333.16666667 1.4166666.33333334 1.9166666.16666666.5.5 1 .91666666 1.4166667.41666667.4166667.91666667.75 1.41666667.9166667.5.1666666 1.08333333.3333333 1.91666667.3333333.83333333 0 1.08333333.0833333 3.25000003.0833333 2.1666666 0 2.4166666 0 3.25-.0833333.8333333 0 1.4166666-.1666667 1.9166666-.3333333.5-.1666667 1-.5 1.4166667-.9166667s.75-.9166667.9166667-1.4166667c.1666666-.5.3333333-1.0833333.3333333-1.9166666 0-.8333334.0833333-1.0833334.0833333-3.25 0-2.1666667 0-2.4166667-.0833333-3.25000003 0-.83333334-.1666667-1.41666667-.3333333-1.91666667-.1666667-.5-.5-1-.9166667-1.41666667-.4166667-.41666666-.9166667-.75-1.4166667-.91666666-.5-.16666667-1.0833333-.33333334-1.9166666-.33333334-.8333334-.08333333-1.0833334-.08333333-3.25-.08333333m0 1.41666667c2.0833333 0 2.3333333 0 3.1666666.08333333.75 0 1.1666667.16666667 1.5.25.3333334.16666667.6666667.33333333.9166667.58333333s.4166667.58333334.5833333.91666667c.0833334.25.25.66666667.25 1.5 0 .8333333.0833334 1.0833333.0833334 3.25 0 2.0833333 0 2.3333333-.0833334 3.1666667 0 .75-.1666666 1.1666666-.25 1.5-.1666666.3333333-.3333333.6666666-.5833333.9166666s-.5.4166667-.9166667.5833334c-.25.0833333-.6666666.25-1.5.25-.8333333 0-1.0833333.0833333-3.1666666.0833333-2.0833334 0-2.3333334 0-3.1666667-.0833333-.75 0-1.16666667-.1666667-1.5-.25-.33333333-.1666667-.66666667-.3333334-.91666667-.5833334s-.41666666-.5833333-.58333333-.9166666c-.08333333-.25-.25-.6666667-.25-1.5 0-.8333334-.08333333-1.0833334-.08333333-3.1666667s0-2.3333333.08333333-3.25c0-.75.16666667-1.16666667.25-1.5.16666667-.33333333.33333333-.66666667.58333333-.91666667s.5-.41666666.91666667-.58333333c.25-.08333333.66666667-.25 1.5-.25.8333333-.08333333 1.0833333-.08333333 3.1666667-.08333333m0 9.16666663c-1.4166667 0-2.6666667-1.1666666-2.6666667-2.6666666s1.1666667-2.6666667 2.6666667-2.6666667 2.6666666 1.1666667 2.6666666 2.6666667-1.25 2.6666666-2.6666666 2.6666666m0-6.74999997c-2.25 0-4.08333337 1.83333337-4.08333337 4.08333337s1.83333337 4.0833333 4.08333337 4.0833333 4.0833333-1.8333333 4.0833333-4.0833333-1.8333333-4.08333337-4.0833333-4.08333337m5.1666666-.16666666c0 .5-.4166666.91666666-.9166666.91666666s-.9166667-.41666666-.9166667-.91666666.4166667-.91666667.9166667-.91666667.9166666.41666667.9166666.91666667"
fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">Instagram</span>
</a>
</li>
<li>
<a href="https://www.linkedin.com/school/umassamherst/" class="ext" target="_blank"
rel="noopener noreferrer" aria-label="LinkedIn (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path
d="m20.8333333 17.8517316c0 1.6450216-1.3365801 2.9816017-2.9816017 2.9816017h-9.87012987c-1.64502164 0-2.98160173-1.3365801-2.98160173-2.9816017v-9.87012987c0-1.64502164 1.33658009-2.98160173 2.98160173-2.98160173h9.87012987c1.6450216 0 2.9816017 1.33658009 2.9816017 2.98160173zm-12.1320346-10.17857143c-.82251082 0-1.33658009.51406927-1.33658009 1.23376624-.10281385.71969697.41125542 1.23376619 1.23376624 1.23376619.82251082 0 1.33658009-.51406922 1.33658009-1.23376619s-.51406927-1.23376624-1.23376624-1.23376624zm1.13095238 10.58982683v-7.1969697h-2.36471861v7.1969697zm8.53354982 0v-4.1125541c0-2.1590909-1.1309524-3.1872294-2.7759741-3.1872294-1.2337662 0-1.7478355.7196969-2.056277 1.2337662v-1.0281385h-2.3647186v7.1969697h2.3647186v-4.1125541c0-.2056278 0-.4112555.1028138-.6168832.1028139-.4112554.5140693-.8225108 1.2337663-.8225108.8225108 0 1.2337662.6168831 1.2337662 1.6450217v3.8041125z"
fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">LinkedIn</span>
</a>
</li>
<li>
<a href="#" class="ext" target="_blank" rel="noopener noreferrer"
aria-label="Snapchat (opens in new window)">
<svg height="17" viewBox="0 0 18 17" width="18" aria-hidden="true" focusable="false">
<path
d="m12.8333333 20.3333333c-.0833333 0-.0833333 0-.1666666 0h-.0833334c-1.0833333 0-1.6666666-.3333333-2.25-.75-.41666663-.3333333-.74999997-.5833333-1.16666663-.5833333-.25 0-.41666667-.0833333-.66666667-.0833333-.33333333 0-.66666667.0833333-.91666667.0833333-.16666666 0-.33333333.0833333-.41666666.0833333-.33333334 0-.5-.25-.5-.4166666 0-.1666667-.08333334-.25-.08333334-.4166667 0-.0833333-.08333333-.25-.08333333-.3333333-1.33333333-.25-2.08333333-.5-2.25-1-.08333333 0-.08333333-.0833334-.08333333-.1666667 0-.25.16666666-.4166667.41666666-.5 2.25-.3333333 3.33333334-2.75 3.33333334-2.8333333.08333333-.25.16666666-.4166667.08333333-.5833334-.08333333-.1666666-.58333333-.3333333-.91666667-.4166666-.08333333 0-.16666666-.0833334-.25-.0833334-.83333333-.3333333-1-.75-.91666666-1.0833333.08333333-.5.83333333-.75 1.25-.5833333.33333333.1666666.58333333.1666666.75.1666666h.25c0-.0833333 0-.25 0-.3333333-.08333334-1.16666667-.25-2.66666667.16666666-3.5 1.16666667-2.66666667 3.66666667-2.83333333 4.33333337-2.83333333h.3333333c.75 0 3.1666667.16666666 4.3333333 2.83333333.4166667.91666667.3333334 2.33333333.25 3.5v.0833333.25h.1666667c.1666667 0 .4166667-.0833333.6666667-.1666666.1666666-.0833334.25-.0833334.4166666-.0833334.1666667 0 .25 0 .4166667.0833334.3333333.0833333.5833333.4166666.5833333.6666666 0 .4166667-.3333333.6666667-.9166666.9166667-.0833334 0-.1666667.0833333-.25.0833333-.3333334.0833334-.8333334.25-.9166667.5-.0833333.1666667 0 .3333334.0833333.5.0833334.25 1.0833334 2.5 3.25 2.8333334.25 0 .4166667.25.4166667.5 0 .0833333 0 .1666666-.0833333.1666666-.1666667.4166667-.9166667.75-2.25 1 0 .0833334-.0833334.25-.0833334.3333334 0 .1666666-.0833333.25-.0833333.4166666-.0833333.25-.25.4166667-.5.4166667-.0833333 0-.25 0-.4166667-.0833333-.25-.0833334-.5-.0833334-.9166666-.0833334-.1666667 0-.4166667 0-.6666667.0833334-.4166667.0833333-.75.3333333-1.1666667.5833333-.6666666.5833333-1.5.8333333-2.5.8333333"
fill="#fff" fill-rule="evenodd" transform="translate(-4 -4)"></path>
</svg>
<span class="visually-hidden">Snapchat</span>
</a>
</li>
</ul>
</nav>
</div>
</nav>
</div>
</div>
<div id="block-footersocial" class="block block-fixed-block-content block-fixed-block-contentfooter-social cc--component-container cc--footer-social">
<div class="c--component c--footer-social">
<nav aria-label="UMass Social Media Links">
<div><nav aria-label="Social media links">
<ul class="social-media-links--platforms platforms">
<li>
<a href="https://www.facebook.com/UMassAmherst/" class="ext" target="_blank" rel="noopener noreferrer" aria-label="Facebook (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path d="m19.9593333 5h-14.0853333c-.48269687 0-.874.39130313-.874.874v14.0853333c0 .4826969.39130313.874.874.874h7.5835333v-6.1230666h-2.0564333v-2.3965334h2.0564333v-1.7625666c0-2.04503337 1.2489334-3.15843337 3.0735667-3.15843337.6158056-.00219435 1.23127.02930829 1.8436333.09436667v2.13623333h-1.2616c-.9899 0-1.1811666.47119997-1.1811666 1.16343337v1.5263333h2.3731l-.3090667 2.3965333h-2.0640333v6.1237h4.0273666c.4826969 0 .874-.3913031.874-.874v-14.0853333c0-.48269687-.3913031-.874-.874-.874z" fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">Facebook</span>
</a>
</li>
<li>
<a href="https://twitter.com/UMassAmherst" class="ext" target="_blank" rel="noopener noreferrer" aria-label="X (formerly Twitter) (opens in new window)">
<svg style="fill:#FFFFFF;" width="24" height="24" y="0px" x="0px" viewBox="0 -2 50 50" aria-hidden="true" focusable="false">
<path d="M 5.9199219 6 L 20.582031 27.375 L 6.2304688 44 L 9.4101562 44 L 21.986328 29.421875 L 31.986328 44 L 44 44 L 28.681641 21.669922 L 42.199219 6 L 39.029297 6 L 27.275391 19.617188 L 17.933594 6 L 5.9199219 6 z M 9.7167969 8 L 16.880859 8 L 40.203125 42 L 33.039062 42 L 9.7167969 8 z"></path>
</svg>
<span class="visually-hidden">X (formerly Twitter)</span>
</a>
</li>
<li>
<a href="https://www.youtube.com/user/UMass" class="ext" target="_blank" rel="noopener noreferrer" aria-label="YouTube (opens in new window)">
<svg height="15" viewBox="0 0 23 15" width="23" aria-hidden="true" focusable="false">
<path d="m22.935049 8.18627451s-.2389706-1.43382353-.8762255-2.07107843c-.7965686-.87622549-1.752451-.87622549-2.1507353-.87622549-2.9473039-.23897059-7.4877451-.23897059-7.4877451-.23897059s-4.54044114 0-7.48774506.23897059c-.47794118 0-1.35416667 0-2.23039216.87622549-.6372549.6372549-.79656863 2.07107843-.79656863 2.07107843s-.23897058 1.67279412-.23897058 3.42524509v1.5931373c0 1.6727941.23897058 3.4252451.23897058 3.4252451s.23897059 1.4338235.8762255 2.0710784c.79656862.8762255 1.9117647.7965686 2.38970588.8762255 1.75245098.1593137 7.32843137.2389706 7.32843137.2389706s4.5404412 0 7.567402-.2389706c.3982843-.0796569 1.3541666-.0796569 2.1507353-.8762255.6372549-.6372549.8762254-2.0710784.8762254-2.0710784s.2389706-1.6727942.2389706-3.4252451v-1.5931373c-.1593137-1.75245097-.3982843-3.42524509-.3982843-3.42524509zm-12.745098 7.16911769v-5.97426475l5.7352941 2.86764705z" fill="#fff" fill-rule="evenodd" transform="translate(-1 -5)"></path>
</svg>
<span class="visually-hidden">YouTube</span>
</a>
</li>
<li>
<a href="https://www.instagram.com/umass/" class="ext" target="_blank" rel="noopener noreferrer" aria-label="Instagram (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path d="m12.9166667 5c-2.1666667 0-2.4166667 0-3.25000003.08333333-.83333334 0-1.41666667.16666667-1.91666667.33333334-.5.16666666-1 .5-1.41666667.91666666-.41666666.41666667-.75.91666667-.91666666 1.41666667-.16666667.5-.33333334 1.08333333-.33333334 1.91666667-.08333333.83333333-.08333333 1.08333333-.08333333 3.25000003 0 2.1666666 0 2.4166666.08333333 3.25 0 .8333333.16666667 1.4166666.33333334 1.9166666.16666666.5.5 1 .91666666 1.4166667.41666667.4166667.91666667.75 1.41666667.9166667.5.1666666 1.08333333.3333333 1.91666667.3333333.83333333 0 1.08333333.0833333 3.25000003.0833333 2.1666666 0 2.4166666 0 3.25-.0833333.8333333 0 1.4166666-.1666667 1.9166666-.3333333.5-.1666667 1-.5 1.4166667-.9166667s.75-.9166667.9166667-1.4166667c.1666666-.5.3333333-1.0833333.3333333-1.9166666 0-.8333334.0833333-1.0833334.0833333-3.25 0-2.1666667 0-2.4166667-.0833333-3.25000003 0-.83333334-.1666667-1.41666667-.3333333-1.91666667-.1666667-.5-.5-1-.9166667-1.41666667-.4166667-.41666666-.9166667-.75-1.4166667-.91666666-.5-.16666667-1.0833333-.33333334-1.9166666-.33333334-.8333334-.08333333-1.0833334-.08333333-3.25-.08333333m0 1.41666667c2.0833333 0 2.3333333 0 3.1666666.08333333.75 0 1.1666667.16666667 1.5.25.3333334.16666667.6666667.33333333.9166667.58333333s.4166667.58333334.5833333.91666667c.0833334.25.25.66666667.25 1.5 0 .8333333.0833334 1.0833333.0833334 3.25 0 2.0833333 0 2.3333333-.0833334 3.1666667 0 .75-.1666666 1.1666666-.25 1.5-.1666666.3333333-.3333333.6666666-.5833333.9166666s-.5.4166667-.9166667.5833334c-.25.0833333-.6666666.25-1.5.25-.8333333 0-1.0833333.0833333-3.1666666.0833333-2.0833334 0-2.3333334 0-3.1666667-.0833333-.75 0-1.16666667-.1666667-1.5-.25-.33333333-.1666667-.66666667-.3333334-.91666667-.5833334s-.41666666-.5833333-.58333333-.9166666c-.08333333-.25-.25-.6666667-.25-1.5 0-.8333334-.08333333-1.0833334-.08333333-3.1666667s0-2.3333333.08333333-3.25c0-.75.16666667-1.16666667.25-1.5.16666667-.33333333.33333333-.66666667.58333333-.91666667s.5-.41666666.91666667-.58333333c.25-.08333333.66666667-.25 1.5-.25.8333333-.08333333 1.0833333-.08333333 3.1666667-.08333333m0 9.16666663c-1.4166667 0-2.6666667-1.1666666-2.6666667-2.6666666s1.1666667-2.6666667 2.6666667-2.6666667 2.6666666 1.1666667 2.6666666 2.6666667-1.25 2.6666666-2.6666666 2.6666666m0-6.74999997c-2.25 0-4.08333337 1.83333337-4.08333337 4.08333337s1.83333337 4.0833333 4.08333337 4.0833333 4.0833333-1.8333333 4.0833333-4.0833333-1.8333333-4.08333337-4.0833333-4.08333337m5.1666666-.16666666c0 .5-.4166666.91666666-.9166666.91666666s-.9166667-.41666666-.9166667-.91666666.4166667-.91666667.9166667-.91666667.9166666.41666667.9166666.91666667" fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">Instagram</span>
</a>
</li>
<li>
<a href="https://www.linkedin.com/school/umassamherst/" class="ext" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path d="m20.8333333 17.8517316c0 1.6450216-1.3365801 2.9816017-2.9816017 2.9816017h-9.87012987c-1.64502164 0-2.98160173-1.3365801-2.98160173-2.9816017v-9.87012987c0-1.64502164 1.33658009-2.98160173 2.98160173-2.98160173h9.87012987c1.6450216 0 2.9816017 1.33658009 2.9816017 2.98160173zm-12.1320346-10.17857143c-.82251082 0-1.33658009.51406927-1.33658009 1.23376624-.10281385.71969697.41125542 1.23376619 1.23376624 1.23376619.82251082 0 1.33658009-.51406922 1.33658009-1.23376619s-.51406927-1.23376624-1.23376624-1.23376624zm1.13095238 10.58982683v-7.1969697h-2.36471861v7.1969697zm8.53354982 0v-4.1125541c0-2.1590909-1.1309524-3.1872294-2.7759741-3.1872294-1.2337662 0-1.7478355.7196969-2.056277 1.2337662v-1.0281385h-2.3647186v7.1969697h2.3647186v-4.1125541c0-.2056278 0-.4112555.1028138-.6168832.1028139-.4112554.5140693-.8225108 1.2337663-.8225108.8225108 0 1.2337662.6168831 1.2337662 1.6450217v3.8041125z" fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">LinkedIn</span>
</a>
</li>
<li>
<a href="https://www.snapchat.com/add/umassamherst" class="ext" target="_blank" rel="noopener noreferrer" aria-label="Snapchat (opens in new window)">
<svg height="17" viewBox="0 0 18 17" width="18" aria-hidden="true" focusable="false">
<path d="m12.8333333 20.3333333c-.0833333 0-.0833333 0-.1666666 0h-.0833334c-1.0833333 0-1.6666666-.3333333-2.25-.75-.41666663-.3333333-.74999997-.5833333-1.16666663-.5833333-.25 0-.41666667-.0833333-.66666667-.0833333-.33333333 0-.66666667.0833333-.91666667.0833333-.16666666 0-.33333333.0833333-.41666666.0833333-.33333334 0-.5-.25-.5-.4166666 0-.1666667-.08333334-.25-.08333334-.4166667 0-.0833333-.08333333-.25-.08333333-.3333333-1.33333333-.25-2.08333333-.5-2.25-1-.08333333 0-.08333333-.0833334-.08333333-.1666667 0-.25.16666666-.4166667.41666666-.5 2.25-.3333333 3.33333334-2.75 3.33333334-2.8333333.08333333-.25.16666666-.4166667.08333333-.5833334-.08333333-.1666666-.58333333-.3333333-.91666667-.4166666-.08333333 0-.16666666-.0833334-.25-.0833334-.83333333-.3333333-1-.75-.91666666-1.0833333.08333333-.5.83333333-.75 1.25-.5833333.33333333.1666666.58333333.1666666.75.1666666h.25c0-.0833333 0-.25 0-.3333333-.08333334-1.16666667-.25-2.66666667.16666666-3.5 1.16666667-2.66666667 3.66666667-2.83333333 4.33333337-2.83333333h.3333333c.75 0 3.1666667.16666666 4.3333333 2.83333333.4166667.91666667.3333334 2.33333333.25 3.5v.0833333.25h.1666667c.1666667 0 .4166667-.0833333.6666667-.1666666.1666666-.0833334.25-.0833334.4166666-.0833334.1666667 0 .25 0 .4166667.0833334.3333333.0833333.5833333.4166666.5833333.6666666 0 .4166667-.3333333.6666667-.9166666.9166667-.0833334 0-.1666667.0833333-.25.0833333-.3333334.0833334-.8333334.25-.9166667.5-.0833333.1666667 0 .3333334.0833333.5.0833334.25 1.0833334 2.5 3.25 2.8333334.25 0 .4166667.25.4166667.5 0 .0833333 0 .1666666-.0833333.1666666-.1666667.4166667-.9166667.75-2.25 1 0 .0833334-.0833334.25-.0833334.3333334 0 .1666666-.0833333.25-.0833333.4166666-.0833333.25-.25.4166667-.5.4166667-.0833333 0-.25 0-.4166667-.0833333-.25-.0833334-.5-.0833334-.9166666-.0833334-.1666667 0-.4166667 0-.6666667.0833334-.4166667.0833333-.75.3333333-1.1666667.5833333-.6666666.5833333-1.5.8333333-2.5.8333333" fill="#fff" fill-rule="evenodd" transform="translate(-4 -4)"></path>
</svg>
<span class="visually-hidden">Snapchat</span>
</a>
</li>
</ul>
</nav></div>
</nav>
</div>
</div>
</div>
<div class="nav-container">
<div id="block-footer"
class="block block-system block-system-menu-blockfooter cc--component-container cc--footer-menu">
<div class="c--component c--footer-menu">
<div id="block-footer" class="block block-system block-system-menu-blockfooter cc--component-container cc--footer-menu">
<div class="c--component c--footer-menu">
<nav class="mc--menu mc--menu-footer" aria-label="UMass Amherst Footer Menu">
<ul class="menu m--menu m--menu-footer">
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Info for...</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/prospective-students" data-drupal-link-system-path="node/42831">Prospective Students</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/current-students" title="Click here for info for current students" data-drupal-link-system-path="node/61">Current Students</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/faculty-and-staff" title="Click here for info for faculty and staff" data-drupal-link-system-path="node/66">Faculty and Staff</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/parents-and-families" title="Click here for info for parents and families" data-drupal-link-system-path="node/81">Parents and Families</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/hr/careers" title="Info for Job Seekers">Job Seekers</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Resources</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/registrar/academic-calendar">Academic Calendar</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/admissions/visit/visitor-info">Campus Maps</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/peoplefinder/">People Finder</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/policy/">Policies</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/atoz" title="A-Z Directory" data-drupal-link-system-path="node/3931">Sites A-Z Directory</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Connect</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umassalumni.com/">Alumni</a>
</li>
<li class="menu-item">
<a href="https://umassathletics.com/">Athletics</a>
</li>
<li class="menu-item">
<a href="https://www.uma-foundation.org/">Giving</a>
</li>
<li class="menu-item">
<a href="https://www.library.umass.edu/">Libraries</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/contact-us" data-drupal-link-system-path="node/256">Contact Us</a>
</li>
</ul>
</div>
</li>
</ul>
</nav>
<nav class="mc--menu mc--menu-footer" aria-label="UMass Amherst Footer Menu">
<ul class="menu m--menu m--menu-footer">
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Thông tin</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="#">Bệnh nhân</a>
</li>
<li class="menu-item">
<a href="#">Người nhà bệnh nhân</a>
</li>
<li class="menu-item">
<a href="#">Bác sĩ / Chuyên gia y tế</a>
</li>
<li class="menu-item">
<a href="#">Khách hàng Doanh nghiệp</a>
</li>
<li class="menu-item">
<a href="#">Đối tác</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Thông tin</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="#">Lịch khám bệnh</a>
</li>
<li class="menu-item">
<a href="#">Sơ đồ bệnh viện</a>
</li>
<li class="menu-item">
<a href="#">Tìm bác sĩ</a>
</li>
<li class="menu-item">
<a href="#">Chính sách & Quy định</a>
</li>
<li class="menu-item">
<a href="#">Chuyên khoa & Dịch vụ</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Kết nối</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="#">Tuyển dụng</a>
</li>
<li class="menu-item">
<a href="#">Tin tức & Sự kiện</a>
</li>
<li class="menu-item">
<a href="#">Hoạt động xã hội</a>
</li>
<li class="menu-item">
<a href="#">Thư viện y học</a>
</li>
<li class="menu-item">
<a href="#">Liên hệ</a>
</li>
</ul>
</div>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -209,41 +287,35 @@
</div>
</div>
</div>
<div id="block-footerutility"
class="block block-system block-system-menu-blockfooter-utility cc--component-container cc--footer-menu">
<div class="c--component c--footer-menu">
<div id="block-footerutility" class="block block-system block-system-menu-blockfooter-utility cc--component-container cc--footer-menu">
<div class="c--component c--footer-menu">
<nav class="mc--menu mc--menu-footer-utility" aria-label="UMass Amherst Footer Utility Menu">
<ul class="menu m--menu m--menu-footer-utility">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/site-policies" data-drupal-link-system-path="node/3861">Site Policies</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/policy/information-privacy-policy" title="UMass Amherst privacy policy">Privacy</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/policy/affirmative-action-non-discrimination-and-title-ix-non-discrimination-policy" title="UMass Amherst non-discrimination policies">Non-Discrimination Notice</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/accessibility/" title="UMass Amherst Accessibility Resources">Accessibility</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/it/policies/it-policy-acceptable-use-interpretation-guidelines" title="UMass Amherst terms of use">Terms of Use</a>
</li>
</ul>
</nav>
<nav class="mc--menu mc--menu-footer-utility" aria-label="UMass Amherst Footer Utility Menu">
<ul class="menu m--menu m--menu-footer-utility">
<li class="menu-item">
<a href="https://sisvietnam.vn/gateway/site-policies"
data-drupal-link-system-path="node/3861">Site Policies</a>
</li>
<li class="menu-item">
<a href="https://sisvietnam.vn/policy/information-privacy-policy"
title="UMass Amherst privacy policy">Privacy</a>
</li>
<li class="menu-item">
<a href="https://sisvietnam.vn/policy/affirmative-action-non-discrimination-and-title-ix-non-discrimination-policy"
title="UMass Amherst non-discrimination policies">Non-Discrimination Notice</a>
</li>
<li class="menu-item">
<a href="https://sisvietnam.vn/accessibility/"
title="UMass Amherst Accessibility Resources">Accessibility</a>
</li>
<li class="menu-item">
<a href="https://sisvietnam.vn/it/policies/it-policy-acceptable-use-interpretation-guidelines"
title="UMass Amherst terms of use">Terms of Use</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
@@ -254,4 +326,4 @@
</div>
</footer>
</footer>
File diff suppressed because one or more lines are too long
@@ -16,14 +16,29 @@
<!-- Add base CSS here if available -->
<th:block layout:fragment="head"></th:block>
<!-- UMASS Styles -->
<link rel="stylesheet" th:href="@{/css/umass.css}">
<!-- Custom CSS (Highest Priority) -->
<link rel="stylesheet" th:href="@{/css/custom.css}">
</head>
<body>
<div layout:fragment="content">
<!-- All elements will be defined in the editor page -->
</div>
<!-- Skip to main content link (Accessibility) -->
<a href="#main-content" class="visually-hidden focusable skip-link">Skip to main content</a>
<!-- Header Fragment -->
<div th:replace="~{fragments/header}"></div>
<!-- Main Content Block -->
<main id="main-content" role="main">
<div layout:fragment="content">
<!-- All elements will be defined in the editor page -->
</div>
</main>
<!-- Footer Fragment -->
<div th:replace="~{fragments/footer}"></div>
</body>
</html>
@@ -55,6 +55,24 @@
Content
</div>
<!-- Nav Item - Posts Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePosts"
aria-expanded="true" aria-controls="collapsePosts">
<i class="fas fa-fw fa-newspaper"></i>
<span>Posts</span>
</a>
<div id="collapsePosts" class="collapse" aria-labelledby="headingPosts" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Post Management:</h6>
<a class="collapse-item" th:href="@{/manage/posts}">All Posts</a>
<a class="collapse-item" th:href="@{/manage/posts/new}">Add New</a>
<a class="collapse-item" th:href="@{/manage/posts/categories}">Categories</a>
<a class="collapse-item" th:href="@{/manage/posts/tags}">Tags</a>
</div>
</div>
</li>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePages"
@@ -65,7 +65,7 @@
</span>
</td>
<td th:text="${page.displayOrder}"></td>
<td th:text="${page.lastModifiedDate != null} ? ${#temporals.format(page.lastModifiedDate, 'yyyy-MM-dd HH:mm')} : '-'"></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">
<i class="fas fa-edit"></i> Edit
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
<head>
<title>Categories</title>
</head>
<body>
<div layout:fragment="content">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Categories</h1>
<a th:href="@{/manage/posts}" class="d-none d-sm-inline-block btn btn-sm btn-secondary shadow-sm">
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Back to Posts
</a>
</div>
<!-- Success Message -->
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
<span th:text="${successMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<!-- Error Message -->
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
<span th:text="${errorMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="row">
<!-- Left Column: Add/Edit Category Form -->
<div class="col-lg-4">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary"
th:text="${isNew} ? 'Add New Category' : 'Edit Category'">Form</h6>
</div>
<div class="card-body">
<form th:action="${isNew} ? @{/manage/posts/categories} : @{/manage/posts/categories/{id}(id=${category.id})}"
th:object="${category}" method="post">
<!-- Validation errors -->
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger alert-sm">
<ul class="mb-0 small">
<li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li>
</ul>
</div>
<!-- Name -->
<div class="form-group">
<label for="categoryName" class="font-weight-bold">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="categoryName" th:field="*{name}"
th:classappend="${#fields.hasErrors('name')} ? 'is-invalid' : ''"
placeholder="e.g. News, Events, Health Tips" required>
<div class="invalid-feedback" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
</div>
<!-- Slug -->
<div class="form-group">
<label for="categorySlug" class="font-weight-bold">Slug</label>
<input type="text" class="form-control" id="categorySlug" th:field="*{slug}"
placeholder="auto-generated-from-name">
<small class="form-text text-muted">Leave blank to auto-generate.</small>
</div>
<!-- Parent Category -->
<div class="form-group">
<label for="parentCategory" class="font-weight-bold">Parent Category</label>
<select class="form-control" id="parentCategory" name="parentId">
<option value="">— None (Top Level) —</option>
<option th:each="cat : ${allCategories}" th:value="${cat.id}" th:text="${cat.name}"
th:if="${category.id == null || cat.id != category.id}"
th:selected="${category.parent != null && category.parent.id == cat.id}"></option>
</select>
</div>
<!-- Description -->
<div class="form-group">
<label for="categoryDescription" class="font-weight-bold">Description</label>
<textarea class="form-control" id="categoryDescription" th:field="*{description}" rows="3"
maxlength="1000"
placeholder="Optional description of this category"></textarea>
</div>
<!-- Buttons -->
<div class="d-flex justify-content-between">
<a th:if="${!isNew}" th:href="@{/manage/posts/categories}" class="btn btn-secondary btn-sm">
<i class="fas fa-times"></i> Cancel
</a>
<button type="submit" class="btn btn-primary btn-sm"
th:classappend="${isNew} ? 'btn-block' : ''">
<i class="fas fa-save"></i>
<span th:text="${isNew} ? 'Add New Category' : 'Update Category'">Save</span>
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Right Column: Categories Table -->
<div class="col-lg-8">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Existing Categories</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" id="categoriesTable" width="100%" cellspacing="0">
<thead class="thead-light">
<tr>
<th width="25%">Name</th>
<th width="20%">Slug</th>
<th width="25%">Description</th>
<th width="10%">Parent</th>
<th width="8%">Posts</th>
<th width="12%">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="cat : ${allCategories}">
<td>
<strong th:text="${cat.name}"></strong>
</td>
<td>
<code th:text="${cat.slug}"></code>
</td>
<td>
<small th:text="${cat.description}" class="text-muted"></small>
<span th:if="${cat.description == null || cat.description.isEmpty()}" class="text-muted"></span>
</td>
<td>
<span th:if="${cat.parent != null}" th:text="${cat.parent.name}" class="badge badge-light"></span>
<span th:if="${cat.parent == null}" class="text-muted"></span>
</td>
<td class="text-center">
<span class="badge badge-primary" th:text="${postCounts.get(cat.id) ?: 0}"></span>
</td>
<td class="text-center">
<a th:href="@{/manage/posts/categories/{id}/edit(id=${cat.id})}"
class="btn btn-sm btn-outline-info mr-1" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form th:action="@{/manage/posts/categories/{id}/delete(id=${cat.id})}" method="post"
style="display:inline;"
onsubmit="return confirm('Are you sure you want to delete this category?');">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(allCategories)}">
<td colspan="6" class="text-center text-muted py-4">
<i class="fas fa-folder-open fa-2x mb-2 d-block"></i>
No categories yet. Use the form on the left to create one.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,479 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/manage-layout}">
<head>
<title th:text="${isNew} ? 'Add New Post' : 'Edit Post'">Post Form</title>
<style>
/* Editor.js container styling */
#editorjs {
border: 1px solid #d1d3e2;
border-radius: 0.35rem;
padding: 16px 12px;
min-height: 350px;
background: #fff;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
#editorjs:focus-within {
border-color: #bac8f3;
box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
}
/* Block tool styling overrides */
.ce-block__content {
max-width: 100% !important;
}
.ce-toolbar__content {
max-width: 100% !important;
}
.codex-editor__redactor {
padding-bottom: 80px !important;
}
.editor-plugin-info {
font-size: 0.75rem;
color: #858796;
}
.editor-plugin-info .badge {
font-weight: 400;
font-size: 0.7rem;
}
/* Full Screen Editor Mode */
.editor-fullscreen {
position: fixed !important;
top: 0;
left: 0;
width: 100vw !important;
height: 100vh !important;
z-index: 9999 !important;
margin: 0 !important;
border-radius: 0 !important;
border: none !important;
overflow-y: auto !important;
padding: 40px !important;
}
/* Tag input styling */
.tag-input-container {
position: relative;
}
.tag-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: #fff;
border: 1px solid #d1d3e2;
border-top: none;
border-radius: 0 0 0.35rem 0.35rem;
max-height: 200px;
overflow-y: auto;
display: none;
}
.tag-suggestions .suggestion-item {
padding: 8px 12px;
cursor: pointer;
transition: background 0.15s;
}
.tag-suggestions .suggestion-item:hover {
background: #eaecf4;
}
/* Sidebar panels styling */
.publish-panel .card-body {
padding: 1rem;
}
.publish-panel .form-group {
margin-bottom: 0.75rem;
}
.publish-panel .form-group:last-child {
margin-bottom: 0;
}
/* Featured image preview */
.featured-image-preview {
max-width: 100%;
max-height: 200px;
object-fit: cover;
border-radius: 0.35rem;
border: 1px solid #d1d3e2;
margin-top: 0.5rem;
display: none;
}
.featured-image-preview.has-image {
display: block;
}
</style>
</head>
<body>
<div layout:fragment="content">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800" th:text="${isNew} ? 'Add New Post' : 'Edit Post'">Post Form</h1>
<a th:href="@{/manage/posts}" class="d-none d-sm-inline-block btn btn-sm btn-secondary shadow-sm">
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Back to All Posts
</a>
</div>
<form id="postForm" th:action="${isNew} ? @{/manage/posts/create} : @{/manage/posts/{id}(id=${post.id})}"
th:object="${post}" method="post">
<!-- Validation errors summary -->
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">
<ul class="mb-0">
<li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li>
</ul>
</div>
<div class="row">
<!-- Main Content Column (Left) -->
<div class="col-lg-8">
<!-- Title Card -->
<div class="card shadow mb-4">
<div class="card-body">
<div class="form-group mb-0">
<input type="text" class="form-control form-control-lg" id="postTitle" th:field="*{title}"
th:classappend="${#fields.hasErrors('title')} ? 'is-invalid' : ''"
placeholder="Enter post title..." required
style="font-size: 1.4rem; font-weight: 600; border: none; padding: 0; box-shadow: none;">
<div class="invalid-feedback" th:if="${#fields.hasErrors('title')}" th:errors="*{title}"></div>
</div>
<!-- Slug -->
<div class="mt-2">
<small class="text-muted">
<i class="fas fa-link"></i> Permalink:
<code>/post/<span id="slugPreview" th:text="${post.slug ?: 'auto-generated'}"></span></code>
</small>
<input type="hidden" id="postSlug" th:field="*{slug}">
</div>
</div>
</div>
<!-- Block Editor Content -->
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-cubes"></i> Post Content (Block Editor)
</h6>
<button type="button" class="btn btn-sm btn-outline-primary" id="toggleFullscreenBtn">
<i class="fas fa-expand"></i> Full Screen
</button>
</div>
<div class="card-body">
<div class="editor-plugin-info mb-2">
Active blocks:
<span class="badge badge-light">Heading</span>
<span class="badge badge-light">List</span>
<span class="badge badge-light">Quote</span>
<span class="badge badge-light">Table</span>
<span class="badge badge-light">Code</span>
<span class="badge badge-light">Delimiter</span>
<span class="badge badge-light">Warning</span>
<span id="pluginBadges"></span>
</div>
<!-- The Editor.js container -->
<div id="editorjs"></div>
<!-- Hidden input to store the JSON content for form submission -->
<input type="hidden" id="editorContent" name="content" th:value="*{content}">
<small class="form-text text-muted mt-2">
<i class="fas fa-info-circle"></i>
Click the <strong>+</strong> button or press <kbd>Tab</kbd> to add new blocks.
</small>
</div>
</div>
<!-- Excerpt -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-align-left"></i> Excerpt
</h6>
</div>
<div class="card-body">
<textarea class="form-control" id="postExcerpt" th:field="*{excerpt}" rows="3"
maxlength="1000"
placeholder="Write a short summary of the post (optional, used for preview cards)..."></textarea>
<small class="form-text text-muted">A brief hand-written summary. If left blank, a generated snippet from the content will be used.</small>
</div>
</div>
<!-- SEO Meta Description -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-search"></i> SEO Settings
</h6>
</div>
<div class="card-body">
<div class="form-group mb-0">
<label for="postMetaDescription" class="font-weight-bold">Meta Description</label>
<textarea class="form-control" id="postMetaDescription" th:field="*{metaDescription}" rows="2"
maxlength="500"
placeholder="Brief description for search engines (max 500 characters)"></textarea>
<small class="form-text text-muted">This appears in Google search results below the post title.</small>
</div>
</div>
</div>
</div>
<!-- Sidebar Column (Right) -->
<div class="col-lg-4">
<!-- Publish Panel -->
<div class="card shadow mb-4 publish-panel">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-paper-plane"></i> Publish
</h6>
</div>
<div class="card-body">
<div class="form-group">
<label for="postStatus" class="font-weight-bold">Status</label>
<select class="form-control" id="postStatus" th:field="*{status}">
<option th:each="s : ${statuses}" th:value="${s}" th:text="${s}"></option>
</select>
</div>
<div class="form-group mt-3">
<label for="postLayout" class="font-weight-bold">Layout</label>
<select class="form-control" id="postLayout" th:field="*{layout}">
<option th:each="l : ${layouts}" th:value="${l}" th:text="${l}"></option>
</select>
<small class="form-text text-muted">Controls the visual appearance on the frontend.</small>
</div>
<hr>
<div class="d-flex justify-content-between">
<a th:href="@{/manage/posts}" class="btn btn-secondary btn-sm">
<i class="fas fa-times"></i> Cancel
</a>
<button type="submit" class="btn btn-primary btn-sm">
<i class="fas fa-save"></i>
<span th:text="${isNew} ? 'Publish' : 'Update'">Save</span>
</button>
</div>
</div>
</div>
<!-- Category Panel -->
<div class="card shadow mb-4 publish-panel">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-folder"></i> Category
</h6>
</div>
<div class="card-body">
<select class="form-control" id="postCategory" name="categoryId">
<option value="">— No Category —</option>
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.name}"
th:selected="${post.category != null && post.category.id == cat.id}"></option>
</select>
<small class="form-text text-muted mt-1">
<a th:href="@{/manage/posts/categories}" target="_blank">
<i class="fas fa-plus-circle"></i> Manage Categories
</a>
</small>
</div>
</div>
<!-- Tags Panel -->
<div class="card shadow mb-4 publish-panel">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-tags"></i> Tags
</h6>
</div>
<div class="card-body">
<div class="tag-input-container">
<input type="text" class="form-control" id="tagNamesInput" name="tagNames"
th:value="${tagNames}"
placeholder="e.g. news, health, update">
<div class="tag-suggestions" id="tagSuggestions"></div>
</div>
<small class="form-text text-muted mt-1">
Separate tags with commas. New tags will be created automatically.
</small>
<div class="mt-2" id="existingTags">
<span th:each="tag : ${allTags}" class="badge badge-light mr-1 mb-1"
style="cursor: pointer;" th:text="${tag.name}"
th:attr="data-tag-name=${tag.name}"
onclick="addTagToInput(this.getAttribute('data-tag-name'))"></span>
</div>
<small class="form-text text-muted">Click a tag to add it.</small>
</div>
</div>
<!-- Featured Image Panel -->
<div class="card shadow mb-4 publish-panel">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-image"></i> Featured Image
</h6>
</div>
<div class="card-body">
<input type="text" class="form-control" id="postFeaturedImage" th:field="*{featuredImage}"
placeholder="Image URL (e.g. /uploads/image.jpg)">
<img id="featuredImagePreview" class="featured-image-preview mt-2"
th:src="${post.featuredImage}" th:classappend="${post.featuredImage != null && !post.featuredImage.isEmpty()} ? 'has-image' : ''"
alt="Featured image preview">
<small class="form-text text-muted mt-1">Enter the URL of the featured image for this post.</small>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- Editor.js Scripts (injected via layout:fragment="scripts") -->
<section layout:fragment="scripts">
<!-- Editor.js Core -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.30.8/dist/editorjs.umd.js"></script>
<!-- Built-in Block Tools -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.8.8/dist/header.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/nested-list@1.4.3/dist/nested-list.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.7.4/dist/quote.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@1.4.2/dist/delimiter.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/table@2.4.2/dist/table.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@2.9.3/dist/code.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/warning@1.4.1/dist/warning.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.4.0/dist/marker.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@1.5.1/dist/inline-code.umd.js"></script>
<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>
<!-- Custom SIS Editor Plugins -->
<script th:src="@{/js/manage/editor-plugins/html-snippet.js}"></script>
<!-- Init Editor -->
<script th:src="@{/js/manage/editor-config.js}"></script>
<!-- Initialize the editor and page-specific scripts -->
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
// --- Editor.js Initialization ---
var existingContent = document.getElementById('editorContent').value;
var initialData = null;
if (existingContent && existingContent.trim() !== '') {
try {
initialData = JSON.parse(existingContent);
} catch (e) {
console.warn('[SIS Editor] Existing content is not valid JSON, starting fresh.');
}
}
window.sisEditor = initSISEditor('editorjs', 'editorContent', initialData);
// Show badges for any injected plugins
var pluginBadges = document.getElementById('pluginBadges');
var pluginKeys = Object.keys(window.SISEditorPlugins || {});
if (pluginBadges && pluginKeys.length > 0) {
pluginKeys.forEach(function (key) {
var badge = document.createElement('span');
badge.className = 'badge badge-info ml-1';
badge.textContent = key + ' (plugin)';
pluginBadges.appendChild(badge);
});
}
// --- Unsaved Changes Warning ---
var isFormSubmitted = false;
document.getElementById('postForm').addEventListener('submit', function () {
isFormSubmitted = true;
});
window.addEventListener('beforeunload', function (e) {
if (!isFormSubmitted) {
var msg = 'You may have unsaved changes. Are you sure you want to leave?';
e.returnValue = msg;
return msg;
}
});
// --- Full Screen Mode Toggle ---
var editorContainer = document.getElementById('editorjs');
var fsBtn = document.getElementById('toggleFullscreenBtn');
var fsIcon = fsBtn.querySelector('i');
fsBtn.addEventListener('click', function () {
editorContainer.classList.toggle('editor-fullscreen');
if (editorContainer.classList.contains('editor-fullscreen')) {
fsIcon.classList.remove('fa-expand');
fsIcon.classList.add('fa-compress');
fsBtn.style.position = 'fixed';
fsBtn.style.top = '10px';
fsBtn.style.right = '20px';
fsBtn.style.zIndex = '10000';
} else {
fsIcon.classList.remove('fa-compress');
fsIcon.classList.add('fa-expand');
fsBtn.style.position = 'static';
}
});
// --- Featured Image Preview ---
var imgInput = document.getElementById('postFeaturedImage');
var imgPreview = document.getElementById('featuredImagePreview');
imgInput.addEventListener('input', function () {
if (imgInput.value && imgInput.value.trim() !== '') {
imgPreview.src = imgInput.value;
imgPreview.classList.add('has-image');
} else {
imgPreview.classList.remove('has-image');
}
});
// --- Slug Auto-generation from Title ---
var titleInput = document.getElementById('postTitle');
var slugInput = document.getElementById('postSlug');
var slugPreview = document.getElementById('slugPreview');
var isSlugManuallySet = slugInput.value && slugInput.value.trim() !== '';
titleInput.addEventListener('input', function () {
if (!isSlugManuallySet) {
var slug = titleInput.value
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/đ/g, 'd').replace(/Đ/g, 'D')
.replace(/[^\w\s-]/g, '')
.replace(/[\s]+/g, '-')
.replace(/-{2,}/g, '-')
.replace(/^-|-$/g, '');
slugPreview.textContent = slug || 'auto-generated';
}
});
});
// --- Tag click-to-add helper ---
function addTagToInput(tagName) {
var input = document.getElementById('tagNamesInput');
var currentTags = input.value.split(',').map(function(t) { return t.trim(); }).filter(function(t) { return t !== ''; });
if (currentTags.indexOf(tagName) === -1) {
currentTags.push(tagName);
input.value = currentTags.join(', ');
}
}
</script>
</section>
</body>
</html>
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
<head>
<title>All Posts</title>
</head>
<body>
<div layout:fragment="content">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">All Posts</h1>
<a th:href="@{/manage/posts/new}" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm">
<i class="fas fa-plus fa-sm text-white-50"></i> Add New Post
</a>
</div>
<!-- Success Message -->
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
<span th:text="${successMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<!-- Error Message -->
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
<span th:text="${errorMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<!-- Filter Bar -->
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-filter"></i> Filter Posts
</h6>
</div>
<div class="card-body">
<form th:action="@{/manage/posts}" method="get" class="form-inline">
<div class="form-group mr-3 mb-2">
<label for="filterCategory" class="mr-2 font-weight-bold">Category:</label>
<select class="form-control form-control-sm" id="filterCategory" name="categoryId">
<option value="">All Categories</option>
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.name}"
th:selected="${selectedCategoryId != null && selectedCategoryId == cat.id}"></option>
</select>
</div>
<div class="form-group mr-3 mb-2">
<label for="filterStatus" class="mr-2 font-weight-bold">Status:</label>
<select class="form-control form-control-sm" id="filterStatus" name="status">
<option value="">All Statuses</option>
<option th:each="s : ${statuses}" th:value="${s}" th:text="${s}"
th:selected="${selectedStatus != null && selectedStatus == s.name()}"></option>
</select>
</div>
<button type="submit" class="btn btn-sm btn-primary mb-2 mr-2">
<i class="fas fa-search"></i> Filter
</button>
<a th:href="@{/manage/posts}" class="btn btn-sm btn-outline-secondary mb-2">
<i class="fas fa-times"></i> Clear
</a>
</form>
</div>
</div>
<!-- Posts Table -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Manage Posts</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" id="postsTable" width="100%" cellspacing="0">
<thead class="thead-light">
<tr>
<th width="5%">#</th>
<th width="28%">Title</th>
<th width="13%">Category</th>
<th width="17%">Tags</th>
<th width="8%">Status</th>
<th width="10%">Author</th>
<th width="12%">Date</th>
<th width="7%">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="post, iterStat : ${posts}">
<td th:text="${iterStat.count}"></td>
<td>
<a th:href="@{/manage/posts/{id}/edit(id=${post.id})}" class="font-weight-bold text-primary"
th:text="${post.title}"></a>
<br>
<small class="text-muted">
<a th:href="@{/post/{slug}(slug=${post.slug})}" target="_blank" class="text-muted">
<i class="fas fa-external-link-alt"></i> /post/<span th:text="${post.slug}"></span>
</a>
</small>
</td>
<td>
<span th:if="${post.category != null}" th:text="${post.category.name}"
class="badge badge-light"></span>
<span th:if="${post.category == null}" class="text-muted"></span>
</td>
<td>
<span th:each="tag : ${post.tags}" class="badge badge-info mr-1 mb-1"
th:text="${tag.name}"></span>
<span th:if="${#sets.isEmpty(post.tags)}" class="text-muted"></span>
</td>
<td>
<span class="badge"
th:classappend="${post.status.name() == 'PUBLISHED'} ? 'badge-success' : (${post.status.name() == 'DRAFT'} ? 'badge-warning' : 'badge-secondary')"
th:text="${post.status}">
</span>
</td>
<td>
<small th:text="${post.createdBy}" class="text-muted"></small>
</td>
<td>
<small th:if="${post.createdDateAsDate != null}" th:text="${#dates.format(post.createdDateAsDate, 'yyyy-MM-dd HH:mm')}"></small>
<small th:if="${post.createdDateAsDate == null}">-</small>
</td>
<td class="text-center">
<a th:href="@{/manage/posts/{id}/edit(id=${post.id})}" class="btn btn-sm btn-outline-info mr-1" title="Edit">
<i class="fas fa-edit"></i>
</a>
<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?');">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(posts)}">
<td colspan="8" class="text-center text-muted py-4">
<i class="fas fa-newspaper fa-2x mb-2 d-block"></i>
No posts found. Click "Add New Post" to create one.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
<head>
<title>Tags</title>
</head>
<body>
<div layout:fragment="content">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Tags</h1>
<a th:href="@{/manage/posts}" class="d-none d-sm-inline-block btn btn-sm btn-secondary shadow-sm">
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Back to Posts
</a>
</div>
<!-- Success Message -->
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
<span th:text="${successMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<!-- Error Message -->
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
<span th:text="${errorMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="row">
<!-- Left Column: Add/Edit Tag Form -->
<div class="col-lg-4">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary"
th:text="${isNew} ? 'Add New Tag' : 'Edit Tag'">Form</h6>
</div>
<div class="card-body">
<form th:action="${isNew} ? @{/manage/posts/tags} : @{/manage/posts/tags/{id}(id=${tag.id})}"
th:object="${tag}" method="post">
<!-- Validation errors -->
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger alert-sm">
<ul class="mb-0 small">
<li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li>
</ul>
</div>
<!-- Name -->
<div class="form-group">
<label for="tagName" class="font-weight-bold">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="tagName" th:field="*{name}"
th:classappend="${#fields.hasErrors('name')} ? 'is-invalid' : ''"
placeholder="e.g. covid-19, health-tips, breaking-news" required>
<div class="invalid-feedback" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
</div>
<!-- Slug -->
<div class="form-group">
<label for="tagSlug" class="font-weight-bold">Slug</label>
<input type="text" class="form-control" id="tagSlug" th:field="*{slug}"
placeholder="auto-generated-from-name">
<small class="form-text text-muted">Leave blank to auto-generate. Used in URLs for tag archive pages.</small>
</div>
<!-- Buttons -->
<div class="d-flex justify-content-between">
<a th:if="${!isNew}" th:href="@{/manage/posts/tags}" class="btn btn-secondary btn-sm">
<i class="fas fa-times"></i> Cancel
</a>
<button type="submit" class="btn btn-primary btn-sm"
th:classappend="${isNew} ? 'btn-block' : ''">
<i class="fas fa-save"></i>
<span th:text="${isNew} ? 'Add New Tag' : 'Update Tag'">Save</span>
</button>
</div>
</form>
</div>
</div>
<!-- Tag Info Card -->
<div class="card shadow mb-4 border-left-info">
<div class="card-body">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">About Tags</div>
<p class="small text-muted mb-0">
Tags are keywords attached to posts for quick searchability.
Unlike categories, tags have no hierarchy. You can assign multiple tags to a single post.
Tags can also be auto-created when composing a post by typing them in the tag input field.
</p>
</div>
</div>
</div>
<!-- Right Column: Tags Table -->
<div class="col-lg-8">
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">Existing Tags</h6>
<span class="badge badge-primary badge-pill" th:text="${#lists.size(allTags)} + ' total'"></span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" id="tagsTable" width="100%" cellspacing="0">
<thead class="thead-light">
<tr>
<th width="30%">Name</th>
<th width="30%">Slug</th>
<th width="15%">Posts</th>
<th width="25%">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="t : ${allTags}">
<td>
<strong th:text="${t.name}"></strong>
</td>
<td>
<code th:text="${t.slug}"></code>
</td>
<td class="text-center">
<span class="badge badge-primary" th:text="${postCounts.get(t.id) ?: 0}"></span>
</td>
<td class="text-center">
<a th:href="@{/manage/posts/tags/{id}/edit(id=${t.id})}"
class="btn btn-sm btn-outline-info mr-1" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form th:action="@{/manage/posts/tags/{id}/delete(id=${t.id})}" method="post"
style="display:inline;"
onsubmit="return confirm('Are you sure you want to delete this tag?');">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(allTags)}">
<td colspan="4" class="text-center text-muted py-4">
<i class="fas fa-tags fa-2x mb-2 d-block"></i>
No tags yet. Use the form on the left to create one.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/layout}">
<head>
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
</head>
<body>
<div layout:fragment="content">
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
<div class="t--template t--article">
<!-- Content Top (e.g. Hero Image & Title) -->
<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--hero-article">
<div class="c--component c--hero-article">
<div class="f--field f--image" th:if="${post.featuredImage != null and !post.featuredImage.isEmpty()}">
<img th:src="${post.featuredImage}" th:alt="${post.title}" style="width:100%; object-fit:cover;">
</div>
<h1 class="f--field f--title text-center" th:text="${post.title}">Post Title</h1>
<p class="f--field f--date text-center" th:if="${formattedDate != null}" th:text="${formattedDate}">Date</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Content Main (Full Width) -->
<div class="content-main">
<div class="lc--layout-container lc--full">
<div class="l--layout l--full">
<div class="lr--layout-region lr--main">
<section class="cc--component-container cc--wysiwyg">
<div class="c--component c--wysiwyg">
<div class="f--field f--wysiwyg editorjs-content" id="postContentRaw" style="display:none;" th:text="${post.content}"></div>
<div class="f--field f--wysiwyg" id="postContentParsed"></div>
</div>
</section>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var rawData = document.getElementById("postContentRaw").textContent;
var parsedContainer = document.getElementById("postContentParsed");
try {
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
data.blocks.forEach(function(block) {
switch(block.type) {
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
case "paragraph":
html += "<p>" + block.data.text + "</p>";
break;
case "image":
html += "<figure><img src='" + block.data.url + "' alt='" + (block.data.caption || '') + "'><figcaption>" + (block.data.caption || '') + "</figcaption></figure>";
break;
case "list":
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
html += "<" + tag + ">";
block.data.items.forEach(function(item) {
html += "<li>" + item + "</li>";
});
html += "</" + tag + ">";
break;
case "quote":
html += "<blockquote>" + block.data.text + " <cite>" + block.data.caption + "</cite></blockquote>";
break;
default:
console.log("Unknown block type", block.type);
}
});
}
parsedContainer.innerHTML = html;
} catch(e) {
parsedContainer.innerHTML = rawData;
}
});
</script>
</div>
</body>
</html>
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/layout}">
<head>
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
</head>
<body>
<div layout:fragment="content">
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
<div class="t--template t--article">
<!-- Content Top (e.g. Hero Image & Title) -->
<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--hero-article">
<div class="c--component c--hero-article">
<div class="f--field f--image" th:if="${post.featuredImage != null and !post.featuredImage.isEmpty()}">
<img th:src="${post.featuredImage}" th:alt="${post.title}">
</div>
<h1 class="f--field f--title" th:text="${post.title}">Post Title</h1>
<p class="f--field f--date" th:if="${formattedDate != null}" th:text="${formattedDate}">Date</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Content Main (Sidebar Layout) -->
<div class="content-main">
<div class="lc--layout-container lc--two-column">
<div class="l--layout l--two-column">
<!-- Sidebar -->
<aside class="lr--layout-region lr--sidebar">
<div class="cc--component-container cc--contact-block">
<div class="c--component c--contact-block">
<h3>About the Author</h3>
<p>This is a placeholder for author details or related links that appear in the sidebar.</p>
<div th:if="${post.category != null}">
<h4>Category</h4>
<p th:text="${post.category.name}">Category Name</p>
</div>
<div th:if="${!post.tags.isEmpty()}">
<h4>Tags</h4>
<ul>
<li th:each="tag : ${post.tags}" th:text="${tag.name}">Tag</li>
</ul>
</div>
</div>
</div>
</aside>
<!-- Main Content -->
<div class="lr--layout-region lr--main">
<section class="cc--component-container cc--wysiwyg">
<div class="c--component c--wysiwyg">
<div class="f--field f--wysiwyg editorjs-content" id="postContentRaw" style="display:none;" th:text="${post.content}"></div>
<div class="f--field f--wysiwyg" id="postContentParsed"></div>
</div>
</section>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var rawData = document.getElementById("postContentRaw").textContent;
var parsedContainer = document.getElementById("postContentParsed");
try {
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
data.blocks.forEach(function(block) {
switch(block.type) {
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
case "paragraph":
html += "<p>" + block.data.text + "</p>";
break;
case "image":
html += "<figure><img src='" + block.data.url + "' alt='" + (block.data.caption || '') + "'><figcaption>" + (block.data.caption || '') + "</figcaption></figure>";
break;
case "list":
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
html += "<" + tag + ">";
block.data.items.forEach(function(item) {
html += "<li>" + item + "</li>";
});
html += "</" + tag + ">";
break;
case "quote":
html += "<blockquote>" + block.data.text + " <cite>" + block.data.caption + "</cite></blockquote>";
break;
default:
console.log("Unknown block type", block.type);
}
});
}
parsedContainer.innerHTML = html;
} catch(e) {
parsedContainer.innerHTML = rawData;
}
});
</script>
</div>
</body>
</html>
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/layout}">
<head>
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
</head>
<body>
<div layout:fragment="content">
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
<div class="t--template t--article">
<!-- Content Top (e.g. Hero Image & Title) -->
<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--hero-article">
<div class="c--component c--hero-article">
<div class="f--field f--image" th:if="${post.featuredImage != null and !post.featuredImage.isEmpty()}">
<img th:src="${post.featuredImage}" th:alt="${post.title}">
</div>
<h1 class="f--field f--title" th:text="${post.title}">Post Title</h1>
<p class="f--field f--date" th:if="${formattedDate != null}" th:text="${formattedDate}">Date</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Content Main (Standard One Column) -->
<div class="content-main">
<div class="lc--layout-container lc--one-column">
<div class="l--layout l--one-column">
<div class="lr--layout-region lr--main">
<section class="cc--component-container cc--wysiwyg">
<div class="c--component c--wysiwyg">
<!-- Output raw HTML from block editor (we need to parse or render it). Since it's saved as raw HTML from the previous plugin or JSON, we just output it. Wait, the editor uses Editor.js, so the content might be JSON! -->
<!-- Assuming the content is parsed or we just dump it for now -->
<div class="f--field f--wysiwyg editorjs-content" id="postContentRaw" style="display:none;" th:text="${post.content}"></div>
<div class="f--field f--wysiwyg" id="postContentParsed"></div>
</div>
</section>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Include Editor.js parser to render the content on the frontend -->
<!-- Because Editor.js saves as JSON, we need to convert it to HTML. -->
<script>
document.addEventListener("DOMContentLoaded", function() {
var rawData = document.getElementById("postContentRaw").textContent;
var parsedContainer = document.getElementById("postContentParsed");
try {
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
data.blocks.forEach(function(block) {
switch(block.type) {
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
case "paragraph":
html += "<p>" + block.data.text + "</p>";
break;
case "image":
html += "<figure><img src='" + block.data.url + "' alt='" + (block.data.caption || '') + "'><figcaption>" + (block.data.caption || '') + "</figcaption></figure>";
break;
case "list":
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
html += "<" + tag + ">";
block.data.items.forEach(function(item) {
html += "<li>" + item + "</li>";
});
html += "</" + tag + ">";
break;
case "quote":
html += "<blockquote>" + block.data.text + " <cite>" + block.data.caption + "</cite></blockquote>";
break;
default:
console.log("Unknown block type", block.type);
}
});
}
parsedContainer.innerHTML = html;
} catch(e) {
// If it's not JSON, it might just be HTML
parsedContainer.innerHTML = rawData;
}
});
</script>
</div>
</body>
</html>