feat: add UMass Amherst theme support, implement HTML snippet management system, and update page rendering logic
This commit is contained in:
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
+1
-1
@@ -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()
|
||||
|
||||
+27
-1
@@ -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");
|
||||
|
||||
+12
-1
@@ -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);
|
||||
|
||||
+66
@@ -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";
|
||||
}
|
||||
}
|
||||
+18
@@ -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();
|
||||
}
|
||||
|
||||
+10
-25
@@ -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;
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
+82
@@ -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<>());
|
||||
}
|
||||
}
|
||||
+65
@@ -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);
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -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() {
|
||||
|
||||
+34
@@ -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);
|
||||
|
||||
+12
@@ -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;}
|
||||
+24
File diff suppressed because one or more lines are too long
+6
@@ -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 |
+2
@@ -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">×</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">×</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><div th:utext="${@snippetService.getSnippetContent('[[${snippet.slug}]]')}"></div></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&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 & Family</a>
|
||||
|
||||
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="https://www.umass.edu/gateway/info/faculty-and-staff">Faculty & 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 & Family</a>
|
||||
|
||||
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="https://www.umass.edu/gateway/info/faculty-and-staff">Faculty & 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 & Colleges</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="https://www.umass.edu/gateway/bachelors-degree-completion-online" title="Complete your bachelor's degree fully online at UMass Amherst." data-drupal-link-system-path="node/84461">Bachelor’s 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 & 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 & 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 & 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 & Aid</a>
|
||||
<button type="button" class="has-submenu arrow-toggle" aria-expanded="false" aria-haspopup="true" aria-label="Display Sub Menu for Admissions & 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 & 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 & 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 & 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 & 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 & 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 & 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 & Events</a>
|
||||
<button type="button" class="has-submenu arrow-toggle" aria-expanded="false" aria-haspopup="true" aria-label="Display Sub Menu for News & 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 & 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 2024–2034</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.
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user