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

This commit is contained in:
2026-06-28 16:25:51 +07:00
parent 38d6b8234a
commit 0326535eb6
64 changed files with 13477 additions and 1603 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
Dưới đây là danh sách đầy đủ toàn bộ các chức năng của trang quản trị WordPress (WordPress Admin Dashboard), được chia làm hai phần chính: Giao diện người dùng (UI/UX) và Các hàm PHP/APIs dành cho Lập trình viên để phát triển/tùy biến trang admin.
PHẦN 1: CÁC CHỨC NĂNG TRÊN GIAO DIỆN ADMIN (UI MENUS)
PHẦN 1: CÁC CHỨC NĂNG TRÊN GISAO DIỆN ADMIN (UI MENUS)
Đây là các chức năng mặc định xuất hiện ở thanh Menu bên trái (Sidebar) của trang quản trị:
Menu chính Menu con Chức năng chi tiết
File diff suppressed because one or more lines are too long
@@ -0,0 +1,900 @@
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@@ -0,0 +1 @@
/* @license GPL-2.0-or-later https://www.drupal.org/licensing/faq */
@@ -0,0 +1,11 @@
/* @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;}
.cc_banner-wrapper{z-index:9001;position:relative}.cc_container .cc_btn{cursor:pointer;text-align:center;font-size:0.6em;transition:font-size 200ms;line-height:1em}.cc_container .cc_message{font-size:0.6em;transition:font-size 200ms;margin:0;padding:0;line-height:1.5em}.cc_container .cc_logo{display:none;text-indent:-1000px;overflow:hidden;width:100px;height:22px;background-size:cover;background-image:url(//cdnjs.cloudflare.com/ajax/libs/cookieconsent2/1.0.10/logo.png);opacity:0.9;transition:opacity 200ms}.cc_container .cc_logo:hover,.cc_container .cc_logo:active{opacity:1}@media screen and (min-width:500px){.cc_container .cc_btn{font-size:0.8em}.cc_container .cc_message{font-size:0.8em}}@media screen and (min-width:768px){.cc_container .cc_btn{font-size:1em}.cc_container .cc_message{font-size:1em;line-height:1em}}@media screen and (min-width:992px){.cc_container .cc_message{font-size:1em}}@media print{.cc_banner-wrapper,.cc_container{display:none}}.cc_container{position:fixed;left:0;right:0;bottom:0;overflow:hidden;padding:10px}.cc_container .cc_btn{padding:8px 10px;background-color:#f1d600;cursor:pointer;transition:font-size 200ms;text-align:center;font-size:0.6em;display:block;width:33%;margin-left:10px;float:right;max-width:120px}.cc_container .cc_message{transition:font-size 200ms;font-size:0.6em;display:block}@media screen and (min-width:500px){.cc_container .cc_btn{font-size:0.8em}.cc_container .cc_message{margin-top:0.5em;font-size:0.8em}}@media screen and (min-width:768px){.cc_container{padding:15px 30px 15px}.cc_container .cc_btn{font-size:1em;padding:8px 15px}.cc_container .cc_message{font-size:1em}}@media screen and (min-width:992px){.cc_container .cc_message{font-size:1em}}.cc_container{background:#222;color:#fff;font-size:17px;font-family:"Helvetica Neue Light","HelveticaNeue-Light","Helvetica Neue",Calibri,Helvetica,Arial;box-sizing:border-box}.cc_container ::-moz-selection{background:#ff5e99;color:#fff;text-shadow:none}.cc_container .cc_btn,.cc_container .cc_btn:visited{color:#000;background-color:#f1d600;transition:background 200ms ease-in-out,color 200ms ease-in-out,box-shadow 200ms ease-in-out;-webkit-transition:background 200ms ease-in-out,color 200ms ease-in-out,box-shadow 200ms ease-in-out;border-radius:5px;-webkit-border-radius:5px}.cc_container .cc_btn:hover,.cc_container .cc_btn:active{background-color:#fff;color:#000}.cc_container a,.cc_container a:visited{text-decoration:none;color:#31a8f0;transition:200ms color}.cc_container a:hover,.cc_container a:active{color:#b2f7ff}@-webkit-keyframes slideUp{0%{-webkit-transform:translateY(66px);transform:translateY(66px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes slideUp{0%{-webkit-transform:translateY(66px);-ms-transform:translateY(66px);transform:translateY(66px)}100%{-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.cc_container,.cc_message,.cc_btn{animation-duration:0.8s;-webkit-animation-duration:0.8s;-moz-animation-duration:0.8s;-o-animation-duration:0.8s;-webkit-animation-name:slideUp;animation-name:slideUp}
span.ext{width:10px;height:10px;padding-right:12px;text-decoration:none;background:url(/modules/contrib/extlink/images/extlink_s.png) 2px center no-repeat;}span.mailto{width:10px;height:10px;padding-right:12px;text-decoration:none;background:url(/modules/contrib/extlink/images/extlink_s.png) -20px center no-repeat;}span.tel{width:10px;height:10px;padding-right:12px;text-decoration:none;background:url(/modules/contrib/extlink/images/extlink_s.png) -42px center no-repeat;}svg.ext{width:14px;height:14px;fill:#727272;font-weight:900;}svg.mailto,svg.tel{width:14px;height:14px;fill:#727272;}[data-extlink-placement='prepend'],[data-extlink-placement='before']{padding-right:0.2rem;}[data-extlink-placement='append'],[data-extlink-placement='after']{padding-left:0.2rem;}svg.ext path,svg.mailto path,svg.tel path{stroke:#727272;stroke-width:3;}@media print{svg.ext,svg.mailto,svg.tel,span.ext,span.mailto,span.tel{display:none;padding:0;}}.extlink i{padding-left:0.2em;}.extlink-nobreak{white-space:nowrap;}
.paragraph--unpublished{background-color:#fff4f4;}
@@ -0,0 +1,43 @@
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
// Drupal's global gtag ID. For more info: https://developers.google.com/tag-platform/devguides/gtag-integration
gtag('set', 'developer_id.dMDhkMT', true);
(function (drupalSettings) {
if (!drupalSettings.gtag) {return;}
const config = drupalSettings.gtag;
if (config.consentMode === true) {
gtag('consent', 'default', {
ad_storage: 'denied',
analytics_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
wait_for_update: 500,
});
}
if (config.tagId.length !== 0) {
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${config.tagId}`;
script.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(script);
}
const additionalConfigInfo = config.additionalConfigInfo || [];
if (additionalConfigInfo.length === 0) {
gtag('config', config.tagId);
} else {
gtag('config', config.tagId, additionalConfigInfo);
}
const otherIds = config.otherIds || [];
otherIds.forEach((id) => gtag('config', id));
const events = config.events || [];
events.forEach((event) => gtag('event', event.name, event.data));
})(drupalSettings);
@@ -0,0 +1,31 @@
const dl = drupalSettings.gtm ? drupalSettings.gtm.settings.data_layer : 'dataLayer';
window[dl] = window[dl] || [];
(function (drupalSettings) {
if (!drupalSettings.gtm) {return;}
const config = drupalSettings.gtm;
window[dl].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
const gtmSettings = config.settings;
if (gtmSettings.include_classes === true) {
window[dl].push({
'gtm.allowlist': gtmSettings.allowlist_classes ?? [],
'gtm.blocklist': gtmSettings.blocklist_classes ?? [],
});
}
let gtm_environment = '';
if (gtmSettings.include_environment === true) {
const gtm_auth = gtmSettings.environment_token ?? '';
const gtm_preview = gtmSettings.environment_id ?? '';
gtm_environment = `&gtm_auth=${gtm_auth}&gtm_preview=${gtm_preview}&gtm_cookies_win=x`;
}
config.tagIds.forEach(function (tagId) {
const script = document.createElement('script');
script.async = true;
const dLink = dl != 'dataLayer' ? `&l=${dl}` : '';
script.src = `https://www.googletagmanager.com/gtm.js?id=${tagId}${gtm_environment}${dLink}`;
script.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(script);
});
})(drupalSettings);
@@ -0,0 +1,14 @@
/* @license GPL-2.0-or-later https://www.drupal.org/licensing/faq */
(function(window,$,Drupal){'use strict';window.DP=window.DP||window.Drupal||{};window.DP.behaviors=window.DP.behaviors||{};if(window.DP._behaviorBootstrapAttached)return;window.DP._behaviorBootstrapAttached=true;Drupal.behaviors.dpBehaviorBootstrap={attach:function(context){if(context!==document)return;$.each(window.DP.behaviors,function(key,behavior){if(behavior&&typeof behavior.attach==='function')behavior.attach(document);});}};}(window,jQuery,Drupal));;
(function(Drupal,$,once){'use strict';var initHoverIntent=function(){var menu=this;$('ul.m--main > li',menu).hoverIntent({over:function(){$(this).addClass('hover');},out:function(){$(this).removeClass('hover');},interval:80});};var initMainMenu=function(){var $menuItems=$('.cc--menu-main ul.m--menu > li');ally.style.focusWithin();$menuItems.on('click',function(){$menuItems.removeClass('ally-focus-within');if(whatInput.ask()==='touch')$(this).addClass('ally-focus-within');});};window.DP.behaviors.mainMenu={attach:function(context){$(once('mainMenu','.nav-container .cc--menu-main',context)).each(function(){initHoverIntent.apply(this);initMainMenu.apply();});}};})(Drupal,jQuery,once);;
(function(Drupal,$,once){'use strict';var $hamburger;var $mobileMenu;var $arrowToggles;var $backButtons;var $subMenus;var breakpoint=window.matchMedia('(min-width: 1200px)');var initMobileMenu=function(){$hamburger=$(this);$mobileMenu=$('.cc--menu-mobile');$arrowToggles=$mobileMenu.find('.arrow-toggle');$subMenus=$mobileMenu.find('.submenu-wrapper');$backButtons=$mobileMenu.find('.button-back');$hamburger.on('click',function(e){e.preventDefault();toggleMobileMenu(this);});$arrowToggles.on('click',function(e){e.preventDefault();var $subMenu=$(this).parents('.menu-item').find('.submenu-wrapper');$subMenu.attr('aria-hidden','false');$subMenu.show();$subMenu.removeClass('is-hidden').addClass('is-visible');$mobileMenu.addClass('is-hidden');});$backButtons.on('click',function(e){e.preventDefault();var $this=$(this);$this.parents('.submenu-wrapper').attr('aria-hidden','true');$this.parents('.submenu-wrapper').addClass('is-hidden');$this.parents('.submenu-wrapper').removeClass('is-visible');$mobileMenu.removeClass('is-hidden');});checkBreakpoint();breakpoint.addListener(checkBreakpoint);};var checkBreakpoint=function(){if(breakpoint.matches){$hamburger.removeClass('is-active');$mobileMenu.removeAttr('style');$('body').removeClass('mobile-menu-is-active');hideAllSubmenus();}};var toggleMobileMenu=function(el){$('body').toggleClass('mobile-menu-is-active');$(el).toggleClass('is-active');$mobileMenu.fadeToggle(function(){if(!$('body').hasClass('mobile-menu-is-active'))hideAllSubmenus();});};var hideAllSubmenus=function(){$subMenus.removeClass('is-visible');$mobileMenu.removeClass('is-hidden');};window.DP.behaviors.mobileMenu={attach:function(context){$(once('mobileMenu','.hamburger',context)).each(function(){initMobileMenu.apply(this);});}};}(Drupal,jQuery,once));;
(function($){'use strict';var $searchIcon;var $body=$('body');var $searchFormContainer=$('.cc--header .cc--header-search');var $searchInput=$searchFormContainer.find('input');var $closeIcon=$searchFormContainer.find('.button-close');var initSearchForm=function(){$searchIcon=$(this);$searchFormContainer.attr({'aria-hidden':'true'});$searchIcon.on('click',function(e){e.preventDefault();openSearch();});$closeIcon.on('click',function(e){e.preventDefault();closeSearch();});$searchFormContainer.on('keydown',function(ev){if(ev.keyCode===9){var inputs=$searchFormContainer.find('input, button').filter(':visible').not(':disabled');if(!ev.shiftKey){if(inputs[inputs.length-1]===ev.target){ev.preventDefault();inputs.first().focus();}}else{if(inputs[0]===ev.target){ev.preventDefault();inputs.last().focus();}}}if(ev.keyCode===27){ev.preventDefault();closeSearch();}});};var openSearch=function(){$searchFormContainer.addClass('is-open').attr('aria-hidden','false');$body.addClass('search-open');$searchInput.first().focus();};var closeSearch=function(){$searchFormContainer.removeClass('is-open').attr('aria-hidden','true');$body.removeClass('search-open');};window.DP.behaviors.searchForm={attach:function(context){$(once('searchForm','.utility-nav-container .search-button',context)).each(function(){initSearchForm.apply(this);});}};}(jQuery));;
/**
* what-input - A global utility for tracking the current input method (mouse, keyboard or touch).
* @version v5.2.3
* @link https://github.com/ten1seven/what-input
* @license MIT
*/
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("whatInput",[],t):"object"==typeof exports?exports.whatInput=t():e.whatInput=t()}(this,function(){return i={},n.m=o=[function(e,t){"use strict";e.exports=function(){if("undefined"==typeof document||"undefined"==typeof window)return{ask:function(){return"initial"},element:function(){return null},ignoreKeys:function(){},specificKeys:function(){},registerOnChange:function(){},unRegisterOnChange:function(){}};var t=document.documentElement,n=null,a="initial",u=a,o=Date.now();try{window.sessionStorage.getItem("what-input")&&(a=window.sessionStorage.getItem("what-input")),window.sessionStorage.getItem("what-intent")&&(u=window.sessionStorage.getItem("what-intent"))}catch(e){}var d=["button","input","select","textarea"],i=[],c=[16,17,18,91,93],w=[],p={keydown:"keyboard",keyup:"keyboard",mousedown:"mouse",mousemove:"mouse",MSPointerDown:"pointer",MSPointerMove:"pointer",pointerdown:"pointer",pointermove:"pointer",touchstart:"touch",touchend:"touch"},r=!1,s={x:null,y:null},f={2:"touch",3:"touch",4:"mouse"},l=!1;try{var e=Object.defineProperty({},"passive",{get:function(){l=!0}});window.addEventListener("test",null,e)}catch(e){}var h=function(){var e=!!l&&{passive:!0};window.PointerEvent?(window.addEventListener("pointerdown",m),window.addEventListener("pointermove",y)):window.MSPointerEvent?(window.addEventListener("MSPointerDown",m),window.addEventListener("MSPointerMove",y)):(window.addEventListener("mousedown",m),window.addEventListener("mousemove",y),"ontouchstart"in window&&(window.addEventListener("touchstart",m,e),window.addEventListener("touchend",m))),window.addEventListener(S(),y,e),window.addEventListener("keydown",m),window.addEventListener("keyup",m),window.addEventListener("focusin",g),window.addEventListener("focusout",E)},m=function(e){var t=e.which,n=p[e.type];"pointer"===n&&(n=L(e));var o=!w.length&&-1===c.indexOf(t),i=w.length&&-1!==w.indexOf(t),r="keyboard"===n&&t&&(o||i)||"mouse"===n||"touch"===n;if(x(n)&&(r=!1),r&&a!==n){a=n;try{window.sessionStorage.setItem("what-input",a)}catch(e){}v("input")}if(r&&u!==n){var s=document.activeElement;if(s&&s.nodeName&&(-1===d.indexOf(s.nodeName.toLowerCase())||"button"===s.nodeName.toLowerCase()&&!O(s,"form"))){u=n;try{window.sessionStorage.setItem("what-intent",u)}catch(e){}v("intent")}}},v=function(e){t.setAttribute("data-what"+e,"input"===e?a:u),b(e)},y=function(e){var t=p[e.type];if("pointer"===t&&(t=L(e)),M(e),(!r&&!x(t)||r&&"wheel"===e.type||"mousewheel"===e.type||"DOMMouseScroll"===e.type)&&u!==t){u=t;try{window.sessionStorage.setItem("what-intent",u)}catch(e){}v("intent")}},g=function(e){e.target.nodeName?(n=e.target.nodeName.toLowerCase(),t.setAttribute("data-whatelement",n),e.target.classList&&e.target.classList.length&&t.setAttribute("data-whatclasses",e.target.classList.toString().replace(" ",","))):E()},E=function(){n=null,t.removeAttribute("data-whatelement"),t.removeAttribute("data-whatclasses")},L=function(e){return"number"==typeof e.pointerType?f[e.pointerType]:"pen"===e.pointerType?"touch":e.pointerType},x=function(e){var t=Date.now(),n="mouse"===e&&"touch"===a&&t-o<200;return o=t,n},S=function(){return"onwheel"in document.createElement("div")?"wheel":void 0!==document.onmousewheel?"mousewheel":"DOMMouseScroll"},b=function(e){for(var t=0,n=i.length;t<n;t++)i[t].type===e&&i[t].fn.call(void 0,"input"===e?a:u)},M=function(e){s.x!==e.screenX||s.y!==e.screenY?(r=!1,s.x=e.screenX,s.y=e.screenY):r=!0},O=function(e,t){var n=window.Element.prototype;if(n.matches||(n.matches=n.msMatchesSelector||n.webkitMatchesSelector),n.closest)return e.closest(t);do{if(e.matches(t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null};return"addEventListener"in window&&Array.prototype.indexOf&&(p[S()]="mouse",h(),v("input"),v("intent")),{ask:function(e){return"intent"===e?u:a},element:function(){return n},ignoreKeys:function(e){c=e},specificKeys:function(e){w=e},registerOnChange:function(e,t){i.push({fn:e,type:t||"input"})},unRegisterOnChange:function(e){var t=function(e){for(var t=0,n=i.length;t<n;t++)if(i[t].fn===e)return t}(e);!t&&0!==t||i.splice(t,1)}}}()}],n.c=i,n.p="",n(0);function n(e){if(i[e])return i[e].exports;var t=i[e]={exports:{},id:e,loaded:!1};return o[e].call(t.exports,t,t.exports,n),t.loaded=!0,t.exports}var o,i});
;
@@ -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);})();;
@@ -0,0 +1 @@
!function(i,r){function o(){}var n,e,t,c,a,d,l,s,u,f,p,y,m,v,h,_,g,E,k,S="addtoany",b=".bycdb5qo",L=i.head;"function"==typeof[].indexOf&&L&&(r.a2a=r.a2a||{},n=r.a2a_config=r.a2a_config||{},_=(e=i.currentScript instanceof HTMLScriptElement?i.currentScript:null)&&e.src?e.src:"",t=e&&e.nonce?e.nonce:null,c=r.a2a.nonce=function(e){t&&e.setAttribute("nonce",t)},a=e&&!e.async&&!e.defer,NodeList&&NodeList.prototype.forEach&&(r.a2a.init=function(e,t){void 0===t&&(t=n);var a=":not([data-a2a-url]):not(.a2a_target)";i.querySelectorAll(".a2a_dd"+a+",.a2a_kit"+a).forEach(function(e){e.matches(".a2a_kit .a2a_dd");e.a2a_index||null!==e.getAttribute("data-a2a-url")||e.matches(".a2a_kit .a2a_dd")||(e.dataset.a2aUrl=t.linkurl||"",t.linkname&&(e.dataset.a2aTitle=t.linkname))}),delete n.linkurl,delete n.linkname},r.a2a_init=r.a2a.init,a)&&r.a2a.init("page",{linkurl:n.linkurl,linkname:n.linkname}),r.a2a.page||(r.a2a.page=!0,d=[],["init_all","svg_css"].forEach(function(a){r.a2a[a]=function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];d.push([a,e])}}),s=(l=n.static_server)?l+"/":"https://static.addtoany.com/menu/",k=_&&-1!==_.split("/")[2].indexOf(S),u=(k=(u=!l&&k?_:s).match(/^[^?#]+\//))?k[0]:u,null!=(_=r.trustedTypes)&&_.createPolicy&&(f=r.a2a.ttp=trustedTypes.createPolicy("addtoany",{createHTML:function(e){return e},createScript:function(e){return e},createScriptURL:function(e){return e}})),p=function(e,t,a){void 0===e&&(e=u+"eso"+b+".js"),void 0===t&&(t=!1);var n=i.createElement((a=void 0===a?!1:a)?"link":"script");a?(a="preload",n.href=e,n.rel=t?"module"+a:a,t||(n.as="script")):(n.src=f?f.createScriptURL(e):e,t&&(n.type="module",n.onerror=function(){return p()})),c(n),L.appendChild(n)},y=function(){var e=u+(l?"":"modules/");p(e+"core"+b+".js",!0);var t=i.createElement("link").relList.supports("modulepreload");n.overlays&&n.overlays.length&&t&&p(e+"overlays"+b+".js",!0,!0),y=o},v="a2a_sm_ifr",h=function(){var e,t,a,n;t="a2a_menu_container",e=i.getElementById(t),r.a2a.main=m=e||i.createElement("div"),m.id!=t&&(i.getElementById(S)||(m.id=S),m.style.position="static",i.body.insertBefore(m,null)),r.addEventListener("message",function(e){var t=e.origin;t&&".addtoany.com"!==t.substr(-13)||"object"==typeof(t=e.data)&&t.a2a&&(t.h1&&(r.a2a.h1=!0),"function"==typeof(e=r.a2a.userServices)?e(t.user_services):r.a2a.userServices=t.user_services,i.getElementById(v).style.display="none")}),e=i.createElement("iframe"),t=i.createElement("div"),a=e.style,n=t.style,e.id=v,a.width=a.height=n.width=n.height="1px",a.top=a.left=a.border="0",a.position=n.position="absolute",a.zIndex=n.zIndex="100000",e.title="AddToAny Utility Frame",e.setAttribute("aria-hidden","true"),e.src="https://static.addtoany.com/menu/sm.25.html#type=core&event=load",n.top="0",n.visibility="hidden",m.insertBefore(t,null),t.insertBefore(e,null),h=o},i.body&&h(),"function"==typeof(k="".matchAll)&&k.toString().includes("[native code]")?("loading"!==i.readyState&&y(),_=s+(l?"":"svg/"),i.querySelector('.a2a_dd:empty,.a2a_kit [class*="a2a_button_"]:empty')&&r.a2a.h1&&p(_+"icons.39.svg.js",!1,!0)):p(),g=function(e){var t=r.a2a.core;"function"!=typeof t||e?e?e():r.a2a.core=function(e){return g(e)}:t()},k=function(){E||(E=!0,y(),h(),g(),d.forEach(function(e){var t;(t=r.a2a)[e[0]].apply(t,e[1])}))},!(a&&L.contains(e)||!i.body)||"loading"!==i.readyState?k():(i.addEventListener("readystatechange",k),i.addEventListener("DOMContentLoaded",k))))}(document,window);
@@ -0,0 +1,4 @@
<!DOCTYPE html>
<!-- saved from url=(0064)https://static.addtoany.com/menu/sm.25.html#type=core&event=load -->
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>A2A</title><meta name="robots" content="noindex"><script>
!function(e){var o=document.cookie,t=0<o.length;var n,a={feed:[],page:[]};t&&["feed","page"].forEach(function(e){var t=e+"_services",n=o.indexOf(t+"=");-1!=n&&(-1==(t=o.indexOf(";",n=n+t.length+1))&&(t=o.length),n=decodeURIComponent(o.substring(n,t)),a[e]=n.split(","))}),n={a2a:!0,h1:(t=!1,"function"==typeof performance.getEntriesByType&&(n=(n=performance.getEntriesByType("navigation")[0])&&n.nextHopProtocol?n.nextHopProtocol:"",t=/^http\/1/.test(n)),t),user_services:{feed:a.feed,page:a.page}},e.postMessage&&e.parent.postMessage(n,"*")}(window);</script></head><body style="background-color:transparent"></body></html>
+5 -1
View File
@@ -323,7 +323,11 @@ We have implemented a WordPress-like static pages management feature at `/manage
```powershell
./gradlew bootRun
```
3. Open your browser and navigate to: [http://localhost:8080/manage/pages](http://localhost:8080/manage/pages)
3. Open your browser and navigate to the Page Management dashboard: [http://localhost:8080/manage/pages](http://localhost:8080/manage/pages)
4. Navigate to the Posts Management dashboards:
- **All Posts**: [http://localhost:8080/manage/posts](http://localhost:8080/manage/posts)
- **Categories**: [http://localhost:8080/manage/categories](http://localhost:8080/manage/categories)
- **Tags**: [http://localhost:8080/manage/tags](http://localhost:8080/manage/tags)
### 2. Form Fields & Features
When creating or editing a page (`/manage/pages/new` or `/manage/pages/{id}/edit`):
-1
View File
@@ -55,7 +55,6 @@ dependencies {
implementation libs.springdoc.openapi.starter.webmvc.api
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3"
implementation "com.oracle.database.jdbc:ojdbc8"
implementation "org.springframework.boot:spring-boot-h2console"
implementation "org.springframework.boot:spring-boot-jackson2"
implementation "org.springframework.boot:spring-boot-starter"
implementation "org.springframework.boot:spring-boot-starter-actuator"
@@ -14,13 +14,12 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.h2console.autoconfigure.H2ConsoleAutoConfiguration;
import org.springframework.boot.liquibase.autoconfigure.LiquibaseProperties;
import org.springframework.core.env.Environment;
import tech.jhipster.config.DefaultProfileUtil;
import tech.jhipster.config.JHipsterConstants;
@SpringBootApplication(exclude = { H2ConsoleAutoConfiguration.class })
@SpringBootApplication
@EnableConfigurationProperties({ LiquibaseProperties.class, ApplicationProperties.class })
public class SisvietnamvnApp {
@@ -5,7 +5,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.h2console.autoconfigure.H2ConsoleProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@@ -14,13 +13,11 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import tech.jhipster.config.JHipsterConstants;
import tech.jhipster.config.h2.H2ConfigurationHelper;
@Configuration
@EnableJpaRepositories({ "com.sisvietnamvn.web.repository" })
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@EnableTransactionManagement
@EnableConfigurationProperties(H2ConsoleProperties.class)
public class DatabaseConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(DatabaseConfiguration.class);
@@ -30,33 +27,4 @@ public class DatabaseConfiguration {
public DatabaseConfiguration(Environment env) {
this.env = env;
}
/**
* Open the TCP port for the H2 database, so it is available remotely.
*
* @return the H2 database TCP server.
* @throws SQLException if the server failed to start.
*/
@Bean(initMethod = "start", destroyMethod = "stop")
@Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)
@ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true")
public Object h2TCPServer() throws SQLException {
String port = getValidPortForH2();
LOG.debug("H2 database is available on port {}", port);
return H2ConfigurationHelper.createServer(port);
}
private String getValidPortForH2() {
var port = Integer.parseInt(env.getProperty("server.port"));
if (port < 10000) {
port = 10000 + port;
} else {
if (port < 63536) {
port = port + 2000;
} else {
port = port - 2000;
}
}
return String.valueOf(port);
}
}
@@ -46,7 +46,7 @@ public class SecurityConfiguration {
authz
.requestMatchers(HttpMethod.GET, "/", "/about", "/flex-finish", "/tin-tuc", "/tin-tuc/**", "/lien-he",
"/manage/**", "/css/**", "/images/**", "/js/**", "/UMass*/**", "/Undergraduate*/**",
"/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**", "/uploads/**", "/api/manage/snippets/**", "/page/**",
"/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")
.permitAll()
.requestMatchers(HttpMethod.POST, "/manage/**", "/api/manage/media/upload").permitAll()
@@ -0,0 +1,54 @@
package com.sisvietnamvn.web.controller;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.repository.PostRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
@Controller
@RequestMapping("/post")
public class PostController {
private final PostRepository postRepository;
public PostController(PostRepository postRepository) {
this.postRepository = postRepository;
}
@GetMapping("/{slug}")
public String viewPost(@PathVariable String slug, Model model) {
Post post = postRepository.findBySlug(slug)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
// Only allow viewing published posts (unless admin, but for now just check published)
if (post.getStatus() != PageStatus.PUBLISHED) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
model.addAttribute("post", post);
// Safely format the Instant to avoid Thymeleaf parsing errors
if (post.getCreatedDate() != null) {
String formattedDate = java.time.format.DateTimeFormatter.ofPattern("MMMM dd, yyyy")
.withZone(java.time.ZoneId.systemDefault())
.format(post.getCreatedDate());
model.addAttribute("formattedDate", formattedDate);
}
switch (post.getLayout()) {
case SIDEBAR:
return "posts/sidebar";
case FULL_WIDTH:
return "posts/full-width";
case STANDARD:
default:
return "posts/standard";
}
}
}
@@ -0,0 +1,141 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.Category;
import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.service.CategoryService;
import jakarta.validation.Valid;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Controller for managing post Categories in the admin panel.
* Uses a WordPress-style split layout: form on the left, table on the right.
*/
@Controller
@RequestMapping("/manage/posts/categories")
public class ManageCategoryController {
private static final Logger LOG = LoggerFactory.getLogger(ManageCategoryController.class);
private final CategoryService categoryService;
private final HookManager hookManager;
public ManageCategoryController(CategoryService categoryService, HookManager hookManager) {
this.categoryService = categoryService;
this.hookManager = hookManager;
}
@ModelAttribute
public void adminInit() {
hookManager.doAction("admin_init");
}
/**
* GET /manage/posts/categories — List all categories with inline create form.
*/
@GetMapping
public String listCategories(Model model) {
LOG.debug("Request to list all categories");
populateModel(model, new Category(), true);
return "manage/posts/categories";
}
/**
* POST /manage/posts/categories — Save a new category.
*/
@PostMapping
public String createCategory(@Valid @ModelAttribute("category") Category category,
BindingResult bindingResult,
@RequestParam(value = "parentId", required = false) Long parentId,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to create Category : {}", category);
if (bindingResult.hasErrors()) {
populateModel(model, category, true);
return "manage/posts/categories";
}
if (parentId != null) {
categoryService.findById(parentId).ifPresent(category::setParent);
}
categoryService.save(category);
redirectAttributes.addFlashAttribute("successMessage", "Category created successfully!");
return "redirect:/manage/posts/categories";
}
/**
* GET /manage/posts/categories/{id}/edit — Show the edit form for a category.
*/
@GetMapping("/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
LOG.debug("Request to show edit form for Category : {}", id);
Optional<Category> categoryOptional = categoryService.findById(id);
if (categoryOptional.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "Category not found.");
return "redirect:/manage/posts/categories";
}
populateModel(model, categoryOptional.get(), false);
return "manage/posts/categories";
}
/**
* POST /manage/posts/categories/{id} — Update an existing category.
*/
@PostMapping("/{id}")
public String updateCategory(@PathVariable Long id,
@Valid @ModelAttribute("category") Category category,
BindingResult bindingResult,
@RequestParam(value = "parentId", required = false) Long parentId,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to update Category : {}", id);
if (bindingResult.hasErrors()) {
populateModel(model, category, false);
return "manage/posts/categories";
}
category.setId(id);
if (parentId != null && !parentId.equals(id)) {
categoryService.findById(parentId).ifPresent(category::setParent);
} else {
category.setParent(null);
}
categoryService.save(category);
redirectAttributes.addFlashAttribute("successMessage", "Category updated successfully!");
return "redirect:/manage/posts/categories";
}
/**
* POST /manage/posts/categories/{id}/delete — Delete a category.
*/
@PostMapping("/{id}/delete")
public String deleteCategory(@PathVariable Long id, RedirectAttributes redirectAttributes) {
LOG.debug("Request to delete Category : {}", id);
try {
categoryService.delete(id);
redirectAttributes.addFlashAttribute("successMessage", "Category deleted successfully!");
} catch (IllegalStateException e) {
redirectAttributes.addFlashAttribute("errorMessage", e.getMessage());
}
return "redirect:/manage/posts/categories";
}
/**
* Populate common model attributes for the categories view.
*/
private void populateModel(Model model, Category category, boolean isNew) {
model.addAttribute("category", category);
model.addAttribute("isNew", isNew);
model.addAttribute("allCategories", categoryService.findAll());
// Build a map of category ID -> post count for the table
java.util.Map<Long, Long> postCounts = new java.util.HashMap<>();
for (Category c : categoryService.findAll()) {
postCounts.put(c.getId(), categoryService.countPosts(c.getId()));
}
model.addAttribute("postCounts", postCounts);
}
}
@@ -0,0 +1,182 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.domain.PostLayout;
import com.sisvietnamvn.web.domain.Tag;
import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.service.CategoryService;
import com.sisvietnamvn.web.service.PostService;
import com.sisvietnamvn.web.service.TagService;
import jakarta.validation.Valid;
import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Controller for managing blog Posts in the admin panel.
* Provides CRUD operations with category and tag management.
*/
@Controller
@RequestMapping("/manage/posts")
public class ManagePostController {
private static final Logger LOG = LoggerFactory.getLogger(ManagePostController.class);
private final PostService postService;
private final CategoryService categoryService;
private final TagService tagService;
private final HookManager hookManager;
public ManagePostController(PostService postService,
CategoryService categoryService,
TagService tagService,
HookManager hookManager) {
this.postService = postService;
this.categoryService = categoryService;
this.tagService = tagService;
this.hookManager = hookManager;
}
@ModelAttribute
public void adminInit() {
hookManager.doAction("admin_init");
}
/**
* GET /manage/posts — List all posts with optional filters.
*/
@GetMapping
public String listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "status", required = false) String statusStr,
Model model) {
LOG.debug("Request to list all posts (categoryId={}, status={})", categoryId, statusStr);
PageStatus status = null;
if (statusStr != null && !statusStr.isBlank()) {
try {
status = PageStatus.valueOf(statusStr);
} catch (IllegalArgumentException e) {
LOG.warn("Invalid status filter: {}", statusStr);
}
}
model.addAttribute("posts", postService.findFiltered(categoryId, status));
model.addAttribute("categories", categoryService.findAll());
model.addAttribute("statuses", PageStatus.values());
model.addAttribute("selectedCategoryId", categoryId);
model.addAttribute("selectedStatus", statusStr);
return "manage/posts/list";
}
/**
* GET /manage/posts/new — Show the "Add New Post" form.
*/
@GetMapping("/new")
public String showCreateForm(Model model) {
LOG.debug("Request to show create post form");
Post post = new Post();
post.setStatus(PageStatus.DRAFT);
populateFormModel(model, post, "", true);
return "manage/posts/form";
}
/**
* POST /manage/posts/create — Save a new post.
*/
@PostMapping("/create")
public String createPost(@Valid @ModelAttribute("post") Post post,
BindingResult bindingResult,
@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "tagNames", required = false, defaultValue = "") String tagNames,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to create Post : {}", post);
if (bindingResult.hasErrors()) {
populateFormModel(model, post, tagNames, true);
return "manage/posts/form";
}
if (categoryId != null) {
categoryService.findById(categoryId).ifPresent(post::setCategory);
}
postService.saveWithTags(post, tagNames);
redirectAttributes.addFlashAttribute("successMessage", "Post created successfully!");
return "redirect:/manage/posts";
}
/**
* GET /manage/posts/{id}/edit — Show the "Edit Post" form.
*/
@GetMapping("/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
LOG.debug("Request to show edit form for Post : {}", id);
Optional<Post> postOptional = postService.findById(id);
if (postOptional.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "Post not found.");
return "redirect:/manage/posts";
}
Post post = postOptional.get();
String tagNames = post.getTags().stream()
.map(Tag::getName)
.collect(Collectors.joining(", "));
populateFormModel(model, post, tagNames, false);
return "manage/posts/form";
}
/**
* POST /manage/posts/{id} — Update an existing post.
*/
@PostMapping("/{id}")
public String updatePost(@PathVariable Long id,
@Valid @ModelAttribute("post") Post post,
BindingResult bindingResult,
@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "tagNames", required = false, defaultValue = "") String tagNames,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to update Post : {}", id);
if (bindingResult.hasErrors()) {
populateFormModel(model, post, tagNames, false);
return "manage/posts/form";
}
post.setId(id);
if (categoryId != null) {
categoryService.findById(categoryId).ifPresent(post::setCategory);
} else {
post.setCategory(null);
}
postService.saveWithTags(post, tagNames);
redirectAttributes.addFlashAttribute("successMessage", "Post updated successfully!");
return "redirect:/manage/posts";
}
/**
* POST /manage/posts/{id}/delete — Delete a post.
*/
@PostMapping("/{id}/delete")
public String deletePost(@PathVariable Long id, RedirectAttributes redirectAttributes) {
LOG.debug("Request to delete Post : {}", id);
postService.delete(id);
redirectAttributes.addFlashAttribute("successMessage", "Post deleted successfully!");
return "redirect:/manage/posts";
}
/**
* Populate common model attributes for the post form.
*/
private void populateFormModel(Model model, Post post, String tagNames, boolean isNew) {
model.addAttribute("post", post);
model.addAttribute("tagNames", tagNames);
model.addAttribute("isNew", isNew);
model.addAttribute("categories", categoryService.findAll());
model.addAttribute("statuses", PageStatus.values());
model.addAttribute("layouts", PostLayout.values());
model.addAttribute("allTags", tagService.findAll());
}
}
@@ -0,0 +1,127 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.Tag;
import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.service.TagService;
import jakarta.validation.Valid;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Controller for managing post Tags in the admin panel.
* Uses a WordPress-style split layout: form on the left, table on the right.
*/
@Controller
@RequestMapping("/manage/posts/tags")
public class ManageTagController {
private static final Logger LOG = LoggerFactory.getLogger(ManageTagController.class);
private final TagService tagService;
private final HookManager hookManager;
public ManageTagController(TagService tagService, HookManager hookManager) {
this.tagService = tagService;
this.hookManager = hookManager;
}
@ModelAttribute
public void adminInit() {
hookManager.doAction("admin_init");
}
/**
* GET /manage/posts/tags — List all tags with inline create form.
*/
@GetMapping
public String listTags(Model model) {
LOG.debug("Request to list all tags");
populateModel(model, new Tag(), true);
return "manage/posts/tags";
}
/**
* POST /manage/posts/tags — Save a new tag.
*/
@PostMapping
public String createTag(@Valid @ModelAttribute("tag") Tag tag,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to create Tag : {}", tag);
if (bindingResult.hasErrors()) {
populateModel(model, tag, true);
return "manage/posts/tags";
}
tagService.save(tag);
redirectAttributes.addFlashAttribute("successMessage", "Tag created successfully!");
return "redirect:/manage/posts/tags";
}
/**
* GET /manage/posts/tags/{id}/edit — Show the edit form for a tag.
*/
@GetMapping("/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
LOG.debug("Request to show edit form for Tag : {}", id);
Optional<Tag> tagOptional = tagService.findById(id);
if (tagOptional.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "Tag not found.");
return "redirect:/manage/posts/tags";
}
populateModel(model, tagOptional.get(), false);
return "manage/posts/tags";
}
/**
* POST /manage/posts/tags/{id} — Update an existing tag.
*/
@PostMapping("/{id}")
public String updateTag(@PathVariable Long id,
@Valid @ModelAttribute("tag") Tag tag,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to update Tag : {}", id);
if (bindingResult.hasErrors()) {
populateModel(model, tag, false);
return "manage/posts/tags";
}
tag.setId(id);
tagService.save(tag);
redirectAttributes.addFlashAttribute("successMessage", "Tag updated successfully!");
return "redirect:/manage/posts/tags";
}
/**
* POST /manage/posts/tags/{id}/delete — Delete a tag.
*/
@PostMapping("/{id}/delete")
public String deleteTag(@PathVariable Long id, RedirectAttributes redirectAttributes) {
LOG.debug("Request to delete Tag : {}", id);
tagService.delete(id);
redirectAttributes.addFlashAttribute("successMessage", "Tag deleted successfully!");
return "redirect:/manage/posts/tags";
}
/**
* Populate common model attributes for the tags view.
*/
private void populateModel(Model model, Tag tag, boolean isNew) {
model.addAttribute("tag", tag);
model.addAttribute("isNew", isNew);
model.addAttribute("allTags", tagService.findAll());
// Build a map of tag ID -> post count for the table
java.util.Map<Long, Long> postCounts = new java.util.HashMap<>();
for (Tag t : tagService.findAll()) {
postCounts.put(t.getId(), tagService.countPosts(t.getId()));
}
model.addAttribute("postCounts", postCounts);
}
}
@@ -74,4 +74,12 @@ public abstract class AbstractAuditingEntity<T> implements Serializable {
public void setLastModifiedDate(Instant lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
public java.util.Date getCreatedDateAsDate() {
return createdDate != null ? java.util.Date.from(createdDate) : null;
}
public java.util.Date getLastModifiedDateAsDate() {
return lastModifiedDate != null ? java.util.Date.from(lastModifiedDate) : null;
}
}
@@ -0,0 +1,110 @@
package com.sisvietnamvn.web.domain;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.util.Objects;
/**
* A Category entity for classifying blog posts.
* Supports hierarchical structure via self-referencing parent relationship.
* Maps to the "sis_category" database table.
*/
@Entity
@Table(name = "sis_category")
public class Category extends AbstractAuditingEntity<Long> {
@Serial
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
@SequenceGenerator(name = "sequenceGenerator")
private Long id;
@NotNull
@Size(max = 255)
@Column(name = "name", length = 255, nullable = false)
private String name;
@NotNull
@Size(max = 255)
@Column(name = "slug", length = 255, nullable = false, unique = true)
private String slug;
@Size(max = 1000)
@Column(name = "description", length = 1000)
private String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
// --- Getters and Setters ---
@Override
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Category getParent() {
return parent;
}
public void setParent(Category parent) {
this.parent = parent;
}
// --- equals, hashCode, toString ---
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Category category)) return false;
return id != null && id.equals(category.id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public String toString() {
return "Category{" +
"id=" + getId() +
", name='" + getName() + "'" +
", slug='" + getSlug() + "'" +
", description='" + getDescription() + "'" +
"}";
}
}
@@ -0,0 +1,189 @@
package com.sisvietnamvn.web.domain;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* A Post entity representing a blog/news article.
* Maps to the "sis_post" database table.
*/
@Entity
@Table(name = "sis_post")
public class Post extends AbstractAuditingEntity<Long> {
@Serial
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
@SequenceGenerator(name = "sequenceGenerator")
private Long id;
@NotNull
@Size(max = 255)
@Column(name = "title", length = 255, nullable = false)
private String title;
@NotNull
@Size(max = 255)
@Column(name = "slug", length = 255, nullable = false, unique = true)
private String slug;
@Lob
@Column(name = "content")
private String content;
@Size(max = 1000)
@Column(name = "excerpt", length = 1000)
private String excerpt;
@Size(max = 500)
@Column(name = "featured_image", length = 500)
private String featuredImage;
@NotNull
@Enumerated(EnumType.STRING)
@Column(name = "status", length = 20, nullable = false)
private PageStatus status = PageStatus.DRAFT;
@Size(max = 500)
@Column(name = "meta_description", length = 500)
private String metaDescription;
@NotNull
@Enumerated(EnumType.STRING)
@Column(name = "layout", length = 20, nullable = false)
private PostLayout layout = PostLayout.STANDARD;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "sis_post_tag",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set<Tag> tags = new HashSet<>();
// --- Getters and Setters ---
@Override
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getExcerpt() {
return excerpt;
}
public void setExcerpt(String excerpt) {
this.excerpt = excerpt;
}
public String getFeaturedImage() {
return featuredImage;
}
public void setFeaturedImage(String featuredImage) {
this.featuredImage = featuredImage;
}
public PageStatus getStatus() {
return status;
}
public void setStatus(PageStatus status) {
this.status = status;
}
public String getMetaDescription() {
return metaDescription;
}
public void setMetaDescription(String metaDescription) {
this.metaDescription = metaDescription;
}
public PostLayout getLayout() {
return layout;
}
public void setLayout(PostLayout layout) {
this.layout = layout;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public Set<Tag> getTags() {
return tags;
}
public void setTags(Set<Tag> tags) {
this.tags = tags;
}
// --- equals, hashCode, toString ---
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Post post)) return false;
return id != null && id.equals(post.id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public String toString() {
return "Post{" +
"id=" + getId() +
", title='" + getTitle() + "'" +
", slug='" + getSlug() + "'" +
", status='" + getStatus() + "'" +
"}";
}
}
@@ -0,0 +1,10 @@
package com.sisvietnamvn.web.domain;
/**
* Enumeration for Post Layout options.
*/
public enum PostLayout {
STANDARD,
SIDEBAR,
FULL_WIDTH
}
@@ -0,0 +1,84 @@
package com.sisvietnamvn.web.domain;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.util.Objects;
/**
* A Tag entity for labeling blog posts with keywords.
* Maps to the "sis_tag" database table.
*/
@Entity
@Table(name = "sis_tag")
public class Tag extends AbstractAuditingEntity<Long> {
@Serial
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
@SequenceGenerator(name = "sequenceGenerator")
private Long id;
@NotNull
@Size(max = 255)
@Column(name = "name", length = 255, nullable = false, unique = true)
private String name;
@NotNull
@Size(max = 255)
@Column(name = "slug", length = 255, nullable = false, unique = true)
private String slug;
// --- Getters and Setters ---
@Override
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
// --- equals, hashCode, toString ---
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tag tag)) return false;
return id != null && id.equals(tag.id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public String toString() {
return "Tag{" +
"id=" + getId() +
", name='" + getName() + "'" +
", slug='" + getSlug() + "'" +
"}";
}
}
@@ -0,0 +1,44 @@
package com.sisvietnamvn.web.repository;
import com.sisvietnamvn.web.domain.Category;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* Spring Data JPA repository for the {@link Category} entity.
*/
@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
/**
* Find all categories ordered by name ascending.
*/
List<Category> findAllByOrderByNameAsc();
/**
* Find a category by its URL-friendly slug.
*/
Optional<Category> findBySlug(String slug);
/**
* Check if a slug already exists (for uniqueness validation on create).
*/
boolean existsBySlug(String slug);
/**
* Check if a slug already exists for a different category (for edit validation).
*/
boolean existsBySlugAndIdNot(String slug, Long id);
/**
* Find all top-level categories (no parent).
*/
List<Category> findByParentIsNullOrderByNameAsc();
/**
* Find child categories of a given parent.
*/
List<Category> findByParentIdOrderByNameAsc(Long parentId);
}
@@ -0,0 +1,80 @@
package com.sisvietnamvn.web.repository;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.domain.Post;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
/**
* Spring Data JPA repository for the {@link Post} entity.
*/
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
/**
* Find all posts ordered by creation date descending (newest first).
*/
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
List<Post> findAllByOrderByCreatedDateDesc();
/**
* Find a post by its ID with eagerly fetched category and tags.
*/
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
Optional<Post> findById(Long id);
/**
* Find a post by its URL-friendly slug.
*/
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
Optional<Post> findBySlug(String slug);
/**
* Find all posts with a given publication status.
*/
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
List<Post> findByStatusOrderByCreatedDateDesc(PageStatus status);
/**
* Find all posts in a given category.
*/
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
List<Post> findByCategoryIdOrderByCreatedDateDesc(Long categoryId);
/**
* Find all posts in a given category with a given status.
*/
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = {"category", "tags"})
List<Post> findByCategoryIdAndStatusOrderByCreatedDateDesc(Long categoryId, PageStatus status);
/**
* Count the number of posts in a given category.
*/
long countByCategoryId(Long categoryId);
/**
* Count the number of posts associated with a given tag.
*/
@Query("SELECT COUNT(p) FROM Post p JOIN p.tags t WHERE t.id = :tagId")
long countByTagId(@Param("tagId") Long tagId);
/**
* Check if a slug already exists (for uniqueness validation on create).
*/
boolean existsBySlug(String slug);
/**
* Check if a slug already exists for a different post (for edit validation).
*/
boolean existsBySlugAndIdNot(String slug, Long id);
/**
* Find all posts that have a specific tag.
*/
@Query("SELECT p FROM Post p JOIN p.tags t WHERE t.id = :tagId ORDER BY p.createdDate DESC")
List<Post> findByTagId(@Param("tagId") Long tagId);
}
@@ -0,0 +1,44 @@
package com.sisvietnamvn.web.repository;
import com.sisvietnamvn.web.domain.Tag;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* Spring Data JPA repository for the {@link Tag} entity.
*/
@Repository
public interface TagRepository extends JpaRepository<Tag, Long> {
/**
* Find all tags ordered by name ascending.
*/
List<Tag> findAllByOrderByNameAsc();
/**
* Find a tag by its URL-friendly slug.
*/
Optional<Tag> findBySlug(String slug);
/**
* Find a tag by its exact name.
*/
Optional<Tag> findByName(String name);
/**
* Search tags by name containing the given string (for autocomplete).
*/
List<Tag> findByNameContainingIgnoreCase(String name);
/**
* Check if a slug already exists (for uniqueness validation on create).
*/
boolean existsBySlug(String slug);
/**
* Check if a slug already exists for a different tag (for edit validation).
*/
boolean existsBySlugAndIdNot(String slug, Long id);
}
@@ -0,0 +1,123 @@
package com.sisvietnamvn.web.service;
import com.sisvietnamvn.web.domain.Category;
import com.sisvietnamvn.web.repository.CategoryRepository;
import com.sisvietnamvn.web.repository.PostRepository;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Service class for managing post categories.
*/
@Service
@Transactional
public class CategoryService {
private static final Logger LOG = LoggerFactory.getLogger(CategoryService.class);
private final CategoryRepository categoryRepository;
private final PostRepository postRepository;
public CategoryService(CategoryRepository categoryRepository, PostRepository postRepository) {
this.categoryRepository = categoryRepository;
this.postRepository = postRepository;
}
/**
* Get all categories sorted by name.
*/
@Transactional(readOnly = true)
public List<Category> findAll() {
LOG.debug("Request to get all Categories");
return categoryRepository.findAllByOrderByNameAsc();
}
/**
* Get a single category by ID.
*/
@Transactional(readOnly = true)
public Optional<Category> findById(Long id) {
LOG.debug("Request to get Category : {}", id);
return categoryRepository.findById(id);
}
/**
* Get a single category by its URL slug.
*/
@Transactional(readOnly = true)
public Optional<Category> findBySlug(String slug) {
LOG.debug("Request to get Category by slug : {}", slug);
return categoryRepository.findBySlug(slug);
}
/**
* Get top-level categories (no parent).
*/
@Transactional(readOnly = true)
public List<Category> findTopLevel() {
LOG.debug("Request to get top-level Categories");
return categoryRepository.findByParentIsNullOrderByNameAsc();
}
/**
* Count the number of posts in a category.
*/
@Transactional(readOnly = true)
public long countPosts(Long categoryId) {
return postRepository.countByCategoryId(categoryId);
}
/**
* Save a category (create or update).
* Auto-generates a URL slug from the name if the slug is empty.
*/
public Category save(Category category) {
LOG.debug("Request to save Category : {}", category);
if (category.getSlug() == null || category.getSlug().isBlank()) {
category.setSlug(PageService.generateSlug(category.getName()));
}
// Ensure slug uniqueness
String baseSlug = category.getSlug();
String candidateSlug = baseSlug;
int counter = 1;
while (isSlugTaken(candidateSlug, category.getId())) {
candidateSlug = baseSlug + "-" + counter;
counter++;
}
category.setSlug(candidateSlug);
return categoryRepository.save(category);
}
/**
* Delete a category by ID.
* @throws IllegalStateException if the category still has posts assigned to it.
*/
public void delete(Long id) {
LOG.debug("Request to delete Category : {}", id);
long postCount = postRepository.countByCategoryId(id);
if (postCount > 0) {
throw new IllegalStateException(
"Cannot delete category: " + postCount + " post(s) are still assigned to it. " +
"Please reassign or delete those posts first."
);
}
categoryRepository.deleteById(id);
}
/**
* Check if a slug is already taken by another category.
*/
private boolean isSlugTaken(String slug, Long currentCategoryId) {
if (currentCategoryId == null) {
return categoryRepository.existsBySlug(slug);
}
return categoryRepository.existsBySlugAndIdNot(slug, currentCategoryId);
}
}
@@ -0,0 +1,174 @@
package com.sisvietnamvn.web.service;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.domain.Tag;
import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.repository.PostRepository;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Service class for managing blog posts.
*/
@Service
@Transactional
public class PostService {
private static final Logger LOG = LoggerFactory.getLogger(PostService.class);
private final PostRepository postRepository;
private final TagService tagService;
private final HookManager hookManager;
public PostService(PostRepository postRepository, TagService tagService, HookManager hookManager) {
this.postRepository = postRepository;
this.tagService = tagService;
this.hookManager = hookManager;
}
/**
* Get all posts ordered by creation date (newest first).
*/
@Transactional(readOnly = true)
public List<Post> findAll() {
LOG.debug("Request to get all Posts");
return postRepository.findAllByOrderByCreatedDateDesc();
}
/**
* Get a single post by ID.
*/
@Transactional(readOnly = true)
public Optional<Post> findById(Long id) {
LOG.debug("Request to get Post : {}", id);
return postRepository.findById(id);
}
/**
* Get a single post by its URL slug.
*/
@Transactional(readOnly = true)
public Optional<Post> findBySlug(String slug) {
LOG.debug("Request to get Post by slug : {}", slug);
return postRepository.findBySlug(slug);
}
/**
* Find all posts with a specific status.
*/
@Transactional(readOnly = true)
public List<Post> findByStatus(PageStatus status) {
LOG.debug("Request to get Posts by status : {}", status);
return postRepository.findByStatusOrderByCreatedDateDesc(status);
}
/**
* Find all posts in a specific category.
*/
@Transactional(readOnly = true)
public List<Post> findByCategoryId(Long categoryId) {
LOG.debug("Request to get Posts by category : {}", categoryId);
return postRepository.findByCategoryIdOrderByCreatedDateDesc(categoryId);
}
/**
* Find all posts filtered by category and/or status.
*/
@Transactional(readOnly = true)
public List<Post> findFiltered(Long categoryId, PageStatus status) {
LOG.debug("Request to get Posts filtered by categoryId={}, status={}", categoryId, status);
if (categoryId != null && status != null) {
return postRepository.findByCategoryIdAndStatusOrderByCreatedDateDesc(categoryId, status);
} else if (categoryId != null) {
return postRepository.findByCategoryIdOrderByCreatedDateDesc(categoryId);
} else if (status != null) {
return postRepository.findByStatusOrderByCreatedDateDesc(status);
}
return postRepository.findAllByOrderByCreatedDateDesc();
}
/**
* Save a post (create or update).
* Auto-generates a URL slug from the title if the slug is empty.
*/
public Post save(Post post) {
LOG.debug("Request to save Post : {}", post);
// Hook: pre_save_post filter
post = hookManager.applyFilters("pre_save_post", post);
if (post.getSlug() == null || post.getSlug().isBlank()) {
post.setSlug(PageService.generateSlug(post.getTitle()));
}
// Ensure slug uniqueness
String baseSlug = post.getSlug();
String candidateSlug = baseSlug;
int counter = 1;
while (isSlugTaken(candidateSlug, post.getId())) {
candidateSlug = baseSlug + "-" + counter;
counter++;
}
// Hook: post_slug filter
candidateSlug = hookManager.applyFilters("post_slug", candidateSlug);
post.setSlug(candidateSlug);
Post savedPost = postRepository.save(post);
// Hook: save_post action
hookManager.doAction("save_post", savedPost);
return savedPost;
}
/**
* Save a post and process comma-separated tag names.
* Auto-creates tags that don't exist yet.
*
* @param post the post entity
* @param tagNames comma-separated tag names (e.g. "news, health, update")
*/
public Post saveWithTags(Post post, String tagNames) {
Set<Tag> tags = new HashSet<>();
if (tagNames != null && !tagNames.isBlank()) {
String[] names = tagNames.split(",");
for (String name : names) {
String trimmed = name.trim();
if (!trimmed.isEmpty()) {
tags.add(tagService.findOrCreateByName(trimmed));
}
}
}
post.setTags(tags);
return save(post);
}
/**
* Delete a post by ID.
*/
public void delete(Long id) {
LOG.debug("Request to delete Post : {}", id);
postRepository.deleteById(id);
// Hook: deleted_post action
hookManager.doAction("deleted_post", id);
}
/**
* Check if a slug is already taken by another post.
*/
private boolean isSlugTaken(String slug, Long currentPostId) {
if (currentPostId == null) {
return postRepository.existsBySlug(slug);
}
return postRepository.existsBySlugAndIdNot(slug, currentPostId);
}
}
@@ -0,0 +1,131 @@
package com.sisvietnamvn.web.service;
import com.sisvietnamvn.web.domain.Tag;
import com.sisvietnamvn.web.repository.PostRepository;
import com.sisvietnamvn.web.repository.TagRepository;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Service class for managing post tags.
*/
@Service
@Transactional
public class TagService {
private static final Logger LOG = LoggerFactory.getLogger(TagService.class);
private final TagRepository tagRepository;
private final PostRepository postRepository;
public TagService(TagRepository tagRepository, PostRepository postRepository) {
this.tagRepository = tagRepository;
this.postRepository = postRepository;
}
/**
* Get all tags sorted by name.
*/
@Transactional(readOnly = true)
public List<Tag> findAll() {
LOG.debug("Request to get all Tags");
return tagRepository.findAllByOrderByNameAsc();
}
/**
* Get a single tag by ID.
*/
@Transactional(readOnly = true)
public Optional<Tag> findById(Long id) {
LOG.debug("Request to get Tag : {}", id);
return tagRepository.findById(id);
}
/**
* Get a single tag by its URL slug.
*/
@Transactional(readOnly = true)
public Optional<Tag> findBySlug(String slug) {
LOG.debug("Request to get Tag by slug : {}", slug);
return tagRepository.findBySlug(slug);
}
/**
* Find or create a tag by name.
* Used when processing comma-separated tag input from the post editor.
*/
public Tag findOrCreateByName(String name) {
String trimmed = name.trim();
LOG.debug("Request to find or create Tag by name : {}", trimmed);
return tagRepository.findByName(trimmed)
.orElseGet(() -> {
Tag tag = new Tag();
tag.setName(trimmed);
tag.setSlug(PageService.generateSlug(trimmed));
return save(tag);
});
}
/**
* Search tags by name (for autocomplete).
*/
@Transactional(readOnly = true)
public List<Tag> searchByName(String query) {
LOG.debug("Request to search Tags by name : {}", query);
return tagRepository.findByNameContainingIgnoreCase(query);
}
/**
* Count the number of posts associated with a tag.
*/
@Transactional(readOnly = true)
public long countPosts(Long tagId) {
return postRepository.countByTagId(tagId);
}
/**
* Save a tag (create or update).
* Auto-generates a URL slug from the name if the slug is empty.
*/
public Tag save(Tag tag) {
LOG.debug("Request to save Tag : {}", tag);
if (tag.getSlug() == null || tag.getSlug().isBlank()) {
tag.setSlug(PageService.generateSlug(tag.getName()));
}
// Ensure slug uniqueness
String baseSlug = tag.getSlug();
String candidateSlug = baseSlug;
int counter = 1;
while (isSlugTaken(candidateSlug, tag.getId())) {
candidateSlug = baseSlug + "-" + counter;
counter++;
}
tag.setSlug(candidateSlug);
return tagRepository.save(tag);
}
/**
* Delete a tag by ID.
*/
public void delete(Long id) {
LOG.debug("Request to delete Tag : {}", id);
tagRepository.deleteById(id);
}
/**
* Check if a slug is already taken by another tag.
*/
private boolean isSlugTaken(String slug, Long currentTagId) {
if (currentTagId == null) {
return tagRepository.existsBySlug(slug);
}
return tagRepository.existsBySlugAndIdNot(slug, currentTagId);
}
}
@@ -14,6 +14,8 @@
# ===================================================================
logging:
file:
name: application.log
level:
ROOT: DEBUG
tech.jhipster: DEBUG
@@ -24,7 +26,7 @@ spring:
devtools:
restart:
enabled: true
additional-exclude: static/**,.h2.server.properties
additional-exclude: static/**
livereload:
enabled: true # enabled for backend-only app (run ./gradlew classes -t)
jackson2:
@@ -32,16 +34,15 @@ spring:
indent-output: true
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:oracle:thin:@192.168.1.15:1521/sisvietnam
url: jdbc:oracle:thin:@localhost:1521/FREEPDB1
username: sisvietnam
password: "sisvietnam"
hikari:
poolName: Hikari
auto-commit: false
h2:
console:
# JHipster uses a custom h2-console initializer
enabled: false
jpa:
properties:
hibernate.dialect: org.hibernate.dialect.OracleDialect
liquibase:
# Remove 'faker' if you do not want the sample data to be loaded automatically
contexts: dev, faker
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<!-- ============================== -->
<!-- 1. Create sis_category table -->
<!-- ============================== -->
<changeSet id="20260628100000-1" author="sisvietnam">
<createTable tableName="sis_category">
<column name="id" type="bigint">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="slug" type="varchar(255)">
<constraints nullable="false" unique="true" uniqueConstraintName="ux_sis_category_slug"/>
</column>
<column name="description" type="varchar(1000)"/>
<column name="parent_id" type="bigint"/>
<column name="created_by" type="varchar(50)">
<constraints nullable="false"/>
</column>
<column name="created_date" type="timestamp"/>
<column name="last_modified_by" type="varchar(50)"/>
<column name="last_modified_date" type="timestamp"/>
</createTable>
<addForeignKeyConstraint baseTableName="sis_category"
baseColumnNames="parent_id"
constraintName="fk_category_parent"
referencedTableName="sis_category"
referencedColumnNames="id"/>
</changeSet>
<!-- ============================== -->
<!-- 2. Create sis_tag table -->
<!-- ============================== -->
<changeSet id="20260628100000-2" author="sisvietnam">
<createTable tableName="sis_tag">
<column name="id" type="bigint">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(255)">
<constraints nullable="false" unique="true" uniqueConstraintName="ux_sis_tag_name"/>
</column>
<column name="slug" type="varchar(255)">
<constraints nullable="false" unique="true" uniqueConstraintName="ux_sis_tag_slug"/>
</column>
<column name="created_by" type="varchar(50)">
<constraints nullable="false"/>
</column>
<column name="created_date" type="timestamp"/>
<column name="last_modified_by" type="varchar(50)"/>
<column name="last_modified_date" type="timestamp"/>
</createTable>
</changeSet>
<!-- ============================== -->
<!-- 3. Create sis_post table -->
<!-- ============================== -->
<changeSet id="20260628100000-3" author="sisvietnam">
<createTable tableName="sis_post">
<column name="id" type="bigint">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="title" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="slug" type="varchar(255)">
<constraints nullable="false" unique="true" uniqueConstraintName="ux_sis_post_slug"/>
</column>
<column name="content" type="${clobType}"/>
<column name="excerpt" type="varchar(1000)"/>
<column name="featured_image" type="varchar(500)"/>
<column name="status" type="varchar(20)" defaultValue="DRAFT">
<constraints nullable="false"/>
</column>
<column name="meta_description" type="varchar(500)"/>
<column name="category_id" type="bigint"/>
<column name="created_by" type="varchar(50)">
<constraints nullable="false"/>
</column>
<column name="created_date" type="timestamp"/>
<column name="last_modified_by" type="varchar(50)"/>
<column name="last_modified_date" type="timestamp"/>
</createTable>
<addForeignKeyConstraint baseTableName="sis_post"
baseColumnNames="category_id"
constraintName="fk_post_category"
referencedTableName="sis_category"
referencedColumnNames="id"/>
</changeSet>
<!-- ============================== -->
<!-- 4. Create sis_post_tag table -->
<!-- ============================== -->
<changeSet id="20260628100000-4" author="sisvietnam">
<createTable tableName="sis_post_tag">
<column name="post_id" type="bigint">
<constraints nullable="false"/>
</column>
<column name="tag_id" type="bigint">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey tableName="sis_post_tag" columnNames="post_id, tag_id"/>
<addForeignKeyConstraint baseTableName="sis_post_tag"
baseColumnNames="post_id"
constraintName="fk_post_tag_post"
referencedTableName="sis_post"
referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="sis_post_tag"
baseColumnNames="tag_id"
constraintName="fk_post_tag_tag"
referencedTableName="sis_tag"
referencedColumnNames="id"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="20260628151700-1" author="antigravity">
<addColumn tableName="sis_post">
<column name="layout" type="varchar(20)" defaultValue="STANDARD">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -21,6 +21,8 @@
<include file="config/liquibase/changelog/00000000000000_initial_schema.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260625100000_add_page_entity.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260625120000_add_page_type.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260628100000_add_post_category_tag.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260628151700_add_post_layout.xml" relativeToChangelogFile="false"/>
<!-- jhipster-needle-liquibase-add-changelog - JHipster will add liquibase changelogs here -->
<!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here -->
<!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here -->
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
<footer id="l--main-footer" class="site-footer" th:fragment="footer">
<h2 class="visually-hidden">Site Footer</h2>
<footer id="l--main-footer" class="site-footer">
<h2 class="visually-hidden">Site Footer</h2>
<div class="region region-footer r--region r--footer">
<div class="lc--layout-container lc--full">
<div class="l--layout l--full">
@@ -11,192 +11,270 @@
<div class="footer-top">
<div class="branding-container">
<div id="block-footerbranding"
class="block block-fixed-block-content block-fixed-block-contentfooter-branding cc--component-container cc--footer-branding">
<div class="c--component c--footer-branding">
<div id="block-footerbranding" class="block block-fixed-block-content block-fixed-block-contentfooter-branding cc--component-container cc--footer-branding">
<div class="c--component c--footer-branding">
<a class="logo" href="https://www.umass.edu/" aria-label="UMass Amherst">
<div><!--?xml version="1.0" encoding="utf-8"?--><!-- Generator: Adobe Illustrator 27.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg id="Layer_1" style="enable-background:new 0 0 117.41 54.06;" version="1.1" viewBox="0 0 117.41 54.06" x="0px" xml:space="preserve" y="0px" xmlns:xlink="http://www.w3.org/1999/xlink">
<style type="text/css">.st0{fill:#FFFFFF;} .st1{fill:#FFFFFF;stroke:#303030;stroke-width:0.7005;stroke-miterlimit:10;} .st2{fill:#891D1C;}
</style>
<g> <g> <path class="st0" d="M53.94,15.8c-0.65,0.04-0.91,0.2-0.96,0.64c-0.03,0.29-0.05,0.76-0.05,1.77v0.91c0,1.29-0.2,2.21-0.83,2.88
c-0.5,0.52-1.23,0.75-1.95,0.75c-0.63,0-1.27-0.15-1.75-0.52c-0.65-0.5-0.96-1.32-0.96-2.6V16.9c0-0.98-0.09-1.03-0.95-1.1v-0.31
h2.83v0.31c-0.86,0.05-0.95,0.12-0.95,1.1v2.49c0,1.76,0.7,2.76,2.05,2.76c1.57,0,2.06-1.31,2.06-3.11v-0.83
c0-1-0.03-1.46-0.08-1.8c-0.07-0.43-0.29-0.56-1.06-0.61v-0.31h2.6C53.94,15.49,53.94,15.8,53.94,15.8z"></path> </g> <g> <path class="st0" d="M56.83,22.59V22.3c0.64-0.07,0.72-0.15,0.72-0.91v-1.82c0-0.75-0.26-1.24-0.99-1.24
c-0.44,0-0.86,0.27-1.19,0.56v2.56c0,0.74,0.07,0.79,0.71,0.85v0.28h-2.37V22.3c0.74-0.08,0.8-0.13,0.8-0.85v-2.26
c0-0.7-0.07-0.73-0.65-0.84V18.1c0.52-0.1,1.04-0.23,1.52-0.45v0.87c0.22-0.16,0.46-0.33,0.73-0.51c0.31-0.2,0.59-0.33,0.91-0.33
c0.86,0,1.41,0.6,1.41,1.66v2.11c0,0.74,0.05,0.79,0.69,0.85v0.28L56.83,22.59L56.83,22.59z"></path> </g> <g> <path class="st0" d="M59.6,22.59V22.3c0.69-0.07,0.76-0.13,0.76-0.87v-2.25c0-0.68-0.03-0.72-0.69-0.83v-0.26
c0.56-0.09,1.07-0.23,1.55-0.43v3.76c0,0.74,0.08,0.81,0.77,0.87v0.28H59.6V22.59z M60.18,16.18c0-0.33,0.26-0.57,0.56-0.57
c0.31,0,0.55,0.24,0.55,0.57c0,0.29-0.24,0.56-0.56,0.56C60.44,16.74,60.18,16.48,60.18,16.18z"></path> </g> <g> <path class="st0" d="M67.08,18.09c-0.57,0.08-0.67,0.17-0.96,0.83c-0.36,0.82-0.94,2.16-1.54,3.79h-0.29
c-0.49-1.31-0.99-2.55-1.51-3.82c-0.25-0.63-0.34-0.71-0.93-0.8v-0.27h2.24v0.27c-0.59,0.08-0.61,0.16-0.44,0.63
c0.28,0.76,0.72,1.91,1.04,2.69c0.38-0.91,0.73-1.9,0.97-2.61c0.16-0.52,0.1-0.64-0.52-0.72v-0.27h1.94
C67.08,17.81,67.08,18.09,67.08,18.09z"></path> </g> <g> <path class="st0" d="M70.71,21.66c-0.69,0.86-1.44,1.06-1.78,1.06c-1.34,0-2.13-1.09-2.13-2.32c0-0.75,0.27-1.42,0.72-1.91
c0.45-0.5,1.04-0.8,1.6-0.8c0.91,0,1.64,0.81,1.64,1.66c0,0.22-0.05,0.31-0.25,0.35c-0.19,0.03-1.55,0.14-2.85,0.19
c0.01,1.48,0.86,2.09,1.63,2.09c0.44,0,0.85-0.17,1.24-0.55L70.71,21.66z M67.72,19.43c0.61,0,1.21-0.01,1.84-0.03
c0.2,0,0.26-0.07,0.26-0.22c0.01-0.57-0.36-1.09-0.96-1.09C68.36,18.09,67.88,18.55,67.72,19.43z"></path> </g> <g> <path class="st0" d="M72.92,18.84c0.33-0.55,0.8-1.16,1.32-1.16c0.37,0,0.63,0.28,0.63,0.55c0,0.22-0.14,0.44-0.35,0.53
c-0.13,0.05-0.23,0.04-0.29-0.02c-0.15-0.16-0.28-0.23-0.46-0.23c-0.28,0-0.63,0.33-0.87,0.94v2c0,0.72,0.05,0.79,0.89,0.85v0.28
h-2.51V22.3c0.68-0.07,0.75-0.13,0.75-0.85v-2.26c0-0.72-0.05-0.76-0.68-0.85V18.1c0.53-0.09,1.01-0.22,1.54-0.45v1.19H72.92z"></path> </g> <g> <path class="st0" d="M77.94,19.1c-0.21-0.64-0.59-1.08-1.12-1.08c-0.41,0-0.71,0.33-0.71,0.75c0,0.49,0.44,0.74,0.93,0.95
c0.82,0.34,1.37,0.73,1.37,1.48c0,0.99-0.93,1.51-1.8,1.51c-0.56,0-1.06-0.21-1.25-0.37c-0.08-0.15-0.17-0.89-0.17-1.29L75.47,21
c0.2,0.72,0.69,1.35,1.34,1.35c0.4,0,0.77-0.31,0.77-0.76c0-0.48-0.32-0.74-0.86-0.99c-0.65-0.31-1.36-0.67-1.36-1.51
c0-0.76,0.65-1.41,1.66-1.41c0.45,0,0.74,0.1,0.97,0.19c0.09,0.25,0.21,0.89,0.24,1.17L77.94,19.1z"></path> </g> <g> <path class="st0" d="M79.03,22.59V22.3c0.69-0.07,0.76-0.13,0.76-0.87v-2.25c0-0.68-0.03-0.72-0.69-0.83v-0.26
c0.56-0.09,1.07-0.23,1.55-0.43v3.76c0,0.74,0.08,0.81,0.77,0.87v0.28h-2.39V22.59z M79.61,16.18c0-0.33,0.26-0.57,0.56-0.57
c0.31,0,0.55,0.24,0.55,0.57c0,0.29-0.24,0.56-0.56,0.56C79.87,16.74,79.61,16.48,79.61,16.18z"></path> </g> <g> <path class="st0" d="M84.01,22.57c-0.19,0.1-0.36,0.14-0.46,0.14c-0.69,0-1.07-0.44-1.07-1.3v-3.17h-0.75l-0.04-0.11l0.29-0.32
h0.5v-0.8l0.71-0.72l0.15,0.02v1.49h1.23c0.1,0.11,0.07,0.35-0.07,0.43h-1.17v2.8c0,0.88,0.36,1.05,0.63,1.05s0.52-0.11,0.68-0.19
l0.1,0.28L84.01,22.57z"></path> </g> <g> <path class="st0" d="M90,18.09c-0.58,0.09-0.68,0.17-0.97,0.8c-0.27,0.58-0.61,1.48-1.36,3.28c-0.73,1.75-0.97,2.37-1.12,2.96
c-0.05,0.21-0.2,0.26-0.35,0.26c-0.31,0-0.62-0.26-0.62-0.52c0-0.17,0.1-0.27,0.28-0.39c0.34-0.22,0.56-0.49,0.81-0.97
c0.21-0.38,0.29-0.61,0.34-0.75c0.05-0.16,0.05-0.31-0.01-0.49c-0.44-1.24-0.93-2.56-1.25-3.34c-0.23-0.6-0.34-0.74-0.94-0.83
v-0.27h2.21v0.27c-0.5,0.09-0.56,0.2-0.41,0.6l0.99,2.74c0.31-0.74,0.77-2.03,1-2.72c0.13-0.41,0.05-0.53-0.57-0.62v-0.27H90
V18.09z"></path> </g> <g> <path class="st0" d="M52.65,31.38v-0.31c0.88-0.09,0.93-0.14,0.92-1.3l-0.03-4.45H53.5l-2.66,5.98h-0.22l-2.44-5.83h-0.02
L48,28.58c-0.04,0.89-0.04,1.37-0.03,1.81c0.02,0.52,0.28,0.6,1,0.68v0.31h-2.54v-0.31c0.63-0.08,0.85-0.2,0.93-0.65
c0.05-0.38,0.12-0.86,0.21-2l0.15-2.27c0.1-1.42,0.04-1.46-0.95-1.56v-0.31h1.79l2.49,5.46l2.52-5.46h1.85v0.31
c-0.96,0.09-1.04,0.11-1,1.23l0.11,3.95c0.03,1.16,0.07,1.21,1.01,1.3v0.31C55.54,31.38,52.65,31.38,52.65,31.38z"></path> </g> <g> <path class="st0" d="M59.34,31.51c-0.16,0-0.39-0.1-0.51-0.21c-0.14-0.15-0.21-0.31-0.26-0.49c-0.44,0.29-0.96,0.7-1.29,0.7
c-0.77,0-1.31-0.64-1.31-1.33c0-0.53,0.28-0.87,0.88-1.08c0.65-0.22,1.45-0.51,1.69-0.71v-0.2c0-0.79-0.37-1.23-0.92-1.23
c-0.22,0-0.38,0.09-0.49,0.22c-0.14,0.14-0.24,0.38-0.33,0.72c-0.04,0.17-0.16,0.25-0.32,0.25c-0.2,0-0.48-0.22-0.48-0.47
c0-0.16,0.14-0.28,0.35-0.44c0.31-0.22,1.01-0.64,1.64-0.76c0.33,0,0.65,0.1,0.89,0.29c0.38,0.32,0.49,0.76,0.49,1.33v2.03
c0,0.49,0.2,0.65,0.38,0.65c0.13,0,0.28-0.05,0.39-0.12l0.11,0.28L59.34,31.51z M58.55,28.76c-0.23,0.12-0.75,0.35-0.98,0.46
c-0.43,0.2-0.67,0.4-0.67,0.8c0,0.57,0.44,0.84,0.77,0.84c0.28-0.01,0.67-0.2,0.87-0.39L58.55,28.76L58.55,28.76z"></path> </g> <g> <path class="st0" d="M63.38,27.9c-0.21-0.64-0.59-1.08-1.12-1.08c-0.41,0-0.71,0.33-0.71,0.75c0,0.49,0.44,0.74,0.93,0.95
c0.82,0.34,1.37,0.73,1.37,1.48c0,0.99-0.93,1.51-1.8,1.51c-0.56,0-1.06-0.21-1.25-0.37c-0.08-0.15-0.17-0.89-0.17-1.29l0.28-0.05
c0.2,0.72,0.69,1.35,1.34,1.35c0.4,0,0.77-0.31,0.77-0.76c0-0.48-0.32-0.74-0.86-0.99c-0.65-0.31-1.36-0.67-1.36-1.51
c0-0.76,0.65-1.41,1.66-1.41c0.45,0,0.74,0.1,0.97,0.19c0.09,0.25,0.21,0.89,0.24,1.17L63.38,27.9z"></path> </g> <g> <path class="st0" d="M67.22,27.9c-0.21-0.64-0.59-1.08-1.12-1.08c-0.41,0-0.71,0.33-0.71,0.75c0,0.49,0.44,0.74,0.93,0.95
c0.82,0.34,1.37,0.73,1.37,1.48c0,0.99-0.93,1.51-1.8,1.51c-0.56,0-1.06-0.21-1.25-0.37c-0.08-0.15-0.17-0.89-0.17-1.29l0.28-0.05
c0.2,0.72,0.69,1.35,1.34,1.35c0.4,0,0.77-0.31,0.77-0.76c0-0.48-0.32-0.74-0.86-0.99c-0.65-0.31-1.36-0.67-1.36-1.51
c0-0.76,0.65-1.41,1.66-1.41c0.45,0,0.74,0.1,0.97,0.19c0.09,0.25,0.21,0.89,0.24,1.17L67.22,27.9z"></path> </g> <g> <path class="st0" d="M71.64,31.51c-0.16,0-0.39-0.1-0.51-0.21c-0.14-0.15-0.21-0.31-0.26-0.49c-0.44,0.29-0.96,0.7-1.29,0.7
c-0.77,0-1.31-0.64-1.31-1.33c0-0.53,0.28-0.87,0.88-1.08c0.65-0.22,1.45-0.51,1.69-0.71v-0.2c0-0.79-0.37-1.23-0.92-1.23
c-0.22,0-0.38,0.09-0.49,0.22c-0.14,0.14-0.24,0.38-0.33,0.72c-0.04,0.17-0.16,0.25-0.32,0.25c-0.2,0-0.48-0.22-0.48-0.47
c0-0.16,0.14-0.28,0.35-0.44c0.31-0.22,1.01-0.64,1.64-0.76c0.33,0,0.65,0.1,0.89,0.29c0.38,0.32,0.49,0.76,0.49,1.33v2.03
c0,0.49,0.2,0.65,0.38,0.65c0.13,0,0.28-0.05,0.39-0.12l0.11,0.28L71.64,31.51z M70.84,28.76c-0.23,0.12-0.75,0.35-0.98,0.46
c-0.43,0.2-0.67,0.4-0.67,0.8c0,0.57,0.44,0.84,0.77,0.84c0.28-0.01,0.67-0.2,0.87-0.39v-1.71H70.84z"></path> </g> <g> <path class="st0" d="M76.75,30.4c-0.48,0.63-1.21,1.11-1.85,1.11c-1.37,0-2.18-1.11-2.18-2.28c0-0.84,0.36-1.57,1.09-2.14
c0.57-0.44,1.22-0.61,1.65-0.61c0.44,0,0.8,0.14,0.97,0.28c0.14,0.12,0.17,0.22,0.17,0.34c0,0.23-0.24,0.47-0.36,0.47
c-0.05,0-0.11-0.03-0.21-0.12C75.71,27.14,75.32,27,74.94,27c-0.72,0-1.35,0.6-1.35,1.75c0,1.52,1.04,2.02,1.65,2.02
c0.47,0,0.83-0.13,1.33-0.6L76.75,30.4z"></path> </g> <g> <path class="st0" d="M79.86,31.38V31.1c0.63-0.07,0.72-0.13,0.72-0.84v-1.81c0-0.89-0.34-1.34-1.07-1.34
c-0.41,0-0.83,0.21-1.13,0.56v2.6c0,0.7,0.05,0.77,0.7,0.84v0.28h-2.31v-0.28c0.65-0.05,0.75-0.12,0.75-0.84v-5.2
c0-0.69-0.02-0.74-0.74-0.81v-0.25c0.52-0.08,1.18-0.25,1.6-0.38v3.67c0.38-0.39,0.97-0.8,1.56-0.8c0.88,0,1.51,0.56,1.51,1.87
v1.91c0,0.72,0.08,0.79,0.72,0.84v0.28h-2.31V31.38z"></path> </g> <g> <path class="st0" d="M87.55,31.12c-0.5,0.08-1.06,0.23-1.6,0.39l-0.07-0.07v-0.77c-0.25,0.19-0.5,0.38-0.81,0.57
c-0.32,0.19-0.55,0.27-0.84,0.27c-0.73,0-1.35-0.45-1.35-1.63v-2.14c0-0.6-0.04-0.64-0.67-0.76v-0.25c0.51-0.03,1.04-0.1,1.56-0.2
c-0.03,0.34-0.03,0.85-0.03,1.59v1.46c0,0.97,0.47,1.23,0.96,1.23c0.39,0,0.81-0.16,1.18-0.52v-2.54c0-0.62-0.07-0.67-0.85-0.77
v-0.25c0.57-0.03,1.13-0.08,1.71-0.2v3.64c0,0.55,0.09,0.62,0.5,0.64l0.31,0.02C87.55,30.83,87.55,31.12,87.55,31.12z"></path> <path class="st0" d="M90.8,27.9c-0.21-0.64-0.59-1.08-1.12-1.08c-0.41,0-0.71,0.33-0.71,0.75c0,0.49,0.44,0.74,0.93,0.95
c0.82,0.34,1.37,0.73,1.37,1.48c0,0.99-0.93,1.51-1.8,1.51c-0.56,0-1.06-0.21-1.25-0.37c-0.08-0.15-0.17-0.89-0.17-1.29l0.28-0.05
c0.2,0.72,0.69,1.35,1.34,1.35c0.4,0,0.77-0.31,0.77-0.76c0-0.48-0.32-0.74-0.86-0.99c-0.65-0.31-1.36-0.67-1.36-1.51
c0-0.76,0.65-1.41,1.66-1.41c0.45,0,0.74,0.1,0.97,0.19c0.09,0.25,0.21,0.89,0.24,1.17L90.8,27.9z"></path> </g> <g> <path class="st0" d="M95.79,30.45c-0.69,0.86-1.44,1.06-1.78,1.06c-1.34,0-2.13-1.09-2.13-2.32c0-0.75,0.27-1.42,0.72-1.91
c0.45-0.5,1.04-0.8,1.6-0.8c0.91,0,1.64,0.81,1.64,1.66c0,0.22-0.05,0.31-0.25,0.35c-0.19,0.03-1.55,0.14-2.85,0.19
c0.01,1.48,0.86,2.09,1.63,2.09c0.44,0,0.85-0.17,1.24-0.55L95.79,30.45z M92.8,28.22c0.61,0,1.21-0.01,1.84-0.03
c0.2,0,0.26-0.07,0.26-0.22c0.01-0.57-0.36-1.09-0.96-1.09C93.44,26.88,92.96,27.34,92.8,28.22z"></path> </g> <g> <path class="st0" d="M98.41,31.37c-0.19,0.1-0.36,0.14-0.46,0.14c-0.69,0-1.07-0.44-1.07-1.3v-3.17h-0.75l-0.04-0.11l0.29-0.32
h0.5v-0.8l0.71-0.72l0.15,0.02v1.49h1.23c0.1,0.11,0.07,0.35-0.07,0.43h-1.17v2.8c0,0.88,0.36,1.05,0.63,1.05s0.52-0.11,0.68-0.19
l0.1,0.28L98.41,31.37z"></path> </g> <g> <path class="st0" d="M101.54,31.37c-0.19,0.1-0.36,0.14-0.46,0.14c-0.69,0-1.07-0.44-1.07-1.3v-3.17h-0.75l-0.04-0.11l0.29-0.32
h0.5v-0.8l0.71-0.72l0.15,0.02v1.49h1.23c0.1,0.11,0.07,0.35-0.07,0.43h-1.17v2.8c0,0.88,0.36,1.05,0.63,1.05s0.52-0.11,0.68-0.19
l0.1,0.28L101.54,31.37z"></path> </g> <g> <path class="st0" d="M105.4,27.9c-0.21-0.64-0.59-1.08-1.12-1.08c-0.41,0-0.71,0.33-0.71,0.75c0,0.49,0.44,0.74,0.93,0.95
c0.82,0.34,1.37,0.73,1.37,1.48c0,0.99-0.93,1.51-1.8,1.51c-0.56,0-1.06-0.21-1.25-0.37c-0.08-0.15-0.17-0.89-0.17-1.29l0.28-0.05
c0.2,0.72,0.69,1.35,1.34,1.35c0.4,0,0.77-0.31,0.77-0.76c0-0.48-0.32-0.74-0.86-0.99c-0.65-0.31-1.36-0.67-1.36-1.51
c0-0.76,0.65-1.41,1.66-1.41c0.45,0,0.74,0.1,0.97,0.19c0.09,0.25,0.21,0.89,0.24,1.17L105.4,27.9z"></path> </g> <g> <path class="st0" d="M91.39,20.4c0-0.95,0.5-1.67,1.11-1.99c0.26-0.14,0.56-0.23,0.79-0.23c0.6,0,0.97,0.41,0.97,1.13
c0,0.63-0.27,1.34-0.78,1.76c-0.31,0.26-0.71,0.46-1.12,0.46C91.66,21.53,91.39,20.99,91.39,20.4z M92.84,21.2
c0.39-0.17,0.79-0.94,0.79-1.87c0-0.59-0.25-0.87-0.59-0.87c-0.11,0-0.2,0.03-0.28,0.07C92.49,18.65,92,19.24,92,20.36
c0,0.54,0.23,0.88,0.61,0.88C92.7,21.24,92.77,21.23,92.84,21.2z"></path> <path class="st0" d="M95.15,18.3c0.11-0.55,0.28-1.08,0.61-1.45s0.75-0.58,1.04-0.58c0.25,0,0.44,0.18,0.44,0.31
c0,0.11-0.1,0.24-0.18,0.3c-0.06,0.04-0.13,0.05-0.18-0.01c-0.15-0.16-0.31-0.25-0.46-0.25c-0.27,0-0.49,0.33-0.71,1.68h0.82
l-0.15,0.23l-0.73,0.07c-0.13,0.8-0.21,1.24-0.37,2.05c-0.25,1.25-0.52,1.8-0.92,2.21c-0.32,0.34-0.63,0.46-0.82,0.46
c-0.21,0-0.46-0.15-0.45-0.31c0-0.09,0.09-0.22,0.2-0.31c0.06-0.05,0.09-0.04,0.14,0c0.12,0.11,0.33,0.18,0.46,0.18
c0.12,0,0.24-0.06,0.31-0.15c0.09-0.11,0.27-0.41,0.51-1.83c0.17-1,0.27-1.61,0.39-2.33h-0.6l-0.02-0.08l0.22-0.2h0.45V18.3z"></path> </g> <g> <path class="st0" d="M49.89,39.11v-0.25c0.55-0.07,0.63-0.14,0.52-0.48s-0.29-0.82-0.48-1.35h-1.87c-0.14,0.4-0.27,0.76-0.39,1.13
c-0.19,0.55-0.11,0.64,0.54,0.7v0.25H46.3v-0.25c0.55-0.08,0.68-0.13,0.98-0.91l1.82-4.61l0.27-0.08c0.54,1.51,1.13,3.18,1.7,4.71
c0.28,0.76,0.4,0.83,0.97,0.89v0.25H49.89z M49.03,34.41h-0.02c-0.28,0.76-0.56,1.52-0.83,2.24h1.62L49.03,34.41z"></path> </g> <g> <path class="st0" d="M57.15,39.11v-0.23c0.52-0.05,0.62-0.09,0.62-0.71V36.7c0-0.62-0.22-1.02-0.77-1.02
c-0.32,0-0.65,0.18-0.98,0.46c0.02,0.11,0.03,0.2,0.03,0.37v1.71c0,0.55,0.08,0.61,0.56,0.66v0.23h-1.88v-0.23
c0.54-0.05,0.62-0.1,0.62-0.68v-1.51c0-0.65-0.23-1.01-0.76-1.01c-0.35,0-0.69,0.24-0.96,0.46v2.07c0,0.57,0.07,0.62,0.5,0.68
v0.23h-1.85v-0.23c0.58-0.05,0.66-0.11,0.66-0.68v-1.82c0-0.56-0.03-0.61-0.54-0.69v-0.2c0.39-0.07,0.83-0.18,1.22-0.36v0.71
c0.18-0.12,0.38-0.28,0.64-0.45c0.23-0.14,0.44-0.24,0.72-0.24c0.42,0,0.8,0.26,0.98,0.72c0.25-0.19,0.51-0.36,0.73-0.49
c0.18-0.12,0.41-0.23,0.66-0.23c0.69,0,1.12,0.48,1.12,1.34v1.7c0,0.59,0.07,0.62,0.57,0.68v0.23
C59.04,39.11,57.15,39.11,57.15,39.11z"></path> </g> <g> <path class="st0" d="M61.79,39.11v-0.23c0.51-0.05,0.58-0.11,0.58-0.68v-1.46c0-0.72-0.27-1.08-0.86-1.08
c-0.33,0-0.67,0.17-0.91,0.45v2.09c0,0.56,0.04,0.62,0.56,0.68v0.23H59.3v-0.23c0.53-0.04,0.61-0.1,0.61-0.68v-4.19
c0-0.55-0.02-0.6-0.6-0.65v-0.2c0.42-0.06,0.95-0.2,1.29-0.31v2.95c0.31-0.32,0.78-0.64,1.26-0.64c0.71,0,1.21,0.45,1.21,1.5v1.54
c0,0.58,0.06,0.63,0.58,0.68v0.23H61.79z"></path> </g> <g> <path class="st0" d="M67.08,38.36c-0.55,0.69-1.16,0.85-1.43,0.85c-1.08,0-1.71-0.88-1.71-1.87c0-0.61,0.22-1.14,0.58-1.54
s0.83-0.64,1.29-0.64c0.73,0,1.32,0.65,1.32,1.34c0,0.18-0.04,0.25-0.2,0.28c-0.15,0.03-1.25,0.11-2.29,0.15
c0.01,1.2,0.69,1.69,1.31,1.69c0.35,0,0.69-0.14,1-0.44L67.08,38.36z M64.67,36.57c0.49,0,0.98-0.01,1.49-0.03
c0.16,0,0.21-0.05,0.21-0.18c0.01-0.46-0.29-0.88-0.77-0.88C65.19,35.49,64.8,35.86,64.67,36.57z"></path> </g> <g> <path class="st0" d="M68.86,36.09c0.26-0.44,0.64-0.93,1.06-0.93c0.3,0,0.51,0.23,0.51,0.44c0,0.18-0.11,0.35-0.28,0.43
c-0.11,0.04-0.18,0.04-0.24-0.02c-0.12-0.13-0.23-0.18-0.37-0.18c-0.23,0-0.51,0.26-0.7,0.76v1.61c0,0.58,0.04,0.63,0.72,0.69
v0.23h-2.02v-0.23c0.54-0.05,0.61-0.11,0.61-0.69v-1.82c0-0.58-0.04-0.62-0.54-0.69V35.5c0.43-0.07,0.82-0.18,1.24-0.36
L68.86,36.09L68.86,36.09L68.86,36.09z"></path> </g> <g> <path class="st0" d="M72.86,36.3c-0.17-0.52-0.47-0.87-0.91-0.87c-0.33,0-0.57,0.26-0.57,0.61c0,0.4,0.35,0.6,0.75,0.76
c0.66,0.27,1.11,0.59,1.11,1.2c0,0.8-0.75,1.21-1.45,1.21c-0.45,0-0.85-0.17-1.01-0.3c-0.06-0.12-0.14-0.72-0.14-1.04l0.23-0.04
c0.16,0.58,0.55,1.09,1.08,1.09c0.33,0,0.62-0.25,0.62-0.62c0-0.39-0.25-0.6-0.69-0.8c-0.53-0.25-1.1-0.54-1.1-1.21
c0-0.62,0.53-1.13,1.34-1.13c0.36,0,0.6,0.08,0.78,0.15c0.07,0.2,0.17,0.72,0.19,0.94L72.86,36.3z"></path> </g> <g> <path class="st0" d="M75.31,39.1c-0.15,0.08-0.29,0.11-0.37,0.11c-0.55,0-0.86-0.35-0.86-1.05V35.6h-0.61l-0.04-0.09l0.24-0.25
h0.4v-0.64l0.57-0.58l0.12,0.02v1.2h0.99c0.08,0.09,0.05,0.28-0.05,0.34h-0.94v2.26c0,0.71,0.29,0.84,0.51,0.84
s0.42-0.09,0.54-0.15l0.08,0.23L75.31,39.1z"></path> </g> </g> <path class="st1" d="M42.32,38.93H33.5l0.08-9.78c-1.2,1.83-4.59,6.89-4.59,6.89l-0.63,0.01c0,0-3.41-5.07-4.61-6.89l0.08,9.78
h-8.82l-0.02-5.76h1.19l-0.09-11.43h-1.16l-0.03-5.97h8.19l5.57,9.03l5.57-9.03h8.19l-0.03,5.97h-1.16l-0.09,11.43h1.19L42.32,38.93
z"></path> <path class="st2" d="M34.85,37.59h6.13l0.01-3.08H39.8l0.11-14.11h1.15l0.02-3.28h-6.09l-6.32,10.25l-6.32-10.25h-6.09l0.02,3.28
h1.15l0.11,14.11h-1.19l0.01,3.08h6.13l-0.1-13l1.25,1.92c1.3,2,2.99,4.55,5.04,7.6c2.04-3.05,3.74-5.6,5.04-7.6l1.25-1.92
L34.85,37.59z"></path> </svg></div>
</a>
</div>
</div>
<a class="logo" href="#" aria-label="SIS">
<div>
<img src="https://sisvietnam.vn/wp-content/uploads/2026/06/LogoSIS-1.png" alt="Logo SIS" />
</div>
</a>
</div>
</div>
<div id="block-footersocial"
class="block block-fixed-block-content block-fixed-block-contentfooter-social cc--component-container cc--footer-social">
<div class="c--component c--footer-social">
<nav aria-label="UMass Social Media Links">
<div>
<nav aria-label="Social media links">
<ul class="social-media-links--platforms platforms">
<li>
<a href="#" class="ext" target="_blank" rel="noopener noreferrer"
aria-label="Facebook (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path
d="m19.9593333 5h-14.0853333c-.48269687 0-.874.39130313-.874.874v14.0853333c0 .4826969.39130313.874.874.874h7.5835333v-6.1230666h-2.0564333v-2.3965334h2.0564333v-1.7625666c0-2.04503337 1.2489334-3.15843337 3.0735667-3.15843337.6158056-.00219435 1.23127.02930829 1.8436333.09436667v2.13623333h-1.2616c-.9899 0-1.1811666.47119997-1.1811666 1.16343337v1.5263333h2.3731l-.3090667 2.3965333h-2.0640333v6.1237h4.0273666c.4826969 0 .874-.3913031.874-.874v-14.0853333c0-.48269687-.3913031-.874-.874-.874z"
fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">Facebook</span>
</a>
</li>
<li>
<a href="https://twitter.com/UMassAmherst" class="ext" target="_blank"
rel="noopener noreferrer" aria-label="X (formerly Twitter) (opens in new window)">
<svg style="fill:#FFFFFF;" width="24" height="24" y="0px" x="0px" viewBox="0 -2 50 50"
aria-hidden="true" focusable="false">
<path
d="M 5.9199219 6 L 20.582031 27.375 L 6.2304688 44 L 9.4101562 44 L 21.986328 29.421875 L 31.986328 44 L 44 44 L 28.681641 21.669922 L 42.199219 6 L 39.029297 6 L 27.275391 19.617188 L 17.933594 6 L 5.9199219 6 z M 9.7167969 8 L 16.880859 8 L 40.203125 42 L 33.039062 42 L 9.7167969 8 z">
</path>
</svg>
<span class="visually-hidden">X (formerly Twitter)</span>
</a>
</li>
<li>
<a href="https://www.youtube.com/user/UMass" class="ext" target="_blank"
rel="noopener noreferrer" aria-label="YouTube (opens in new window)">
<svg height="15" viewBox="0 0 23 15" width="23" aria-hidden="true" focusable="false">
<path
d="m22.935049 8.18627451s-.2389706-1.43382353-.8762255-2.07107843c-.7965686-.87622549-1.752451-.87622549-2.1507353-.87622549-2.9473039-.23897059-7.4877451-.23897059-7.4877451-.23897059s-4.54044114 0-7.48774506.23897059c-.47794118 0-1.35416667 0-2.23039216.87622549-.6372549.6372549-.79656863 2.07107843-.79656863 2.07107843s-.23897058 1.67279412-.23897058 3.42524509v1.5931373c0 1.6727941.23897058 3.4252451.23897058 3.4252451s.23897059 1.4338235.8762255 2.0710784c.79656862.8762255 1.9117647.7965686 2.38970588.8762255 1.75245098.1593137 7.32843137.2389706 7.32843137.2389706s4.5404412 0 7.567402-.2389706c.3982843-.0796569 1.3541666-.0796569 2.1507353-.8762255.6372549-.6372549.8762254-2.0710784.8762254-2.0710784s.2389706-1.6727942.2389706-3.4252451v-1.5931373c-.1593137-1.75245097-.3982843-3.42524509-.3982843-3.42524509zm-12.745098 7.16911769v-5.97426475l5.7352941 2.86764705z"
fill="#fff" fill-rule="evenodd" transform="translate(-1 -5)"></path>
</svg>
<span class="visually-hidden">YouTube</span>
</a>
</li>
<li>
<a href="#" class="ext" target="_blank" rel="noopener noreferrer"
aria-label="Instagram (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path
d="m12.9166667 5c-2.1666667 0-2.4166667 0-3.25000003.08333333-.83333334 0-1.41666667.16666667-1.91666667.33333334-.5.16666666-1 .5-1.41666667.91666666-.41666666.41666667-.75.91666667-.91666666 1.41666667-.16666667.5-.33333334 1.08333333-.33333334 1.91666667-.08333333.83333333-.08333333 1.08333333-.08333333 3.25000003 0 2.1666666 0 2.4166666.08333333 3.25 0 .8333333.16666667 1.4166666.33333334 1.9166666.16666666.5.5 1 .91666666 1.4166667.41666667.4166667.91666667.75 1.41666667.9166667.5.1666666 1.08333333.3333333 1.91666667.3333333.83333333 0 1.08333333.0833333 3.25000003.0833333 2.1666666 0 2.4166666 0 3.25-.0833333.8333333 0 1.4166666-.1666667 1.9166666-.3333333.5-.1666667 1-.5 1.4166667-.9166667s.75-.9166667.9166667-1.4166667c.1666666-.5.3333333-1.0833333.3333333-1.9166666 0-.8333334.0833333-1.0833334.0833333-3.25 0-2.1666667 0-2.4166667-.0833333-3.25000003 0-.83333334-.1666667-1.41666667-.3333333-1.91666667-.1666667-.5-.5-1-.9166667-1.41666667-.4166667-.41666666-.9166667-.75-1.4166667-.91666666-.5-.16666667-1.0833333-.33333334-1.9166666-.33333334-.8333334-.08333333-1.0833334-.08333333-3.25-.08333333m0 1.41666667c2.0833333 0 2.3333333 0 3.1666666.08333333.75 0 1.1666667.16666667 1.5.25.3333334.16666667.6666667.33333333.9166667.58333333s.4166667.58333334.5833333.91666667c.0833334.25.25.66666667.25 1.5 0 .8333333.0833334 1.0833333.0833334 3.25 0 2.0833333 0 2.3333333-.0833334 3.1666667 0 .75-.1666666 1.1666666-.25 1.5-.1666666.3333333-.3333333.6666666-.5833333.9166666s-.5.4166667-.9166667.5833334c-.25.0833333-.6666666.25-1.5.25-.8333333 0-1.0833333.0833333-3.1666666.0833333-2.0833334 0-2.3333334 0-3.1666667-.0833333-.75 0-1.16666667-.1666667-1.5-.25-.33333333-.1666667-.66666667-.3333334-.91666667-.5833334s-.41666666-.5833333-.58333333-.9166666c-.08333333-.25-.25-.6666667-.25-1.5 0-.8333334-.08333333-1.0833334-.08333333-3.1666667s0-2.3333333.08333333-3.25c0-.75.16666667-1.16666667.25-1.5.16666667-.33333333.33333333-.66666667.58333333-.91666667s.5-.41666666.91666667-.58333333c.25-.08333333.66666667-.25 1.5-.25.8333333-.08333333 1.0833333-.08333333 3.1666667-.08333333m0 9.16666663c-1.4166667 0-2.6666667-1.1666666-2.6666667-2.6666666s1.1666667-2.6666667 2.6666667-2.6666667 2.6666666 1.1666667 2.6666666 2.6666667-1.25 2.6666666-2.6666666 2.6666666m0-6.74999997c-2.25 0-4.08333337 1.83333337-4.08333337 4.08333337s1.83333337 4.0833333 4.08333337 4.0833333 4.0833333-1.8333333 4.0833333-4.0833333-1.8333333-4.08333337-4.0833333-4.08333337m5.1666666-.16666666c0 .5-.4166666.91666666-.9166666.91666666s-.9166667-.41666666-.9166667-.91666666.4166667-.91666667.9166667-.91666667.9166666.41666667.9166666.91666667"
fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">Instagram</span>
</a>
</li>
<li>
<a href="https://www.linkedin.com/school/umassamherst/" class="ext" target="_blank"
rel="noopener noreferrer" aria-label="LinkedIn (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path
d="m20.8333333 17.8517316c0 1.6450216-1.3365801 2.9816017-2.9816017 2.9816017h-9.87012987c-1.64502164 0-2.98160173-1.3365801-2.98160173-2.9816017v-9.87012987c0-1.64502164 1.33658009-2.98160173 2.98160173-2.98160173h9.87012987c1.6450216 0 2.9816017 1.33658009 2.9816017 2.98160173zm-12.1320346-10.17857143c-.82251082 0-1.33658009.51406927-1.33658009 1.23376624-.10281385.71969697.41125542 1.23376619 1.23376624 1.23376619.82251082 0 1.33658009-.51406922 1.33658009-1.23376619s-.51406927-1.23376624-1.23376624-1.23376624zm1.13095238 10.58982683v-7.1969697h-2.36471861v7.1969697zm8.53354982 0v-4.1125541c0-2.1590909-1.1309524-3.1872294-2.7759741-3.1872294-1.2337662 0-1.7478355.7196969-2.056277 1.2337662v-1.0281385h-2.3647186v7.1969697h2.3647186v-4.1125541c0-.2056278 0-.4112555.1028138-.6168832.1028139-.4112554.5140693-.8225108 1.2337663-.8225108.8225108 0 1.2337662.6168831 1.2337662 1.6450217v3.8041125z"
fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">LinkedIn</span>
</a>
</li>
<li>
<a href="#" class="ext" target="_blank" rel="noopener noreferrer"
aria-label="Snapchat (opens in new window)">
<svg height="17" viewBox="0 0 18 17" width="18" aria-hidden="true" focusable="false">
<path
d="m12.8333333 20.3333333c-.0833333 0-.0833333 0-.1666666 0h-.0833334c-1.0833333 0-1.6666666-.3333333-2.25-.75-.41666663-.3333333-.74999997-.5833333-1.16666663-.5833333-.25 0-.41666667-.0833333-.66666667-.0833333-.33333333 0-.66666667.0833333-.91666667.0833333-.16666666 0-.33333333.0833333-.41666666.0833333-.33333334 0-.5-.25-.5-.4166666 0-.1666667-.08333334-.25-.08333334-.4166667 0-.0833333-.08333333-.25-.08333333-.3333333-1.33333333-.25-2.08333333-.5-2.25-1-.08333333 0-.08333333-.0833334-.08333333-.1666667 0-.25.16666666-.4166667.41666666-.5 2.25-.3333333 3.33333334-2.75 3.33333334-2.8333333.08333333-.25.16666666-.4166667.08333333-.5833334-.08333333-.1666666-.58333333-.3333333-.91666667-.4166666-.08333333 0-.16666666-.0833334-.25-.0833334-.83333333-.3333333-1-.75-.91666666-1.0833333.08333333-.5.83333333-.75 1.25-.5833333.33333333.1666666.58333333.1666666.75.1666666h.25c0-.0833333 0-.25 0-.3333333-.08333334-1.16666667-.25-2.66666667.16666666-3.5 1.16666667-2.66666667 3.66666667-2.83333333 4.33333337-2.83333333h.3333333c.75 0 3.1666667.16666666 4.3333333 2.83333333.4166667.91666667.3333334 2.33333333.25 3.5v.0833333.25h.1666667c.1666667 0 .4166667-.0833333.6666667-.1666666.1666666-.0833334.25-.0833334.4166666-.0833334.1666667 0 .25 0 .4166667.0833334.3333333.0833333.5833333.4166666.5833333.6666666 0 .4166667-.3333333.6666667-.9166666.9166667-.0833334 0-.1666667.0833333-.25.0833333-.3333334.0833334-.8333334.25-.9166667.5-.0833333.1666667 0 .3333334.0833333.5.0833334.25 1.0833334 2.5 3.25 2.8333334.25 0 .4166667.25.4166667.5 0 .0833333 0 .1666666-.0833333.1666666-.1666667.4166667-.9166667.75-2.25 1 0 .0833334-.0833334.25-.0833334.3333334 0 .1666666-.0833333.25-.0833333.4166666-.0833333.25-.25.4166667-.5.4166667-.0833333 0-.25 0-.4166667-.0833333-.25-.0833334-.5-.0833334-.9166666-.0833334-.1666667 0-.4166667 0-.6666667.0833334-.4166667.0833333-.75.3333333-1.1666667.5833333-.6666666.5833333-1.5.8333333-2.5.8333333"
fill="#fff" fill-rule="evenodd" transform="translate(-4 -4)"></path>
</svg>
<span class="visually-hidden">Snapchat</span>
</a>
</li>
</ul>
</nav>
</div>
</nav>
</div>
</div>
<div id="block-footersocial" class="block block-fixed-block-content block-fixed-block-contentfooter-social cc--component-container cc--footer-social">
<div class="c--component c--footer-social">
<nav aria-label="UMass Social Media Links">
<div><nav aria-label="Social media links">
<ul class="social-media-links--platforms platforms">
<li>
<a href="https://www.facebook.com/UMassAmherst/" class="ext" target="_blank" rel="noopener noreferrer" aria-label="Facebook (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path d="m19.9593333 5h-14.0853333c-.48269687 0-.874.39130313-.874.874v14.0853333c0 .4826969.39130313.874.874.874h7.5835333v-6.1230666h-2.0564333v-2.3965334h2.0564333v-1.7625666c0-2.04503337 1.2489334-3.15843337 3.0735667-3.15843337.6158056-.00219435 1.23127.02930829 1.8436333.09436667v2.13623333h-1.2616c-.9899 0-1.1811666.47119997-1.1811666 1.16343337v1.5263333h2.3731l-.3090667 2.3965333h-2.0640333v6.1237h4.0273666c.4826969 0 .874-.3913031.874-.874v-14.0853333c0-.48269687-.3913031-.874-.874-.874z" fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">Facebook</span>
</a>
</li>
<li>
<a href="https://twitter.com/UMassAmherst" class="ext" target="_blank" rel="noopener noreferrer" aria-label="X (formerly Twitter) (opens in new window)">
<svg style="fill:#FFFFFF;" width="24" height="24" y="0px" x="0px" viewBox="0 -2 50 50" aria-hidden="true" focusable="false">
<path d="M 5.9199219 6 L 20.582031 27.375 L 6.2304688 44 L 9.4101562 44 L 21.986328 29.421875 L 31.986328 44 L 44 44 L 28.681641 21.669922 L 42.199219 6 L 39.029297 6 L 27.275391 19.617188 L 17.933594 6 L 5.9199219 6 z M 9.7167969 8 L 16.880859 8 L 40.203125 42 L 33.039062 42 L 9.7167969 8 z"></path>
</svg>
<span class="visually-hidden">X (formerly Twitter)</span>
</a>
</li>
<li>
<a href="https://www.youtube.com/user/UMass" class="ext" target="_blank" rel="noopener noreferrer" aria-label="YouTube (opens in new window)">
<svg height="15" viewBox="0 0 23 15" width="23" aria-hidden="true" focusable="false">
<path d="m22.935049 8.18627451s-.2389706-1.43382353-.8762255-2.07107843c-.7965686-.87622549-1.752451-.87622549-2.1507353-.87622549-2.9473039-.23897059-7.4877451-.23897059-7.4877451-.23897059s-4.54044114 0-7.48774506.23897059c-.47794118 0-1.35416667 0-2.23039216.87622549-.6372549.6372549-.79656863 2.07107843-.79656863 2.07107843s-.23897058 1.67279412-.23897058 3.42524509v1.5931373c0 1.6727941.23897058 3.4252451.23897058 3.4252451s.23897059 1.4338235.8762255 2.0710784c.79656862.8762255 1.9117647.7965686 2.38970588.8762255 1.75245098.1593137 7.32843137.2389706 7.32843137.2389706s4.5404412 0 7.567402-.2389706c.3982843-.0796569 1.3541666-.0796569 2.1507353-.8762255.6372549-.6372549.8762254-2.0710784.8762254-2.0710784s.2389706-1.6727942.2389706-3.4252451v-1.5931373c-.1593137-1.75245097-.3982843-3.42524509-.3982843-3.42524509zm-12.745098 7.16911769v-5.97426475l5.7352941 2.86764705z" fill="#fff" fill-rule="evenodd" transform="translate(-1 -5)"></path>
</svg>
<span class="visually-hidden">YouTube</span>
</a>
</li>
<li>
<a href="https://www.instagram.com/umass/" class="ext" target="_blank" rel="noopener noreferrer" aria-label="Instagram (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path d="m12.9166667 5c-2.1666667 0-2.4166667 0-3.25000003.08333333-.83333334 0-1.41666667.16666667-1.91666667.33333334-.5.16666666-1 .5-1.41666667.91666666-.41666666.41666667-.75.91666667-.91666666 1.41666667-.16666667.5-.33333334 1.08333333-.33333334 1.91666667-.08333333.83333333-.08333333 1.08333333-.08333333 3.25000003 0 2.1666666 0 2.4166666.08333333 3.25 0 .8333333.16666667 1.4166666.33333334 1.9166666.16666666.5.5 1 .91666666 1.4166667.41666667.4166667.91666667.75 1.41666667.9166667.5.1666666 1.08333333.3333333 1.91666667.3333333.83333333 0 1.08333333.0833333 3.25000003.0833333 2.1666666 0 2.4166666 0 3.25-.0833333.8333333 0 1.4166666-.1666667 1.9166666-.3333333.5-.1666667 1-.5 1.4166667-.9166667s.75-.9166667.9166667-1.4166667c.1666666-.5.3333333-1.0833333.3333333-1.9166666 0-.8333334.0833333-1.0833334.0833333-3.25 0-2.1666667 0-2.4166667-.0833333-3.25000003 0-.83333334-.1666667-1.41666667-.3333333-1.91666667-.1666667-.5-.5-1-.9166667-1.41666667-.4166667-.41666666-.9166667-.75-1.4166667-.91666666-.5-.16666667-1.0833333-.33333334-1.9166666-.33333334-.8333334-.08333333-1.0833334-.08333333-3.25-.08333333m0 1.41666667c2.0833333 0 2.3333333 0 3.1666666.08333333.75 0 1.1666667.16666667 1.5.25.3333334.16666667.6666667.33333333.9166667.58333333s.4166667.58333334.5833333.91666667c.0833334.25.25.66666667.25 1.5 0 .8333333.0833334 1.0833333.0833334 3.25 0 2.0833333 0 2.3333333-.0833334 3.1666667 0 .75-.1666666 1.1666666-.25 1.5-.1666666.3333333-.3333333.6666666-.5833333.9166666s-.5.4166667-.9166667.5833334c-.25.0833333-.6666666.25-1.5.25-.8333333 0-1.0833333.0833333-3.1666666.0833333-2.0833334 0-2.3333334 0-3.1666667-.0833333-.75 0-1.16666667-.1666667-1.5-.25-.33333333-.1666667-.66666667-.3333334-.91666667-.5833334s-.41666666-.5833333-.58333333-.9166666c-.08333333-.25-.25-.6666667-.25-1.5 0-.8333334-.08333333-1.0833334-.08333333-3.1666667s0-2.3333333.08333333-3.25c0-.75.16666667-1.16666667.25-1.5.16666667-.33333333.33333333-.66666667.58333333-.91666667s.5-.41666666.91666667-.58333333c.25-.08333333.66666667-.25 1.5-.25.8333333-.08333333 1.0833333-.08333333 3.1666667-.08333333m0 9.16666663c-1.4166667 0-2.6666667-1.1666666-2.6666667-2.6666666s1.1666667-2.6666667 2.6666667-2.6666667 2.6666666 1.1666667 2.6666666 2.6666667-1.25 2.6666666-2.6666666 2.6666666m0-6.74999997c-2.25 0-4.08333337 1.83333337-4.08333337 4.08333337s1.83333337 4.0833333 4.08333337 4.0833333 4.0833333-1.8333333 4.0833333-4.0833333-1.8333333-4.08333337-4.0833333-4.08333337m5.1666666-.16666666c0 .5-.4166666.91666666-.9166666.91666666s-.9166667-.41666666-.9166667-.91666666.4166667-.91666667.9166667-.91666667.9166666.41666667.9166666.91666667" fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">Instagram</span>
</a>
</li>
<li>
<a href="https://www.linkedin.com/school/umassamherst/" class="ext" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn (opens in new window)">
<svg height="16" viewBox="0 0 16 16" width="16" aria-hidden="true" focusable="false">
<path d="m20.8333333 17.8517316c0 1.6450216-1.3365801 2.9816017-2.9816017 2.9816017h-9.87012987c-1.64502164 0-2.98160173-1.3365801-2.98160173-2.9816017v-9.87012987c0-1.64502164 1.33658009-2.98160173 2.98160173-2.98160173h9.87012987c1.6450216 0 2.9816017 1.33658009 2.9816017 2.98160173zm-12.1320346-10.17857143c-.82251082 0-1.33658009.51406927-1.33658009 1.23376624-.10281385.71969697.41125542 1.23376619 1.23376624 1.23376619.82251082 0 1.33658009-.51406922 1.33658009-1.23376619s-.51406927-1.23376624-1.23376624-1.23376624zm1.13095238 10.58982683v-7.1969697h-2.36471861v7.1969697zm8.53354982 0v-4.1125541c0-2.1590909-1.1309524-3.1872294-2.7759741-3.1872294-1.2337662 0-1.7478355.7196969-2.056277 1.2337662v-1.0281385h-2.3647186v7.1969697h2.3647186v-4.1125541c0-.2056278 0-.4112555.1028138-.6168832.1028139-.4112554.5140693-.8225108 1.2337663-.8225108.8225108 0 1.2337662.6168831 1.2337662 1.6450217v3.8041125z" fill="#fff" fill-rule="evenodd" transform="translate(-5 -5)"></path>
</svg>
<span class="visually-hidden">LinkedIn</span>
</a>
</li>
<li>
<a href="https://www.snapchat.com/add/umassamherst" class="ext" target="_blank" rel="noopener noreferrer" aria-label="Snapchat (opens in new window)">
<svg height="17" viewBox="0 0 18 17" width="18" aria-hidden="true" focusable="false">
<path d="m12.8333333 20.3333333c-.0833333 0-.0833333 0-.1666666 0h-.0833334c-1.0833333 0-1.6666666-.3333333-2.25-.75-.41666663-.3333333-.74999997-.5833333-1.16666663-.5833333-.25 0-.41666667-.0833333-.66666667-.0833333-.33333333 0-.66666667.0833333-.91666667.0833333-.16666666 0-.33333333.0833333-.41666666.0833333-.33333334 0-.5-.25-.5-.4166666 0-.1666667-.08333334-.25-.08333334-.4166667 0-.0833333-.08333333-.25-.08333333-.3333333-1.33333333-.25-2.08333333-.5-2.25-1-.08333333 0-.08333333-.0833334-.08333333-.1666667 0-.25.16666666-.4166667.41666666-.5 2.25-.3333333 3.33333334-2.75 3.33333334-2.8333333.08333333-.25.16666666-.4166667.08333333-.5833334-.08333333-.1666666-.58333333-.3333333-.91666667-.4166666-.08333333 0-.16666666-.0833334-.25-.0833334-.83333333-.3333333-1-.75-.91666666-1.0833333.08333333-.5.83333333-.75 1.25-.5833333.33333333.1666666.58333333.1666666.75.1666666h.25c0-.0833333 0-.25 0-.3333333-.08333334-1.16666667-.25-2.66666667.16666666-3.5 1.16666667-2.66666667 3.66666667-2.83333333 4.33333337-2.83333333h.3333333c.75 0 3.1666667.16666666 4.3333333 2.83333333.4166667.91666667.3333334 2.33333333.25 3.5v.0833333.25h.1666667c.1666667 0 .4166667-.0833333.6666667-.1666666.1666666-.0833334.25-.0833334.4166666-.0833334.1666667 0 .25 0 .4166667.0833334.3333333.0833333.5833333.4166666.5833333.6666666 0 .4166667-.3333333.6666667-.9166666.9166667-.0833334 0-.1666667.0833333-.25.0833333-.3333334.0833334-.8333334.25-.9166667.5-.0833333.1666667 0 .3333334.0833333.5.0833334.25 1.0833334 2.5 3.25 2.8333334.25 0 .4166667.25.4166667.5 0 .0833333 0 .1666666-.0833333.1666666-.1666667.4166667-.9166667.75-2.25 1 0 .0833334-.0833334.25-.0833334.3333334 0 .1666666-.0833333.25-.0833333.4166666-.0833333.25-.25.4166667-.5.4166667-.0833333 0-.25 0-.4166667-.0833333-.25-.0833334-.5-.0833334-.9166666-.0833334-.1666667 0-.4166667 0-.6666667.0833334-.4166667.0833333-.75.3333333-1.1666667.5833333-.6666666.5833333-1.5.8333333-2.5.8333333" fill="#fff" fill-rule="evenodd" transform="translate(-4 -4)"></path>
</svg>
<span class="visually-hidden">Snapchat</span>
</a>
</li>
</ul>
</nav></div>
</nav>
</div>
</div>
</div>
<div class="nav-container">
<div id="block-footer"
class="block block-system block-system-menu-blockfooter cc--component-container cc--footer-menu">
<div class="c--component c--footer-menu">
<div id="block-footer" class="block block-system block-system-menu-blockfooter cc--component-container cc--footer-menu">
<div class="c--component c--footer-menu">
<nav class="mc--menu mc--menu-footer" aria-label="UMass Amherst Footer Menu">
<ul class="menu m--menu m--menu-footer">
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Info for...</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/prospective-students" data-drupal-link-system-path="node/42831">Prospective Students</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/current-students" title="Click here for info for current students" data-drupal-link-system-path="node/61">Current Students</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/faculty-and-staff" title="Click here for info for faculty and staff" data-drupal-link-system-path="node/66">Faculty and Staff</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/info/parents-and-families" title="Click here for info for parents and families" data-drupal-link-system-path="node/81">Parents and Families</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/hr/careers" title="Info for Job Seekers">Job Seekers</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Resources</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umass.edu/registrar/academic-calendar">Academic Calendar</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/admissions/visit/visitor-info">Campus Maps</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/peoplefinder/">People Finder</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/policy/">Policies</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/atoz" title="A-Z Directory" data-drupal-link-system-path="node/3931">Sites A-Z Directory</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Connect</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="https://www.umassalumni.com/">Alumni</a>
</li>
<li class="menu-item">
<a href="https://umassathletics.com/">Athletics</a>
</li>
<li class="menu-item">
<a href="https://www.uma-foundation.org/">Giving</a>
</li>
<li class="menu-item">
<a href="https://www.library.umass.edu/">Libraries</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/gateway/contact-us" data-drupal-link-system-path="node/256">Contact Us</a>
</li>
</ul>
</div>
</li>
</ul>
</nav>
<nav class="mc--menu mc--menu-footer" aria-label="UMass Amherst Footer Menu">
<ul class="menu m--menu m--menu-footer">
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Thông tin</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="#">Bệnh nhân</a>
</li>
<li class="menu-item">
<a href="#">Người nhà bệnh nhân</a>
</li>
<li class="menu-item">
<a href="#">Bác sĩ / Chuyên gia y tế</a>
</li>
<li class="menu-item">
<a href="#">Khách hàng Doanh nghiệp</a>
</li>
<li class="menu-item">
<a href="#">Đối tác</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Thông tin</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="#">Lịch khám bệnh</a>
</li>
<li class="menu-item">
<a href="#">Sơ đồ bệnh viện</a>
</li>
<li class="menu-item">
<a href="#">Tìm bác sĩ</a>
</li>
<li class="menu-item">
<a href="#">Chính sách & Quy định</a>
</li>
<li class="menu-item">
<a href="#">Chuyên khoa & Dịch vụ</a>
</li>
</ul>
</div>
</li>
<li class="menu-item menu-item--expanded">
<span class="navigation-title">Kết nối</span>
<div class="submenu-wrapper">
<ul class="submenu">
<li class="menu-item">
<a href="#">Tuyển dụng</a>
</li>
<li class="menu-item">
<a href="#">Tin tức & Sự kiện</a>
</li>
<li class="menu-item">
<a href="#">Hoạt động xã hội</a>
</li>
<li class="menu-item">
<a href="#">Thư viện y học</a>
</li>
<li class="menu-item">
<a href="#">Liên hệ</a>
</li>
</ul>
</div>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -209,41 +287,35 @@
</div>
</div>
</div>
<div id="block-footerutility"
class="block block-system block-system-menu-blockfooter-utility cc--component-container cc--footer-menu">
<div class="c--component c--footer-menu">
<div id="block-footerutility" class="block block-system block-system-menu-blockfooter-utility cc--component-container cc--footer-menu">
<div class="c--component c--footer-menu">
<nav class="mc--menu mc--menu-footer-utility" aria-label="UMass Amherst Footer Utility Menu">
<ul class="menu m--menu m--menu-footer-utility">
<li class="menu-item">
<a href="https://www.umass.edu/gateway/site-policies" data-drupal-link-system-path="node/3861">Site Policies</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/policy/information-privacy-policy" title="UMass Amherst privacy policy">Privacy</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/policy/affirmative-action-non-discrimination-and-title-ix-non-discrimination-policy" title="UMass Amherst non-discrimination policies">Non-Discrimination Notice</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/accessibility/" title="UMass Amherst Accessibility Resources">Accessibility</a>
</li>
<li class="menu-item">
<a href="https://www.umass.edu/it/policies/it-policy-acceptable-use-interpretation-guidelines" title="UMass Amherst terms of use">Terms of Use</a>
</li>
</ul>
</nav>
<nav class="mc--menu mc--menu-footer-utility" aria-label="UMass Amherst Footer Utility Menu">
<ul class="menu m--menu m--menu-footer-utility">
<li class="menu-item">
<a href="https://sisvietnam.vn/gateway/site-policies"
data-drupal-link-system-path="node/3861">Site Policies</a>
</li>
<li class="menu-item">
<a href="https://sisvietnam.vn/policy/information-privacy-policy"
title="UMass Amherst privacy policy">Privacy</a>
</li>
<li class="menu-item">
<a href="https://sisvietnam.vn/policy/affirmative-action-non-discrimination-and-title-ix-non-discrimination-policy"
title="UMass Amherst non-discrimination policies">Non-Discrimination Notice</a>
</li>
<li class="menu-item">
<a href="https://sisvietnam.vn/accessibility/"
title="UMass Amherst Accessibility Resources">Accessibility</a>
</li>
<li class="menu-item">
<a href="https://sisvietnam.vn/it/policies/it-policy-acceptable-use-interpretation-guidelines"
title="UMass Amherst terms of use">Terms of Use</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
@@ -254,4 +326,4 @@
</div>
</footer>
</footer>
File diff suppressed because one or more lines are too long
@@ -16,14 +16,29 @@
<!-- Add base CSS here if available -->
<th:block layout:fragment="head"></th:block>
<!-- UMASS Styles -->
<link rel="stylesheet" th:href="@{/css/umass.css}">
<!-- Custom CSS (Highest Priority) -->
<link rel="stylesheet" th:href="@{/css/custom.css}">
</head>
<body>
<div layout:fragment="content">
<!-- All elements will be defined in the editor page -->
</div>
<!-- Skip to main content link (Accessibility) -->
<a href="#main-content" class="visually-hidden focusable skip-link">Skip to main content</a>
<!-- Header Fragment -->
<div th:replace="~{fragments/header}"></div>
<!-- Main Content Block -->
<main id="main-content" role="main">
<div layout:fragment="content">
<!-- All elements will be defined in the editor page -->
</div>
</main>
<!-- Footer Fragment -->
<div th:replace="~{fragments/footer}"></div>
</body>
</html>
@@ -55,6 +55,24 @@
Content
</div>
<!-- Nav Item - Posts Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePosts"
aria-expanded="true" aria-controls="collapsePosts">
<i class="fas fa-fw fa-newspaper"></i>
<span>Posts</span>
</a>
<div id="collapsePosts" class="collapse" aria-labelledby="headingPosts" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Post Management:</h6>
<a class="collapse-item" th:href="@{/manage/posts}">All Posts</a>
<a class="collapse-item" th:href="@{/manage/posts/new}">Add New</a>
<a class="collapse-item" th:href="@{/manage/posts/categories}">Categories</a>
<a class="collapse-item" th:href="@{/manage/posts/tags}">Tags</a>
</div>
</div>
</li>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePages"
@@ -65,7 +65,7 @@
</span>
</td>
<td th:text="${page.displayOrder}"></td>
<td th:text="${page.lastModifiedDate != null} ? ${#temporals.format(page.lastModifiedDate, 'yyyy-MM-dd HH:mm')} : '-'"></td>
<td th:text="${page.lastModifiedDate != null} ? ${#dates.format(page.lastModifiedDateAsDate, 'yyyy-MM-dd HH:mm')} : '-'"></td>
<td>
<a th:href="@{/manage/pages/{id}/edit(id=${page.id})}" class="btn btn-sm btn-info" title="Edit">
<i class="fas fa-edit"></i> Edit
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
<head>
<title>Categories</title>
</head>
<body>
<div layout:fragment="content">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Categories</h1>
<a th:href="@{/manage/posts}" class="d-none d-sm-inline-block btn btn-sm btn-secondary shadow-sm">
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Back to Posts
</a>
</div>
<!-- Success Message -->
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
<span th:text="${successMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<!-- Error Message -->
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
<span th:text="${errorMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="row">
<!-- Left Column: Add/Edit Category Form -->
<div class="col-lg-4">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary"
th:text="${isNew} ? 'Add New Category' : 'Edit Category'">Form</h6>
</div>
<div class="card-body">
<form th:action="${isNew} ? @{/manage/posts/categories} : @{/manage/posts/categories/{id}(id=${category.id})}"
th:object="${category}" method="post">
<!-- Validation errors -->
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger alert-sm">
<ul class="mb-0 small">
<li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li>
</ul>
</div>
<!-- Name -->
<div class="form-group">
<label for="categoryName" class="font-weight-bold">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="categoryName" th:field="*{name}"
th:classappend="${#fields.hasErrors('name')} ? 'is-invalid' : ''"
placeholder="e.g. News, Events, Health Tips" required>
<div class="invalid-feedback" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
</div>
<!-- Slug -->
<div class="form-group">
<label for="categorySlug" class="font-weight-bold">Slug</label>
<input type="text" class="form-control" id="categorySlug" th:field="*{slug}"
placeholder="auto-generated-from-name">
<small class="form-text text-muted">Leave blank to auto-generate.</small>
</div>
<!-- Parent Category -->
<div class="form-group">
<label for="parentCategory" class="font-weight-bold">Parent Category</label>
<select class="form-control" id="parentCategory" name="parentId">
<option value="">— None (Top Level) —</option>
<option th:each="cat : ${allCategories}" th:value="${cat.id}" th:text="${cat.name}"
th:if="${category.id == null || cat.id != category.id}"
th:selected="${category.parent != null && category.parent.id == cat.id}"></option>
</select>
</div>
<!-- Description -->
<div class="form-group">
<label for="categoryDescription" class="font-weight-bold">Description</label>
<textarea class="form-control" id="categoryDescription" th:field="*{description}" rows="3"
maxlength="1000"
placeholder="Optional description of this category"></textarea>
</div>
<!-- Buttons -->
<div class="d-flex justify-content-between">
<a th:if="${!isNew}" th:href="@{/manage/posts/categories}" class="btn btn-secondary btn-sm">
<i class="fas fa-times"></i> Cancel
</a>
<button type="submit" class="btn btn-primary btn-sm"
th:classappend="${isNew} ? 'btn-block' : ''">
<i class="fas fa-save"></i>
<span th:text="${isNew} ? 'Add New Category' : 'Update Category'">Save</span>
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Right Column: Categories Table -->
<div class="col-lg-8">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Existing Categories</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" id="categoriesTable" width="100%" cellspacing="0">
<thead class="thead-light">
<tr>
<th width="25%">Name</th>
<th width="20%">Slug</th>
<th width="25%">Description</th>
<th width="10%">Parent</th>
<th width="8%">Posts</th>
<th width="12%">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="cat : ${allCategories}">
<td>
<strong th:text="${cat.name}"></strong>
</td>
<td>
<code th:text="${cat.slug}"></code>
</td>
<td>
<small th:text="${cat.description}" class="text-muted"></small>
<span th:if="${cat.description == null || cat.description.isEmpty()}" class="text-muted"></span>
</td>
<td>
<span th:if="${cat.parent != null}" th:text="${cat.parent.name}" class="badge badge-light"></span>
<span th:if="${cat.parent == null}" class="text-muted"></span>
</td>
<td class="text-center">
<span class="badge badge-primary" th:text="${postCounts.get(cat.id) ?: 0}"></span>
</td>
<td class="text-center">
<a th:href="@{/manage/posts/categories/{id}/edit(id=${cat.id})}"
class="btn btn-sm btn-outline-info mr-1" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form th:action="@{/manage/posts/categories/{id}/delete(id=${cat.id})}" method="post"
style="display:inline;"
onsubmit="return confirm('Are you sure you want to delete this category?');">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(allCategories)}">
<td colspan="6" class="text-center text-muted py-4">
<i class="fas fa-folder-open fa-2x mb-2 d-block"></i>
No categories yet. Use the form on the left to create one.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,479 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/manage-layout}">
<head>
<title th:text="${isNew} ? 'Add New Post' : 'Edit Post'">Post Form</title>
<style>
/* Editor.js container styling */
#editorjs {
border: 1px solid #d1d3e2;
border-radius: 0.35rem;
padding: 16px 12px;
min-height: 350px;
background: #fff;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
#editorjs:focus-within {
border-color: #bac8f3;
box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
}
/* Block tool styling overrides */
.ce-block__content {
max-width: 100% !important;
}
.ce-toolbar__content {
max-width: 100% !important;
}
.codex-editor__redactor {
padding-bottom: 80px !important;
}
.editor-plugin-info {
font-size: 0.75rem;
color: #858796;
}
.editor-plugin-info .badge {
font-weight: 400;
font-size: 0.7rem;
}
/* Full Screen Editor Mode */
.editor-fullscreen {
position: fixed !important;
top: 0;
left: 0;
width: 100vw !important;
height: 100vh !important;
z-index: 9999 !important;
margin: 0 !important;
border-radius: 0 !important;
border: none !important;
overflow-y: auto !important;
padding: 40px !important;
}
/* Tag input styling */
.tag-input-container {
position: relative;
}
.tag-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: #fff;
border: 1px solid #d1d3e2;
border-top: none;
border-radius: 0 0 0.35rem 0.35rem;
max-height: 200px;
overflow-y: auto;
display: none;
}
.tag-suggestions .suggestion-item {
padding: 8px 12px;
cursor: pointer;
transition: background 0.15s;
}
.tag-suggestions .suggestion-item:hover {
background: #eaecf4;
}
/* Sidebar panels styling */
.publish-panel .card-body {
padding: 1rem;
}
.publish-panel .form-group {
margin-bottom: 0.75rem;
}
.publish-panel .form-group:last-child {
margin-bottom: 0;
}
/* Featured image preview */
.featured-image-preview {
max-width: 100%;
max-height: 200px;
object-fit: cover;
border-radius: 0.35rem;
border: 1px solid #d1d3e2;
margin-top: 0.5rem;
display: none;
}
.featured-image-preview.has-image {
display: block;
}
</style>
</head>
<body>
<div layout:fragment="content">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800" th:text="${isNew} ? 'Add New Post' : 'Edit Post'">Post Form</h1>
<a th:href="@{/manage/posts}" class="d-none d-sm-inline-block btn btn-sm btn-secondary shadow-sm">
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Back to All Posts
</a>
</div>
<form id="postForm" th:action="${isNew} ? @{/manage/posts/create} : @{/manage/posts/{id}(id=${post.id})}"
th:object="${post}" method="post">
<!-- Validation errors summary -->
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">
<ul class="mb-0">
<li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li>
</ul>
</div>
<div class="row">
<!-- Main Content Column (Left) -->
<div class="col-lg-8">
<!-- Title Card -->
<div class="card shadow mb-4">
<div class="card-body">
<div class="form-group mb-0">
<input type="text" class="form-control form-control-lg" id="postTitle" th:field="*{title}"
th:classappend="${#fields.hasErrors('title')} ? 'is-invalid' : ''"
placeholder="Enter post title..." required
style="font-size: 1.4rem; font-weight: 600; border: none; padding: 0; box-shadow: none;">
<div class="invalid-feedback" th:if="${#fields.hasErrors('title')}" th:errors="*{title}"></div>
</div>
<!-- Slug -->
<div class="mt-2">
<small class="text-muted">
<i class="fas fa-link"></i> Permalink:
<code>/post/<span id="slugPreview" th:text="${post.slug ?: 'auto-generated'}"></span></code>
</small>
<input type="hidden" id="postSlug" th:field="*{slug}">
</div>
</div>
</div>
<!-- Block Editor Content -->
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-cubes"></i> Post Content (Block Editor)
</h6>
<button type="button" class="btn btn-sm btn-outline-primary" id="toggleFullscreenBtn">
<i class="fas fa-expand"></i> Full Screen
</button>
</div>
<div class="card-body">
<div class="editor-plugin-info mb-2">
Active blocks:
<span class="badge badge-light">Heading</span>
<span class="badge badge-light">List</span>
<span class="badge badge-light">Quote</span>
<span class="badge badge-light">Table</span>
<span class="badge badge-light">Code</span>
<span class="badge badge-light">Delimiter</span>
<span class="badge badge-light">Warning</span>
<span id="pluginBadges"></span>
</div>
<!-- The Editor.js container -->
<div id="editorjs"></div>
<!-- Hidden input to store the JSON content for form submission -->
<input type="hidden" id="editorContent" name="content" th:value="*{content}">
<small class="form-text text-muted mt-2">
<i class="fas fa-info-circle"></i>
Click the <strong>+</strong> button or press <kbd>Tab</kbd> to add new blocks.
</small>
</div>
</div>
<!-- Excerpt -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-align-left"></i> Excerpt
</h6>
</div>
<div class="card-body">
<textarea class="form-control" id="postExcerpt" th:field="*{excerpt}" rows="3"
maxlength="1000"
placeholder="Write a short summary of the post (optional, used for preview cards)..."></textarea>
<small class="form-text text-muted">A brief hand-written summary. If left blank, a generated snippet from the content will be used.</small>
</div>
</div>
<!-- SEO Meta Description -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-search"></i> SEO Settings
</h6>
</div>
<div class="card-body">
<div class="form-group mb-0">
<label for="postMetaDescription" class="font-weight-bold">Meta Description</label>
<textarea class="form-control" id="postMetaDescription" th:field="*{metaDescription}" rows="2"
maxlength="500"
placeholder="Brief description for search engines (max 500 characters)"></textarea>
<small class="form-text text-muted">This appears in Google search results below the post title.</small>
</div>
</div>
</div>
</div>
<!-- Sidebar Column (Right) -->
<div class="col-lg-4">
<!-- Publish Panel -->
<div class="card shadow mb-4 publish-panel">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-paper-plane"></i> Publish
</h6>
</div>
<div class="card-body">
<div class="form-group">
<label for="postStatus" class="font-weight-bold">Status</label>
<select class="form-control" id="postStatus" th:field="*{status}">
<option th:each="s : ${statuses}" th:value="${s}" th:text="${s}"></option>
</select>
</div>
<div class="form-group mt-3">
<label for="postLayout" class="font-weight-bold">Layout</label>
<select class="form-control" id="postLayout" th:field="*{layout}">
<option th:each="l : ${layouts}" th:value="${l}" th:text="${l}"></option>
</select>
<small class="form-text text-muted">Controls the visual appearance on the frontend.</small>
</div>
<hr>
<div class="d-flex justify-content-between">
<a th:href="@{/manage/posts}" class="btn btn-secondary btn-sm">
<i class="fas fa-times"></i> Cancel
</a>
<button type="submit" class="btn btn-primary btn-sm">
<i class="fas fa-save"></i>
<span th:text="${isNew} ? 'Publish' : 'Update'">Save</span>
</button>
</div>
</div>
</div>
<!-- Category Panel -->
<div class="card shadow mb-4 publish-panel">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-folder"></i> Category
</h6>
</div>
<div class="card-body">
<select class="form-control" id="postCategory" name="categoryId">
<option value="">— No Category —</option>
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.name}"
th:selected="${post.category != null && post.category.id == cat.id}"></option>
</select>
<small class="form-text text-muted mt-1">
<a th:href="@{/manage/posts/categories}" target="_blank">
<i class="fas fa-plus-circle"></i> Manage Categories
</a>
</small>
</div>
</div>
<!-- Tags Panel -->
<div class="card shadow mb-4 publish-panel">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-tags"></i> Tags
</h6>
</div>
<div class="card-body">
<div class="tag-input-container">
<input type="text" class="form-control" id="tagNamesInput" name="tagNames"
th:value="${tagNames}"
placeholder="e.g. news, health, update">
<div class="tag-suggestions" id="tagSuggestions"></div>
</div>
<small class="form-text text-muted mt-1">
Separate tags with commas. New tags will be created automatically.
</small>
<div class="mt-2" id="existingTags">
<span th:each="tag : ${allTags}" class="badge badge-light mr-1 mb-1"
style="cursor: pointer;" th:text="${tag.name}"
th:attr="data-tag-name=${tag.name}"
onclick="addTagToInput(this.getAttribute('data-tag-name'))"></span>
</div>
<small class="form-text text-muted">Click a tag to add it.</small>
</div>
</div>
<!-- Featured Image Panel -->
<div class="card shadow mb-4 publish-panel">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-image"></i> Featured Image
</h6>
</div>
<div class="card-body">
<input type="text" class="form-control" id="postFeaturedImage" th:field="*{featuredImage}"
placeholder="Image URL (e.g. /uploads/image.jpg)">
<img id="featuredImagePreview" class="featured-image-preview mt-2"
th:src="${post.featuredImage}" th:classappend="${post.featuredImage != null && !post.featuredImage.isEmpty()} ? 'has-image' : ''"
alt="Featured image preview">
<small class="form-text text-muted mt-1">Enter the URL of the featured image for this post.</small>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- Editor.js Scripts (injected via layout:fragment="scripts") -->
<section layout:fragment="scripts">
<!-- Editor.js Core -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.30.8/dist/editorjs.umd.js"></script>
<!-- Built-in Block Tools -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.8.8/dist/header.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/nested-list@1.4.3/dist/nested-list.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.7.4/dist/quote.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@1.4.2/dist/delimiter.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/table@2.4.2/dist/table.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@2.9.3/dist/code.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/warning@1.4.1/dist/warning.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.4.0/dist/marker.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@1.5.1/dist/inline-code.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/underline@1.1.0/dist/bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.9.0/dist/image.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/attaches@1.3.0/dist/bundle.js"></script>
<!-- Custom SIS Editor Plugins -->
<script th:src="@{/js/manage/editor-plugins/html-snippet.js}"></script>
<!-- Init Editor -->
<script th:src="@{/js/manage/editor-config.js}"></script>
<!-- Initialize the editor and page-specific scripts -->
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
// --- Editor.js Initialization ---
var existingContent = document.getElementById('editorContent').value;
var initialData = null;
if (existingContent && existingContent.trim() !== '') {
try {
initialData = JSON.parse(existingContent);
} catch (e) {
console.warn('[SIS Editor] Existing content is not valid JSON, starting fresh.');
}
}
window.sisEditor = initSISEditor('editorjs', 'editorContent', initialData);
// Show badges for any injected plugins
var pluginBadges = document.getElementById('pluginBadges');
var pluginKeys = Object.keys(window.SISEditorPlugins || {});
if (pluginBadges && pluginKeys.length > 0) {
pluginKeys.forEach(function (key) {
var badge = document.createElement('span');
badge.className = 'badge badge-info ml-1';
badge.textContent = key + ' (plugin)';
pluginBadges.appendChild(badge);
});
}
// --- Unsaved Changes Warning ---
var isFormSubmitted = false;
document.getElementById('postForm').addEventListener('submit', function () {
isFormSubmitted = true;
});
window.addEventListener('beforeunload', function (e) {
if (!isFormSubmitted) {
var msg = 'You may have unsaved changes. Are you sure you want to leave?';
e.returnValue = msg;
return msg;
}
});
// --- Full Screen Mode Toggle ---
var editorContainer = document.getElementById('editorjs');
var fsBtn = document.getElementById('toggleFullscreenBtn');
var fsIcon = fsBtn.querySelector('i');
fsBtn.addEventListener('click', function () {
editorContainer.classList.toggle('editor-fullscreen');
if (editorContainer.classList.contains('editor-fullscreen')) {
fsIcon.classList.remove('fa-expand');
fsIcon.classList.add('fa-compress');
fsBtn.style.position = 'fixed';
fsBtn.style.top = '10px';
fsBtn.style.right = '20px';
fsBtn.style.zIndex = '10000';
} else {
fsIcon.classList.remove('fa-compress');
fsIcon.classList.add('fa-expand');
fsBtn.style.position = 'static';
}
});
// --- Featured Image Preview ---
var imgInput = document.getElementById('postFeaturedImage');
var imgPreview = document.getElementById('featuredImagePreview');
imgInput.addEventListener('input', function () {
if (imgInput.value && imgInput.value.trim() !== '') {
imgPreview.src = imgInput.value;
imgPreview.classList.add('has-image');
} else {
imgPreview.classList.remove('has-image');
}
});
// --- Slug Auto-generation from Title ---
var titleInput = document.getElementById('postTitle');
var slugInput = document.getElementById('postSlug');
var slugPreview = document.getElementById('slugPreview');
var isSlugManuallySet = slugInput.value && slugInput.value.trim() !== '';
titleInput.addEventListener('input', function () {
if (!isSlugManuallySet) {
var slug = titleInput.value
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/đ/g, 'd').replace(/Đ/g, 'D')
.replace(/[^\w\s-]/g, '')
.replace(/[\s]+/g, '-')
.replace(/-{2,}/g, '-')
.replace(/^-|-$/g, '');
slugPreview.textContent = slug || 'auto-generated';
}
});
});
// --- Tag click-to-add helper ---
function addTagToInput(tagName) {
var input = document.getElementById('tagNamesInput');
var currentTags = input.value.split(',').map(function(t) { return t.trim(); }).filter(function(t) { return t !== ''; });
if (currentTags.indexOf(tagName) === -1) {
currentTags.push(tagName);
input.value = currentTags.join(', ');
}
}
</script>
</section>
</body>
</html>
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
<head>
<title>All Posts</title>
</head>
<body>
<div layout:fragment="content">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">All Posts</h1>
<a th:href="@{/manage/posts/new}" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm">
<i class="fas fa-plus fa-sm text-white-50"></i> Add New Post
</a>
</div>
<!-- Success Message -->
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
<span th:text="${successMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<!-- Error Message -->
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
<span th:text="${errorMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<!-- Filter Bar -->
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-filter"></i> Filter Posts
</h6>
</div>
<div class="card-body">
<form th:action="@{/manage/posts}" method="get" class="form-inline">
<div class="form-group mr-3 mb-2">
<label for="filterCategory" class="mr-2 font-weight-bold">Category:</label>
<select class="form-control form-control-sm" id="filterCategory" name="categoryId">
<option value="">All Categories</option>
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.name}"
th:selected="${selectedCategoryId != null && selectedCategoryId == cat.id}"></option>
</select>
</div>
<div class="form-group mr-3 mb-2">
<label for="filterStatus" class="mr-2 font-weight-bold">Status:</label>
<select class="form-control form-control-sm" id="filterStatus" name="status">
<option value="">All Statuses</option>
<option th:each="s : ${statuses}" th:value="${s}" th:text="${s}"
th:selected="${selectedStatus != null && selectedStatus == s.name()}"></option>
</select>
</div>
<button type="submit" class="btn btn-sm btn-primary mb-2 mr-2">
<i class="fas fa-search"></i> Filter
</button>
<a th:href="@{/manage/posts}" class="btn btn-sm btn-outline-secondary mb-2">
<i class="fas fa-times"></i> Clear
</a>
</form>
</div>
</div>
<!-- Posts Table -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Manage Posts</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" id="postsTable" width="100%" cellspacing="0">
<thead class="thead-light">
<tr>
<th width="5%">#</th>
<th width="28%">Title</th>
<th width="13%">Category</th>
<th width="17%">Tags</th>
<th width="8%">Status</th>
<th width="10%">Author</th>
<th width="12%">Date</th>
<th width="7%">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="post, iterStat : ${posts}">
<td th:text="${iterStat.count}"></td>
<td>
<a th:href="@{/manage/posts/{id}/edit(id=${post.id})}" class="font-weight-bold text-primary"
th:text="${post.title}"></a>
<br>
<small class="text-muted">
<a th:href="@{/post/{slug}(slug=${post.slug})}" target="_blank" class="text-muted">
<i class="fas fa-external-link-alt"></i> /post/<span th:text="${post.slug}"></span>
</a>
</small>
</td>
<td>
<span th:if="${post.category != null}" th:text="${post.category.name}"
class="badge badge-light"></span>
<span th:if="${post.category == null}" class="text-muted"></span>
</td>
<td>
<span th:each="tag : ${post.tags}" class="badge badge-info mr-1 mb-1"
th:text="${tag.name}"></span>
<span th:if="${#sets.isEmpty(post.tags)}" class="text-muted"></span>
</td>
<td>
<span class="badge"
th:classappend="${post.status.name() == 'PUBLISHED'} ? 'badge-success' : (${post.status.name() == 'DRAFT'} ? 'badge-warning' : 'badge-secondary')"
th:text="${post.status}">
</span>
</td>
<td>
<small th:text="${post.createdBy}" class="text-muted"></small>
</td>
<td>
<small th:if="${post.createdDateAsDate != null}" th:text="${#dates.format(post.createdDateAsDate, 'yyyy-MM-dd HH:mm')}"></small>
<small th:if="${post.createdDateAsDate == null}">-</small>
</td>
<td class="text-center">
<a th:href="@{/manage/posts/{id}/edit(id=${post.id})}" class="btn btn-sm btn-outline-info mr-1" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form th:action="@{/manage/posts/{id}/delete(id=${post.id})}" method="post" style="display:inline;"
onsubmit="return confirm('Are you sure you want to delete this post?');">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(posts)}">
<td colspan="8" class="text-center text-muted py-4">
<i class="fas fa-newspaper fa-2x mb-2 d-block"></i>
No posts found. Click "Add New Post" to create one.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
<head>
<title>Tags</title>
</head>
<body>
<div layout:fragment="content">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Tags</h1>
<a th:href="@{/manage/posts}" class="d-none d-sm-inline-block btn btn-sm btn-secondary shadow-sm">
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Back to Posts
</a>
</div>
<!-- Success Message -->
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
<span th:text="${successMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<!-- Error Message -->
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
<span th:text="${errorMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="row">
<!-- Left Column: Add/Edit Tag Form -->
<div class="col-lg-4">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary"
th:text="${isNew} ? 'Add New Tag' : 'Edit Tag'">Form</h6>
</div>
<div class="card-body">
<form th:action="${isNew} ? @{/manage/posts/tags} : @{/manage/posts/tags/{id}(id=${tag.id})}"
th:object="${tag}" method="post">
<!-- Validation errors -->
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger alert-sm">
<ul class="mb-0 small">
<li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li>
</ul>
</div>
<!-- Name -->
<div class="form-group">
<label for="tagName" class="font-weight-bold">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="tagName" th:field="*{name}"
th:classappend="${#fields.hasErrors('name')} ? 'is-invalid' : ''"
placeholder="e.g. covid-19, health-tips, breaking-news" required>
<div class="invalid-feedback" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
</div>
<!-- Slug -->
<div class="form-group">
<label for="tagSlug" class="font-weight-bold">Slug</label>
<input type="text" class="form-control" id="tagSlug" th:field="*{slug}"
placeholder="auto-generated-from-name">
<small class="form-text text-muted">Leave blank to auto-generate. Used in URLs for tag archive pages.</small>
</div>
<!-- Buttons -->
<div class="d-flex justify-content-between">
<a th:if="${!isNew}" th:href="@{/manage/posts/tags}" class="btn btn-secondary btn-sm">
<i class="fas fa-times"></i> Cancel
</a>
<button type="submit" class="btn btn-primary btn-sm"
th:classappend="${isNew} ? 'btn-block' : ''">
<i class="fas fa-save"></i>
<span th:text="${isNew} ? 'Add New Tag' : 'Update Tag'">Save</span>
</button>
</div>
</form>
</div>
</div>
<!-- Tag Info Card -->
<div class="card shadow mb-4 border-left-info">
<div class="card-body">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">About Tags</div>
<p class="small text-muted mb-0">
Tags are keywords attached to posts for quick searchability.
Unlike categories, tags have no hierarchy. You can assign multiple tags to a single post.
Tags can also be auto-created when composing a post by typing them in the tag input field.
</p>
</div>
</div>
</div>
<!-- Right Column: Tags Table -->
<div class="col-lg-8">
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">Existing Tags</h6>
<span class="badge badge-primary badge-pill" th:text="${#lists.size(allTags)} + ' total'"></span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" id="tagsTable" width="100%" cellspacing="0">
<thead class="thead-light">
<tr>
<th width="30%">Name</th>
<th width="30%">Slug</th>
<th width="15%">Posts</th>
<th width="25%">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="t : ${allTags}">
<td>
<strong th:text="${t.name}"></strong>
</td>
<td>
<code th:text="${t.slug}"></code>
</td>
<td class="text-center">
<span class="badge badge-primary" th:text="${postCounts.get(t.id) ?: 0}"></span>
</td>
<td class="text-center">
<a th:href="@{/manage/posts/tags/{id}/edit(id=${t.id})}"
class="btn btn-sm btn-outline-info mr-1" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form th:action="@{/manage/posts/tags/{id}/delete(id=${t.id})}" method="post"
style="display:inline;"
onsubmit="return confirm('Are you sure you want to delete this tag?');">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(allTags)}">
<td colspan="4" class="text-center text-muted py-4">
<i class="fas fa-tags fa-2x mb-2 d-block"></i>
No tags yet. Use the form on the left to create one.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/layout}">
<head>
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
</head>
<body>
<div layout:fragment="content">
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
<div class="t--template t--article">
<!-- Content Top (e.g. Hero Image & Title) -->
<div class="content-top">
<div class="lc--layout-container lc--full">
<div class="l--layout l--full">
<div class="lr--layout-region lr--main">
<div class="cc--component-container cc--hero-article">
<div class="c--component c--hero-article">
<div class="f--field f--image" th:if="${post.featuredImage != null and !post.featuredImage.isEmpty()}">
<img th:src="${post.featuredImage}" th:alt="${post.title}" style="width:100%; object-fit:cover;">
</div>
<h1 class="f--field f--title text-center" th:text="${post.title}">Post Title</h1>
<p class="f--field f--date text-center" th:if="${formattedDate != null}" th:text="${formattedDate}">Date</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Content Main (Full Width) -->
<div class="content-main">
<div class="lc--layout-container lc--full">
<div class="l--layout l--full">
<div class="lr--layout-region lr--main">
<section class="cc--component-container cc--wysiwyg">
<div class="c--component c--wysiwyg">
<div class="f--field f--wysiwyg editorjs-content" id="postContentRaw" style="display:none;" th:text="${post.content}"></div>
<div class="f--field f--wysiwyg" id="postContentParsed"></div>
</div>
</section>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var rawData = document.getElementById("postContentRaw").textContent;
var parsedContainer = document.getElementById("postContentParsed");
try {
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
data.blocks.forEach(function(block) {
switch(block.type) {
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
case "paragraph":
html += "<p>" + block.data.text + "</p>";
break;
case "image":
html += "<figure><img src='" + block.data.url + "' alt='" + (block.data.caption || '') + "'><figcaption>" + (block.data.caption || '') + "</figcaption></figure>";
break;
case "list":
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
html += "<" + tag + ">";
block.data.items.forEach(function(item) {
html += "<li>" + item + "</li>";
});
html += "</" + tag + ">";
break;
case "quote":
html += "<blockquote>" + block.data.text + " <cite>" + block.data.caption + "</cite></blockquote>";
break;
default:
console.log("Unknown block type", block.type);
}
});
}
parsedContainer.innerHTML = html;
} catch(e) {
parsedContainer.innerHTML = rawData;
}
});
</script>
</div>
</body>
</html>
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/layout}">
<head>
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
</head>
<body>
<div layout:fragment="content">
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
<div class="t--template t--article">
<!-- Content Top (e.g. Hero Image & Title) -->
<div class="content-top">
<div class="lc--layout-container lc--full">
<div class="l--layout l--full">
<div class="lr--layout-region lr--main">
<div class="cc--component-container cc--hero-article">
<div class="c--component c--hero-article">
<div class="f--field f--image" th:if="${post.featuredImage != null and !post.featuredImage.isEmpty()}">
<img th:src="${post.featuredImage}" th:alt="${post.title}">
</div>
<h1 class="f--field f--title" th:text="${post.title}">Post Title</h1>
<p class="f--field f--date" th:if="${formattedDate != null}" th:text="${formattedDate}">Date</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Content Main (Sidebar Layout) -->
<div class="content-main">
<div class="lc--layout-container lc--two-column">
<div class="l--layout l--two-column">
<!-- Sidebar -->
<aside class="lr--layout-region lr--sidebar">
<div class="cc--component-container cc--contact-block">
<div class="c--component c--contact-block">
<h3>About the Author</h3>
<p>This is a placeholder for author details or related links that appear in the sidebar.</p>
<div th:if="${post.category != null}">
<h4>Category</h4>
<p th:text="${post.category.name}">Category Name</p>
</div>
<div th:if="${!post.tags.isEmpty()}">
<h4>Tags</h4>
<ul>
<li th:each="tag : ${post.tags}" th:text="${tag.name}">Tag</li>
</ul>
</div>
</div>
</div>
</aside>
<!-- Main Content -->
<div class="lr--layout-region lr--main">
<section class="cc--component-container cc--wysiwyg">
<div class="c--component c--wysiwyg">
<div class="f--field f--wysiwyg editorjs-content" id="postContentRaw" style="display:none;" th:text="${post.content}"></div>
<div class="f--field f--wysiwyg" id="postContentParsed"></div>
</div>
</section>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var rawData = document.getElementById("postContentRaw").textContent;
var parsedContainer = document.getElementById("postContentParsed");
try {
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
data.blocks.forEach(function(block) {
switch(block.type) {
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
case "paragraph":
html += "<p>" + block.data.text + "</p>";
break;
case "image":
html += "<figure><img src='" + block.data.url + "' alt='" + (block.data.caption || '') + "'><figcaption>" + (block.data.caption || '') + "</figcaption></figure>";
break;
case "list":
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
html += "<" + tag + ">";
block.data.items.forEach(function(item) {
html += "<li>" + item + "</li>";
});
html += "</" + tag + ">";
break;
case "quote":
html += "<blockquote>" + block.data.text + " <cite>" + block.data.caption + "</cite></blockquote>";
break;
default:
console.log("Unknown block type", block.type);
}
});
}
parsedContainer.innerHTML = html;
} catch(e) {
parsedContainer.innerHTML = rawData;
}
});
</script>
</div>
</body>
</html>
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/layout}">
<head>
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
</head>
<body>
<div layout:fragment="content">
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
<div class="t--template t--article">
<!-- Content Top (e.g. Hero Image & Title) -->
<div class="content-top">
<div class="lc--layout-container lc--full">
<div class="l--layout l--full">
<div class="lr--layout-region lr--main">
<div class="cc--component-container cc--hero-article">
<div class="c--component c--hero-article">
<div class="f--field f--image" th:if="${post.featuredImage != null and !post.featuredImage.isEmpty()}">
<img th:src="${post.featuredImage}" th:alt="${post.title}">
</div>
<h1 class="f--field f--title" th:text="${post.title}">Post Title</h1>
<p class="f--field f--date" th:if="${formattedDate != null}" th:text="${formattedDate}">Date</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Content Main (Standard One Column) -->
<div class="content-main">
<div class="lc--layout-container lc--one-column">
<div class="l--layout l--one-column">
<div class="lr--layout-region lr--main">
<section class="cc--component-container cc--wysiwyg">
<div class="c--component c--wysiwyg">
<!-- Output raw HTML from block editor (we need to parse or render it). Since it's saved as raw HTML from the previous plugin or JSON, we just output it. Wait, the editor uses Editor.js, so the content might be JSON! -->
<!-- Assuming the content is parsed or we just dump it for now -->
<div class="f--field f--wysiwyg editorjs-content" id="postContentRaw" style="display:none;" th:text="${post.content}"></div>
<div class="f--field f--wysiwyg" id="postContentParsed"></div>
</div>
</section>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Include Editor.js parser to render the content on the frontend -->
<!-- Because Editor.js saves as JSON, we need to convert it to HTML. -->
<script>
document.addEventListener("DOMContentLoaded", function() {
var rawData = document.getElementById("postContentRaw").textContent;
var parsedContainer = document.getElementById("postContentParsed");
try {
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
data.blocks.forEach(function(block) {
switch(block.type) {
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
case "paragraph":
html += "<p>" + block.data.text + "</p>";
break;
case "image":
html += "<figure><img src='" + block.data.url + "' alt='" + (block.data.caption || '') + "'><figcaption>" + (block.data.caption || '') + "</figcaption></figure>";
break;
case "list":
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
html += "<" + tag + ">";
block.data.items.forEach(function(item) {
html += "<li>" + item + "</li>";
});
html += "</" + tag + ">";
break;
case "quote":
html += "<blockquote>" + block.data.text + " <cite>" + block.data.caption + "</cite></blockquote>";
break;
default:
console.log("Unknown block type", block.type);
}
});
}
parsedContainer.innerHTML = html;
} catch(e) {
// If it's not JSON, it might just be HTML
parsedContainer.innerHTML = rawData;
}
});
</script>
</div>
</body>
</html>