feat: add UMass Amherst theme support, implement HTML snippet management system, and update page rendering logic

This commit is contained in:
2026-07-06 23:50:55 +07:00
parent 3dc5a03484
commit e6d98edc45
364 changed files with 9967 additions and 36960 deletions
+26
View File
@@ -0,0 +1,26 @@
(function (el) {
if (!el) {
console.error("Vui lòng chọn thẻ HTML bên tab Elements trước!");
return;
}
const clone = el.cloneNode(true);
function applyStyles(original, cloned) {
const computed = window.getComputedStyle(original);
let styleString = "";
for (let i = 0; i < computed.length; i++) {
const prop = computed[i];
const val = computed.getPropertyValue(prop);
if (val && val !== "none" && val !== "normal" && val !== "auto") {
styleString += `${prop}: ${val}; `;
}
}
cloned.setAttribute("style", styleString);
// Đệ quy quét qua tất cả các con, cháu bên trong để áp dụng CSS
for (let i = 0; i < original.children.length; i++) {
applyStyles(original.children[i], cloned.children[i]);
}
}
applyStyles(el, clone);
copy(clone.outerHTML);
console.log("👉 Đã copy thành công cả THẺ CHA & CÁC THẺ CON kèm CSS!");
})($0);
+18
View File
@@ -0,0 +1,18 @@
import os
from bs4 import BeautifulSoup
file_path = "/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/UMass Amherst _ UMass Amherst.html"
with open(file_path, "r", encoding="utf-8") as f:
soup = BeautifulSoup(f, "html.parser")
content_top = soup.find(class_="content-top")
if content_top:
# Let's save it to an artifact directly via python, or just print it and I will grab it.
output_path = "/home/x79/sisvietnamvn_01/sisvietnamvn_main/content_top_snippet.html"
with open(output_path, "w", encoding="utf-8") as f:
f.write(content_top.prettify())
print(f"Extracted content-top to {output_path}")
else:
print("Could not find class='content-top'")
@@ -0,0 +1,36 @@
import sys
file_path = "/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/UMass Amherst _ UMass Amherst.html"
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
start_idx = -1
for i, line in enumerate(lines):
if '<div class="content-top">' in line:
start_idx = i
break
if start_idx == -1:
print("Could not find <div class=\"content-top\">")
sys.exit(1)
extracted = []
div_count = 0
found_start = False
for line in lines[start_idx:]:
# Simple tag counting to find the matching closing div
# Note: this is a naive counter, it doesn't account for HTML comments or script tags containing strings that look like tags,
# but it usually works well for standard HTML layout sections.
div_count += line.count("<div")
div_count -= line.count("</div")
extracted.append(line)
if div_count == 0:
break
output_path = "src/main/resources/templates/themes/umass/content-top.html"
with open(output_path, "w", encoding="utf-8") as f:
f.writelines(extracted)
print(f"Extracted to {output_path}")
@@ -12,6 +12,7 @@ springBoot {
bootRun {
args = ["--spring.profiles.active=${springProfiles}"]
sourceResources sourceSets.main
}
+16
View File
@@ -0,0 +1,16 @@
import re
filepath = "src/main/resources/templates/themes/umass/footer.html"
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# We want to replace the `a.logo` block with our new logo
# Using DOTALL to match across newlines
pattern = r'<a class="logo"[^>]*>.*?</a>'
replacement = '<a class="logo" href="/" aria-label="Sisvietnamvn">\n <div>\n <img th:src="@{/theme-assets/umass/images/logo.png}" alt="Sisvietnamvn" style="height: 60px; width: auto;"/>\n </div>\n</a>'
content = re.sub(pattern, replacement, content, count=1, flags=re.DOTALL)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
print("Updated footer.html")
+23
View File
@@ -0,0 +1,23 @@
import re
filepath = "src/main/resources/templates/themes/umass/header.html"
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# Tophat logo
content = re.sub(
r'<div class="tophat-logo">\s*<a href="[^"]*">\s*<img[^>]*>\s*</a>\s*</div>',
'<div class="tophat-logo">\n <a href="/">\n <img th:src="@{/theme-assets/umass/images/logo.png}" alt="Sisvietnamvn" style="height: 35px; width: auto;"/>\n </a>\n </div>',
content
)
# Header branding logos
content = re.sub(
r'<div data-component-id="umass_base:header-branding">\s*<a href="[^"]*">\s*<img[^>]*>\s*<img[^>]*>\s*</a>\s*</div>',
'<div data-component-id="umass_base:header-branding">\n <a href="/">\n <img th:src="@{/theme-assets/umass/images/logo.png}" alt="Sisvietnamvn" style="height: 60px; width: auto;"/>\n </a>\n</div>',
content
)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
print("Updated header.html")
@@ -57,7 +57,7 @@ public class SecurityConfiguration {
.requestMatchers(HttpMethod.GET, "/", "/about", "/flex-finish", "/tin-tuc", "/tin-tuc/**", "/lien-he",
"/manage/login", "/css/**", "/images/**", "/js/**", "/vendor/**", "/fonts/**", "/login-assets/**", "/UMass*/**", "/Undergraduate*/**",
"/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**", "/uploads/**", "/api/manage/snippets/**", "/page/**", "/news/article/**", "/post/**", "/error",
"/about-us", "/specialty", "/doctor", "/service", "/health-library", "/news-and-events", "/patient-support", "/medical-expert", "/umcers", "/bidding", "/contact-us")
"/about-us", "/specialty", "/doctor", "/service", "/health-library", "/news-and-events", "/patient-support", "/medical-expert", "/umcers", "/bidding", "/contact-us", "/theme-assets/**")
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/manage/media/upload").permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui/**", "/v3/api-docs/**").permitAll()
@@ -17,12 +17,18 @@ public class GlobalControllerAdvice {
private final SettingService settingService;
private final com.sisvietnamvn.web.service.MenuService menuService;
private final com.sisvietnamvn.web.hook.HookManager hookManager;
private final com.sisvietnamvn.web.security.AdminContext adminContext;
private final com.sisvietnamvn.web.security.AdminMenuManager adminMenuManager;
private final com.sisvietnamvn.web.security.AdminSettingsManager adminSettingsManager;
private final PathMatchingResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
public GlobalControllerAdvice(SettingService settingService, com.sisvietnamvn.web.service.MenuService menuService, com.sisvietnamvn.web.hook.HookManager hookManager) {
public GlobalControllerAdvice(SettingService settingService, com.sisvietnamvn.web.service.MenuService menuService, com.sisvietnamvn.web.hook.HookManager hookManager, com.sisvietnamvn.web.security.AdminContext adminContext, com.sisvietnamvn.web.security.AdminMenuManager adminMenuManager, com.sisvietnamvn.web.security.AdminSettingsManager adminSettingsManager) {
this.settingService = settingService;
this.menuService = menuService;
this.hookManager = hookManager;
this.adminContext = adminContext;
this.adminMenuManager = adminMenuManager;
this.adminSettingsManager = adminSettingsManager;
}
/**
@@ -71,6 +77,26 @@ public class GlobalControllerAdvice {
return hookManager;
}
@ModelAttribute("adminCtx")
public com.sisvietnamvn.web.security.AdminContext getAdminContext() {
return adminContext;
}
@ModelAttribute("adminScreen")
public com.sisvietnamvn.web.security.AdminScreen getAdminScreen() {
return adminContext.getCurrentScreen();
}
@ModelAttribute("dynamicAdminMenus")
public java.util.List<com.sisvietnamvn.web.security.AdminMenuItem> getDynamicAdminMenus() {
return adminMenuManager.getAuthorizedMenus();
}
@ModelAttribute("settingsManager")
public com.sisvietnamvn.web.security.AdminSettingsManager getSettingsManager() {
return adminSettingsManager;
}
@ModelAttribute("themeModPrimaryColor")
public String getThemeModPrimaryColor() {
return settingService.getValue("theme_mod_primaryColor", "#007bff");
@@ -33,11 +33,13 @@ public class PageController {
private final PageService pageService;
private final ObjectMapper objectMapper;
private final HookManager hookManager;
private final com.sisvietnamvn.web.service.HtmlSnippetService snippetService;
public PageController(PageService pageService, ObjectMapper objectMapper, HookManager hookManager) {
public PageController(PageService pageService, ObjectMapper objectMapper, HookManager hookManager, com.sisvietnamvn.web.service.HtmlSnippetService snippetService) {
this.pageService = pageService;
this.objectMapper = objectMapper;
this.hookManager = hookManager;
this.snippetService = snippetService;
}
@GetMapping("/page/{slug}")
@@ -109,6 +111,15 @@ public class PageController {
Map<String, Object> editorData = objectMapper.readValue(page.getContent(), new TypeReference<>() {});
if (editorData.containsKey("blocks")) {
blocks = (List<Map<String, Object>>) editorData.get("blocks");
for (Map<String, Object> block : blocks) {
if ("snippet".equals(block.get("type"))) {
Map<String, Object> data = (Map<String, Object>) block.get("data");
if (data != null && data.containsKey("id")) {
String snippetId = (String) data.get("id");
data.put("htmlContent", snippetService.getSnippetContent(snippetId));
}
}
}
}
} catch (JsonProcessingException e) {
LOG.error("Failed to parse Editor.js JSON for page ID: {}", page.getId(), e);
@@ -0,0 +1,66 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.HtmlSnippet;
import com.sisvietnamvn.web.service.HtmlSnippetService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/manage/snippets")
public class ManageSnippetController {
private final HtmlSnippetService snippetService;
public ManageSnippetController(HtmlSnippetService snippetService) {
this.snippetService = snippetService;
}
@GetMapping
public String listSnippets(Model model) {
model.addAttribute("snippets", snippetService.findAll());
return "manage/snippets/list";
}
@GetMapping("/new")
public String newSnippetForm(Model model) {
model.addAttribute("snippet", new HtmlSnippet());
model.addAttribute("isNew", true);
return "manage/snippets/form";
}
@PostMapping("/create")
public String createSnippet(@ModelAttribute HtmlSnippet snippet, RedirectAttributes redirectAttributes) {
snippetService.save(snippet);
redirectAttributes.addFlashAttribute("successMessage", "Snippet created successfully.");
return "redirect:/manage/snippets";
}
@GetMapping("/{id}/edit")
public String editSnippetForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
return snippetService.findById(id).map(snippet -> {
model.addAttribute("snippet", snippet);
model.addAttribute("isNew", false);
return "manage/snippets/form";
}).orElseGet(() -> {
redirectAttributes.addFlashAttribute("errorMessage", "Snippet not found.");
return "redirect:/manage/snippets";
});
}
@PostMapping("/{id}")
public String updateSnippet(@PathVariable Long id, @ModelAttribute HtmlSnippet snippet, RedirectAttributes redirectAttributes) {
snippet.setId(id);
snippetService.save(snippet);
redirectAttributes.addFlashAttribute("successMessage", "Snippet updated successfully.");
return "redirect:/manage/snippets";
}
@PostMapping("/{id}/delete")
public String deleteSnippet(@PathVariable Long id, RedirectAttributes redirectAttributes) {
snippetService.deleteById(id);
redirectAttributes.addFlashAttribute("successMessage", "Snippet deleted successfully.");
return "redirect:/manage/snippets";
}
}
@@ -106,9 +106,17 @@ public class ManageThemeController {
try {
java.nio.file.Path themePath = java.nio.file.Paths.get("src/main/resources/templates/themes/", themeKey);
java.nio.file.Path buildThemePath = java.nio.file.Paths.get("build/resources/main/templates/themes/", themeKey);
if (java.nio.file.Files.exists(themePath)) {
// Delete directory recursively
org.springframework.util.FileSystemUtils.deleteRecursively(themePath);
// Also delete from Gradle build cache if it exists
if (java.nio.file.Files.exists(buildThemePath)) {
org.springframework.util.FileSystemUtils.deleteRecursively(buildThemePath);
}
redirectAttributes.addFlashAttribute("successMessage", "Theme deleted successfully.");
} else {
redirectAttributes.addFlashAttribute("errorMessage", "Theme folder not found.");
@@ -128,6 +136,7 @@ public class ManageThemeController {
try {
java.nio.file.Path targetDir = java.nio.file.Paths.get("src/main/resources/templates/themes/");
java.nio.file.Path buildDir = java.nio.file.Paths.get("build/resources/main/templates/themes/");
java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(file.getInputStream());
java.util.zip.ZipEntry zipEntry = zis.getNextEntry();
@@ -142,6 +151,15 @@ public class ManageThemeController {
}
}
java.nio.file.Files.copy(zis, newPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
// Also copy to Gradle build cache if it exists, so changes appear without restarting server
if (java.nio.file.Files.exists(java.nio.file.Paths.get("build/resources/main"))) {
java.nio.file.Path buildNewPath = zipSlipProtect(zipEntry, buildDir);
if (buildNewPath.getParent() != null && java.nio.file.Files.notExists(buildNewPath.getParent())) {
java.nio.file.Files.createDirectories(buildNewPath.getParent());
}
java.nio.file.Files.copy(newPath, buildNewPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
}
zipEntry = zis.getNextEntry();
}
@@ -2,16 +2,13 @@ package com.sisvietnamvn.web.controller.manage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.HtmlSnippetService;
/**
* REST controller for fetching predefined HTML snippets to be previewed in Editor.js.
@@ -22,10 +19,10 @@ import com.sisvietnamvn.web.security.AuthoritiesConstants;
public class SnippetController {
private static final Logger LOG = LoggerFactory.getLogger(SnippetController.class);
private final ResourceLoader resourceLoader;
private final HtmlSnippetService snippetService;
public SnippetController(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
public SnippetController(HtmlSnippetService snippetService) {
this.snippetService = snippetService;
}
/**
@@ -35,25 +32,13 @@ public class SnippetController {
public ResponseEntity<String> getSnippet(@PathVariable("id") String id) {
LOG.debug("REST request to get Snippet : {}", id);
// Prevent path traversal attacks
if (id == null || id.contains("..") || id.contains("/")) {
return ResponseEntity.badRequest().body("Invalid Snippet ID");
String content = snippetService.getSnippetContent(id);
if (content == null || content.isEmpty()) {
LOG.warn("Snippet not found or is inactive: {}", id);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("<div style=\"padding: 20px; border: 1px dashed red; color: red;\">Snippet ID <strong>" + id + "</strong> not found or inactive.</div>");
}
String path = "classpath:templates/snippets/" + id + ".html";
Resource resource = resourceLoader.getResource(path);
if (!resource.exists() || !resource.isReadable()) {
LOG.warn("Snippet not found at path: {}", path);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("<div style=\"padding: 20px; border: 1px dashed red; color: red;\">Snippet ID <strong>" + id + "</strong> not found.</div>");
}
try {
String content = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
return ResponseEntity.ok().body(content);
} catch (IOException e) {
LOG.error("Error reading snippet file: {}", path, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error reading snippet file");
}
return ResponseEntity.ok().body(content);
}
}
@@ -0,0 +1,72 @@
package com.sisvietnamvn.web.domain;
import jakarta.persistence.*;
/**
* Entity representing a reusable HTML Snippet.
*/
@Entity
@Table(name = "html_snippet")
public class HtmlSnippet extends AbstractAuditingEntity<Long> {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String slug;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String content;
private boolean active = true;
// Getters and Setters
@Override
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
@@ -0,0 +1,16 @@
package com.sisvietnamvn.web.repository;
import com.sisvietnamvn.web.domain.HtmlSnippet;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* Spring Data JPA repository for the HtmlSnippet entity.
*/
@Repository
public interface HtmlSnippetRepository extends JpaRepository<HtmlSnippet, Long> {
Optional<HtmlSnippet> findBySlug(String slug);
}
@@ -0,0 +1,141 @@
package com.sisvietnamvn.web.security;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility for managing the Admin context, providing equivalents to WordPress's
* is_admin(), current_user_can(), and get_current_screen().
*/
@Service
public class AdminContext {
private static final String MANAGE_PREFIX = "/manage";
// Regex for parsing /manage/{base}/{id}/{section}
private static final Pattern SCREEN_PATTERN = Pattern.compile("^/manage/([^/]+)(?:/(\\d+))?(?:/([^/]+))?/?$");
/**
* Map of WordPress capabilities to Spring Boot roles/authorities.
*/
private static final Map<String, String> CAPABILITY_TO_ROLE = Map.ofEntries(
Map.entry("manage_options", AuthoritiesConstants.ADMIN),
Map.entry("edit_themes", AuthoritiesConstants.ADMIN),
Map.entry("install_plugins", AuthoritiesConstants.ADMIN),
Map.entry("edit_users", AuthoritiesConstants.ADMIN),
Map.entry("edit_others_posts", AuthoritiesConstants.EDITOR),
Map.entry("manage_categories", AuthoritiesConstants.EDITOR),
Map.entry("moderate_comments", AuthoritiesConstants.EDITOR),
Map.entry("edit_pages", AuthoritiesConstants.EDITOR),
Map.entry("delete_posts", AuthoritiesConstants.EDITOR),
Map.entry("publish_posts", AuthoritiesConstants.AUTHOR),
Map.entry("upload_files", AuthoritiesConstants.AUTHOR),
Map.entry("edit_posts", AuthoritiesConstants.CONTRIBUTOR),
Map.entry("read", AuthoritiesConstants.SUBSCRIBER)
);
/**
* Equivalents to WordPress's is_admin().
* Checks if the request is for an admin page (/manage).
*/
public static boolean isAdmin(HttpServletRequest request) {
if (request == null) {
return false;
}
String requestUri = request.getRequestURI();
return requestUri != null && requestUri.startsWith(MANAGE_PREFIX);
}
/**
* Instance method for Thymeleaf usage without passing the request.
*/
public boolean isAdminRequest() {
HttpServletRequest request = getCurrentHttpRequest();
return isAdmin(request);
}
/**
* Equivalents to WordPress's current_user_can().
* Checks if the current user has the specified capability or role.
*/
public boolean currentUserCan(String capability) {
if (capability == null) {
return false;
}
// If capability maps to a known Spring Boot role, check that role
if (CAPABILITY_TO_ROLE.containsKey(capability)) {
return SecurityUtils.hasCurrentUserThisAuthority(CAPABILITY_TO_ROLE.get(capability));
}
// Otherwise, assume the capability string itself is the authority (e.g., "ROLE_ADMIN")
return SecurityUtils.hasCurrentUserThisAuthority(capability);
}
/**
* Equivalents to WordPress's get_current_screen().
* Parses the current request URI to determine the admin screen context.
*/
public AdminScreen getCurrentScreen(HttpServletRequest request) {
if (!isAdmin(request)) {
return null; // Not an admin screen
}
String requestUri = request.getRequestURI();
// Special case for root dashboard
if (MANAGE_PREFIX.equals(requestUri) || (MANAGE_PREFIX + "/").equals(requestUri)) {
return new AdminScreen("manage-dashboard", "dashboard", "index", null);
}
Matcher matcher = SCREEN_PATTERN.matcher(requestUri);
if (matcher.find()) {
String base = matcher.group(1);
String idStr = matcher.group(2);
String sectionStr = matcher.group(3);
String id = "manage-" + base;
Long entityId = null;
if (idStr != null) {
try {
entityId = Long.parseLong(idStr);
} catch (NumberFormatException ignored) {}
}
String section = "list"; // Default for /manage/posts
if (sectionStr != null) {
section = sectionStr; // e.g., "edit" or "new"
} else if (idStr == null && requestUri.endsWith("/new")) {
// Handling /manage/posts/new which might not perfectly match the numeric id pattern
section = "new";
}
return new AdminScreen(id, base, section, entityId);
}
// Fallback for unknown /manage/xyz paths
return new AdminScreen("manage-unknown", "unknown", "index", null);
}
/**
* Instance method for Thymeleaf usage without passing the request.
*/
public AdminScreen getCurrentScreen() {
HttpServletRequest request = getCurrentHttpRequest();
return getCurrentScreen(request);
}
private HttpServletRequest getCurrentHttpRequest() {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return (attrs != null) ? attrs.getRequest() : null;
}
}
@@ -0,0 +1,94 @@
package com.sisvietnamvn.web.security;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a menu item in the WordPress-style admin sidebar.
*/
public class AdminMenuItem implements Comparable<AdminMenuItem> {
private String menuTitle;
private String capability;
private String menuSlug;
private String iconUrl;
private int position;
private String url;
private List<AdminMenuItem> submenus;
public AdminMenuItem(String menuTitle, String capability, String menuSlug, String iconUrl, int position, String url) {
this.menuTitle = menuTitle;
this.capability = capability;
this.menuSlug = menuSlug;
this.iconUrl = iconUrl;
this.position = position;
this.url = url;
this.submenus = new ArrayList<>();
}
public void addSubmenu(AdminMenuItem submenu) {
this.submenus.add(submenu);
this.submenus.sort(null); // Sort by position
}
public String getMenuTitle() {
return menuTitle;
}
public void setMenuTitle(String menuTitle) {
this.menuTitle = menuTitle;
}
public String getCapability() {
return capability;
}
public void setCapability(String capability) {
this.capability = capability;
}
public String getMenuSlug() {
return menuSlug;
}
public void setMenuSlug(String menuSlug) {
this.menuSlug = menuSlug;
}
public String getIconUrl() {
return iconUrl;
}
public void setIconUrl(String iconUrl) {
this.iconUrl = iconUrl;
}
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public List<AdminMenuItem> getSubmenus() {
return submenus;
}
public void setSubmenus(List<AdminMenuItem> submenus) {
this.submenus = submenus;
}
@Override
public int compareTo(AdminMenuItem o) {
return Integer.compare(this.position, o.position);
}
}
@@ -0,0 +1,135 @@
package com.sisvietnamvn.web.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Service for managing dynamic admin menus, equivalent to WordPress's
* add_menu_page(), add_submenu_page(), etc.
*/
@Service
public class AdminMenuManager {
private static final Logger LOG = LoggerFactory.getLogger(AdminMenuManager.class);
private final List<AdminMenuItem> menuRegistry = new ArrayList<>();
private final AdminContext adminContext;
public AdminMenuManager(AdminContext adminContext) {
this.adminContext = adminContext;
}
/**
* Equivalent to add_menu_page()
*/
public void addMenuPage(String pageTitle, String menuTitle, String capability, String menuSlug, String iconUrl, int position) {
// Find if exists to avoid duplicates
if (getMenuBySlug(menuSlug).isPresent()) {
LOG.warn("Menu slug '{}' is already registered.", menuSlug);
return;
}
String url = "/manage/" + menuSlug;
AdminMenuItem item = new AdminMenuItem(menuTitle, capability, menuSlug, iconUrl, position, url);
// In WordPress, the main menu also acts as the first submenu.
// We will add it as a submenu with the same URL, but empty title to skip rendering duplicate if needed,
// or just let the UI handle the top link. For SB Admin 2, top level is just a dropdown trigger.
// We will add the main link as the first submenu item.
item.addSubmenu(new AdminMenuItem(pageTitle, capability, menuSlug, iconUrl, 0, url));
menuRegistry.add(item);
Collections.sort(menuRegistry);
LOG.debug("Added admin menu: {}", menuSlug);
}
/**
* Equivalent to add_submenu_page()
*/
public void addSubmenuPage(String parentSlug, String pageTitle, String menuTitle, String capability, String menuSlug, int position) {
Optional<AdminMenuItem> parentOpt = getMenuBySlug(parentSlug);
if (parentOpt.isPresent()) {
String url = "/manage/" + menuSlug;
// Some special WP cases like manage-settings actually map to /manage/settings/...
if (menuSlug.contains("/")) {
url = "/manage/" + menuSlug;
} else if (parentSlug.startsWith("manage-")) {
String base = parentSlug.replace("manage-", "");
url = "/manage/" + base + "/" + menuSlug;
}
AdminMenuItem subItem = new AdminMenuItem(menuTitle, capability, menuSlug, "", position, url);
parentOpt.get().addSubmenu(subItem);
LOG.debug("Added admin submenu: {} to parent: {}", menuSlug, parentSlug);
} else {
LOG.warn("Cannot add submenu '{}'. Parent menu '{}' not found.", menuSlug, parentSlug);
}
}
public void addSubmenuPage(String parentSlug, String pageTitle, String menuTitle, String capability, String menuSlug) {
addSubmenuPage(parentSlug, pageTitle, menuTitle, capability, menuSlug, 10);
}
// --- Helper Functions ---
public void addOptionsPage(String pageTitle, String menuTitle, String capability, String menuSlug) {
addSubmenuPage("manage-settings", pageTitle, menuTitle, capability, "settings/" + menuSlug);
}
public void addThemePage(String pageTitle, String menuTitle, String capability, String menuSlug) {
addSubmenuPage("manage-themes", pageTitle, menuTitle, capability, menuSlug);
}
public void addPluginsPage(String pageTitle, String menuTitle, String capability, String menuSlug) {
addSubmenuPage("manage-plugins", pageTitle, menuTitle, capability, menuSlug);
}
public void addUsersPage(String pageTitle, String menuTitle, String capability, String menuSlug) {
addSubmenuPage("manage-users", pageTitle, menuTitle, capability, menuSlug);
}
public void addDashboardPage(String pageTitle, String menuTitle, String capability, String menuSlug) {
addSubmenuPage("manage-dashboard", pageTitle, menuTitle, capability, menuSlug);
}
public void addManagementPage(String pageTitle, String menuTitle, String capability, String menuSlug) {
addSubmenuPage("manage-tools", pageTitle, menuTitle, capability, "tools/" + menuSlug);
}
// --- Internal & Rendering Helpers ---
private Optional<AdminMenuItem> getMenuBySlug(String slug) {
return menuRegistry.stream().filter(m -> m.getMenuSlug().equals(slug)).findFirst();
}
/**
* Gets all menus that the current user has access to.
*/
public List<AdminMenuItem> getAuthorizedMenus() {
return menuRegistry.stream()
.filter(menu -> adminContext.currentUserCan(menu.getCapability()))
.map(this::filterAuthorizedSubmenus)
.collect(Collectors.toList());
}
private AdminMenuItem filterAuthorizedSubmenus(AdminMenuItem menu) {
AdminMenuItem filteredMenu = new AdminMenuItem(
menu.getMenuTitle(), menu.getCapability(), menu.getMenuSlug(),
menu.getIconUrl(), menu.getPosition(), menu.getUrl()
);
List<AdminMenuItem> authorizedSubmenus = menu.getSubmenus().stream()
.filter(sub -> adminContext.currentUserCan(sub.getCapability()))
.collect(Collectors.toList());
filteredMenu.setSubmenus(authorizedSubmenus);
return filteredMenu;
}
}
@@ -0,0 +1,50 @@
package com.sisvietnamvn.web.security;
import java.util.Objects;
/**
* Represents the current admin screen context parsed from the request URI.
* Equivalent to WordPress's WP_Screen object (returned by get_current_screen()).
*/
public record AdminScreen(
String id, // e.g., "manage-posts"
String base, // e.g., "posts", "dashboard", "media"
String section, // e.g., "list", "edit", "new", "index"
Long entityId // e.g., 42 (if editing entity with ID 42)
) {
/**
* Checks if the screen matches a specific ID.
*/
public boolean is(String screenId) {
return Objects.equals(this.id, screenId);
}
/**
* Checks if the screen belongs to a specific base function.
*/
public boolean isBase(String baseName) {
return Objects.equals(this.base, baseName);
}
/**
* Checks if this is an editing screen.
*/
public boolean isEditing() {
return "edit".equals(this.section);
}
/**
* Checks if this is a creation screen.
*/
public boolean isCreating() {
return "new".equals(this.section);
}
/**
* Checks if this is a listing screen.
*/
public boolean isList() {
return "list".equals(this.section);
}
}
@@ -0,0 +1,82 @@
package com.sisvietnamvn.web.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import com.sisvietnamvn.web.service.SettingService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Service for managing the Settings API, equivalent to WordPress's
* register_setting(), add_settings_section(), add_settings_field().
*/
@Service
public class AdminSettingsManager {
private static final Logger LOG = LoggerFactory.getLogger(AdminSettingsManager.class);
// Map: Page Slug -> List of Sections
private final Map<String, List<SettingsSection>> registry = new HashMap<>();
private final SettingService settingService;
public AdminSettingsManager(SettingService settingService) {
this.settingService = settingService;
}
/**
* Equivalent to add_settings_section()
*/
public void addSettingsSection(String page, String sectionId, String title, String description) {
registry.putIfAbsent(page, new ArrayList<>());
List<SettingsSection> sections = registry.get(page);
// Prevent duplicate sections
boolean exists = sections.stream().anyMatch(s -> s.getId().equals(sectionId));
if (!exists) {
sections.add(new SettingsSection(sectionId, title, description));
LOG.debug("Added settings section '{}' to page '{}'", sectionId, page);
}
}
/**
* Equivalent to add_settings_field()
*/
public void addSettingsField(String page, String sectionId, SettingsField field) {
List<SettingsSection> sections = registry.get(page);
if (sections != null) {
sections.stream()
.filter(s -> s.getId().equals(sectionId))
.findFirst()
.ifPresent(section -> {
section.addField(field);
LOG.debug("Added settings field '{}' to section '{}' on page '{}'", field.getId(), sectionId, page);
});
} else {
LOG.warn("Cannot add field '{}'. Page '{}' or Section '{}' not found.", field.getId(), page, sectionId);
}
}
/**
* Equivalent to register_setting()
* In Spring, settingService handles generic keys automatically, but this ensures a default value exists.
*/
public void registerSetting(String optionGroup, String optionName, String defaultValue) {
String existingValue = settingService.getValue(optionName, null);
if (existingValue == null && defaultValue != null) {
settingService.setValue(optionName, defaultValue);
LOG.debug("Registered setting '{}' with default value '{}'", optionName, defaultValue);
}
}
/**
* Helper for Thymeleaf to get all sections for a specific page.
*/
public List<SettingsSection> getSectionsForPage(String page) {
return registry.getOrDefault(page, new ArrayList<>());
}
}
@@ -0,0 +1,65 @@
package com.sisvietnamvn.web.security;
import com.sisvietnamvn.web.security.AdminMenuManager;
import com.sisvietnamvn.web.security.AdminSettingsManager;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Registers core WordPress-like settings using the dynamic Settings API on startup.
*/
@Component
public class CoreSettingsRegistrar {
private final AdminSettingsManager settingsManager;
private final AdminMenuManager menuManager;
public CoreSettingsRegistrar(AdminSettingsManager settingsManager, AdminMenuManager menuManager) {
this.settingsManager = settingsManager;
this.menuManager = menuManager;
}
@PostConstruct
public void initCoreSettings() {
// --- General Settings ---
String page = "general";
settingsManager.addSettingsSection(page, "default", "", "");
settingsManager.addSettingsField(page, "default",
new SettingsField("site_name", "Site Title", "text", null, ""));
settingsManager.addSettingsField(page, "default",
new SettingsField("tagline", "Tagline", "text", null, "In a few words, explain what this site is about."));
settingsManager.addSettingsField(page, "default",
new SettingsField("admin_email", "Administration Email Address", "email", null, "This address is used for admin purposes."));
Map<String, String> timezones = new LinkedHashMap<>();
timezones.put("UTC", "UTC");
timezones.put("Asia/Ho_Chi_Minh", "Asia/Ho Chi Minh");
timezones.put("America/New_York", "America/New York");
timezones.put("Europe/London", "Europe/London");
settingsManager.addSettingsField(page, "default",
new SettingsField("timezone", "Timezone", "select", timezones, ""));
Map<String, String> dateFormats = new LinkedHashMap<>();
dateFormats.put("F j, Y", "November 6, 2010 (F j, Y)");
dateFormats.put("Y-m-d", "2010-11-06 (Y-m-d)");
dateFormats.put("m/d/Y", "11/06/2010 (m/d/Y)");
dateFormats.put("d/m/Y", "06/11/2010 (d/m/Y)");
settingsManager.addSettingsField(page, "default",
new SettingsField("date_format", "Date Format", "radio", dateFormats, ""));
// Register default values for some
settingsManager.registerSetting(page, "site_name", "SIS Vietnam");
settingsManager.registerSetting(page, "timezone", "UTC");
settingsManager.registerSetting(page, "date_format", "F j, Y");
// --- Menu Registration for Modules ---
menuManager.addMenuPage("HTML Snippets", "Snippets", "manage_options", "snippets", "fas fa-code", 30);
}
}
@@ -79,7 +79,14 @@ public class DomainUserDetailsService implements UserDetailsService {
}
public static UserWithId fromUser(User user) {
List<GrantedAuthority> grantedAuthorities = Collections.singletonList(new SimpleGrantedAuthority(AuthoritiesConstants.PRE_AUTH_2FA));
List<GrantedAuthority> grantedAuthorities;
if (user.isUsing2FA()) {
grantedAuthorities = Collections.singletonList(new SimpleGrantedAuthority(AuthoritiesConstants.PRE_AUTH_2FA));
} else {
grantedAuthorities = user.getAuthorities().stream()
.map(authority -> (GrantedAuthority) new SimpleGrantedAuthority(authority.getName()))
.toList();
}
return new UserWithId(
user.getLogin(),
@@ -0,0 +1,62 @@
package com.sisvietnamvn.web.security;
import java.util.Map;
/**
* Represents a single setting field in the Admin Settings API.
*/
public class SettingsField {
private String id;
private String title;
private String type;
private Map<String, String> options;
private String description;
public SettingsField(String id, String title, String type, Map<String, String> options, String description) {
this.id = id;
this.title = title;
this.type = type;
this.options = options;
this.description = description;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Map<String, String> getOptions() {
return options;
}
public void setOptions(Map<String, String> options) {
this.options = options;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
@@ -0,0 +1,57 @@
package com.sisvietnamvn.web.security;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a section of settings fields in the Admin Settings API.
*/
public class SettingsSection {
private String id;
private String title;
private String description;
private List<SettingsField> fields;
public SettingsSection(String id, String title, String description) {
this.id = id;
this.title = title;
this.description = description;
this.fields = new ArrayList<>();
}
public void addField(SettingsField field) {
this.fields.add(field);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List<SettingsField> getFields() {
return fields;
}
public void setFields(List<SettingsField> fields) {
this.fields = fields;
}
}
@@ -0,0 +1,58 @@
package com.sisvietnamvn.web.service;
import com.sisvietnamvn.web.domain.HtmlSnippet;
import com.sisvietnamvn.web.repository.HtmlSnippetRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
/**
* Service for managing HtmlSnippets and providing them to Thymeleaf.
*/
@Service("snippetService") // Named bean so it can be called from Thymeleaf via @snippetService
@Transactional
public class HtmlSnippetService {
private final HtmlSnippetRepository snippetRepository;
public HtmlSnippetService(HtmlSnippetRepository snippetRepository) {
this.snippetRepository = snippetRepository;
}
public List<HtmlSnippet> findAll() {
return snippetRepository.findAll();
}
public Optional<HtmlSnippet> findById(Long id) {
return snippetRepository.findById(id);
}
public HtmlSnippet save(HtmlSnippet snippet) {
return snippetRepository.save(snippet);
}
public void deleteById(Long id) {
snippetRepository.deleteById(id);
}
/**
* Gets the content of a snippet by slug.
* If the snippet is not found or is inactive, returns an empty string.
* Can be called in Thymeleaf using: ${@snippetService.getSnippetContent('slug')}
*
* @param slug the unique slug of the snippet
* @return the HTML content or empty string
*/
@Transactional(readOnly = true)
public String getSnippetContent(String slug) {
if (slug == null || slug.isEmpty()) {
return "";
}
return snippetRepository.findBySlug(slug)
.filter(HtmlSnippet::isActive)
.map(HtmlSnippet::getContent)
.orElse("");
}
}
@@ -203,6 +203,7 @@ public class UserService {
user.setImageUrl(userDTO.getImageUrl());
user.setActivated(userDTO.isActivated());
user.setLangKey(userDTO.getLangKey());
user.setUsing2FA(userDTO.isUsing2FA());
Set<Authority> managedAuthorities = user.getAuthorities();
managedAuthorities.clear();
userDTO
@@ -53,6 +53,8 @@ public class AdminUserDTO implements Serializable {
private Set<String> authorities;
private boolean using2FA = false;
public AdminUserDTO() {
// Empty constructor needed for Jackson.
}
@@ -71,6 +73,7 @@ public class AdminUserDTO implements Serializable {
this.lastModifiedBy = user.getLastModifiedBy();
this.lastModifiedDate = user.getLastModifiedDate();
this.authorities = user.getAuthorities().stream().map(Authority::getName).collect(Collectors.toSet());
this.using2FA = user.isUsing2FA();
}
public Long getId() {
@@ -177,6 +180,14 @@ public class AdminUserDTO implements Serializable {
this.authorities = authorities;
}
public boolean isUsing2FA() {
return using2FA;
}
public void setUsing2FA(boolean using2FA) {
this.using2FA = using2FA;
}
// prettier-ignore
@Override
public String toString() {
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="20260706081000-1" author="jhipster">
<createTable tableName="html_snippet">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="slug" type="varchar(255)">
<constraints nullable="false" unique="true" uniqueConstraintName="ux_html_snippet__slug" />
</column>
<column name="name" type="varchar(255)">
<constraints nullable="false" />
</column>
<column name="content" type="${clobType}">
<constraints nullable="true" />
</column>
<column name="active" type="boolean">
<constraints nullable="false" />
</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>
</databaseChangeLog>
@@ -30,6 +30,7 @@
<include file="config/liquibase/changelog/20260702144700_add_plugin_entity.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260702153000_add_setting_entity.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260702184000_add_menu_entity.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260706081000_add_html_snippet_entity.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 -->
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

@@ -24,6 +24,9 @@ class HtmlSnippetTool {
this.wrapper.style.padding = '15px';
this.wrapper.style.borderRadius = '5px';
this.wrapper.style.background = '#fafafa';
this.wrapper.style.width = '100%';
this.wrapper.style.boxSizing = 'border-box';
this.wrapper.style.overflowX = 'auto'; // allow scrolling instead of stretching editor
if (this.data.id) {
this._showPreview(this.data.id);
@@ -0,0 +1,12 @@
/* @license GPL-2.0-or-later https://www.drupal.org/licensing/faq */
.progress{position:relative;}.progress__track{min-width:100px;max-width:100%;height:16px;margin-top:5px;border:1px solid;background-color:#fff;}.progress__bar{width:3%;min-width:3%;max-width:100%;height:1.5em;background-color:#000;}.progress__description,.progress__percentage{overflow:hidden;margin-top:0.2em;color:#555;font-size:0.875em;}.progress__description{float:left;}[dir="rtl"] .progress__description{float:right;}.progress__percentage{float:right;}[dir="rtl"] .progress__percentage{float:left;}.progress--small .progress__track{height:7px;}.progress--small .progress__bar{height:7px;background-size:20px 20px;}
.ajax-progress{display:inline-block;padding:1px 5px 2px 5px;}[dir="rtl"] .ajax-progress{float:right;}.ajax-progress-throbber .throbber{display:inline;padding:1px 5px 2px;background:transparent url(/themes/contrib/stable/images/core/throbber-active.gif) no-repeat 0 center;}.ajax-progress-throbber .message{display:inline;padding:1px 5px 2px;}tr .ajax-progress-throbber .throbber{margin:0 2px;}.ajax-progress-bar{width:16em;}.ajax-progress-fullscreen{position:fixed;z-index:1000;top:48.5%;left:49%;width:24px;height:24px;padding:4px;opacity:0.9;border-radius:7px;background-color:#232323;background-image:url(/themes/contrib/stable/images/core/loading-small.gif);background-repeat:no-repeat;background-position:center center;}[dir="rtl"] .ajax-progress-fullscreen{right:49%;left:auto;}
.text-align-left{text-align:left;}.text-align-right{text-align:right;}.text-align-center{text-align:center;}.text-align-justify{text-align:justify;}.align-left{float:left;}.align-right{float:right;}.align-center{display:block;margin-right:auto;margin-left:auto;}
.container-inline div,.container-inline label{display:inline;}.container-inline .details-wrapper{display:block;}
.clearfix:after{display:table;clear:both;content:"";}
.hidden{display:none;}.visually-hidden{position:absolute !important;overflow:hidden;clip:rect(1px,1px,1px,1px);width:1px;height:1px;word-wrap:normal;}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{position:static !important;overflow:visible;clip:auto;width:auto;height:auto;}.invisible{visibility:hidden;}
.js .js-hide{display:none;}.js-show{display:none;}.js .js-show{display:block;}
.media-oembed-content{max-width:100%;}
.cc_banner-wrapper{z-index:9001;position:relative}.cc_container .cc_btn{cursor:pointer;text-align:center;font-size:0.6em;transition:font-size 200ms;line-height:1em}.cc_container .cc_message{font-size:0.6em;transition:font-size 200ms;margin:0;padding:0;line-height:1.5em}.cc_container .cc_logo{display:none;text-indent:-1000px;overflow:hidden;width:100px;height:22px;background-size:cover;background-image:url(//cdnjs.cloudflare.com/ajax/libs/cookieconsent2/1.0.10/logo.png);opacity:0.9;transition:opacity 200ms}.cc_container .cc_logo:hover,.cc_container .cc_logo:active{opacity:1}@media screen and (min-width:500px){.cc_container .cc_btn{font-size:0.8em}.cc_container .cc_message{font-size:0.8em}}@media screen and (min-width:768px){.cc_container .cc_btn{font-size:1em}.cc_container .cc_message{font-size:1em;line-height:1em}}@media screen and (min-width:992px){.cc_container .cc_message{font-size:1em}}@media print{.cc_banner-wrapper,.cc_container{display:none}}.cc_container{position:fixed;left:0;right:0;bottom:0;overflow:hidden;padding:10px}.cc_container .cc_btn{padding:8px 10px;background-color:#f1d600;cursor:pointer;transition:font-size 200ms;text-align:center;font-size:0.6em;display:block;width:33%;margin-left:10px;float:right;max-width:120px}.cc_container .cc_message{transition:font-size 200ms;font-size:0.6em;display:block}@media screen and (min-width:500px){.cc_container .cc_btn{font-size:0.8em}.cc_container .cc_message{margin-top:0.5em;font-size:0.8em}}@media screen and (min-width:768px){.cc_container{padding:15px 30px 15px}.cc_container .cc_btn{font-size:1em;padding:8px 15px}.cc_container .cc_message{font-size:1em}}@media screen and (min-width:992px){.cc_container .cc_message{font-size:1em}}.cc_container{background:#222;color:#fff;font-size:17px;font-family:"Helvetica Neue Light","HelveticaNeue-Light","Helvetica Neue",Calibri,Helvetica,Arial;box-sizing:border-box}.cc_container ::-moz-selection{background:#ff5e99;color:#fff;text-shadow:none}.cc_container .cc_btn,.cc_container .cc_btn:visited{color:#000;background-color:#f1d600;transition:background 200ms ease-in-out,color 200ms ease-in-out,box-shadow 200ms ease-in-out;-webkit-transition:background 200ms ease-in-out,color 200ms ease-in-out,box-shadow 200ms ease-in-out;border-radius:5px;-webkit-border-radius:5px}.cc_container .cc_btn:hover,.cc_container .cc_btn:active{background-color:#fff;color:#000}.cc_container a,.cc_container a:visited{text-decoration:none;color:#31a8f0;transition:200ms color}.cc_container a:hover,.cc_container a:active{color:#b2f7ff}@-webkit-keyframes slideUp{0%{-webkit-transform:translateY(66px);transform:translateY(66px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes slideUp{0%{-webkit-transform:translateY(66px);-ms-transform:translateY(66px);transform:translateY(66px)}100%{-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.cc_container,.cc_message,.cc_btn{animation-duration:0.8s;-webkit-animation-duration:0.8s;-moz-animation-duration:0.8s;-o-animation-duration:0.8s;-webkit-animation-name:slideUp;animation-name:slideUp}
span.ext{width:10px;height:10px;padding-right:12px;text-decoration:none;background:url(/modules/contrib/extlink/images/extlink_s.png) 2px center no-repeat;}span.mailto{width:10px;height:10px;padding-right:12px;text-decoration:none;background:url(/modules/contrib/extlink/images/extlink_s.png) -20px center no-repeat;}span.tel{width:10px;height:10px;padding-right:12px;text-decoration:none;background:url(/modules/contrib/extlink/images/extlink_s.png) -42px center no-repeat;}svg.ext{width:14px;height:14px;fill:#727272;font-weight:900;}svg.mailto,svg.tel{width:14px;height:14px;fill:#727272;}[data-extlink-placement='prepend'],[data-extlink-placement='before']{padding-right:0.2rem;}[data-extlink-placement='append'],[data-extlink-placement='after']{padding-left:0.2rem;}svg.ext path,svg.mailto path,svg.tel path{stroke:#727272;stroke-width:3;}@media print{svg.ext,svg.mailto,svg.tel,span.ext,span.mailto,span.tel{display:none;padding:0;}}.extlink i{padding-left:0.2em;}.extlink-nobreak{white-space:nowrap;}
.paragraph--unpublished{background-color:#fff4f4;}
@@ -0,0 +1,6 @@
/* @license GPL-2.0-or-later https://www.drupal.org/licensing/faq */
.c--card-grid [data-component-id="umass_base:description"].f--description a.field-stat-link{display:inline-flex;justify-content:center;align-items:center;position:relative;width:auto;min-width:10.625rem;height:3.125rem;padding:0 0.9375rem;font-family:'Open Sans',Arial,Helvetica,sans-serif;font-size:0.875rem;font-weight:800;line-height:1.2858;text-align:center;text-decoration:none;text-transform:uppercase;white-space:normal;appearance:none;border:0;border-radius:0;color:#fff;background-color:#881c1c;transition:background-color 0.1s ease-in-out;}.c--card-grid [data-component-id="umass_base:description"].f--description a.field-stat-link:hover{background-color:var(--color-brand-dark);color:var(--color-white);}
.is-chrome .fi--form-item input[type="date"]::after{top:1rem;}
.block-microsite-menu-block svg.ext,.block-microsite-menu-block svg.mailto,.block-microsite-menu-block svg.tel{fill:#ffffff;}.block-microsite-menu-block svg.ext path,.block-microsite-menu-block svg.mailto path,.block-microsite-menu-block svg.tel path{stroke:#ffffff;}
.page-node-type-story .publication-date,.story-listing .publication-date{display:none;}.story-listing .f--field.f--eyebrow{display:none;}
.mc--quicklink-button-menu svg.ext{fill:#fff;}.mc--quicklink-button-menu svg.ext path{stroke:#fff;}
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

@@ -0,0 +1,2 @@
/* @license GPL-2.0-or-later https://www.drupal.org/licensing/faq */
(function(){const settingsElement=document.querySelector('head > script[type="application/json"][data-drupal-selector="drupal-settings-json"], body > script[type="application/json"][data-drupal-selector="drupal-settings-json"]');window.drupalSettings={};if(settingsElement!==null)window.drupalSettings=JSON.parse(settingsElement.textContent);})();;
@@ -11,12 +11,16 @@
<title layout:title-pattern="$CONTENT_TITLE - $LAYOUT_TITLE">SIS Vietnam - Manage</title>
<!-- Custom fonts for this template-->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet"
type="text/css">
<link
href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
rel="stylesheet">
<!-- Custom styles for this template-->
<link href="https://cdnjs.cloudflare.com/ajax/libs/startbootstrap-sb-admin-2/4.1.4/css/sb-admin-2.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/startbootstrap-sb-admin-2/4.1.4/css/sb-admin-2.min.css"
rel="stylesheet">
<!-- Project-wide CSS Variables & Custom Styles -->
<link rel="stylesheet" th:href="@{/css/custom.css}">
<!-- Admin Head Hook -->
@@ -32,11 +36,12 @@
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center" th:href="@{/manage}">
<div class="sidebar-brand-icon rotate-n-15">
<i class="fas fa-laugh-wink"></i>
<a class="sidebar-brand d-flex align-items-center justify-content-center" th:href="@{/manage}"
style="height: 130px;">
<div class="sidebar-brand-icon">
<img th:src="@{/images/LogoSIS_White.png}" alt="Logo" style="width: 100%;">
</div>
<div class="sidebar-brand-text mx-3">SIS Manage</div>
<!-- <div class="sidebar-brand-text mx-3">SIS Manage</div> -->
</a>
<!-- Divider -->
@@ -49,7 +54,8 @@
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Dashboard</span>
</a>
<div id="collapseDashboard" class="collapse" aria-labelledby="headingDashboard" data-parent="#accordionSidebar">
<div id="collapseDashboard" class="collapse" aria-labelledby="headingDashboard"
data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" th:href="@{/manage}">Home</a>
<a class="collapse-item" th:href="@{/manage/updates}">Updates</a>
@@ -122,7 +128,8 @@
<i class="fas fa-fw fa-paint-brush"></i>
<span>Appearance</span>
</a>
<div id="collapseAppearance" class="collapse" aria-labelledby="headingAppearance" data-parent="#accordionSidebar">
<div id="collapseAppearance" class="collapse" aria-labelledby="headingAppearance"
data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Design Management:</h6>
<a class="collapse-item" th:href="@{/manage/themes}">Themes</a>
@@ -149,7 +156,8 @@
<i class="fas fa-fw fa-plug"></i>
<span>Plugins</span>
</a>
<div id="collapsePlugins" class="collapse" aria-labelledby="headingPlugins" data-parent="#accordionSidebar">
<div id="collapsePlugins" class="collapse" aria-labelledby="headingPlugins"
data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Plugin Management:</h6>
<a class="collapse-item" th:href="@{/manage/plugins}">Installed Plugins</a>
@@ -206,7 +214,8 @@
<i class="fas fa-fw fa-cogs"></i>
<span>Settings</span>
</a>
<div id="collapseSettings" class="collapse" aria-labelledby="headingSettings" data-parent="#accordionSidebar">
<div id="collapseSettings" class="collapse" aria-labelledby="headingSettings"
data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Configuration:</h6>
<a class="collapse-item" th:href="@{/manage/settings/general}">General</a>
@@ -227,8 +236,7 @@
<i class="fas fa-fw fa-wrench"></i>
<span>Tools</span>
</a>
<div id="collapseTools" class="collapse" aria-labelledby="headingTools"
data-parent="#accordionSidebar">
<div id="collapseTools" class="collapse" aria-labelledby="headingTools" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">System Tools:</h6>
<a class="collapse-item" th:href="@{/manage/tools/import-export}">Import / Export</a>
@@ -241,6 +249,35 @@
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">
<!-- Dynamic Extensions Heading (only if dynamic menus exist) -->
<th:block th:if="${!dynamicAdminMenus.isEmpty()}">
<div class="sidebar-heading">
Extensions
</div>
<!-- Dynamic Menus Loop -->
<li class="nav-item" th:each="menu : ${dynamicAdminMenus}">
<!-- Dropdown Trigger -->
<a class="nav-link collapsed" href="#" data-toggle="collapse"
th:data-target="'#collapse_' + ${#strings.replace(menu.menuSlug, '-', '_')}"
aria-expanded="true">
<i th:class="${menu.iconUrl}"></i>
<span th:text="${menu.menuTitle}">Menu Title</span>
</a>
<!-- Dropdown Content -->
<div th:id="'collapse_' + ${#strings.replace(menu.menuSlug, '-', '_')}" class="collapse"
data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" th:each="sub : ${menu.submenus}" th:href="@{${sub.url}}"
th:text="${sub.menuTitle}">Submenu</a>
</div>
</div>
</li>
<hr class="sidebar-divider d-none d-md-block">
</th:block>
<!-- Sidebar Toggler (Sidebar) -->
<div class="text-center d-none d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
@@ -270,7 +307,8 @@
<li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small" sec:authentication="name">Admin User</span>
<span class="mr-2 d-none d-lg-inline text-gray-600 small"
sec:authentication="name">Admin User</span>
<img class="img-profile rounded-circle"
src="https://startbootstrap.github.io/startbootstrap-sb-admin-2/img/undraw_profile.svg">
</a>
@@ -283,9 +321,10 @@
</a>
<div class="dropdown-divider"></div>
<form method="post" th:action="@{/manage/logout}" id="logoutForm" class="d-none">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
</form>
<a class="dropdown-item" href="#" onclick="document.getElementById('logoutForm').submit(); return false;">
<a class="dropdown-item" href="#"
onclick="document.getElementById('logoutForm').submit(); return false;">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout
</a>
@@ -365,4 +404,4 @@
</body>
</html>
</html>
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<!--
Fragment simulating WordPress's do_settings_sections($page).
Iterates over dynamically registered sections and fields.
-->
<div th:fragment="do_settings_sections(page)" th:with="sections=${settingsManager.getSectionsForPage(page)}">
<div th:each="section : ${sections}">
<h4 class="mt-4 mb-3" th:if="${section.title != null and !section.title.isEmpty()}" th:text="${section.title}"></h4>
<p th:if="${section.description != null and !section.description.isEmpty()}" th:text="${section.description}"></p>
<div class="form-group row" th:each="field : ${section.fields}">
<label class="col-sm-3 col-form-label font-weight-bold text-right" th:text="${field.title}"></label>
<div class="col-sm-6">
<!-- Text / Email Input -->
<input th:if="${field.type == 'text' or field.type == 'email'}"
th:type="${field.type}" class="form-control"
th:name="${field.id}" th:value="${settings[field.id]}">
<!-- Checkbox Input -->
<div class="form-check mt-2" th:if="${field.type == 'checkbox'}">
<input type="checkbox" class="form-check-input"
th:name="${field.id}" value="1" th:checked="${settings[field.id] == '1'}">
<label class="form-check-label" th:if="${field.description}" th:text="${field.description}"></label>
</div>
<!-- Select Input -->
<select class="form-control" th:if="${field.type == 'select'}" th:name="${field.id}">
<option th:each="opt : ${field.options}"
th:value="${opt.key}"
th:text="${opt.value}"
th:selected="${settings[field.id] == opt.key}">
</option>
</select>
<!-- Radio Input -->
<th:block th:if="${field.type == 'radio'}">
<div class="form-check" th:each="opt : ${field.options}">
<input class="form-check-input" type="radio"
th:name="${field.id}"
th:value="${opt.key}"
th:checked="${settings[field.id] == opt.key}">
<label class="form-check-label" th:text="${opt.value}"></label>
</div>
</th:block>
<!-- Description Helper Text (for inputs other than checkbox) -->
<small class="form-text text-muted"
th:if="${field.type != 'checkbox' and field.description != null and !field.description.isEmpty()}"
th:text="${field.description}">
</small>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -25,6 +25,14 @@
max-width: 100% !important;
}
/* Auto scale media inside snippets to fit editor */
.ce-snippet-wrapper img,
.ce-snippet-wrapper video,
.ce-snippet-wrapper iframe {
max-width: 100% !important;
height: auto !important;
}
.ce-toolbar__content {
max-width: 100% !important;
}
@@ -25,6 +25,14 @@
max-width: 100% !important;
}
/* Auto scale media inside snippets to fit editor */
.ce-snippet-wrapper img,
.ce-snippet-wrapper video,
.ce-snippet-wrapper iframe {
max-width: 100% !important;
height: auto !important;
}
.ce-toolbar__content {
max-width: 100% !important;
}
@@ -23,64 +23,11 @@
<div class="card shadow mb-4">
<div class="card-body">
<form th:action="@{/manage/settings/general/save}" method="post">
<!-- settings_fields() equivalent via Spring Security -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<div class="form-group row">
<label class="col-sm-3 col-form-label font-weight-bold text-right">Site Title</label>
<div class="col-sm-6">
<input type="text" class="form-control" name="site_name" th:value="${settings['site_name']}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label font-weight-bold text-right">Tagline</label>
<div class="col-sm-6">
<input type="text" class="form-control" name="tagline" th:value="${settings['tagline']}">
<small class="form-text text-muted">In a few words, explain what this site is about.</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label font-weight-bold text-right">Administration Email Address</label>
<div class="col-sm-6">
<input type="email" class="form-control" name="admin_email" th:value="${settings['admin_email']}">
<small class="form-text text-muted">This address is used for admin purposes.</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label font-weight-bold text-right">Timezone</label>
<div class="col-sm-6">
<select class="form-control" name="timezone">
<option value="UTC" th:selected="${settings['timezone'] == 'UTC'}">UTC</option>
<option value="Asia/Ho_Chi_Minh" th:selected="${settings['timezone'] == 'Asia/Ho_Chi_Minh'}">Asia/Ho Chi Minh</option>
<option value="America/New_York" th:selected="${settings['timezone'] == 'America/New_York'}">America/New York</option>
<option value="Europe/London" th:selected="${settings['timezone'] == 'Europe/London'}">Europe/London</option>
</select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label font-weight-bold text-right">Date Format</label>
<div class="col-sm-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="date_format" value="F j, Y" th:checked="${settings['date_format'] == 'F j, Y'}">
<label class="form-check-label">November 6, 2010 (F j, Y)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="date_format" value="Y-m-d" th:checked="${settings['date_format'] == 'Y-m-d'}">
<label class="form-check-label">2010-11-06 (Y-m-d)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="date_format" value="m/d/Y" th:checked="${settings['date_format'] == 'm/d/Y'}">
<label class="form-check-label">11/06/2010 (m/d/Y)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="date_format" value="d/m/Y" th:checked="${settings['date_format'] == 'd/m/Y'}">
<label class="form-check-label">06/11/2010 (d/m/Y)</label>
</div>
</div>
</div>
<!-- do_settings_sections('general') equivalent -->
<div th:replace="~{fragments/settings-api :: do_settings_sections('general')}"></div>
<div class="form-group row">
<div class="col-sm-3"></div>
@@ -0,0 +1,663 @@
<!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 Snippet' : 'Edit Snippet'">Edit Snippet</title>
<!-- GrapesJS CSS -->
<link href="https://unpkg.com/grapesjs/dist/css/grapes.min.css" rel="stylesheet">
<style>
/* ── GrapesJS Layout Overrides ── */
#gjs-editor-wrap {
position: relative;
height: 75vh;
min-height: 550px;
border: 1px solid #d1d3e2;
border-radius: 0.35rem;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Top Panel */
.panel__top {
padding: 0;
width: 100%;
display: flex;
justify-content: space-between;
background-color: #1a252f !important; /* Darker background */
border-bottom: 2px solid #00d2ff; /* Bright border line */
z-index: 10;
position: relative;
}
.panel__top .gjs-pn-panel {
position: relative !important;
top: auto !important;
left: auto !important;
right: auto !important;
display: flex !important;
background-color: transparent !important;
}
.gjs-pn-panel { background-color: transparent !important; }
.gjs-pn-btn {
color: #ffffff !important;
opacity: 1 !important; /* Full opacity by default */
font-size: 15px; /* Larger icons */
padding: 10px 12px;
transition: all 0.2s ease;
}
.gjs-pn-btn:hover {
color: #1a252f !important;
background-color: #00d2ff !important; /* High contrast hover */
}
.gjs-pn-btn.gjs-pn-active {
color: #1a252f !important;
background-color: #00d2ff !important; /* High contrast active */
}
/* Main Row */
.editor-row {
display: flex;
flex-grow: 1;
overflow: hidden;
}
/* Canvas */
.editor-canvas {
flex-grow: 1;
position: relative;
}
.gjs-cv-canvas {
top: 0;
width: 100%;
height: 100%;
}
/* Right Panel */
.panel__right {
flex-basis: 250px;
min-width: 250px;
max-width: 250px;
overflow-y: auto;
background: #f8f9fa;
border-left: 1px solid #e2e8f0;
}
/* Blocks */
.gjs-block {
width: auto;
min-height: 50px;
padding: 10px;
font-size: 11px;
}
.gjs-block svg { width: 30px; }
.gjs-block-label { margin-top: 4px; }
.gjs-blocks-cs { padding: 5px; }
.gjs-block-category .gjs-title {
background: #2c3e50;
color: #ecf0f1;
font-weight: 600;
letter-spacing: 0.5px;
padding: 8px 12px;
border-bottom: 1px solid #34495e;
}
/* Style manager */
.gjs-sm-sector .gjs-sm-sector-title {
background: #2c3e50;
color: #ecf0f1;
}
.gjs-clm-tags { padding: 5px; }
.gjs-layer-name { font-size: 12px; }
</style>
</head>
<body>
<div layout:fragment="content">
<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 HTML Snippet' : 'Edit HTML Snippet'">Edit Snippet</h1>
<a th:href="@{/manage/snippets}" 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 List
</a>
</div>
<form id="snippetForm" th:action="${isNew} ? @{/manage/snippets/create} : @{/manage/snippets/{id}(id=${snippet.id})}" method="post" th:object="${snippet}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="hidden" th:field="*{createdBy}" />
<input type="hidden" th:field="*{createdDate}" />
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Snippet Details</h6>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" th:field="*{name}" required placeholder="E.g., Footer Contact Text">
</div>
<div class="form-group">
<label for="slug">Slug</label>
<input type="text" class="form-control" id="slug" th:field="*{slug}" required placeholder="E.g., footer-contact">
<small class="form-text text-muted">A unique identifier used to call this snippet in templates. Use lowercase letters and hyphens.</small>
</div>
<div class="form-group">
<div class="d-flex justify-content-between align-items-center mb-2">
<label for="snippetContent" class="mb-0">HTML Content</label>
<button type="button" class="btn btn-sm btn-outline-secondary" id="toggleEditorBtn">
<i class="fas fa-code"></i> Use Code Editor
</button>
</div>
<!-- Raw Textarea (hidden by default) -->
<textarea class="form-control" id="snippetContent" name="content" th:text="*{content}" rows="20" style="font-family: monospace; display: none;"></textarea>
<!-- Visual Builder Container -->
<div id="gjs-editor-wrap">
<div class="panel__top">
<div class="panel__basic-actions"></div>
<div class="panel__devices" style="display:flex; align-items:center;"></div>
<div class="panel__switcher"></div>
</div>
<div class="editor-row">
<div class="editor-canvas">
<div id="gjs"></div>
</div>
<div class="panel__right">
<div class="blocks-container"></div>
<div class="styles-container" style="display:none;"></div>
<div class="layers-container" style="display:none;"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Publish</h6>
</div>
<div class="card-body">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="active" th:field="*{active}">
<label class="custom-control-label" for="active">Active (Visible on site)</label>
</div>
</div>
<hr>
<button type="submit" class="btn btn-primary btn-block">Save Snippet</button>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- Page Specific Scripts -->
<section layout:fragment="scripts">
<script src="https://unpkg.com/grapesjs"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var textArea = document.getElementById('snippetContent');
var gjsWrap = document.getElementById('gjs-editor-wrap');
var toggleBtn = document.getElementById('toggleEditorBtn');
var form = document.getElementById('snippetForm');
if (!textArea || !gjsWrap || !toggleBtn || !form) {
console.error('GrapesJS: missing DOM elements');
return;
}
var isVisual = true;
// ═══════════════════════════════════════
// INITIALIZE GRAPESJS
// ═══════════════════════════════════════
var editor = grapesjs.init({
container: '#gjs',
height: '100%',
width: 'auto',
fromElement: false,
storageManager: false,
// ── Device Manager ──
deviceManager: {
devices: [
{ id: 'desktop', name: 'Desktop', width: '' },
{ id: 'tablet', name: 'Tablet', width: '768px', widthMedia: '992px' },
{ id: 'mobilePortrait', name: 'Mobile', width: '320px', widthMedia: '575px' }
]
},
// ── Style Manager ──
styleManager: {
appendTo: '.panel__right',
sectors: [
{
name: 'Dimension',
open: false,
buildProps: ['width', 'min-width', 'max-width', 'height', 'min-height', 'max-height', 'padding', 'margin'],
},
{
name: 'Typography',
open: false,
buildProps: ['font-family', 'font-size', 'font-weight', 'letter-spacing', 'color', 'line-height', 'text-align', 'text-decoration', 'text-transform'],
},
{
name: 'Decorations',
open: false,
buildProps: ['background-color', 'background', 'border-radius', 'border', 'box-shadow', 'opacity'],
},
{
name: 'Extra',
open: false,
buildProps: ['transition', 'perspective', 'transform'],
},
{
name: 'Flex',
open: false,
buildProps: ['display', 'flex-direction', 'flex-wrap', 'justify-content', 'align-items', 'align-content', 'order', 'flex-basis', 'flex-grow', 'flex-shrink', 'align-self'],
}
]
},
// ── Layer Manager ──
layerManager: { appendTo: '.layers-container' },
// ── Panels ──
panels: {
defaults: [
{
id: 'panel-top',
className: 'my-panel-top'
},
{
id: 'panel-devices',
className: 'my-panel-devices',
buttons: [
{ id: 'device-desktop', command: 'set-device-desktop', label: '<i class="fa fa-desktop"></i>', active: true, togglable: false },
{ id: 'device-tablet', command: 'set-device-tablet', label: '<i class="fa fa-tablet"></i>', togglable: false },
{ id: 'device-mobile', command: 'set-device-mobile', label: '<i class="fa fa-mobile"></i>', togglable: false }
]
},
{
id: 'basic-actions',
className: 'my-basic-actions',
buttons: [
{ id: 'visibility', active: true, className: 'btn-toggle-borders', label: '<i class="fa fa-clone"></i>', command: 'sw-visibility' },
{ id: 'fullscreen', className: 'btn-fullscreen', label: '<i class="fa fa-arrows-alt"></i>', command: 'fullscreen' },
{ id: 'export', className: 'btn-export', label: '<i class="fa fa-code"></i>', command: 'export-template' },
{ id: 'undo', className: 'btn-undo', label: '<i class="fa fa-undo"></i>', command: 'core:undo' },
{ id: 'redo', className: 'btn-redo', label: '<i class="fa fa-repeat"></i>', command: 'core:redo' },
{ id: 'canvas-clear', className: 'btn-clear', label: '<i class="fa fa-trash"></i>', command: 'canvas-clear' },
],
},
{
id: 'panel-switcher',
className: 'my-panel-switcher',
buttons: [
{ id: 'show-blocks', active: true, label: '<i class="fa fa-th-large"></i>', command: 'show-blocks', togglable: false },
{ id: 'show-style', label: '<i class="fa fa-paint-brush"></i>', command: 'show-styles', togglable: false },
{ id: 'show-layers', label: '<i class="fa fa-bars"></i>', command: 'show-layers', togglable: false },
{ id: 'toggle-sidebar', label: '<i class="fa fa-chevron-right" id="sidebar-toggle-icon"></i>', command: 'toggle-sidebar', togglable: false }
],
}
]
},
// ── Block Manager ──
blockManager: {
appendTo: '.blocks-container',
blocks: [] // We'll add blocks below
}
});
// After GrapesJS loads, move the generated panels into our custom HTML layout wrappers!
editor.on('load', function() {
// GrapesJS automatically assigns the class .gjs-pn-{id} to panels
var pBasic = document.querySelector('.gjs-pn-basic-actions');
if (pBasic) document.querySelector('.panel__basic-actions').appendChild(pBasic);
var pDevices = document.querySelector('.gjs-pn-panel-devices');
if (pDevices) document.querySelector('.panel__devices').appendChild(pDevices);
var pSwitcher = document.querySelector('.gjs-pn-panel-switcher');
if (pSwitcher) document.querySelector('.panel__switcher').appendChild(pSwitcher);
});
var editorEl = document.getElementById('gjs-editor-wrap');
// ── Panel switching commands ──
editor.Commands.add('show-blocks', {
run: function(editor) {
var bc = editorEl.querySelector('.blocks-container');
var sc = editorEl.querySelector('.styles-container');
var lc = editorEl.querySelector('.layers-container');
var rp = editorEl.querySelector('.panel__right');
if (rp) rp.style.display = '';
if (bc) bc.style.display = '';
if (sc) sc.style.display = 'none';
if (lc) lc.style.display = 'none';
setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 0);
}
});
editor.Commands.add('show-styles', {
run: function(editor) {
var bc = editorEl.querySelector('.blocks-container');
var sc = editorEl.querySelector('.styles-container');
var lc = editorEl.querySelector('.layers-container');
var rp = editorEl.querySelector('.panel__right');
if (rp) rp.style.display = '';
if (bc) bc.style.display = 'none';
if (sc) sc.style.display = '';
if (lc) lc.style.display = 'none';
setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 0);
}
});
editor.Commands.add('show-layers', {
run: function(editor) {
var bc = editorEl.querySelector('.blocks-container');
var sc = editorEl.querySelector('.styles-container');
var lc = editorEl.querySelector('.layers-container');
var rp = editorEl.querySelector('.panel__right');
if (rp) rp.style.display = '';
if (bc) bc.style.display = 'none';
if (sc) sc.style.display = 'none';
if (lc) lc.style.display = '';
setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 0);
}
});
editor.Commands.add('toggle-sidebar', {
run: function(editor) {
var rp = editorEl.querySelector('.panel__right');
var icon = document.getElementById('sidebar-toggle-icon');
if (rp) {
if (rp.style.display === 'none') {
rp.style.display = '';
if (icon) { icon.classList.remove('fa-chevron-left'); icon.classList.add('fa-chevron-right'); }
} else {
rp.style.display = 'none';
if (icon) { icon.classList.remove('fa-chevron-right'); icon.classList.add('fa-chevron-left'); }
}
// Trigger resize event to force GrapesJS canvas to recalculate size
setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 0);
}
}
});
editor.Commands.add('canvas-clear', {
run: function(editor) {
if (confirm('Are you sure you want to clear the canvas?')) {
editor.DomComponents.clear();
}
}
});
// ── Device switching commands ──
editor.Commands.add('set-device-desktop', {
run: function(editor) { editor.setDevice('desktop'); }
});
editor.Commands.add('set-device-tablet', {
run: function(editor) { editor.setDevice('tablet'); }
});
editor.Commands.add('set-device-mobile', {
run: function(editor) { editor.setDevice('mobilePortrait'); }
});
// ═══════════════════════════════════════
// ELEMENTOR-STYLE BLOCKS
// ═══════════════════════════════════════
var bm = editor.BlockManager;
// ── BASIC ──
bm.add('heading', {
label: 'Heading',
category: 'Basic',
content: '<h2 style="padding: 10px; font-family: Arial, sans-serif;">Heading Text</h2>',
media: '<i class="fa fa-header fa-2x"></i>'
});
bm.add('text-section', {
label: 'Text Editor',
category: 'Basic',
content: '<div style="padding: 10px; font-family: Arial, sans-serif; line-height: 1.6;"><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis.</p></div>',
media: '<i class="fa fa-align-left fa-2x"></i>'
});
bm.add('image', {
label: 'Image',
category: 'Basic',
content: { type: 'image' },
activate: true,
media: '<i class="fa fa-image fa-2x"></i>'
});
bm.add('video', {
label: 'Video',
category: 'Basic',
content: { type: 'video', src: 'https://www.youtube.com/embed/dQw4w9WgXcQ', style: 'width: 100%; height: 350px;' },
media: '<i class="fa fa-youtube-play fa-2x"></i>'
});
bm.add('button', {
label: 'Button',
category: 'Basic',
content: '<a href="#" style="display: inline-block; padding: 12px 30px; background: #6366f1; color: #fff; text-decoration: none; border-radius: 5px; font-weight: 600; font-family: Arial, sans-serif; text-align: center;">Click Here</a>',
media: '<i class="fa fa-link fa-2x"></i>'
});
bm.add('divider', {
label: 'Divider',
category: 'Basic',
content: '<hr style="border: 0; border-top: 2px solid #e0e0e0; margin: 20px 0;">',
media: '<i class="fa fa-minus fa-2x"></i>'
});
bm.add('spacer', {
label: 'Spacer',
category: 'Basic',
content: '<div style="height: 50px;"></div>',
media: '<i class="fa fa-arrows-v fa-2x"></i>'
});
bm.add('icon', {
label: 'Icon',
category: 'Basic',
content: '<span style="font-size: 40px; color: #6366f1; display: inline-block; text-align: center; width: 60px; height: 60px; line-height: 60px;">★</span>',
media: '<i class="fa fa-star fa-2x"></i>'
});
// ── LAYOUT ──
bm.add('section', {
label: 'Section',
category: 'Layout',
content: '<section style="padding: 60px 20px; background: #f8f9fa;"><div style="max-width: 1140px; margin: 0 auto;"><h2 style="font-family: Arial, sans-serif;">Section Title</h2><p style="font-family: Arial, sans-serif;">Section content goes here.</p></div></section>',
media: '<i class="fa fa-square-o fa-2x"></i>'
});
bm.add('columns-2', {
label: '2 Columns',
category: 'Layout',
content: '<div style="display: flex; gap: 20px; padding: 20px;"><div style="flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;">Column 1</div><div style="flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;">Column 2</div></div>',
media: '<i class="fa fa-columns fa-2x"></i>'
});
bm.add('columns-3', {
label: '3 Columns',
category: 'Layout',
content: '<div style="display: flex; gap: 20px; padding: 20px;"><div style="flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;">Column 1</div><div style="flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;">Column 2</div><div style="flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;">Column 3</div></div>',
media: '<i class="fa fa-th fa-2x"></i>'
});
bm.add('columns-4', {
label: '4 Columns',
category: 'Layout',
content: '<div style="display: flex; gap: 15px; padding: 20px;"><div style="flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;">Col 1</div><div style="flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;">Col 2</div><div style="flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;">Col 3</div><div style="flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;">Col 4</div></div>',
media: '<i class="fa fa-th-large fa-2x"></i>'
});
bm.add('container', {
label: 'Container',
category: 'Layout',
content: '<div style="max-width: 1140px; margin: 0 auto; padding: 20px;"></div>',
media: '<i class="fa fa-object-group fa-2x"></i>'
});
// ── GENERAL ──
bm.add('icon-box', {
label: 'Icon Box',
category: 'General',
content: '<div style="text-align: center; padding: 30px; background: #fff; border-radius: 12px; box-shadow: 0 2px 15px rgba(0,0,0,0.08);"><div style="font-size: 48px; color: #6366f1; margin-bottom: 15px;">★</div><h3 style="font-family: Arial, sans-serif; font-size: 20px; margin-bottom: 10px;">Feature Title</h3><p style="font-family: Arial, sans-serif; color: #666; font-size: 14px; line-height: 1.6;">Short description of this feature goes here.</p></div>',
media: '<i class="fa fa-cube fa-2x"></i>'
});
bm.add('image-box', {
label: 'Image Box',
category: 'General',
content: '<div style="text-align: center; padding: 25px; background: #fff; border-radius: 12px; box-shadow: 0 2px 15px rgba(0,0,0,0.08);"><img src="https://placehold.co/300x200/6366f1/ffffff?text=Image" alt="Image" style="width: 100%; border-radius: 8px; margin-bottom: 15px;"><h4 style="font-family: Arial, sans-serif; font-size: 18px; margin-bottom: 8px;">Image Title</h4><p style="font-family: Arial, sans-serif; color: #666; font-size: 14px;">Description text here.</p></div>',
media: '<i class="fa fa-file-image-o fa-2x"></i>'
});
bm.add('counter', {
label: 'Counter',
category: 'General',
content: '<div style="text-align: center; padding: 25px;"><div style="font-size: 48px; font-weight: 700; color: #6366f1; font-family: Arial, sans-serif;">1,250</div><div style="font-size: 14px; color: #888; text-transform: uppercase; letter-spacing: 2px; font-family: Arial, sans-serif; margin-top: 8px;">Happy Customers</div></div>',
media: '<i class="fa fa-sort-numeric-asc fa-2x"></i>'
});
bm.add('progress-bar', {
label: 'Progress Bar',
category: 'General',
content: '<div style="padding: 15px;"><div style="font-family: Arial, sans-serif; font-size: 14px; margin-bottom: 5px; display: flex; justify-content: space-between;"><span>Web Design</span><span>85%</span></div><div style="background: #e9ecef; border-radius: 10px; height: 12px; overflow: hidden;"><div style="background: linear-gradient(90deg, #6366f1, #8b5cf6); width: 85%; height: 100%; border-radius: 10px;"></div></div></div>',
media: '<i class="fa fa-tasks fa-2x"></i>'
});
bm.add('icon-list', {
label: 'Icon List',
category: 'General',
content: '<ul style="list-style: none; padding: 15px; font-family: Arial, sans-serif;"><li style="padding: 8px 0; display: flex; align-items: center;"><span style="color: #6366f1; margin-right: 12px; font-size: 18px;">✓</span> List item one</li><li style="padding: 8px 0; display: flex; align-items: center;"><span style="color: #6366f1; margin-right: 12px; font-size: 18px;">✓</span> List item two</li><li style="padding: 8px 0; display: flex; align-items: center;"><span style="color: #6366f1; margin-right: 12px; font-size: 18px;">✓</span> List item three</li></ul>',
media: '<i class="fa fa-list fa-2x"></i>'
});
bm.add('alert', {
label: 'Alert',
category: 'General',
content: '<div style="padding: 15px 20px; background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; border-radius: 6px; font-family: Arial, sans-serif; font-size: 14px;">️ This is an informational alert — check it out!</div>',
media: '<i class="fa fa-exclamation-triangle fa-2x"></i>'
});
bm.add('blockquote', {
label: 'Blockquote',
category: 'General',
content: '<blockquote style="border-left: 4px solid #6366f1; padding: 20px 25px; margin: 20px 0; background: #f8f9fa; border-radius: 0 8px 8px 0;"><p style="font-family: Georgia, serif; font-size: 18px; font-style: italic; color: #333; line-height: 1.6; margin-bottom: 10px;">"The only way to do great work is to love what you do."</p><cite style="font-family: Arial, sans-serif; font-size: 14px; color: #888;">— Steve Jobs</cite></blockquote>',
media: '<i class="fa fa-quote-left fa-2x"></i>'
});
bm.add('tabs', {
label: 'Tabs',
category: 'General',
content: '<div style="font-family: Arial, sans-serif;"><div style="display: flex; border-bottom: 2px solid #e9ecef;"><div style="padding: 12px 24px; cursor: pointer; border-bottom: 2px solid #6366f1; margin-bottom: -2px; font-weight: 600; color: #6366f1;">Tab 1</div><div style="padding: 12px 24px; cursor: pointer; color: #888;">Tab 2</div><div style="padding: 12px 24px; cursor: pointer; color: #888;">Tab 3</div></div><div style="padding: 20px; border: 1px solid #e9ecef; border-top: none; border-radius: 0 0 8px 8px;"><p>Tab content goes here. Click to edit this text.</p></div></div>',
media: '<i class="fa fa-folder-o fa-2x"></i>'
});
bm.add('accordion', {
label: 'Accordion',
category: 'General',
content: '<div style="font-family: Arial, sans-serif; border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden;"><div style="border-bottom: 1px solid #e9ecef;"><div style="padding: 15px 20px; background: #f8f9fa; font-weight: 600; cursor: pointer;">▸ Accordion Item 1</div><div style="padding: 15px 20px;">This is the content for the first accordion item.</div></div><div style="border-bottom: 1px solid #e9ecef;"><div style="padding: 15px 20px; background: #f8f9fa; font-weight: 600; cursor: pointer;">▸ Accordion Item 2</div><div style="padding: 15px 20px; display: none;">Content for the second item.</div></div><div><div style="padding: 15px 20px; background: #f8f9fa; font-weight: 600; cursor: pointer;">▸ Accordion Item 3</div><div style="padding: 15px 20px; display: none;">Content for the third item.</div></div></div>',
media: '<i class="fa fa-bars fa-2x"></i>'
});
bm.add('google-map', {
label: 'Map',
category: 'General',
content: '<iframe src="https://maps.google.com/maps?q=Ho+Chi+Minh+City&t=&z=13&ie=UTF8&iwloc=&output=embed" style="width: 100%; height: 300px; border: 0; border-radius: 8px;" allowfullscreen></iframe>',
media: '<i class="fa fa-map-marker fa-2x"></i>'
});
bm.add('social-icons', {
label: 'Social Icons',
category: 'General',
content: '<div style="display: flex; gap: 12px; padding: 15px; justify-content: center;"><a href="#" style="display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; background: #1877f2; color: #fff; border-radius: 50%; text-decoration: none; font-size: 18px; font-weight: bold;">f</a><a href="#" style="display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; background: #1da1f2; color: #fff; border-radius: 50%; text-decoration: none; font-size: 18px; font-weight: bold;">t</a><a href="#" style="display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; background: #e4405f; color: #fff; border-radius: 50%; text-decoration: none; font-size: 18px; font-weight: bold;">ig</a><a href="#" style="display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; background: #0077b5; color: #fff; border-radius: 50%; text-decoration: none; font-size: 18px; font-weight: bold;">in</a></div>',
media: '<i class="fa fa-share-alt fa-2x"></i>'
});
// ── MARKETING ──
bm.add('cta', {
label: 'Call to Action',
category: 'Marketing',
content: '<div style="background: linear-gradient(135deg, #6366f1, #8b5cf6); padding: 60px 40px; text-align: center; border-radius: 12px;"><h2 style="font-family: Arial, sans-serif; color: #fff; font-size: 32px; margin-bottom: 15px;">Ready to Get Started?</h2><p style="font-family: Arial, sans-serif; color: rgba(255,255,255,0.85); font-size: 16px; margin-bottom: 25px; max-width: 500px; margin-left: auto; margin-right: auto;">Join thousands of happy customers and take your business to the next level.</p><a href="#" style="display: inline-block; padding: 14px 36px; background: #fff; color: #6366f1; text-decoration: none; border-radius: 6px; font-weight: 700; font-family: Arial, sans-serif;">Get Started Now</a></div>',
media: '<i class="fa fa-bullhorn fa-2x"></i>'
});
bm.add('pricing-table', {
label: 'Pricing Table',
category: 'Marketing',
content: '<div style="background: #fff; border: 2px solid #e9ecef; border-radius: 16px; padding: 40px 30px; text-align: center; font-family: Arial, sans-serif; max-width: 350px;"><div style="text-transform: uppercase; font-size: 14px; letter-spacing: 2px; color: #888; margin-bottom: 10px;">Professional</div><div style="font-size: 48px; font-weight: 700; color: #333; margin-bottom: 5px;">$49<span style="font-size: 16px; color: #888; font-weight: 400;">/month</span></div><hr style="border: 0; border-top: 1px solid #e9ecef; margin: 25px 0;"><ul style="list-style: none; padding: 0; margin: 0 0 30px 0; text-align: left;"><li style="padding: 10px 0; color: #555;">✓ Unlimited Projects</li><li style="padding: 10px 0; color: #555;">✓ Priority Support</li><li style="padding: 10px 0; color: #555;">✓ Custom Domain</li><li style="padding: 10px 0; color: #555;">✓ Analytics Dashboard</li></ul><a href="#" style="display: block; padding: 14px; background: #6366f1; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600;">Choose Plan</a></div>',
media: '<i class="fa fa-tags fa-2x"></i>'
});
bm.add('testimonial', {
label: 'Testimonial',
category: 'Marketing',
content: '<div style="background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 2px 15px rgba(0,0,0,0.08); text-align: center; font-family: Arial, sans-serif;"><div style="font-size: 20px; color: #f5c518; margin-bottom: 12px;">★★★★★</div><p style="font-style: italic; color: #555; line-height: 1.7; font-size: 15px; margin-bottom: 20px;">"This product has completely transformed our workflow. Highly recommended!"</p><div style="display: flex; align-items: center; justify-content: center; gap: 12px;"><img src="https://placehold.co/50x50/6366f1/ffffff?text=JD" alt="avatar" style="border-radius: 50%;"><div style="text-align: left;"><div style="font-weight: 600; color: #333;">John Doe</div><div style="font-size: 13px; color: #888;">CEO, Company</div></div></div></div>',
media: '<i class="fa fa-comment-o fa-2x"></i>'
});
bm.add('hero-banner', {
label: 'Hero Banner',
category: 'Marketing',
content: '<div style="background: linear-gradient(135deg, #1e293b, #334155); padding: 80px 40px; text-align: center;"><h1 style="font-family: Arial, sans-serif; color: #fff; font-size: 42px; margin-bottom: 20px; line-height: 1.2;">Build Amazing Websites</h1><p style="font-family: Arial, sans-serif; color: rgba(255,255,255,0.7); font-size: 18px; max-width: 600px; margin: 0 auto 30px; line-height: 1.6;">Create stunning web experiences with our powerful visual builder. No coding required.</p><div style="display: flex; gap: 15px; justify-content: center;"><a href="#" style="display: inline-block; padding: 14px 32px; background: #6366f1; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 600; font-family: Arial, sans-serif;">Get Started</a><a href="#" style="display: inline-block; padding: 14px 32px; background: transparent; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 600; font-family: Arial, sans-serif; border: 2px solid rgba(255,255,255,0.3);">Learn More</a></div></div>',
media: '<i class="fa fa-picture-o fa-2x"></i>'
});
bm.add('team-member', {
label: 'Team Member',
category: 'Marketing',
content: '<div style="text-align: center; padding: 30px; font-family: Arial, sans-serif;"><img src="https://placehold.co/150x150/6366f1/ffffff?text=Photo" alt="team member" style="border-radius: 50%; width: 120px; height: 120px; margin-bottom: 15px;"><h4 style="font-size: 18px; margin-bottom: 5px; color: #333;">Jane Smith</h4><div style="color: #6366f1; font-size: 14px; margin-bottom: 12px;">Lead Designer</div><p style="color: #888; font-size: 14px; line-height: 1.6; max-width: 300px; margin: 0 auto;">Passionate about creating beautiful user experiences and pixel-perfect designs.</p></div>',
media: '<i class="fa fa-user-circle-o fa-2x"></i>'
});
bm.add('feature-grid', {
label: 'Feature Grid',
category: 'Marketing',
content: '<div style="display: flex; flex-wrap: wrap; gap: 20px; padding: 20px;"><div style="flex: 1; min-width: 250px; background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 2px 15px rgba(0,0,0,0.06); text-align: center;"><div style="font-size: 36px; margin-bottom: 12px;">🚀</div><h4 style="font-family: Arial, sans-serif; font-size: 18px; margin-bottom: 8px;">Fast Performance</h4><p style="font-family: Arial, sans-serif; font-size: 14px; color: #666;">Lightning fast loading times for the best user experience.</p></div><div style="flex: 1; min-width: 250px; background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 2px 15px rgba(0,0,0,0.06); text-align: center;"><div style="font-size: 36px; margin-bottom: 12px;">🔒</div><h4 style="font-family: Arial, sans-serif; font-size: 18px; margin-bottom: 8px;">Secure</h4><p style="font-family: Arial, sans-serif; font-size: 14px; color: #666;">Enterprise-grade security to protect your data.</p></div><div style="flex: 1; min-width: 250px; background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 2px 15px rgba(0,0,0,0.06); text-align: center;"><div style="font-size: 36px; margin-bottom: 12px;">📱</div><h4 style="font-family: Arial, sans-serif; font-size: 18px; margin-bottom: 8px;">Responsive</h4><p style="font-family: Arial, sans-serif; font-size: 14px; color: #666;">Looks great on every device and screen size.</p></div></div>',
media: '<i class="fa fa-rocket fa-2x"></i>'
});
// ── FORM ──
bm.add('contact-form', {
label: 'Contact Form',
category: 'Form',
content: '<form style="padding: 30px; font-family: Arial, sans-serif; background: #fff; border-radius: 12px; box-shadow: 0 2px 15px rgba(0,0,0,0.08);"><h3 style="margin-bottom: 20px; color: #333;">Contact Us</h3><div style="margin-bottom: 15px;"><label style="display: block; font-size: 14px; color: #555; margin-bottom: 5px;">Full Name</label><input type="text" placeholder="Your name" style="width: 100%; padding: 12px 15px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; box-sizing: border-box;"></div><div style="margin-bottom: 15px;"><label style="display: block; font-size: 14px; color: #555; margin-bottom: 5px;">Email Address</label><input type="email" placeholder="your@email.com" style="width: 100%; padding: 12px 15px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; box-sizing: border-box;"></div><div style="margin-bottom: 15px;"><label style="display: block; font-size: 14px; color: #555; margin-bottom: 5px;">Message</label><textarea rows="4" placeholder="Your message..." style="width: 100%; padding: 12px 15px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; box-sizing: border-box; resize: vertical;"></textarea></div><button type="submit" style="padding: 12px 30px; background: #6366f1; color: #fff; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 14px;">Send Message</button></form>',
media: '<i class="fa fa-envelope-o fa-2x"></i>'
});
bm.add('newsletter', {
label: 'Newsletter',
category: 'Form',
content: '<div style="background: #f0f4ff; padding: 40px 30px; border-radius: 12px; text-align: center; font-family: Arial, sans-serif;"><h3 style="color: #333; margin-bottom: 8px;">Subscribe to Our Newsletter</h3><p style="color: #666; font-size: 14px; margin-bottom: 20px;">Get the latest updates delivered to your inbox.</p><div style="display: flex; gap: 10px; max-width: 450px; margin: 0 auto;"><input type="email" placeholder="Enter your email" style="flex: 1; padding: 12px 15px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;"><button style="padding: 12px 24px; background: #6366f1; color: #fff; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; white-space: nowrap;">Subscribe</button></div></div>',
media: '<i class="fa fa-newspaper-o fa-2x"></i>'
});
// ── MEDIA ──
bm.add('image-gallery', {
label: 'Image Gallery',
category: 'Media',
content: '<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 15px;"><img src="https://placehold.co/300x200/6366f1/ffffff?text=1" alt="gallery" style="width: 100%; border-radius: 8px;"><img src="https://placehold.co/300x200/8b5cf6/ffffff?text=2" alt="gallery" style="width: 100%; border-radius: 8px;"><img src="https://placehold.co/300x200/a855f7/ffffff?text=3" alt="gallery" style="width: 100%; border-radius: 8px;"><img src="https://placehold.co/300x200/c084fc/ffffff?text=4" alt="gallery" style="width: 100%; border-radius: 8px;"><img src="https://placehold.co/300x200/6366f1/ffffff?text=5" alt="gallery" style="width: 100%; border-radius: 8px;"><img src="https://placehold.co/300x200/8b5cf6/ffffff?text=6" alt="gallery" style="width: 100%; border-radius: 8px;"></div>',
media: '<i class="fa fa-camera-retro fa-2x"></i>'
});
// ═══════════════════════════════════════
// CONTENT LOAD / TOGGLE / SAVE
// ═══════════════════════════════════════
var existingContent = textArea.value;
if (existingContent && existingContent.trim() !== '') {
editor.setComponents(existingContent);
}
toggleBtn.addEventListener('click', function() {
if (isVisual) {
var html = editor.getHtml();
var css = editor.getCss();
textArea.value = html + (css ? '<style>' + css + '</style>' : '');
gjsWrap.style.display = 'none';
textArea.style.display = 'block';
toggleBtn.innerHTML = '<i class="fas fa-paint-brush"></i> Use Visual Builder';
} else {
editor.setComponents(textArea.value);
textArea.style.display = 'none';
gjsWrap.style.display = 'block';
toggleBtn.innerHTML = '<i class="fas fa-code"></i> Use Code Editor';
}
isVisual = !isVisual;
});
form.addEventListener('submit', function() {
if (isVisual) {
var html = editor.getHtml();
var css = editor.getCss();
var finalOutput = html;
if (css && css.trim() !== '' && css.trim() !== '* { box-sizing: border-box; } body {margin: 0;}') {
finalOutput += '<style>' + css + '</style>';
}
textArea.value = finalOutput;
}
});
});
</script>
</section>
</body>
</html>
@@ -0,0 +1,75 @@
<!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>HTML Snippets</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">HTML Snippets</h1>
<a th:href="@{/manage/snippets/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 Snippet
</a>
</div>
<!-- Alerts -->
<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>
<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="card shadow mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" width="100%" cellspacing="0">
<thead>
<tr>
<th>Name</th>
<th>Slug</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="snippet : ${snippets}">
<td th:text="${snippet.name}">Name</td>
<td>
<code th:text="${snippet.slug}">slug</code>
<div class="small text-muted mt-1">Usage: <code>&lt;div th:utext="${@snippetService.getSnippetContent('[[${snippet.slug}]]')}"&gt;&lt;/div&gt;</code></div>
</td>
<td>
<span class="badge badge-success" th:if="${snippet.active}">Active</span>
<span class="badge badge-secondary" th:unless="${snippet.active}">Inactive</span>
</td>
<td>
<a th:href="@{/manage/snippets/{id}/edit(id=${snippet.id})}" class="btn btn-sm btn-outline-info mr-1" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form th:action="@{/manage/snippets/{id}/delete(id=${snippet.id})}" method="post" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this snippet?');">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<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(snippets)}">
<td colspan="4" class="text-center">No HTML Snippets found.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -86,6 +86,15 @@
<small class="form-text text-muted">Deactivated users cannot log in.</small>
</div>
<!-- Two-Factor Authentication (2FA) -->
<div class="form-group mt-3">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="using2FA" th:field="*{using2FA}">
<label class="custom-control-label font-weight-bold" for="using2FA">Two-Factor Authentication (2FA)</label>
</div>
<small class="form-text text-muted">Require 2FA verification for this user.</small>
</div>
<!-- Language -->
<div class="form-group mt-3">
<label for="langKey" class="font-weight-bold">Language</label>
@@ -26,7 +26,7 @@
<!-- 1. HTML Snippet Block (Custom) -->
<th:block th:if="${block.type == 'snippet'}">
<div th:insert="~{'snippets/' + ${block.data.id}}" th:remove="tag"></div>
<div th:utext="${block.data.htmlContent}" th:remove="tag"></div>
</th:block>
<!-- 2. Header Block -->
@@ -13,9 +13,12 @@
<!-- Replace with Header Fragment -->
<header th:replace="~{themes/__${activeTheme}__/header :: header}"></header>
<div style="display: flex; min-height: 400px; padding: 20px;">
<div style="flex: 3; padding-right: 20px;" layout:fragment="content"></div>
<aside style="flex: 1; background-color: #f4f4f4; padding: 15px; border-radius: 5px;">
<div th:with="isFullWidth=${(page != null and page.layout != null and page.layout.name() == 'FULL_WIDTH') or (post != null and post.layout != null and post.layout.name() == 'FULL_WIDTH')}"
th:style="${isFullWidth} ? 'min-height: 400px;' : 'display: flex; min-height: 400px; padding: 20px;'">
<div th:style="${isFullWidth} ? 'width: 100%;' : 'flex: 3; padding-right: 20px;'" layout:fragment="content"></div>
<aside th:unless="${isFullWidth}" style="flex: 1; background-color: #f4f4f4; padding: 15px; border-radius: 5px;">
<h4>Sidebar</h4>
<div th:each="widget : ${sidebarWidgets}" style="margin-bottom: 20px;">
<h5 th:text="${widget.title}" style="border-bottom: 1px solid #ddd; padding-bottom: 5px;">Widget Title</h5>
@@ -0,0 +1,184 @@
<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--homepage-hero ">
<div class="c--component c--homepage-hero">
<div class="slides-container">
<div class="image-video-container has-video">
<img src="https://www.umass.edu/sites/default/files/styles/1_1_1920x1920/public/2025-09/250616_UMASS_4515.jpg" alt="Students collaborate using a driving simulator in the UMass Center for Transportation." srcset="https://www.umass.edu/sites/default/files/styles/1_1_1920x1920/public/2025-09/250616_UMASS_4515.jpg?h=9855f42d&amp;itok=d27r8GaT 1920w">
<div class="f--ambient-video">
<video role="presentation" tabindex="-1" loop="" autoplay="" playsinline="" muted="">
<source src="https://api-files.sproutvideo.com/file/aa9ad8b61e11ebc420/de09e4945c49091f/1080.mp4" type="video/mp4"></video>
</div>
<div class="video-controls" data-once="ambientVideo">
<div class="video-controls-inner">
<button aria-labelledby="pauseBtn" class="video-button video-pause-button">
<svg height="14" viewBox="0 0 10 14" width="10" xmlns="http://www.w3.org/2000/svg">
<title id="pauseBtn">Pause Background Video</title>
<path d="m1143 711v14h-3v-14zm7 0v14h-3v-14z" fill="#fff" fill-rule="evenodd" transform="translate(-1140 -711)"></path>
</svg>
</button>
<button aria-labelledby="playBtn" class="video-button video-play-button">
<svg height="29" viewBox="0 0 29 29" width="29" xmlns="http://www.w3.org/2000/svg">
<title id="playBtn">Play Background Video</title>
<path d="m17.5 3c-7.99789474 0-14.5 6.50210526-14.5 14.5 0 7.9978947 6.50210526 14.5 14.5 14.5 7.9978947 0 14.5-6.5021053 14.5-14.5 0-7.99789474-6.5021053-14.5-14.5-14.5zm5.6763936 15.2939067-7.3503366 4.5939604c-.6635721.408352-1.5313202-.051044-1.5313202-.8422261v-9.1879207c0-.7911821.8677481-1.2761001 1.5313202-.8422261l7.3503366 4.5939604c.612528.38283.612528 1.3016221 0 1.6844521z" fill="#fff" fill-rule="evenodd" transform="translate(-3 -3)"></path>
</svg>
</button>
</div>
</div>
</div>
<div class="text-container centered">
<div class="slide-text-container-inner">
<h3>
<a href="https://www.umass.edu/gateway/why-umass">Be the Future You Want to See.</a>
</h3>
<div class="f--field f--button">
<a href="https://www.umass.edu/gateway/why-umass" class="button-secondary button-context-dark " aria-label="Learn More Learn more about UMass Amherst." data-component-id="umass_base:button">
<span class="button-text">
Learn More
</span>
</a>
</div>
<nav class="mc--menu mc--info-menu">
<ul class="menu m--menu m--info-menu">
<li class="menu-item menu-item--expanded">
<details class="utility-button-wrapper mc--info-menu-container">
<summary type="button" class="utility-button arrow-toggle information-for-toggle" aria-label="Display submenu for Information menu" aria-expanded="false" aria-haspopup="true">
Info For
<svg version="1.1" class="arrow" 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">
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#ffffff" points="7,7 0,0 16,0 "></polygon>
</svg>
</summary>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/prospective-students">Prospective Students</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/current-students">Current Students</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/parents-and-families">Parents &amp; Family</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/faculty-and-staff">Faculty &amp; Staff</a>
</li>
<li class="menu-item">
<a href="https://www.umassalumni.com/">Alumni</a>
</li>
</ul>
</div>
</details>
<div class="submenu-wrapper-desktop">
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/prospective-students">Prospective Students</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/current-students">Current Students</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/parents-and-families">Parents &amp; Family</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/faculty-and-staff">Faculty &amp; Staff</a>
</li>
<li class="menu-item">
<a href="https://www.umassalumni.com/">Alumni</a>
</li>
</ul>
</div>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div></div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,196 @@
<footer th:fragment="footer">
<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">
<section class="cc-component-container cc--footer" aria-label="Site Footer">
<div class="c--component c--footer">
<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">
<a class="logo" href="/" aria-label="Sisvietnamvn">
<div>
<img th:src="@{/theme-assets/umass/images/logo.png}" alt="Sisvietnamvn" style="height: 180px ; width: auto;" />
</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="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">
<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>
</div>
</div>
</div>
</div>
<div class="footer-bottom">
<div class="cc--component-container cc--footer-copyright ">
<div class="c--component c--footer-copyright">
<div class="f--field f--description">
<p>© 2026 University of Massachusetts Amherst</p>
</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">
<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>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
</footer>
</footer>
@@ -0,0 +1,625 @@
<header th:fragment="header">
<style>
.transparent-header #l--main-header {
position: absolute;
width: 100%;
top: 0;
left: 0;
z-index: 102;
}
</style>
<header id="l--main-header">
<h1 class="visually-hidden">The University of Massachusetts Amherst</h1>
<div class="region region-header r--region r--header">
<section data-component-id="umass_base:site-header" aria-label="Site Header">
<div data-component-id="umass_base:header-branding">
<a href="/">
<img th:src="@{/theme-assets/umass/images/logo_white.png}" alt="Sisvietnamvn" style="height: auto; max-width: 250px;" />
</a>
</div>
<div class="navigation-container">
<div id="mobile-flyout-container">
<div class="primary-navigation">
<nav class="mc--menu mc--main-menu">
<ul class="menu m--menu m--main-menu">
<li class="megamenu menu-item menu-item--expanded">
<div class="link-arrow-wrapper">
<a href="https://www.umass.edu/gateway/academics" data-drupal-link-system-path="node/11">Academics</a>
<button type="button" class="has-submenu arrow-toggle" aria-expanded="false" aria-haspopup="true" aria-label="Display Sub Menu for Academics" 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">
<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">Academics</span>
<ul class="submenu">
<li class="menu-item menu-item--expanded">
<span>Get Your Degree</span>
<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">Get Your Degree</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/undergraduate" data-drupal-link-system-path="node/106">Undergraduate</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/bdic/">Design Your Own Major</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/graduate" data-drupal-link-system-path="node/111">Graduate</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/accelerated-masters-4plus1-degree" data-drupal-link-system-path="node/42816">Accelerated Master's</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/schools-colleges" data-drupal-link-system-path="node/596">Schools &amp; Colleges</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/bachelors-degree-completion-online" title="Complete your bachelor&#39;s degree fully online at UMass Amherst." data-drupal-link-system-path="node/84461">Bachelors Degree Completion</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<span>Ways to Learn</span>
<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">Ways to Learn</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/explore-our-campuses" data-drupal-link-system-path="node/84076">Our Campuses</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/summer-winter-sessions" data-drupal-link-system-path="node/83931">Summer &amp; Winter Sessions</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/study-abroad" data-drupal-link-system-path="node/84116">Study Abroad</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/first-year-launch-programs" data-drupal-link-system-path="node/83936">First-Year Launch Programs</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/lifelong/">Pre-College &amp; Lifelong Learning</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<span>Resources</span>
<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">Resources</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/university-libraries" data-drupal-link-system-path="node/616">University Libraries</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/academic-advising" data-drupal-link-system-path="node/26366">Academic Advising</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/careers/">Career &amp; Internship Services</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/academics/explore-our-programs" data-drupal-link-system-path="node/251">Explore Our Programs</a>
</li>
</ul>
</div>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<div class="link-arrow-wrapper">
<a href="https://www.umass.edu/admissions" data-drupal-link-system-path="node/36">Admissions &amp; Aid</a>
<button type="button" class="has-submenu arrow-toggle" aria-expanded="false" aria-haspopup="true" aria-label="Display Sub Menu for Admissions &amp; Aid" 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">
<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">Admissions &amp; Aid</span>
<ul class="submenu">
<li class="menu-item menu-item--expanded">
<a href="https://www.umass.edu/admissions/undergraduate-admissions" data-drupal-link-system-path="node/266">Undergraduate Admissions</a>
<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">Undergraduate Admissions</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/admissions/undergraduate-admissions" data-drupal-link-system-path="node/266">Home</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/admissions/explore" data-drupal-link-system-path="node/626">Explore</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/admissions/visit" data-drupal-link-system-path="node/1366">Visit</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/admissions/apply" data-drupal-link-system-path="node/271">Apply</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/admissions/undergraduate-admissions/costs-aid" data-drupal-link-system-path="node/2651">Costs &amp; Aid</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/admissions/connect" data-drupal-link-system-path="node/656">Connect</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/admissions/admitted-students" data-drupal-link-system-path="node/1601">Admitted Students</a>
</li>
</ul>
</div>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/graduate/apply">Graduate Admissions</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/universityplus/admissions">Undergraduate Online Admissions</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/uww/pre-college">Pre-College Admissions</a>
</li>
<li class="menu-item menu-item--expanded">
<a href="https://www.umass.edu/financialaid" data-drupal-link-system-path="node/781">Financial Aid</a>
<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">Financial Aid</span>
<ul class="submenu">
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/financialaid/financial-aid-services" data-drupal-link-system-path="node/856">Financial Aid Services</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/financialaid/undergraduate" data-drupal-link-system-path="node/786">Undergraduate</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/financialaid/graduate" data-drupal-link-system-path="node/19936">Graduate</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/financialaid/non-degree-seeking-students" data-drupal-link-system-path="node/20531">Non-Degree</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/financialaid/university" data-drupal-link-system-path="node/991">University+</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/financialaid/student-employment" data-drupal-link-system-path="node/1056">Student Employment</a>
</li>
</ul>
</div>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<div class="link-arrow-wrapper">
<a href="https://www.umass.edu/gateway/campus-life" data-drupal-link-system-path="node/16">Campus Life</a>
<button type="button" class="has-submenu arrow-toggle" aria-expanded="false" aria-haspopup="true" aria-label="Display Sub Menu for Campus Life" 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">
<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">Campus Life</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/campus-life/living-and-dining" data-drupal-link-system-path="node/3666">Living and Dining</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/campus-life/student-activities" data-drupal-link-system-path="node/3676">Student Activities</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/campus-life/student-support-and-wellness" data-drupal-link-system-path="node/3681">Student Support and Wellness</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/campus-life/life-amherst" data-drupal-link-system-path="node/3661">Life in Amherst</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/arts-culture" data-drupal-link-system-path="node/84101">Arts &amp; Culture</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<div class="link-arrow-wrapper">
<a href="https://www.umass.edu/gateway/why-umass" data-drupal-link-system-path="node/82851">Why UMass?</a>
<button type="button" class="has-submenu arrow-toggle" aria-expanded="false" aria-haspopup="true" aria-label="Display Sub Menu for Why UMass?" 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">
<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">Why UMass?</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/why-umass/discover-ideas" data-drupal-link-system-path="node/83286">Discover Ideas</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/why-umass/choose-your-path" data-drupal-link-system-path="node/83471">Choose Your Path</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/why-umass/find-your-purpose" data-drupal-link-system-path="node/83511">Find Your Purpose</a>
</li>
<li class="menu-item menu-item--expanded">
<a href="https://www.umass.edu/gateway/why-umass/world-class-public-research-university" data-drupal-link-system-path="node/3716">About UMass Amherst</a>
<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">About UMass Amherst</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/umass-edge/about-umass-amherst/umass-history" data-drupal-link-system-path="node/3721">UMass History</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/why-umass/about-umass-amherst/umass-amherst-numbers" data-drupal-link-system-path="node/74316">UMass Amherst By the Numbers</a>
</li>
<li class="menu-item menu-item--collapsed">
<a href="https://www.umass.edu/chancellor/">Office of the Chancellor</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/umass-edge/about-umass-amherst/mascot/sam-minuteman" data-drupal-link-system-path="node/21126">Sam the Minuteman</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/umass-stories" data-drupal-link-system-path="node/55696">UMass Amherst Stories</a>
</li>
</ul>
</div>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<div class="link-arrow-wrapper">
<a href="https://www.umass.edu/gateway/research" data-drupal-link-system-path="node/79406">Research</a>
<button type="button" class="has-submenu arrow-toggle" aria-expanded="false" aria-haspopup="true" aria-label="Display Sub Menu for Research" 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">
<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">Research</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/research/student-research" data-drupal-link-system-path="node/606">Student Research</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/research/faculty-excellence" data-drupal-link-system-path="node/19971">Faculty Excellence</a>
</li>
<li class="menu-item menu-item--expanded">
<a href="https://www.umass.edu/gateway/research/innovation-impact" data-drupal-link-system-path="node/601">Innovation &amp; Impact</a>
<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">Innovation &amp; Impact</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/research/stories" data-drupal-link-system-path="node/35236">Research News &amp; Stories</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/research/timeline" data-drupal-link-system-path="gateway/research/timeline">Timeline of Research Breakthroughs</a>
</li>
</ul>
</div>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/research/centers-and-institutes" data-drupal-link-system-path="node/611">Centers and Institutes</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<div class="link-arrow-wrapper">
<a href="https://www.umass.edu/news/news-events" data-drupal-link-system-path="node/31">News &amp; Events</a>
<button type="button" class="has-submenu arrow-toggle" aria-expanded="false" aria-haspopup="true" aria-label="Display Sub Menu for News &amp; Events" 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">
<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">News &amp; Events</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/news/news-events/news-headlines" data-drupal-link-system-path="node/79501">News Headlines</a>
</li>
<li class="menu-item">
<a href="https://events.umass.edu/">Events</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/news/news-events/experts-umass" data-drupal-link-system-path="node/971">Experts at UMass</a>
</li>
<li class="menu-item menu-item--expanded">
<a href="https://www.umass.edu/news/federal-actions" data-drupal-link-system-path="node/75351">Federal Actions</a>
<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">Federal Actions</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/news/federal-actions/all-updates" data-drupal-link-system-path="node/76176">View All Federal Actions Updates</a>
</li>
</ul>
</div>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/news/news-events/olympia-drive-fire-response" data-drupal-link-system-path="node/81266">Olympia Drive Fire Response</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/chancellor/">Office of Chancellor Javier Reyes</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/institutional-effectiveness/strategic-plan">Strategic Plan 20242034</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/news/news-events/news-office-frequently-asked-questions" data-drupal-link-system-path="node/21221">Frequently Asked Questions</a>
</li>
<li class="menu-item menu-item--expanded">
<a href="https://www.umass.edu/news/news-events/inside-umass" data-drupal-link-system-path="node/21016">Inside UMass</a>
<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">Inside UMass</span>
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/news/news-events/inside-umass/submit-inside-umass-procedures-and-guidelines" data-drupal-link-system-path="node/26211">Submit to Inside UMass</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/news/news-events/news-headlines">View Most Recent News Headlines</a>
</li>
<li class="menu-item">
<a href="https://subscribe.umass.edu/subscriptioncenter/manageList/subscribeFromLink?channel_id=a2w6S000007eAfOQAU">Subscribe to Inside UMass</a>
</li>
</ul>
</div>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/news/news-events/hometown-news-requests" data-drupal-link-system-path="node/21091">Hometown News Requests</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/university-relations/toolkit/social-media/social-media-directory">Social Media Directory</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/news/news-events/video-services" data-drupal-link-system-path="node/20596">Video Services</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/news/news-events/job-opportunities-students" data-drupal-link-system-path="node/20546">Job Opportunities for Students</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/news/news-events/contact-us" data-drupal-link-system-path="node/3751">Contact Us</a>
</li>
</ul>
</div>
</li>
</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">
<svg class="search-trigger-open" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 25 25">
<title>Open Search</title>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#fff" d="M10.03,2.67c-4.07,0-7.36,3.29-7.36,7.36s3.29,7.36,7.36,7.36,7.36-3.29,7.36-7.36c-.01-4.07-3.3-7.36-7.36-7.36ZM10.03,0c5.54,0,10.03,4.49,10.03,10.03,0,2.29-.76,4.39-2.05,6.08l7,7-1.89,1.89-7.01-7.01c-1.68,1.29-3.79,2.05-6.08,2.05C4.49,20.04,0,15.55,0,10.03S4.49,0,10.03,0Z"></path>
</svg>
<svg class="search-trigger-close" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 22 22">
<title>Close Search</title>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#fff" d="M3.64,1.2l-1.21-1.2L0,2.39l1.21,1.19,7.36,7.25-7.36,7.25-1.21,1.19,2.43,2.39,1.21-1.2,7.36-7.24,7.36,7.24,1.22,1.2,2.42-2.39-1.21-1.19-7.36-7.25,7.36-7.25,1.21-1.19-2.42-2.39-1.22,1.2-7.36,7.24L3.64,1.2Z"></path>
</svg>
</button>
<button id="mobile-hamburger" class="mobile-hamburger hamburger--squeeze" aria-label="Open Main Navigation" data-once="site-header-hamburger">
<span class="hamburger-box">
<span class="hamburger-inner"></span>
</span>
</button>
</div>
</section>
<div data-component-id="umass_base:header-search">
<div id="search-flyout-container">
<div class="search-flyout">
<div data-component-id="umass_base:header-branding">
<a href="/">
<img th:src="@{/theme-assets/umass/images/logo.png}" alt="Sisvietnamvn" style="height: 60px; width: auto;" />
</a>
</div>
<div>
<form autocomplete="on" name="search-form" action="https://www.umass.edu/search" method="GET">
<div>
<label class="visually-hidden" for="search">Search UMass</label>
<input type="text" name="q" id="search" maxlength="50" placeholder="Search UMass" size="50" class="ally-focus-within">
</div>
<button type="submit" class="icon-search" aria-label="Search">
<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 19 19" enable-background="new 0 0 19 19" xml:space="preserve" width="19" height="19">
<title>Search</title>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#151515" d="M7.62,2.03c-3.09,0-5.59,2.5-5.59,5.59
s2.5,5.59,5.59,5.59s5.59-2.5,5.59-5.59C13.2,4.53,10.7,2.03,7.62,2.03z M7.62,0c4.21,0,7.62,3.41,7.62,7.62
c0,1.74-0.58,3.34-1.56,4.62L19,17.56L17.56,19l-5.33-5.33c-1.28,0.98-2.88,1.56-4.62,1.56C3.41,15.23,0,11.82,0,7.62S3.41,0,7.62,0
z"></path>
</svg>
</button>
</form>
<ul class="menu">
<li class="menu-item">
<a href="https://www.umass.edu/admissions/visit">Campus tours</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/financialaid/undergraduate-costs">Tuition</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/financialaid">Financial aid</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/admissions/apply">How to apply</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/umass-stories/study-abroad-umass">Study abroad</a>
</li>
</ul>
</div>
<button id="search-drawer-trigger-active" aria-label="Open Search" data-once="header-search">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 22 22">
<title>Close Search</title>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#fff" d="M3.64,1.2l-1.21-1.2L0,2.39l1.21,1.19,7.36,7.25-7.36,7.25-1.21,1.19,2.43,2.39,1.21-1.2,7.36-7.24,7.36,7.24,1.22,1.2,2.42-2.39-1.21-1.19-7.36-7.25,7.36-7.25,1.21-1.19-2.42-2.39-1.22,1.2-7.36,7.24L3.64,1.2Z"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</header>
</header>
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>UMass Amherst Theme</title>
<!-- UMass Amherst Assets -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
<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}">
<!-- Theme Customizer CSS Output -->
<style th:utext="${themeCss}"></style>
<!-- wp_head hook -->
<th:block th:utext="${hookManager.doActionAndReturn('wp_head')}"></th:block>
</head>
<body class="umass-platform-homepage path-frontpage page-node-type-homepage homepage transparent-header">
<div class="dialog-off-canvas-main-canvas" data-off-canvas-main-canvas="">
<!-- Include Header Fragment -->
<header th:replace="~{themes/__${activeTheme}__/header :: header}"></header>
<main id="main-content">
<div class="l--content">
<div class="content">
<div class="region region-content r--region r--content">
<!-- Content Area -->
<div th:with="isFullWidth=${(page != null and page.layout != null and page.layout.name() == 'FULL_WIDTH') or (post != null and post.layout != null and post.layout.name() == 'FULL_WIDTH')}" th:style="${isFullWidth} ? 'min-height: 400px;' : 'display: flex; min-height: 400px; padding: 20px; max-width: 1400px; margin: 0 auto;'">
<div th:style="${isFullWidth} ? 'width: 100%;' : 'flex: 3; padding-right: 20px;'" layout:fragment="content"></div>
<aside th:unless="${isFullWidth}" style="flex: 1; padding: 15px; border-radius: 5px;">
<h4>Sidebar</h4>
<div th:each="widget : ${sidebarWidgets}" style="margin-bottom: 20px;">
<h5 th:text="${widget.title}" style="border-bottom: 1px solid #ddd; padding-bottom: 5px;">Widget Title</h5>
<div th:if="${widget.type == 'HTML'}" th:utext="${widget.content}"></div>
<div th:if="${widget.type == 'TEXT'}" th:text="${widget.content}"></div>
</div>
<div th:if="${sidebarWidgets == null || sidebarWidgets.empty}">
<p>No widgets added to sidebar.</p>
</div>
</aside>
</div>
</div>
</div>
</div>
</main>
<!-- Include Footer Fragment -->
<footer th:replace="~{themes/__${activeTheme}__/footer :: footer}"></footer>
</div>
<!-- UMass Scripts -->
<script th:src="@{/theme-assets/umass/js/js_aQtUyeGxehNR84AlGzGB1VfMu1Wn3lqHxvL8rocj1EU.js}"></script>
<!-- wp_footer hook -->
<th:block th:utext="${hookManager.doActionAndReturn('wp_footer')}"></th:block>
</body>
</html>
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class update_theme {
public static void main(String[] args) {
String url = "jdbc:oracle:thin:@localhost:1521/sisvietnam";
String user = "sisvietnam";
String password = "sisvietnam";
try (Connection conn = DriverManager.getConnection(url, user, password)) {
// Let's first see what's in sis_setting
try (PreparedStatement checkStmt = conn.prepareStatement("SELECT ID, THEME FROM sis_setting")) {
ResultSet rs = checkStmt.executeQuery();
while (rs.next()) {
System.out.println("ID: " + rs.getLong("ID") + ", THEME: " + rs.getString("THEME"));
}
} catch (Exception e) {
System.out.println("Error reading theme: " + e.getMessage());
}
// Update theme
String updateQuery = "UPDATE sis_setting SET THEME = 'umass'";
try (PreparedStatement updateStmt = conn.prepareStatement(updateQuery)) {
int rows = updateStmt.executeUpdate();
System.out.println("Rows updated: " + rows);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}