feat: implement livestream post functionality and update site theme assets

This commit is contained in:
2026-07-14 23:34:50 +07:00
parent 1b905d8c60
commit f4e24a0e00
130 changed files with 17284 additions and 277 deletions
@@ -56,6 +56,8 @@ public class PostController {
return "posts/sidebar";
case FULL_WIDTH:
return "posts/full-width";
case LIVESTREAM:
return "posts/livestream";
case STANDARD:
default:
return "posts/standard";
@@ -156,7 +156,7 @@ public class ManagePostController {
}
postService.saveWithTags(post, tagNames);
redirectAttributes.addFlashAttribute("successMessage", "Post updated successfully!");
return "redirect:/manage/posts";
return "redirect:/manage/posts/" + id + "/edit";
}
/**
@@ -63,6 +63,10 @@ public class Post extends AbstractAuditingEntity<Long> {
@Column(name = "event_time")
private java.time.Instant eventTime;
@Size(max = 255)
@Column(name = "location", length = 255)
private String location;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@@ -166,6 +170,14 @@ public class Post extends AbstractAuditingEntity<Long> {
this.eventTime = eventTime;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
@jakarta.persistence.Transient
public String getEventTimeLocal() {
if (this.eventTime != null) {
@@ -7,5 +7,6 @@ public enum PostLayout {
STANDARD,
SIDEBAR,
FULL_WIDTH,
EVENT
EVENT,
LIVESTREAM
}
@@ -1,19 +1,20 @@
package com.sisvietnamvn.web.service;
import com.sisvietnamvn.web.domain.ComponentTemplate;
import com.sisvietnamvn.web.domain.Menu;
import com.sisvietnamvn.web.domain.MenuItem;
import com.sisvietnamvn.web.repository.ComponentTemplateRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.sisvietnamvn.web.domain.ComponentTemplate;
import com.sisvietnamvn.web.domain.Menu;
import com.sisvietnamvn.web.domain.MenuItem;
import com.sisvietnamvn.web.repository.ComponentTemplateRepository;
/**
* Service for managing Component Templates and rendering them with dynamic data.
*
@@ -221,6 +222,8 @@ public class ComponentTemplateService {
itemHtml = itemHtml.replace("{{title}}", safeTitle);
itemHtml = itemHtml.replace("{{slug}}", post.getSlug() != null ? post.getSlug() : "");
itemHtml = itemHtml.replace("{{excerpt}}", safeExcerpt);
itemHtml = itemHtml.replace("{{content}}", post.getContent() != null ? parseContentToHtml(post.getContent()) : "");
itemHtml = itemHtml.replace("{{metaDescription}}", post.getMetaDescription() != null ? org.springframework.web.util.HtmlUtils.htmlEscape(post.getMetaDescription()) : "");
itemHtml = itemHtml.replace("{{featuredImageUrl}}", post.getFeaturedImage() != null ? post.getFeaturedImage() : "");
String dateStr = post.getCreatedDate() != null ? formatter.format(post.getCreatedDate()) : "";
itemHtml = itemHtml.replace("{{date}}", dateStr);
@@ -235,6 +238,13 @@ public class ComponentTemplateService {
itemHtml = itemHtml.replace("{{eventTimeStr}}", "");
}
if (post.getLocation() != null && !post.getLocation().isEmpty()) {
String locHtml = "<div class=\"event-location\">" + org.springframework.web.util.HtmlUtils.htmlEscape(post.getLocation()) + "</div>";
itemHtml = itemHtml.replace("{{locationHtml}}", locHtml);
} else {
itemHtml = itemHtml.replace("{{locationHtml}}", "");
}
itemHtml = itemHtml.replace("{{index}}", String.valueOf(index));
rendered.append(itemHtml);
index++;
@@ -257,6 +267,56 @@ public class ComponentTemplateService {
public void seedDefaults() {
seedCtaButtons();
seedInfoDropdown();
seedNewsGrid();
}
private void seedNewsGrid() {
Optional<ComponentTemplate> existing = templateRepository.findBySlug("news-grid");
if (existing.isPresent()) {
ComponentTemplate t = existing.get();
if (t.getHtmlTemplate() == null || !t.getHtmlTemplate().contains("<style>")) {
return; // Already clean
}
// Update the existing one
t.setHtmlTemplate(getNewsGridHtml());
templateRepository.save(t);
LOG.info("Cleaned up hardcoded CSS in news-grid component template");
return;
}
ComponentTemplate t = new ComponentTemplate();
t.setSlug("news-grid");
t.setName("News Grid");
t.setDescription("Dynamic grid layout for news.");
t.setActive(true);
t.setHtmlTemplate(getNewsGridHtml());
templateRepository.save(t);
LOG.info("Seeded default component template: news-grid");
}
private String getNewsGridHtml() {
return "{{#each news_posts}}\n" +
"<div class=\"featured-grid__item\">\n" +
" <article class=\"news-card news-card--featured\">\n" +
" <div class=\"news-card__image\">\n" +
" <div>\n" +
" <img loading=\"lazy\" src=\"{{featuredImageUrl}}\" alt=\"{{title}}\">\n" +
" </div>\n" +
" </div>\n" +
" <div class=\"news-card__content\">\n" +
" <div class=\"featured-grid__category\">\n" +
" <a href=\"#\">TIN TỨC</a>\n" +
" </div>\n" +
" <h3 class=\"news-card__title\">\n" +
" <a href=\"/post/{{slug}}\" rel=\"bookmark\">\n" +
" <span>{{title}}</span>\n" +
" </a>\n" +
" </h3>\n" +
" <div class=\"news-card__date\">{{date}}</div>\n" +
" </div>\n" +
" </article>\n" +
"</div>\n" +
"{{/each}}";
}
private void seedCtaButtons() {
@@ -325,4 +385,44 @@ public class ComponentTemplateService {
templateRepository.save(t);
LOG.info("Seeded default component template: info-dropdown");
}
private String parseContentToHtml(String content) {
if (content == null || !content.trim().startsWith("{")) {
return content;
}
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
java.util.Map<String, Object> data = mapper.readValue(content, new com.fasterxml.jackson.core.type.TypeReference<>() {});
if (data.containsKey("blocks")) {
java.util.List<java.util.Map<String, Object>> blocks = (java.util.List<java.util.Map<String, Object>>) data.get("blocks");
StringBuilder sb = new StringBuilder();
for (java.util.Map<String, Object> block : blocks) {
String type = (String) block.get("type");
java.util.Map<String, Object> blockData = (java.util.Map<String, Object>) block.get("data");
if (blockData != null) {
if ("paragraph".equals(type)) {
sb.append("<p>").append(blockData.get("text")).append("</p>");
} else if ("header".equals(type)) {
sb.append("<h").append(blockData.get("level")).append(">").append(blockData.get("text")).append("</h").append(blockData.get("level")).append(">");
} else if ("list".equals(type)) {
String style = (String) blockData.get("style");
String listTag = "ordered".equals(style) ? "ol" : "ul";
sb.append("<").append(listTag).append(">");
java.util.List<String> items = (java.util.List<String>) blockData.get("items");
if (items != null) {
for (String item : items) {
sb.append("<li>").append(item).append("</li>");
}
}
sb.append("</").append(listTag).append(">");
}
}
}
return sb.toString();
}
} catch (Exception e) {
// Fallback to raw content if parsing fails
}
return content;
}
}
@@ -6,12 +6,16 @@
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
<changeSet id="20260714080000-1" author="antigravity">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM sis_component_template WHERE slug = 'news-grid'</sqlCheck>
</preConditions>
<insert tableName="sis_component_template">
<column name="id" valueComputed="(select coalesce(max(id), 0) + 1 from sis_component_template)"/>
<column name="slug" value="news-grid"/>
<column name="name" value="News Grid"/>
<column name="description" value="Dynamic grid layout for news."/>
<column name="active" valueBoolean="true"/>
<column name="created_by" value="system"/>
<column name="html_template"><![CDATA[{{#each news_posts}}
<div class="featured-grid__item">
<article class="news-card news-card--featured">
@@ -43,6 +47,7 @@
<column name="name" value="Events List"/>
<column name="description" value="List layout for events."/>
<column name="active" valueBoolean="true"/>
<column name="created_by" value="system"/>
<column name="html_template"><![CDATA[<style>
.campus-events-widget { background: transparent; padding: 0; font-family: inherit; }
.campus-events-widget .events-list { list-style: none; margin: 0; padding: 0; }
@@ -95,6 +100,8 @@
</div>]]></column>
<column name="created_date" valueComputed="CURRENT_TIMESTAMP"/>
</insert>
</changeSet>
<changeSet id="20260714080000-3" author="antigravity" runAlways="true">
<update tableName="sis_component_template">
<column name="html_template"><![CDATA[{{#each news_posts}}
@@ -122,9 +129,6 @@
<where>slug = 'news-grid'</where>
</update>
</changeSet>
</databaseChangeLog>
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
<changeSet id="20260714080000-2" author="antigravity" runAlways="true">
<update tableName="sis_component_template">
@@ -0,0 +1,20 @@
<?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="20260714112000-1" author="antigravity">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="sis_post" columnName="location"/>
</not>
</preConditions>
<addColumn tableName="sis_post">
<column name="location" type="varchar(255)">
<constraints nullable="true" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,64 @@
<?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="20260714113000-1" author="antigravity" runAlways="true">
<update tableName="sis_component_template">
<column name="html_template"><![CDATA[<style>
.campus-events-widget { background: transparent; padding: 0; font-family: inherit; }
.campus-events-widget .events-list { list-style: none; margin: 0; padding: 0; }
.campus-events-widget .teaser-item { padding: 20px 0; border-bottom: 1px solid #e5e5e5; }
.campus-events-widget .teaser-item:last-child { border-bottom: none; }
.campus-events-widget .event-wrapper { display: flex; gap: 20px; align-items: flex-start; }
.campus-events-widget .event-date { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 0.125rem; padding: 0.25rem; background: #881C1C; width: 2.875rem; height: 3.125rem; box-sizing: border-box; }
.campus-events-widget .event-date__month { font-size: 1rem; font-weight: 800; line-height: 1.25rem; text-transform: uppercase; text-align: center; color: #fff; }
.campus-events-widget .event-date__day { font-size: 1rem; font-weight: 800; line-height: 1.25rem; text-transform: uppercase; text-align: center; color: #fff; }
.campus-events-widget .event-details { flex: 1; position: relative; }
.campus-events-widget .event-title { font-size: 1rem; font-weight: 800; line-height: 1.25rem; text-transform: uppercase; color: #000; }
.campus-events-widget .event-title a { color: inherit; text-decoration: none; }
.campus-events-widget .event-title a:hover { text-decoration: underline; }
.campus-events-widget .event-time { font-size: 16px; color: #000; margin-bottom: 4px; }
.campus-events-widget .event-location { font-size: 1rem; font-weight: 400; line-height: 1.5rem; font-style: normal; color: #000; }
@media (max-width: 768px) {
.campus-events-widget .event-title { font-size: 0.75rem; }
.campus-events-widget .event-time { font-size: 0.75rem; }
.campus-events-widget .event-location { font-size: 0.75rem; }
.campus-events-widget .event-wrapper { display: flex; flex: 1; padding-right: 10px; align-items: stretch; gap: 12px; border: 1px solid #CCC; }
.campus-events-widget .event-date { display: flex; padding: 4px 8px; flex-direction: column; justify-content: center; align-items: center; align-self: stretch; background: #881C1C; min-width: 48px; height: auto; }
.campus-events-widget .event-details { padding: 8px 10px 8px 0; }
.campus-events-widget .teaser-item { flex: 0 0 279px; display: flex; border: 0px solid #CCC; padding-bottom: 0; }
.campus-events-widget .events-list { display: flex; gap: 1rem; overflow-x: auto; padding-bottom: 1rem; }
}
@media (max-width: 480px) {
.campus-events-widget .event-wrapper { gap: 10px; }
.campus-events-widget .event-date__day { font-size: 18px; }
}
</style>
<div class="campus-events-widget">
<div class="events-list">
{{#each event_posts}}
<div class="teaser-item">
<div class="event-wrapper">
<div class="event-date">
<div class="event-date__month">{{eventMonth}}</div>
<div class="event-date__day">{{eventDay}}</div>
</div>
<div class="event-details">
<div class="event-title">
<a href="/post/{{slug}}">{{title}}</a>
</div>
<div class="event-time">{{eventTimeStr}}</div>
{{locationHtml}}
</div>
</div>
</div>
{{/each}}
</div>
</div>]]></column>
<where>slug = 'events-list'</where>
</update>
</changeSet>
</databaseChangeLog>
@@ -37,4 +37,6 @@
<!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here -->
<!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here -->
<include file="config/liquibase/changelog/20260714080000_seed_news_events_components.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260714112000_add_location_to_post.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260714113000_update_events_list.xml" relativeToChangelogFile="false"/>
</databaseChangeLog>
@@ -60,4 +60,465 @@
/* Ensures it sits above the video */
}
/* News Grid Tablet Layout */
@media screen and (min-width: 769px) and (max-width: 1024px) {
.news-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
align-items: stretch;
}
.featured-grid__item {
height: 100%;
display: flex;
}
.featured-grid__item .news-card--featured {
display: flex;
flex-direction: column;
align-items: flex-start;
border: 1px solid #881C1C;
border-radius: 16px;
padding: 16px;
box-sizing: border-box;
width: 100%;
height: 100%;
gap: 16px;
background: #fff;
margin-bottom: 0 !important;
}
.featured-grid__item .news-card__image {
flex: none;
width: 100%;
aspect-ratio: 1 / 1;
margin-bottom: 0 !important;
}
.featured-grid__item .news-card__image > div {
height: 100%;
width: 100%;
}
.featured-grid__item .news-card__image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
border-radius: 8px;
}
.featured-grid__item .news-card__content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-width: 0;
padding: 0 !important;
margin: 0 !important;
}
.featured-grid__item .featured-grid__category {
margin-bottom: 12px;
}
.featured-grid__item .featured-grid__category a {
display: inline-block;
background-color: #f2f2f2;
color: #881C1C;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: bold;
text-decoration: none;
text-transform: uppercase;
}
.featured-grid__item .news-card__title {
font-size: 1.1rem;
font-weight: 700;
margin-top: 0;
margin-bottom: 12px;
line-height: 1.4;
}
.featured-grid__item .news-card__title a {
color: #000;
text-decoration: none;
}
.featured-grid__item .news-card__title a:hover {
color: #881C1C;
}
.featured-grid__item .news-card__date {
margin-top: auto;
color: #881C1C;
font-size: 0.85rem;
font-weight: 500;
}
}
/* News Grid Desktop Layout */
@media screen and (min-width: 1025px){
.news-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
align-items: stretch;
}
.education-column .news-list {
grid-template-columns: repeat(3, 1fr);
}
.featured-grid__item {
height: 100%;
display: flex;
}
.featured-grid__item .news-card--featured {
display: flex;
flex-direction: row;
align-items: flex-start;
border: 1px solid #881C1C;
border-radius: 16px;
padding: 16px;
box-sizing: border-box;
width: 100%;
height: 100%;
gap: 16px;
background: #fff;
margin-bottom: 0 !important;
}
.featured-grid__item .news-card__image {
flex: 0 0 170px;
width: 100%;
height: 170px;
margin-bottom: 0 !important;
}
.featured-grid__item .news-card__image > div {
height: 100%;
width: 100%;
}
.featured-grid__item .news-card__image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
border-radius: 8px;
}
.featured-grid__item .news-card__content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-width: 0;
padding: 0 !important;
margin: 0 !important;
}
.featured-grid__item .featured-grid__category {
margin-bottom: 12px;
}
.featured-grid__item .featured-grid__category a {
display: inline-block;
background-color: #f2f2f2;
color: #881C1C;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: bold;
text-decoration: none;
text-transform: uppercase;
}
.featured-grid__item .news-card__title {
font-size: 1.1rem;
font-weight: 700;
margin-top: 0;
margin-bottom: 12px;
line-height: 1.4;
}
.featured-grid__item .news-card__title a {
color: #000;
text-decoration: none;
}
.featured-grid__item .news-card__title a:hover {
color: #881C1C;
}
.featured-grid__item .news-card__date {
margin-top: auto;
color: #881C1C;
font-size: 0.85rem;
font-weight: 500;
}
}
/* ==========================================================================
Desktop Layouts (>= 1024px)
========================================================================== */
@media screen and (min-width: 1025px) {
/* Side-by-side layout for News and Events */
.news-events-row {
display: flex;
flex-direction: row;
gap: 40px;
align-items: stretch;
}
.news-events-row > .news-column {
flex: 1.8;
min-width: 0;
}
.news-events-row > .events-column {
flex: 1;
min-width: 0;
}
}
/* ==========================================================================
Navigation Container & Mobile Menu
========================================================================== */
/* --- Desktop (>1024px) --- */
.navigation-container {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 1.5rem;
flex-grow: 1;
}
/* Desktop: show horizontal menu, hide hamburger */
@media (min-width: 1025px) {
#mobile-hamburger {
display: none !important;
}
#mobile-flyout-container {
display: flex !important;
align-items: center;
}
.primary-navigation {
display: flex;
align-items: center;
}
/* Desktop: hide arrow toggles and back buttons */
.mc--main-menu .arrow-toggle,
.mc--main-menu .button-back,
.mc--main-menu .navigation-title {
display: none !important;
}
/* Desktop: horizontal main menu */
.m--main-menu {
display: flex;
gap: 0;
list-style: none;
margin: 0;
padding: 0;
}
.m--main-menu > .menu-item > .link-arrow-wrapper > a {
text-transform: uppercase;
font-weight: 700;
padding: 10px 15px;
white-space: nowrap;
}
}
/* --- Mobile (<= 768px) --- */
@media (max-width: 768px) {
/* Hide ambient video on mobile to save bandwidth and use image fallback */
.f--ambient-video,
.video-controls {
display: none !important;
}
}
/* Override Tabbed Media Content text colors since background was changed to light gray */
.cc--tabbed-media-content .f--description p {
color: #000000 !important;
}
.cc--tabbed-media-content .f--description a {
color: var(--color-brand) !important;
}
/* Style tab labels and borders to black */
.cc--tabbed-media-content .tab-labels-inner {
border-bottom-color: #cccccc !important; /* light gray line for the tab row */
}
.cc--tabbed-media-content .tab-label {
color: #555555 !important; /* dark gray for inactive tabs */
font-weight: 700 !important;
}
.cc--tabbed-media-content .tab-label[aria-selected="true"] {
color: #000000 !important; /* solid black for active tab */
border-bottom: 3px solid #000000 !important; /* solid black underline for active tab */
}
/* Center children inside tabbed-media-content-media-col */
.cc--tabbed-media-content .tabbed-media-content-media-col {
align-items: center !important;
justify-content: center !important;
}
/* --- Mobile & Tablet (<=1024px) --- */
@media (max-width: 1024px) {
.navigation-container {
gap: 0.5rem;
}
/* Hamburger button */
#mobile-hamburger {
display: block;
z-index: 1001;
background: none;
border: none;
cursor: pointer;
padding: 10px;
}
.hamburger-box {
width: 30px;
height: 24px;
display: inline-block;
position: relative;
}
.hamburger-inner,
.hamburger-inner::before,
.hamburger-inner::after {
width: 30px;
height: 3px;
background-color: #fff;
border-radius: 2px;
position: absolute;
transition: transform 0.3s ease, opacity 0.2s ease;
}
.hamburger-inner {
top: 50%;
transform: translateY(-50%);
}
.hamburger-inner::before {
content: '';
top: -9px;
}
.hamburger-inner::after {
content: '';
bottom: -9px;
}
/* Hamburger X state */
#mobile-hamburger.is-active .hamburger-inner {
transform: rotate(45deg);
}
#mobile-hamburger.is-active .hamburger-inner::before {
top: 0;
opacity: 0;
}
#mobile-hamburger.is-active .hamburger-inner::after {
bottom: 0;
transform: rotate(-90deg);
}
/* Mobile flyout container */
#mobile-flyout-container {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: auto;
background: #fff;
z-index: 1000;
overflow-y: auto;
overflow-x: hidden;
padding-top: 80px;
}
/* Primary navigation inside flyout */
.primary-navigation {
display: block;
}
/* Main menu list - vertical */
.m--main-menu {
list-style: none;
margin: 0;
padding: 0;
}
.m--main-menu > .menu-item {
border-bottom: 1px solid #e0e0e0;
}
/* Link + arrow wrapper */
.link-arrow-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0;
}
.link-arrow-wrapper > a {
flex: 1;
padding: 15px 20px;
color: #333;
text-decoration: none;
font-weight: 700;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Arrow toggle button */
.arrow-toggle {
background: none;
border: none;
/* border-left: 1px solid #e0e0e0; */
/* padding: 15px 20px; */
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
min-width: 50px;
}
.arrow-toggle svg {
transition: transform 0.3s ease;
}
.arrow-toggle.is-active svg {
transform: rotate(180deg);
}
/* Submenu list */
.submenu {
list-style: none;
margin: 0;
padding: 0;
}
.submenu .menu-item {
border-bottom: 1px solid #f0f0f0;
}
.submenu .menu-item a,
.submenu .menu-item span {
display: block;
padding: 12px 20px;
color: #333;
text-decoration: none;
font-size: 15px;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.03em;
}
.submenu .menu-item a:hover {
background-color: #f5f5f5;
color: #881c1c;
}
/* Hide desktop-only elements on mobile */
.desktop-only {
display: none !important;
}
/* Show mobile-only elements on mobile */
.mobile-only {
display: block !important;
}
/* Hide quicklink buttons, utility menu, and social links on mobile flyout */
.mc--quicklink-button-menu,
.mc--utility-menu,
[data-component-id="umass_base:header-socials"] {
margin-top: 20px;
border-top: 2px solid #881c1c;
padding-top: 10px;
}
}
/* On Desktop, hide mobile-only elements */
@media (min-width: 1366) {
.mobile-only {
display: none !important;
}
}
@@ -0,0 +1,45 @@
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.video-controls').forEach(function (controls) {
var playBtn = controls.querySelector('.video-play-button');
var pauseBtn = controls.querySelector('.video-pause-button');
// Find the associated ambient video. In the HTML structure, the .f--ambient-video
// container is a sibling (specifically previousElementSibling) of .video-controls.
// If not, we fall back to searching within the closest parent.
var videoContainer = controls.previousElementSibling;
if (!videoContainer || !videoContainer.classList.contains('f--ambient-video')) {
var parent = controls.closest('.image-video-container');
if (parent) {
videoContainer = parent.querySelector('.f--ambient-video');
}
}
if (!videoContainer) return;
var video = videoContainer.querySelector('video');
if (!video) return;
if (pauseBtn) {
pauseBtn.addEventListener('click', function (e) {
e.preventDefault();
pauseBtn.classList.add('hidden');
if (playBtn) {
playBtn.classList.add('active');
playBtn.focus();
}
video.pause();
});
}
if (playBtn) {
playBtn.addEventListener('click', function (e) {
e.preventDefault();
playBtn.classList.remove('active');
if (pauseBtn) {
pauseBtn.classList.remove('hidden');
pauseBtn.focus();
}
video.play();
});
}
});
});
@@ -0,0 +1,88 @@
/**
* Mobile Menu - Hamburger toggle, submenu accordion interactions.
* Pure vanilla JS — jQuery is NOT available.
* Only active on mobile/tablet (≤1024px via CSS).
*/
document.addEventListener('DOMContentLoaded', function () {
var hamburger = document.getElementById('mobile-hamburger');
var flyout = document.getElementById('mobile-flyout-container');
var header = document.querySelector('[data-component-id="umass_base:site-header"]');
// Helper functions for fade animation
function fadeIn(el) {
el.style.display = 'block';
var anim = el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300, fill: 'forwards' });
el._fadeAnim = anim;
}
function fadeOut(el) {
var anim = el.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 300, fill: 'forwards' });
el._fadeAnim = anim;
anim.onfinish = function() {
if (el._fadeAnim === anim) {
el.style.display = 'none';
}
};
}
// --- Hamburger toggle ---
if (hamburger && flyout) {
hamburger.addEventListener('click', function () {
var isActive = hamburger.classList.toggle('is-active');
if (isActive) {
fadeIn(flyout);
} else {
fadeOut(flyout);
// Close all open submenus when closing the flyout
flyout.querySelectorAll('.submenu-wrapper').forEach(function (w) {
if (w.style.display !== 'none') fadeOut(w);
});
flyout.querySelectorAll('.arrow-toggle.is-active').forEach(function (a) {
a.classList.remove('is-active');
});
}
if (header) header.classList.toggle('mobile-menu-active', isActive);
});
}
// --- Arrow toggle: open/close submenu (fade style) ---
document.querySelectorAll('#mobile-flyout-container .arrow-toggle').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
// The submenu-wrapper is the next sibling of .link-arrow-wrapper
var linkArrowWrapper = btn.closest('.link-arrow-wrapper');
if (!linkArrowWrapper) return;
var wrapper = linkArrowWrapper.nextElementSibling;
if (!wrapper || !wrapper.classList.contains('submenu-wrapper')) return;
var isActive = btn.classList.toggle('is-active');
if (isActive) {
fadeIn(wrapper);
} else {
fadeOut(wrapper);
}
});
});
// --- Back button: close submenu ---
document.querySelectorAll('.button-back').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
var wrapper = btn.closest('.submenu-wrapper');
if (wrapper) {
fadeOut(wrapper);
// Also reset the arrow toggle on the parent li
var li = wrapper.closest('li');
if (li) {
var toggle = li.querySelector('.link-arrow-wrapper > .arrow-toggle');
if (toggle) {
toggle.classList.remove('is-active');
}
}
}
});
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

@@ -19,10 +19,33 @@
<nav class="mc--menu mc--main-menu" th:if="${primaryMenu != null}">
<ul class="menu m--menu m--main-menu">
<li class="megamenu menu-item" th:each="item : ${primaryMenu.items}">
<li th:each="item : ${primaryMenu.items}" th:if="${item.parent == null}"
th:class="${item.children.empty} ? 'menu-item' : 'menu-item menu-item--expanded'">
<div class="link-arrow-wrapper">
<a th:href="${item.url}" th:text="${item.label}">Menu Item</a>
<button th:if="${!item.children.empty}" type="button" class="has-submenu arrow-toggle" aria-expanded="false" aria-haspopup="true" th:attr="aria-label='Display Sub Menu for ' + ${item.label}" data-once="site-header-arrow">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 16 7" enable-background="new 0 0 16 7" xml:space="preserve" width="16" height="7">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#881c1c" d="M7.88,7L15.75,0H0s7.88,7,7.88,7Z"></path>
</svg>
</button>
</div>
<div class="submenu-wrapper" th:if="${!item.children.empty}">
<button class="button-back">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 5 10" enable-background="new 0 0 5 10" xml:space="preserve" width="5" height="10">
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#881c1c" points="0,5 5,0 5,10 "></polygon>
</svg> Back
</button>
<span class="navigation-title" th:text="${item.label}">Submenu Title</span>
<ul class="submenu">
<li class="menu-item" th:each="child : ${item.children}">
<a th:href="${child.url}" th:text="${child.label}">Child Item</a>
</li>
</ul>
</div>
</li>
</ul>
</nav>
@@ -2242,11 +2242,17 @@
</a>
</div>
<h3 class="section-title">
BÀI VIẾT NỔI BẬT
</h3>
</div>
<div class="featured-grid">
<div class="news-events-row">
<div class="news-column">
<h3 class="section-title--news">
TIN TỨC
</h3>
<div class="featured-grid">
<div class="featured-grid__item">
<article class="news-card news-card--featured">
@@ -2405,108 +2411,8 @@
</article>
</div>
</div>
<div class="news-events-row">
<div class="news-column">
<h3 class="section-title--news">
TIN TỨC MỚI NHẤT
</h3>
<div class="news-list">
<div class="news-list__item">
<article class="news-teaser">
<div class="news-teaser__image">
<div> <img loading="lazy"
src="https://sisvietnam.vn/wp-content/uploads/2026/06/S.I.S-Can-Tho-trao-tang-xe-dap-cho-thieu-nhi-1.jpg"
width="4500" height="3006"
alt="Pyrotechnics erupt over the Football Performance Center at the conclusion of the 2026 UMass Amherst Undergraduate Commencement Ceremony">
</div>
</div>
<div class="news-teaser__content">
<div class="news-list__category">
<a href="https://sisvietnam.vn/taxonomy/term/1001" hreflang="vi">Tin Tức Bệnh
Viện</a>
</div>
<h3 class="news-teaser__title">
<a href="https://sisvietnam.vn/news/article/tuan-le-nang-cao-nhan-thuc"
rel="bookmark">
<span>Bệnh Viện S.I.S Cần Thơ Tổ Chức Thành Công Tuần Lễ Nâng Cao Nhận Thức
Về Đột Quỵ</span>
</a>
</h3>
<div class="news-teaser__summary">
<div>
<p>Ban Giám đốc Bệnh viện cùng đội ngũ chuyên gia đã tổ chức chuỗi sự kiện
truyền thông, tư vấn sức khỏe miễn phí thu hút hàng nghìn người dân tham
gia tìm hiểu và phòng ngừa bệnh.</p>
</div>
</div>
</div>
</article>
</div>
<div class="news-list__item">
<article class="news-teaser">
<div class="news-teaser__image">
<div> <img loading="lazy"
src="https://sisvietnam.vn/wp-content/uploads/2026/04/Mo-hinh-S.I.S-gop-phan-hoan-thien-mang-luoi-dot-quy-khu-vuc-huong-den-APEC-2027-08.jpg"
width="5392" height="3592"
alt="Student speaker Mansi Maheshwari addresses the 2026 Master’s and Education Specialist Commencement Ceremony at the Mullins Center on May 15, 2026.">
</div>
</div>
<div class="news-teaser__content">
<div class="news-list__category">
<a href="https://sisvietnam.vn/taxonomy/term/1001" hreflang="vi">Tin Tức Bệnh
Viện</a>
</div>
<h3 class="news-teaser__title">
<a href="https://sisvietnam.vn/news/article/tiep-nhan-thiet-bi" rel="bookmark">
<span>Bệnh Viện Đa Khoa Quốc Tế S.I.S Cần Thơ Tiếp Nhận Thêm Dàn Thiết Bị Y
Tế Hiện Đại Bậc Nhất</span>
</a>
</h3>
<div class="news-teaser__summary">
<div>
<p>Nhằm nâng cao chất lượng chẩn đoán và điều trị, S.I.S Cần Thơ đã chính
thức đưa vào hoạt động hệ thống máy DSA và MRI thế hệ mới, hỗ trợ tối đa
cho các ca can thiệp thần kinh phức tạp.</p>
</div>
</div>
</div>
</article>
</div>
</div>
</div>
<div class="events-column">
@@ -275,6 +275,13 @@
<input type="datetime-local" class="form-control" id="postEventTime" th:field="*{eventTimeLocal}">
<small class="form-text text-muted">Specify the date and time for this event.</small>
</div>
<div class="form-group mt-3" id="eventLocationGroup" style="display: none;">
<label for="postLocation" class="font-weight-bold text-danger">
<i class="fas fa-map-marker-alt"></i> Location
</label>
<input type="text" class="form-control" id="postLocation" th:field="*{location}" placeholder="e.g. Room 101">
<small class="form-text text-muted">Specify the location for this event.</small>
</div>
<hr>
<div class="d-flex justify-content-between">
<a th:href="@{/manage/posts}" class="btn btn-secondary btn-sm">
@@ -481,11 +488,14 @@
// --- Event Time Layout Toggle ---
var layoutSelect = document.getElementById('postLayout');
var eventTimeGroup = document.getElementById('eventTimeGroup');
var eventLocationGroup = document.getElementById('eventLocationGroup');
function toggleEventTime() {
if (layoutSelect.value === 'EVENT') {
eventTimeGroup.style.display = 'block';
if (eventLocationGroup) eventLocationGroup.style.display = 'block';
} else {
eventTimeGroup.style.display = 'none';
if (eventLocationGroup) eventLocationGroup.style.display = 'none';
}
}
layoutSelect.addEventListener('change', toggleEventTime);
@@ -0,0 +1,150 @@
<!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}">
<style>
.livestream-video-wrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 Aspect Ratio */
height: 0;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
margin-bottom: 2rem;
background: #000;
}
.livestream-video-wrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
.livestream-meta {
margin-bottom: 2rem;
border-bottom: 1px solid #eee;
padding-bottom: 1rem;
}
.livestream-title {
font-size: 2.25rem;
font-weight: 800;
color: #111;
margin-bottom: 0.5rem;
line-height: 1.25;
}
.livestream-date {
color: #881C1C;
font-weight: 700;
font-size: 0.95rem;
text-transform: uppercase;
}
</style>
</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 (Video Embed or Placeholder) -->
<div class="content-top">
<div class="lc--layout-container lc--one-column">
<div class="l--layout l--one-column">
<div class="lr--layout-region lr--main">
<div class="cc--component-container">
<!-- YouTube Embed -->
<div th:if="${post.metaDescription != null and !post.metaDescription.isEmpty()}" class="livestream-video-wrapper">
<iframe th:src="${post.metaDescription}"
title="Livestream Video"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen></iframe>
</div>
<!-- Fallback Hero Image if no video -->
<div th:if="${post.metaDescription == null or post.metaDescription.isEmpty()}" 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>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Content Main (Description and Info) -->
<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">
<div class="livestream-meta">
<!-- If the excerpt is used for the display title, show it. Otherwise show standard post title -->
<h1 class="livestream-title" th:text="${post.excerpt != null and !post.excerpt.isEmpty() ? post.excerpt : post.title}">Post Title</h1>
<p class="livestream-date" th:if="${formattedDate != null}" th:text="${formattedDate}">Date</p>
</div>
<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>
<!-- Include Editor.js parser to render the content on the frontend -->
<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 "paragraph":
html += "<p>" + block.data.text + "</p>";
break;
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
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 "image":
html += "<figure><img src='" + block.data.file.url + "' alt='" + (block.data.caption || "") + "'><figcaption>" + (block.data.caption || "") + "</figcaption></figure>";
break;
default:
break;
}
});
parsedContainer.innerHTML = html;
} else {
parsedContainer.innerHTML = rawData.replace(/\n/g, "<br>");
}
} catch(e) {
// Fallback to raw text if it is not JSON
parsedContainer.innerHTML = rawData.replace(/\n/g, "<br>");
}
});
</script>
</div>
</body>
</html>
@@ -242,8 +242,15 @@
</svg> Back </button>
<span class="navigation-title" th:text="${item.label}">Menu Item</span>
<!-- Mobile Submenu Layout (Simple List) -->
<ul class="submenu mobile-only">
<li class="menu-item" th:each="child : ${item.children}">
<a th:href="${child.url}" th:text="${child.label}">Child Item</a>
</li>
</ul>
<!-- Mega Menu Content Layout -->
<div class="mega-menu-content">
<div class="mega-menu-content desktop-only">
<!-- Column 1: Info (Title and Text) -->
<div class="mega-col-info">
<h3 th:text="${item.label}">Về Bệnh viện</h3>
@@ -275,115 +282,6 @@
</th:block>
</ul>
</nav>
<div data-component-id="umass_base:nav-quicklink-buttons">
<nav class="mc--menu mc--quicklink-button-menu">
<ul class="menu m--menu m--quicklink-button-menu">
<li class="menu-item">
<div class="f--field f--button">
<a href="https://www.umass.edu/admissions/apply" class="button-primary button-context-light " aria-label="Apply" data-component-id="umass_base:button">
<span class="button-text"> Apply </span>
</a>
</div>
</li>
<li class="menu-item">
<div class="f--field f--button">
<a href="https://minutefund.uma-foundation.org/project/29554/donate" class="button-primary button-context-light ext" aria-label="Give" data-component-id="umass_base:button" data-extlink="">
<span class="button-text"> Give </span>
<svg focusable="false" width="1em" height="1em" class="ext" data-extlink-placement="append" aria-label="(link is external)" viewBox="0 0 80 40" role="img" aria-hidden="false">
<title>(link is external)</title>
<path d="M48 26c-1.1 0-2 0.9-2 2v26H10V18h26c1.1 0 2-0.9 2-2s-0.9-2-2-2H8c-1.1 0-2 0.9-2 2v40c0 1.1 0.9 2 2 2h40c1.1 0 2-0.9 2-2V28C50 26.9 49.1 26 48 26z"></path>
<path d="M56 6H44c-1.1 0-2 0.9-2 2s0.9 2 2 2h7.2L30.6 30.6c-0.8 0.8-0.8 2 0 2.8C31 33.8 31.5 34 32 34s1-0.2 1.4-0.6L54 12.8V20c0 1.1 0.9 2 2 2s2-0.9 2-2V8C58 6.9 57.1 6 56 6z"></path>
</svg>
</a>
</div>
</li>
<li class="menu-item">
<div class="f--field f--button">
<a href="https://www.umass.edu/admissions/visit" class="button-primary button-context-light " aria-label="Visit" data-component-id="umass_base:button">
<span class="button-text"> Visit </span>
</a>
</div>
</li>
</ul>
</nav>
</div>
<nav class="mc--menu mc--utility-menu">
<ul class="menu m--menu m--utility-menu">
<li class="menu-item menu-item--expanded">
<div class="utility-button-wrapper">
<button type="button" class="utility-button arrow-toggle information-for-toggle" aria-label="Display submenu for Information menu" aria-expanded="false" aria-haspopup="true" data-once="site-header-arrow"> Info For <svg version="1.1" class="arrow-down" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 10 5" enable-background="new 0 0 10 5" xml:space="preserve" width="10" height="5">
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#881c1c" points="5,5 0,0 10,0 "></polygon>
</svg>
<svg version="1.1" class="arrow-right" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 5 10" enable-background="new 0 0 5 10" xml:space="preserve" width="5" height="10">
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#881c1c" points="5,5 0,10 0,0 "></polygon>
</svg>
</button>
<div class="submenu-wrapper">
<button class="button-back">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 5 10" enable-background="new 0 0 5 10" xml:space="preserve" width="5" height="10">
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#881c1c" points="0,5 5,0 5,10 "></polygon>
</svg> Back </button>
<span class="navigation-title">Info For</span>
<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" data-drupal-link-system-path="node/81">Parents and Families</a>
</li>
</ul>
</div>
</div>
</li>
<li class="menu-item">
<a href="https://www.umassalumni.com/" title="UMass Amherst Alumni Association">Alumni</a>
</li>
<li class="menu-item">
<a href="https://umassathletics.com/" title="UMass Amherst Athletics">Athletics</a>
</li>
<li class="menu-item">
<a href="https://www.uma-foundation.org/" title="Giving to UMass Amherst">Giving</a>
</li>
<li class="menu-item">
<a href="https://www.library.umass.edu/">Libraries</a>
</li>
</ul>
</nav>
<nav data-component-id="umass_base:header-socials">
<ul class="social-media-links--platforms platforms ally-focus-within">
<li>
<a class="ext" href="https://www.facebook.com/UMassAmherst/" target="_blank" rel="noopener noreferrer">
<svg aria-hidden="true" focusable="false" viewBox="0 0 17 17">
<title>Find UMass Amherst on Facebook</title>
<path fill-rule="evenodd" fill="currentColor" d="M17,8.5C17,3.81,13.19,0,8.5,0S0,3.81,0,8.5c0,3.98,2.75,7.33,6.45,8.25v-5.65h-1.75v-2.6h1.75v-1.12c0-2.89,1.31-4.23,4.15-4.23.54,0,1.47.11,1.85.21v2.35c-.2-.02-.55-.03-.98-.03-1.39,0-1.93.53-1.93,1.9v.92h2.78l-.48,2.6h-2.3v5.84c4.21-.51,7.47-4.09,7.47-8.44Z"></path>
</svg>
</a>
</li>
<li>
<a class="ext" href="https://www.instagram.com/umass/" target="_blank" rel="noopener noreferrer">
<!--?xml version="1.0" encoding="UTF-8"?-->
<svg aria-hidden="true" focusable="false" viewBox="0 0 17 20">
<title>Find UMass Amherst on Instagram</title>
<path fill-rule="evenodd" fill="currentColor" <path="" d="M8.51,5.51c-.57,0-1.14.11-1.67.34-.53.22-1.01.55-1.42.97-.82.84-1.28,1.98-1.29,3.17,0,1.19.45,2.34,1.27,3.18.82.84,1.93,1.32,3.08,1.32,1.16,0,2.27-.47,3.09-1.31.82-.84,1.28-1.98,1.29-3.17,0-1.19-.45-2.34-1.27-3.18-.82-.84-1.93-1.32-3.08-1.32ZM8.49,7.09c.37,0,.74.07,1.08.22.34.15.66.36.92.63.26.27.47.59.62.94.14.35.22.73.22,1.11,0,.38-.07.76-.21,1.12-.14.35-.35.68-.61.95-.26.27-.57.49-.92.64-.34.15-.71.22-1.08.23-.37,0-.74-.07-1.08-.22-.34-.15-.66-.36-.92-.63-.26-.27-.47-.59-.62-.94-.14-.35-.22-.73-.22-1.11,0-.38.07-.76.21-1.12.14-.35.35-.68.61-.95.26-.27.57-.49.92-.64.34-.15.71-.22,1.08-.23ZM12.03,5.32c0-.28.11-.54.3-.74.19-.2.45-.31.72-.31s.53.11.72.31c.19.2.3.46.3.74s-.11.54-.3.74c-.19.2-.45.31-.72.31s-.53-.11-.72-.31c-.19-.2-.3-.46-.3-.74ZM16.95,6.39c-.06-1.4-.38-2.64-1.37-3.67-.99-1.02-2.2-1.34-3.56-1.41-1.4-.08-5.61-.08-7.02,0-1.36.07-2.57.39-3.56,1.41C.44,3.74.13,4.98.06,6.38-.02,7.83-.02,12.16.06,13.61c.06,1.4.38,2.64,1.37,3.67,1,1.02,2.2,1.34,3.56,1.41,1.4.08,5.61.08,7.02,0,1.36-.07,2.57-.39,3.56-1.41.99-1.02,1.31-2.27,1.37-3.67.08-1.45.08-5.77,0-7.22ZM15.14,15.16c-.3.77-.87,1.36-1.62,1.66-1.12.46-3.78.35-5.01.35s-3.9.1-5.01-.35c-.74-.3-1.32-.89-1.62-1.66-.44-1.15-.34-3.89-.34-5.16s-.1-4.01.34-5.16c.3-.77.87-1.36,1.62-1.66,1.12-.46,3.78-.35,5.01-.35s3.9-.1,5.01.35c.74.3,1.32.89,1.62,1.66.44,1.15.34,3.89.34,5.16s.1,4.01-.34,5.16Z"></path>
</svg>
</a>
</li>
<li>
<a class="ext" href="https://www.snapchat.com/add/umassamherst" target="_blank" rel="noopener noreferrer">
<svg aria-hidden="true" focusable="false" viewBox="0 0 17 17">
<title>Add UMass Amherst on Snapchat</title>
<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="currentColor" fill-rule="evenodd" transform="translate(-4 -3)"></path>
</svg>
</a>
</li>
</ul>
</nav>
</div>
</div>
<button id="search-drawer-trigger" aria-label="Open Search" data-once="header-search">
@@ -9,6 +9,8 @@
<link rel="stylesheet" media="all" th:href="@{/theme-assets/umass/css/css_09S7WcUyhLeUXabknzECn_ua5zOangtv2BF4OXAW2fM.css}">
<link rel="stylesheet" media="all" th:href="@{/theme-assets/umass/css/css_5USufIKxGNJyomvgpy2m9ITQlTN9qcrtJQsw_06dSbo.css}">
<link rel="stylesheet" media="all" th:href="@{/theme-assets/umass/css/css_G5ztZKRblBn54t8h9F_EK5Y3CsZrMvA2BWPMI5jJoO4.css}">
<!-- Custom CSS -->
<link rel="stylesheet" th:href="@{/css/custom.css}">
<!-- Theme Customizer CSS Output -->
<style th:utext="${themeCss}"></style>
<!-- wp_head hook -->
@@ -46,6 +48,8 @@
</div>
<!-- UMass Scripts -->
<script th:src="@{/theme-assets/umass/js/js_aQtUyeGxehNR84AlGzGB1VfMu1Wn3lqHxvL8rocj1EU.js}"></script>
<script th:src="@{/js/mobile-menu.js}"></script>
<script th:src="@{/js/ambient-video.js}"></script>
<!-- wp_footer hook -->
<th:block th:utext="${hookManager.doActionAndReturn('wp_footer')}"></th:block>
</body>