19 Commits

Author SHA1 Message Date
basketballcantho 3dc5a03484 add Hooks 2026-07-02 19:21:25 +07:00
basketballcantho 0dd68c3649 develop core Hooks, remain hooks is defined but not develop 2026-07-02 19:11:03 +07:00
basketballcantho a7dbf52eea hoàn thành trang settings 2026-07-02 18:59:48 +07:00
basketballcantho 302785b60b bổ sung thêm các chức năng còn thiếu của appearance 2026-07-02 18:48:00 +07:00
basketballcantho e91e607d14 hoàn thành phần tạo theme 2026-07-02 18:30:12 +07:00
basketballcantho 986bd08e5f done dashboard 2026-07-02 16:06:26 +07:00
basketballcantho 2181547184 have done setting page 2026-07-02 15:46:35 +07:00
basketballcantho 4793dc9c17 finish plugin function 2026-07-02 15:24:07 +07:00
basketballcantho ba7a43e637 fix profile cannot access while logining from GG account 2026-07-02 14:26:08 +07:00
basketballcantho ba587a40cb login form okie 2026-07-01 15:40:46 +07:00
basketballcantho e9909784c7 chore: update Oracle database service name to FREEPDB1 in docker-compose.yml 2026-06-28 21:48:45 +07:00
basketballcantho 554ef1d15d chore: configure app service to wait for oracle database health check 2026-06-28 21:43:02 +07:00
basketballcantho 39443b95ab chore: update oracle data volume to use a named volume instead of a host path 2026-06-28 21:34:31 +07:00
basketballcantho 23134517ce update index.html 2026-06-28 21:28:38 +07:00
basketballcantho 47ce0da13a Update logo and docker compose configuration 2026-06-28 20:06:37 +07:00
basketballcantho d4d9543a98 build project 2026-06-28 17:28:16 +07:00
basketballcantho 0326535eb6 Phần Post đã thiết kế xong chỉ cần sửa lại tempalte nữa là được 2026-06-28 16:25:51 +07:00
basketballcantho 38d6b8234a Hoan thanh chuc nang Page 2026-06-25 21:34:36 +07:00
basketballcantho 6fe098bc5b hoàn thành block editor của Pages 2026-06-25 16:38:03 +07:00
262 changed files with 45683 additions and 1592 deletions
+4 -2
View File
@@ -7,14 +7,16 @@
"request": "launch",
"mainClass": "com.sisvietnamvn.web.SisvietnamvnApp",
"projectName": "sisvietnamvn",
"cwd": "${workspaceFolder}/sisvietnamvn_main",
"vmArgs": "-Dspring.profiles.active=dev"
},
{
"type": "java",
"name": "Debug (Attach) - Port 5005",
"request": "attach",
"hostName": "localhost",
"port": 5005
"hostName": "127.0.0.1",
"port": 5005,
"projectName": "sisvietnamvn"
}
]
}
+3 -3
View File
@@ -1,11 +1,11 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"jdk.jdkhome": "C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.11.10-hotspot",
"java.jdt.ls.java.home": "C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.11.10-hotspot",
"jdk.jdkhome": "/usr/lib/jvm/java-21-openjdk-amd64",
"java.jdt.ls.java.home": "/usr/lib/jvm/java-21-openjdk-amd64",
"java.configuration.runtimes": [
{
"name": "JavaSE-21",
"path": "C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.11.10-hotspot",
"path": "/usr/lib/jvm/java-21-openjdk-amd64",
"default": true
}
],
+70
View File
@@ -0,0 +1,70 @@
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 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
(done)1. Dashboard (Bảng điều khiển) Home Xem tổng quan trạng thái website (Site Health, số lượng bài viết/trang, bình luận gần đây, hoạt động viết nháp nhanh).
Updates Cập nhật phiên bản WordPress Core, Themes, Plugins và các gói dịch vụ ngôn ngữ.
(Done) 2. Posts (Bài viết) All Posts Quản lý toàn bộ bài viết tin tức, blog, lọc theo ngày/chuyên mục.
Add New Soạn thảo bài viết mới bằng Block Editor (Gutenberg) hoặc Classic Editor.
Categories Tạo và quản lý các Chuyên mục (thư mục phân loại bài viết).
Tags Tạo và quản lý các Thẻ (nhãn từ khóa tìm kiếm nhanh).
(Done) 3. Media (Thư viện) Library Quản lý toàn bộ hình ảnh, tài liệu (PDF, Docx), video, âm thanh đã tải lên.
Add New Tải tệp tin mới lên máy chủ.
(Done) 4. Pages (Trang) All Pages Quản lý các trang tĩnh (ví dụ: Trang chủ, Trang liên hệ, Giới thiệu bệnh viện, Giờ làm việc...).
Add New Tạo trang tĩnh mới.
(done) 6. Appearance (Giao diện) Themes Cài đặt, kích hoạt hoặc xóa các giao diện hiển thị của website.
Customize Chỉnh sửa giao diện trực quan theo thời gian thực (Header, Footer, Colors, Font, v.v.).
Widgets Quản lý các khối nội dung ở Sidebar hoặc Footer.
Menus Thiết kế menu điều hướng chính ở đầu trang và chân trang.
Theme File Editor Chỉnh sửa trực tiếp file code của giao diện (chỉ dùng cho lập trình viên).
(done)7. Plugins (Gói mở rộng) Installed Plugins Quản lý, kích hoạt, vô hiệu hóa hoặc xóa các Plugin đang có trên hệ thống.
Add New Tìm kiếm và cài đặt Plugin mới từ thư viện WordPress.org hoặc upload file .zip.
Plugin File Editor Chỉnh sửa mã nguồn của các Plugin trực tiếp từ admin.
(done) 8. Users (Thành viên) All Users Quản lý danh sách toàn bộ tài khoản đăng ký trên hệ thống.
Add New Tạo tài khoản người dùng mới và chỉ định vai trò (Role).
Profile Chỉnh sửa thông tin cá nhân của tài khoản đang đăng nhập (đổi pass, ngôn ngữ hiển thị).
(done) 9. Tools (Công cụ) Available Tools Các công cụ hỗ trợ chuyển đổi dữ liệu.
(done) Import / Export Nhập/Xuất toàn bộ bài viết, trang, media sang file .xml để sao lưu hoặc chuyển host.
(done) Site Health Kiểm tra sức khỏe hệ thống (phiên bản PHP, MySQL, các lỗi bảo mật).
(done) Export / Erase Personal Data Xuất hoặc xóa dữ liệu cá nhân của người dùng để tuân thủ luật bảo mật (GDPR).
(done) 10. Settings (Cài đặt) General Thiết lập tên website, câu khẩu hiệu, địa chỉ email admin, múi giờ, định dạng ngày tháng.
Writing Cấu hình mặc định khi soạn thảo văn bản và gửi bài viết qua email.
Reading Thiết lập trang chủ hiển thị, số lượng bài viết tối đa trên một trang blog, chặn công cụ tìm kiếm.
Discussion Cấu hình bình luận (yêu cầu phê duyệt, chặn từ ngữ nhạy cảm).
Media Cài đặt kích thước mặc định cho ảnh khi upload (Thumbnail, Medium, Large).
Permalinks Định dạng cấu trúc đường dẫn tĩnh (URL) tối ưu cho SEO (ví dụ: /post-name/).
Privacy Thiết lập trang chính sách bảo mật cho website.
PHẦN 2: CÁC HÀM PHP & HOOKS ĐỂ PHÁT TRIỂN TRANG ADMIN (DEVELOPER API)
Nếu bạn đang lập trình tùy biến trang admin (hoặc xây dựng CMS tương tự), WordPress cung cấp bộ API để tạo và kiểm soát trang admin sau:
1. Các hàm tạo Menu & Submenu trong Admin
add_menu_page(): Thêm một Menu chính mới (cấp cao nhất) ở thanh sidebar bên trái.
add_submenu_page(): Thêm một Menu con (Submenu) vào dưới một Menu chính bất kỳ.
Các hàm phái sinh viết nhanh (Helper functions):
add_options_page(): Thêm trang con vào menu Settings (Cài đặt).
add_theme_page(): Thêm trang con vào menu Appearance (Giao diện).
add_plugins_page(): Thêm trang con vào menu Plugins.
add_users_page(): Thêm trang con vào menu Users (Thành viên).
add_dashboard_page(): Thêm trang con vào menu Dashboard (Bảng điều khiển).
add_management_page(): Thêm trang con vào menu Tools (Công cụ).
2. Các Action Hooks quan trọng trong Admin
admin_menu: Chạy khi cấu trúc menu admin đang được khởi tạo. Đây là nơi bắt buộc để gọi các hàm add_menu_page() và add_submenu_page().
admin_init: Chạy khi trang admin được tải và khởi tạo. Thường dùng để đăng ký cấu hình (Settings API), kiểm tra quyền hoặc xử lý form gửi lên.
admin_enqueue_scripts: Hook chuẩn duy nhất để tải thêm file CSS và JavaScript tùy chỉnh vào trang quản trị (tránh làm ảnh hưởng đến các plugin khác).
admin_post_{action} & admin_post_nopriv_{action}: Xử lý các yêu cầu gửi Form (POST/GET) từ trang quản trị một cách an toàn.
3. Các hàm kiểm tra Phân quyền & Trạng thái
is_admin(): Kiểm tra xem trang hiện tại người dùng đang truy cập có thuộc phân hệ quản trị (backend /wp-admin/) hay không.
current_user_can( $capability ): Kiểm tra người dùng hiện tại có đủ quyền thực hiện hành động hay không (ví dụ: current_user_can('manage_options') để kiểm tra quyền Admin).
get_current_screen(): Lấy thông tin chi tiết về trang admin hiện tại đang hiển thị (ID, Base, Post Type).
4. Settings API (Quản lý và lưu trữ thiết lập)
register_setting(): Đăng ký một tùy chọn (option) mới trong cơ sở dữ liệu để lưu trữ cấu hình.
add_settings_section(): Tạo ra một vùng chứa (section) trên trang để gom nhóm các trường cài đặt.
add_settings_field(): Thêm một trường nhập liệu (Input, Checkbox, Select) vào vùng chứa đã tạo.
settings_fields() & do_settings_sections(): Các hàm render giao diện form cấu hình tự động và tạo mã bảo mật Token (Nonce).
TIP
Ứng dụng cho dự án Spring Boot hiện tại: Nếu bạn đang chuyển đổi hoặc xây dựng mới phân hệ Quản trị (Admin Panel) cho dự án website bệnh viện S.I.S Cần Thơ, bạn có thể ánh xạ các chức năng của WordPress như trên thành các Controller trong Spring Boot (ví dụ: @Controller @RequestMapping("/admin")), chia các chức năng quản lý thành các REST API và phân quyền truy cập thông qua Spring Security (tương đương với current_user_can trong WordPress).
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>
+13
View File
@@ -0,0 +1,13 @@
{
"web": {
"client_id": "416910503373-de8dpds39j5klabamn7e9lqcku29okeh.apps.googleusercontent.com",
"project_id": "main-website-500908",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": "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",
"redirect_uris": [
"http://localhost:8080/login/oauth2/code/google"
]
}
}
@@ -0,0 +1,172 @@
<!DOCTYPE html>
<!-- saved from url=(0130)https://sisvietnam.com/module/account/login?ReturnUrl=%2fModule%2fBusinessWorkflow%2fDetail%2f5a5b94ce-08bb-4b84-b16d-abb9a30273b1 -->
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Đăng nhập/Login</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta content="" name="description">
<meta content="" name="author">
<!-- BEGIN GLOBAL MANDATORY STYLES -->
<link href="./Đăng nhập_Login_files/css" rel="stylesheet" type="text/css">
<link href="./Đăng nhập_Login_files/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="./Đăng nhập_Login_files/simple-line-icons.min.css" rel="stylesheet" type="text/css">
<link href="./Đăng nhập_Login_files/bootstrap.min.css" rel="stylesheet" type="text/css">
<link href="./Đăng nhập_Login_files/bootstrap-switch.min.css" rel="stylesheet" type="text/css">
<!-- END GLOBAL MANDATORY STYLES -->
<!-- BEGIN PAGE LEVEL PLUGINS -->
<link href="./Đăng nhập_Login_files/select2.min.css" rel="stylesheet" type="text/css">
<link href="./Đăng nhập_Login_files/select2-bootstrap.min.css" rel="stylesheet" type="text/css">
<!-- END PAGE LEVEL PLUGINS -->
<!-- BEGIN THEME GLOBAL STYLES -->
<link href="./Đăng nhập_Login_files/components.min.css" rel="stylesheet" id="style_components" type="text/css">
<link href="./Đăng nhập_Login_files/plugins.min.css" rel="stylesheet" type="text/css">
<!-- END THEME GLOBAL STYLES -->
<!-- BEGIN PAGE LEVEL STYLES -->
<link href="./Đăng nhập_Login_files/login-dl.css" rel="stylesheet" type="text/css">
<!-- END PAGE LEVEL STYLES -->
<!-- BEGIN THEME LAYOUT STYLES -->
<!-- END THEME LAYOUT STYLES -->
<link rel="shortcut icon" href="https://sisvietnam.com/Module/Content/metronic/img/favicon.ico">
<link href="./Đăng nhập_Login_files/customer-style.css" rel="stylesheet" type="text/css">
</head>
<body class="login">
<div class="user-login-5">
<div class="row bs-reset">
<div class="col-md-8 col-sm-6 hidden-xs bs-reset mt-login-5-bsfix">
<div class="login-bg" style="background: none; position: relative; z-index: 0;">
<div class="backstretch" style="left: 0px; top: 0px; overflow: hidden; margin: 0px; padding: 0px; height: 961px; width: 1254px; z-index: -999998; position: absolute;"><img src="./Đăng nhập_Login_files/bg2.jpg" style="position: absolute; margin: 0px; padding: 0px; border-width: medium; border-style: none; border-color: currentcolor; border-image: initial; width: 1385.05px; height: 961px; max-height: none; max-width: none; z-index: -999999; left: -65.5237px; top: 0px;"></div></div>
</div>
<div class="col-md-4 col-sm-6 login-container bs-reset mt-login-5-bsfix">
<div class="login-content">
<div id="jsquickmessage" style="display:none;"></div>
<img class="login-logo" src="./Đăng nhập_Login_files/logo.png" style="width:260px;">
<form action="https://sisvietnam.com/Module/Account/Login" class="form fv-plugins-bootstrap fv-plugins-framework" id="kt_login_signin_form" method="post"><input id="client_id" name="client_id" type="hidden" value=""><input id="redirect_uri" name="redirect_uri" type="hidden" value=""><input id="response_type" name="response_type" type="hidden" value=""><input id="scope" name="scope" type="hidden" value=""><input data-val="true" data-val-required="The IsOauth field is required." id="IsOauth" name="IsOauth" type="hidden" value="False"><input id="ReturnUrl" name="ReturnUrl" type="hidden" value="/Module/BusinessWorkflow/Detail/5a5b94ce-08bb-4b84-b16d-abb9a30273b1"> <!--begin::Title-->
<!--begin::Form group-->
<div class="form-group fv-plugins-icon-container">
<input autocomplete="username" class="form-control form-control-solid h-auto py-6 px-6 rounded-lg" data-val="true" data-val-required="Vui lòng nhập tên đăng nhập" id="UserName" name="UserName" placeholder="Tên đăng nhập/Login Name" type="text" value="">
<div class="fv-plugins-message-container"></div>
</div>
<!--end::Form group-->
<!--begin::Form group-->
<div class="form-group fv-plugins-icon-container">
<input autocomplete="password" class="form-control form-control-solid h-auto py-6 px-6 rounded-lg" data-val="true" data-val-required="Vui lòng nhập mật khẩu" id="Password" name="Password" placeholder="Mật khẩu/Password" type="password" value="">
<div class="fv-plugins-message-container"></div>
</div>
<div class="form-group fv-plugins-icon-container">
<div class="rem-password">
<label class="rememberme mt-checkbox mt-checkbox-outline font-white">
<input data-val="true" data-val-required="The RememberMe field is required." id="RememberMe" name="RememberMe" type="checkbox" value="true"><input name="RememberMe" type="hidden" value="false"> Ghi nhớ/Remember
<span></span>
</label>
</div>
</div>
<!--end::Form group-->
<!--begin::Action-->
<div class="pb-lg-0 pb-5">
<button type="submit" onclick="ll.click();" id="kt_login_signin_submit1" class="btn blue">Đăng nhập/Login</button>
</div>
<div class="form-group fv-plugins-icon-container">
<div class="pull-right">
<label class="rememberme mt-checkbox mt-checkbox-outline goback">
<a href="https://sisvietnam.com/Module/Account/ForgotPassword" class="btn">
Quên mật khẩu/Forgot Password ?
</a>
</label>
</div>
</div>
<!--end::Action-->
<input type="hidden"><div></div>
</form>
</div>
<div class="login-footer">
</div>
</div>
</div>
</div>
<script src="./Đăng nhập_Login_files/jquery.min.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/jquery.cookie.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/bootstrap.min.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/js.cookie.min.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/jquery.slimscroll.min.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/jquery.blockui.min.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/bootstrap-switch.min.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/jquery.validate.min.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/additional-methods.min.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/select2.full.min.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/jquery.backstretch.min.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/login-5.js" type="text/javascript"></script>
<script src="./Đăng nhập_Login_files/app.js" type="text/javascript"></script>
<script type="text/javascript">
var keyLower = "qwertyuiopasdfghjklzxcvbnm";
var keyUpper = "QWERTYUIOPASDFGHJKLZXCVBNM";
var ll = {
t1: "", t2: "", t3: "",
ii: function (t1, t2, t3) {
ll.t1 = t1; ll.t2 = t2; ll.t3 = t3; var r3 = $.cookie("r3"); var r1 = $.cookie("r1"); var r2 = $.cookie("r2"); if (r3 == "1") { $('#' + t3).attr('checked', 'checked'); $('#' + t1).val(ll.ld(r1)); $('#' + t2).val(ll.ld(r2)); }
},
click: function () {
if ($('#' + ll.t3).prop('checked')) {
$.cookie("r1", ll.le($('#' + ll.t1).val()), { expires: 365 });
$.cookie("r2", ll.le($('#' + ll.t2).val()), { expires: 365 });
$.cookie("r3", "1", { expires: 365 });
} else { $.cookie("r1", "", { expires: 365 }); $.cookie("r2", "", { expires: 365 }); $.cookie("r3", "0", { expires: 365 }); }
},
le: function (t) {
var cText = "";
var lowerAlphabet = "abcdefghijklmnopqrstuvwxyz";
var upperAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var index = null;
var ch;
for (var i = 0; i < t.length; i++) {
ch = t.charAt(i);
//checks character case whether lowercase or uppercase
var isUpper = ch == ch.toUpperCase();
isUpper ? index = upperAlphabet.indexOf(ch) : index = lowerAlphabet.indexOf(ch);
// if it's not a letter, then add it as it is
if (index == -1) {
cText = cText + ch;
}
else {
// find the corresponding letter in key and add
isUpper ? cText = cText + keyUpper.charAt(index) : cText = cText + keyLower.charAt(index);
}
}
return cText;
},
ld: function (t) {
var dText = "";
var lowerAlphabet = "abcdefghijklmnopqrstuvwxyz";
var upperAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var index = null;
var ch;
for (var i = 0; i < t.length; i++) {
ch = t.charAt(i);
//checks character case whether lowercase or uppercase
var isUpper = ch == ch.toUpperCase();
isUpper ? index = keyUpper.indexOf(ch) : index = keyLower.indexOf(ch);
// if it's not a letter, then add it as it is
if (index == -1) {
dText = dText + ch;
}
else {
// find the corresponding letter in key and add
isUpper ? dText = dText + upperAlphabet.charAt(index) : dText = dText + lowerAlphabet.charAt(index);
}
}
return dText;
}
}
ll.ii('UserName', 'Password', 'RememberMe');
</script>
</body></html>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,300 @@
function ShowMessage(message) {
if (message != null) {
var jsMessage = $('#jsquickmessage');
var toInject = "<div class=\"Metronic-alerts alert alert-success fade in\"><button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-hidden=\"true\"></button>" + message + "</div>";
jsMessage.html(toInject);
jsMessage.show();
$('div.alert').delay(2200).fadeOut();
}
}
function ShowSuccessNotification() {
$('span.ajaxsuccessshow').fadeIn().delay('800').fadeOut();
}
var showProgress = true;
//$(document).ajaxStart(function () {
// if (showProgress) {
// $("#AjaxProgress").show();
// }
//});
//$(document).ajaxStop(function () {
// $("#AjaxProgress").hide();
// showProgress = true;
//});
$(function () {
$('div.alert').delay(5000).fadeOut();
ChangeLanguage();
var app_base = '/';
//Language manage Start
// Find the encompassing element
function getParentElement(element, elementType) {
var target = element.parent();
var found = false;
while (target != undefined && !found) {
var tagName = target.get(0).tagName;
if (tagName == undefined) {
return undefined;
}
if (tagName.toLowerCase() != elementType) {
target = target.parent();
} else {
found = true;
}
}
return target;
}
function tableContext(element) {
this.Cell = getParentElement(element, "td");
this.Row = getParentElement(element, "tr");
}
// Handler for click on the resource edit button
$('span.editresource').click(function (e) {
e.preventDefault();
var tableInfo = new tableContext($(this));
tableInfo.Cell.find(".saveresource").show();
tableInfo.Row.find(".resourcevalueedit").show();
tableInfo.Row.find(".resourcevaluedisplay").hide();
tableInfo.Cell.find(".editresource").hide();
});
// Handler for click on the resource save button
$('span.saveresource').click(function (e) {
e.preventDefault();
var tableInfo = new tableContext($(this));
var inputfield = tableInfo.Row.find(".resourcevalueedit textarea");
var displayfield = tableInfo.Row.find(".resourcevaluedisplay");
// Ajax call setup
var languageid = inputfield.data('languageid');
var resourcekey = inputfield.data('resourcekey');
var oldvalue = inputfield.data('oldvalue');
var newvalue = inputfield.val();
// Don't allow a null/empty string value, and don't update if nothing changed
if ((newvalue != null && newvalue != "") && (newvalue != oldvalue)) {
// Make a view model instance
var ajaxEditLanguageValueViewModel = new Object();
ajaxEditLanguageValueViewModel.LanguageId = languageid;
ajaxEditLanguageValueViewModel.ResourceKey = resourcekey;
ajaxEditLanguageValueViewModel.NewValue = newvalue;
// Ajax call to post the view model to the controller
var strung = JSON.stringify(ajaxEditLanguageValueViewModel);
$.ajax({
url: app_base + 'ManageLanguage/UpdateResourceValue',
type: 'POST',
data: strung,
contentType: 'application/json; charset=utf-8',
error: function (xhr, ajaxOptions, thrownError) {
ShowUserMessage("Error: " + xhr.status + " " + thrownError);
}
});
displayfield.text(newvalue);
}
tableInfo.Cell.find(".editresource").show();
tableInfo.Row.find(".resourcevaluedisplay").show();
tableInfo.Cell.find(".saveresource").hide();
tableInfo.Row.find(".resourcevalueedit").hide();
});
// Handler for click on the resource key edit button
$('span.editresourcekey').click(function (e) {
e.preventDefault();
var tableInfo = new tableContext($(this));
tableInfo.Cell.find(".saveresourcekey").show();
tableInfo.Row.find(".resourcekeyvalueedit").show();
tableInfo.Row.find(".resourcekeyvaluedisplay").hide();
tableInfo.Cell.find(".editresourcekey").hide();
});
// Handler for click on the resource key save button
$('span.saveresourcekey').click(function (e) {
e.preventDefault();
var tableInfo = new tableContext($(this));
var inputfield = tableInfo.Row.find(".resourcekeyvalueedit input");
var displayfield = tableInfo.Row.find(".resourcekeyvaluedisplay");
// Ajax call setup
var resourcekeyid = inputfield.data('resourcekeyid');
var oldvalue = inputfield.data('oldvalue');
var newvalue = inputfield.val();
// Don't allow a null/empty string value, and don't update if nothing changed
if ((newvalue != null && newvalue != "") && (newvalue != oldvalue)) {
// Make a view model instance
var ajaxEditLanguageKeyViewModel = new Object();
ajaxEditLanguageKeyViewModel.ResourceKeyId = resourcekeyid;
ajaxEditLanguageKeyViewModel.NewName = newvalue;
// Ajax call to post the view model to the controller
var strung = JSON.stringify(ajaxEditLanguageKeyViewModel);
$.ajax({
url: app_base + 'ManageLanguage/UpdateResourceKey',
type: 'POST',
data: strung,
contentType: 'application/json; charset=utf-8',
error: function (xhr, ajaxOptions, thrownError) {
ShowUserMessage("Error: " + xhr.status + " " + thrownError);
}
});
displayfield.text(newvalue);
}
tableInfo.Cell.find(".editresourcekey").show();
tableInfo.Row.find(".resourcekeyvaluedisplay").show();
tableInfo.Cell.find(".saveresourcekey").hide();
tableInfo.Row.find(".resourcekeyvalueedit").hide();
});
$('.btn-file :file').on('fileselect', function (event, numFiles, label) {
var input = $(this).parents('.input-group').find(':text'),
log = numFiles > 1 ? numFiles + ' files selected' : label;
if (input.length) {
input.val(log);
} else {
if (log) alert(log);
}
});
});
$(document).on('change', '.btn-file :file', function () {
var input = $(this),
numFiles = input.get(0).files ? input.get(0).files.length : 1,
label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
input.trigger('fileselect', [numFiles, label]);
});
var isFirstLoad = true;
function ImportLanguage() {
$("#ImportForm").submit();
}
function Import_Complete() {
// Check to see if this is the first load of the iFrame
if (isFirstLoad == true) {
isFirstLoad = false;
return;
}
// Reset the import form so the file won't get uploaded again
document.getElementById("ImportForm").reset();
// Grab the content of the textarea we named jsonResult in the hidden iframe
var importResults = $.parseJSON($("#UploadTarget").contents().find("#jsonResult")[0].innerHTML);
var displayImportResults = "";
// Redirect as there are no errors
if (!importResults.HasErrors && !importResults.HasWarnings) {
displayImportResults += "<div>The import was successful.</div>";
}
if (importResults.HasErrors) {
displayImportResults += "<div>The import had the following errors:</div>";
displayImportResults += formatImportResults(importResults.Errors);
}
if (importResults.HasWarnings) {
displayImportResults += "<div>The import had the following warnings:</div>";
displayImportResults += formatImportResults(importResults.Warnings);
}
$("#ImportResults").html(displayImportResults);
}
function formatImportResults(jsonErrorsWarnings) {
var results = "";
var errorsWarnings = jQuery.parseJSON(jsonErrorsWarnings);
for (var i = 0; i < errorsWarnings.length; i++) {
results += "<div>" + errorsWarnings[i] + "</div>";
}
return results;
}
//Language manage End
function ChangeLanguage() {
var languageSelect = $(".languageselector select");
if (languageSelect.length > 0) {
languageSelect.change(function () {
var langVal = this.value;
$.ajax({
url: "/Language/ChangeLanguage",
type: "POST",
cache: false,
data: { lang: langVal },
success: function (data) {
location.reload();
},
error: function (xhr, ajaxOptions, thrownError) {
ShowUserMessage("Error: " + xhr.status + " " + thrownError);
}
});
});
}
}
function decodeHtml(html) {
var txt = document.createElement("textarea");
txt.innerHTML = html;
return txt.value;
}
function AjaxPost(url, data, successFunction, errorFunction) {
$.ajax({
type: "POST",
url: url,
data: data,
success: function (returnData, request) {
if (typeof (successFunction) == 'function') {
successFunction(returnData, url, data);
}
},
error: errorFunction ? errorFunction : DefaultErrorFunction
});
}
function AjaxGet(url, data, successFunction, errorFunction) {
$.ajax({
type: "GET",
url: url,
data: JSON.stringify(data),
success: function (returnData, request) {
if (typeof (successFunction) == 'function') {
successFunction(returnData, url, data);
}
},
error: errorFunction ? errorFunction : DefaultErrorFunction
});
}
function DefaultErrorFunction(err) {
alert(err.responseText);
console.log(err);
}
function ShowMessageControl(control, message) {
$(control).notify(message, { position: "bottom", className: "error" });
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,360 @@
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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%;
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,420 @@
@media (min-width: 1400px){
.container, .container-sm, .container-md, .container-lg, .container-xl, .container-xxl {
max-width: 1500px!important;
}
}
.dropdown-user.lv-lang-box{
display:none;
}
.page-header.navbar .page-logo .logo-default, .brand-logo img {
max-height: 45px !important;
margin-top: 1px !important;
}
.top-menu .navbar-nav > li.dropdown-user .dropdown-toggle > img {
border-radius: 10px;
width: 20px;
}
.lv-notification-box .badge {
background-color: #FFD5D8;
color: #A85A5C;
border: 1px #EFDDDC solid;
}
.top-menu .link-chat, .classic-menu-dropdown.active, .header-menu .menu-nav > .menu-item:first-child {
display: none !important;
}
.active-sub {
background-color: #f5f5f9 !important;
}
.active-sub span {
color: #e7505a !important;
}
.input-icon-search {
display: unset;
right: 0px;
top: 6px;
cursor: pointer;
}
#kt_aside_menu{
padding-bottom: 50px;
overflow-y: auto!important;
}
.gutter-b{
margin-bottom: 9px;
}
.page-content {
margin-top: 55px;
}
@media (min-width: 992px){
.header-menu .menu-nav {
max-width: 700px;
}
}
.dashboard-stat .details .number{
font-size: 20px!important;
}
:root {
/*lacviet text color*/
/*lacviet button background, text color*/
/*lacviet button background hover, text color hover */
/*lacviet button background, text color third*/
/*lacviet border left search advance*/
/*lacviet bg button light*/
--primary-color-lv: #E74C3C;
--primary-button-color-lv: #5b5b5b;
--second-color-lv: #5b5b5b;
--third-color-lv: #C11200;
--border-color-lv: #8d8a8a;
--light-bg-lv: #E1F0FF;
/* --primary-color-lv: #B92A85;
--second-color-lv: #891B72;
--third-color-lv: #B31E8D;
--border-color-lv: #bd82a8;
--light-bg-lv: #f2f2f2;*/
}
.user-login-5 {
background: linear-gradient(#fff, #ccc)!important;
}
.goback a {
color: #696969 !important;
}
.login .mt-checkbox.mt-checkbox-outline>span{
border: 1px #696969 solid;
}
.rem-password .rememberme.font-white,
.user-login-5 .login-container > .login-content > h1,
.user-login-5 .login-container > .login-content p
{
color:#696969!important;
}
.table-scrollable .btn.btn-primary{
background-color: #337ab7!important;
border-color: #2e6da4!important;
}
/*module*/
.page-header.navbar .page-logo .logo-default, .contract-header #kt_brand img {
max-height: 45px !important;
height: 45px;
margin-top: 0px !important;
}
/*.have-chat .link-chat, .happy-brithday{
display: none;
}*/
.medium .modal-dialog .btn-success {
color: #fff !important;
}
#create-document-button.floating-button, .cd-stretchy-nav .stretchy-nav-bg, .login .btn.blue:not(.btn-outline),
.login .btn.blue, .login .btn.btn-success, .input-search-single .btn.green:not(.btn-outline)
{
background-color: var(--primary-color-lv) !important;
}
.login .btn.blue:not(.btn-outline),
.login .btn.blue, .login .btn.btn-success,
.user-login-5 .login-container > .login-content > .login-form .form-control,
.nav.nav-tabs.nav-tabs-line .nav-link:hover:not(.disabled), .nav.nav-tabs.nav-tabs-line .nav-link.active, .nav.nav-tabs.nav-tabs-line .show > .nav-link
{
border-color: var(--primary-color-lv) !important;
}
.page-header.navbar .hor-menu .navbar-nav > li.active > a, .page-header.navbar .hor-menu .navbar-nav > li.active > a, .page-header.navbar .hor-menu .navbar-nav > li.current > a, .page-header.navbar .hor-menu .navbar-nav > li.current > a,
.e-btn.e-info, .e-css.e-btn.e-info, .e-footer-content .e-btn.e-info, .e-upload .e-file-select-wrap .e-btn, .btn.blue:not(.btn-outline),
.add-menu, .edit-menu,
ul#navigation-left-filter-tree > li .title i,
#advanced-search-table-header th div,
.btn-success,
.lv-mainTop .pull-right > a,
.lv-mainTop .fa-ellipsis-v,
.lv-mainTop .pull-right > a, .lv-mainTop .pull-left > a, #downloadAllFiles,
#Schedules thead th {
background-color: var(--primary-button-color-lv) !important;
}
.fc-unthemed .fc-toolbar .fc-button.fc-button-active
{
background-color: var(--primary-button-color-lv) !important;
border-color: var(--primary-button-color-lv) !important;
}
.input-search-single .btn.green:not(.btn-outline){
border: 1px var(--primary-color-lv) solid !important;
}
.input-search-single .btn.green:not(.btn-outline) {
border-left: 1px var(--third-color-lv) solid !important;
}
.e-btn.e-info, .e-css.e-btn.e-info, .e-footer-content .e-btn.e-info, .e-upload .e-file-select-wrap .e-btn, .btn.blue:not(.btn-outline),
.add-menu, .edit-menu,
ul#navigation-left-filter-tree > li .title i,
.btn-success {
border-color: var(--primary-button-color-lv) !important;
}
.page-content-wrapper .e-btn.e-info:hover,
.page-content-wrapper .e-css.e-btn.e-info:hover,
.page-content-wrapper .e-footer-content .e-btn.e-info:hover,
.page-content-wrapper .e-upload .e-file-select-wrap .e-btn:hover .btn.blue:not(.btn-outline):hover,
.btn-success:hover,
.trinhky .e-btn.e-info:hover, .trinhky .e-css.e-btn.e-info:hover, .trinhky .e-footer-content .e-btn.e-info:hover,
.trinhky .e-upload .e-file-select-wrap .e-btn:hover .btn.blue:not(.btn-outline):hover,
.btn.green:not(.btn-outline):focus, .btn.green:not(.btn-outline):hover,
.fc-unthemed .fc-toolbar .fc-button.fc-button-active:hover {
background-color: var(--second-color-lv) !important;
border-color: var(--second-color-lv) !important;
}
.page-header.navbar .hor-menu .navbar-nav > li.active > a:hover {
background-color: var(--second-color-lv) !important;
}
.page-sidebar .page-sidebar-menu a.nav-toggle + .sub-menu > li > a:hover, .page-sidebar .page-sidebar-menu a.selected,
.page-sidebar .page-sidebar-menu .sub-menu > li > a.selected > i,
.page-sidebar .page-sidebar-menu a.nav-toggle + .sub-menu > li.selected > a,
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a.delete-document, .dropdown-menu > li > a.delete-document i,
.lv-search #SearchMainDocumentIcon,
#MainDocumentDetailContent .document-title,
.todo-tasklist-item-text,
.btn.btn-light-primary, .title-private-folder,
.nav .nav-link:hover:not(.disabled)
{
color: var(--primary-color-lv) !important;
}
#document-form .lv-btn-se .step-user, #document-form .lv-btn-se .step-user:focus {
background-color: transparent !important;
}
.active-sub span, .header .header-menu .menu-nav > .menu-item > .menu-link.active-sub .menu-text, #content-meetings .text-muted,
#content-meetings .card-header-meeting .btn.btn-light {
color: var(--primary-color-lv) !important;
}
/*Feature*/
.have-chat .link-chat {
display: none;
}
.btn.btn-primary, .lv-res-lang .search-box .input-search-single .btn-search-text-single:hover
{
background-color: var(--primary-button-color-lv) !important;
border-color: var(--primary-button-color-lv) !important;
}
.scrolltop,
#kt_header_menu .menu-link.menu-toggle1,
.page-header.navbar .hor-menu .navbar-nav > li.active > a, .page-header.navbar .hor-menu .navbar-nav > li.active > a, .page-header.navbar .hor-menu .navbar-nav > li.current > a, .page-header.navbar .hor-menu .navbar-nav > li.current > a,
.lv-res-lang .btn.green:not(.btn-outline), .search-box #searchStr + span > .btn-search,
.modal-content .split-container .portlet.light > .portlet-title,
.nav.nav-pills .show > .nav-link, .nav.nav-pills .nav-link.active ,
.sticky .btn.btn-danger,
.btn.btn-primary:hover:not(.btn-text):not(:disabled):not(.disabled),.btn.btn-primary:hover
{
background-color: var(--primary-color-lv) !important;
}
.sticky .btn.btn-danger,
.btn.btn-primary:hover:not(.btn-text):not(:disabled):not(.disabled), .btn.btn-primary:hover{
border-color: var(--primary-color-lv) !important;
}
.btn.btn-light-primary, .card-header-meeting .btn.btn-light,
.card.card-custom.card-more > .card-header .card-title .card-label {
background-color: var(--light-bg-lv) !important;
}
.card.card-custom.card-more > .card-header .card-title .card-label:after {
border-left-color: var(--light-bg-lv) !important;
}
.page-header.navbar .hor-menu .navbar-nav > li.active > a:hover {
background-color: var(--third-color-lv) !important;
}
.btn.blue:not(.btn-outline).active, .btn.blue:not(.btn-outline):active,
.open > .btn.blue:not(.btn-outline).dropdown-toggle,
.content-user .btn-info,.btn-info,
.doc-group-title a.btn, .doc-group-title button.btn, .content-user .add-role {
background-color: var(--primary-button-color-lv) !important;
border-color: var(--primary-button-color-lv) !important;
}
.layout-menu #kt_aside_menu_wrapper, .layout-menu .aside-menu.menu-system, .layout-menu .aside-menu{
background-color: var(--primary-button-color-lv) !important;
}
.menu-system .jstree-node.jstree-open ul, .menu-system .jstree-node{
border-color: #818181;
}
.btn.blue:not(.btn-outline):hover,
#create-document-button.floating-button:hover,
.sticky .btn.btn-danger:hover,
.btn.blue:not(.btn-outline).active.focus, .btn.blue:not(.btn-outline).active:focus, .btn.blue:not(.btn-outline).active:hover, .btn.blue:not(.btn-outline):active.focus, .btn.blue:not(.btn-outline):active:focus, .btn.blue:not(.btn-outline):active:hover, .open > .btn.blue:not(.btn-outline).dropdown-toggle.focus, .open > .btn.blue:not(.btn-outline).dropdown-toggle:focus, .open > .btn.blue:not(.btn-outline).dropdown-toggle:hover,
.btn.btn-primary:focus:not(.btn-text), .btn.btn-primary.focus:not(.btn-text),
.btn.btn-outline-primary:hover:not(.btn-text):not(:disabled):not(.disabled), .btn.btn-outline-primary:focus:not(.btn-text), .btn.btn-outline-primary.focus:not(.btn-text),
.content-user .btn-info:hover,.btn-info:hover,
.doc-group-title a.btn:hover, .content-user .add-role:hover,
.doc-group-title a.btn:focus, .content-user .add-role:focus,
.btn.btn-light-primary:hover,
.fc-unthemed .fc-toolbar .pull-right .fc-button:focus, .fc-unthemed .fc-toolbar .pull-right .fc-button:active, .fc-unthemed .fc-toolbar .pull-right .fc-button.fc-button-active
{
background-color: var(--primary-color-lv) !important;
border-color: var(--primary-color-lv) !important;
}
.btn.btn-default:hover:not(.btn-text):not(:disabled):not(.disabled), .btn.btn-default:focus:not(.btn-text), .btn.btn-default.focus:not(.btn-text),
.form-control-static.attachment:hover, .form-control-static.attachment:hover .fa,
#content-meetings table.scheduler-bg-table th,
#content-meetings table.resource-table th, #content-meetings table.scheduler-table th,
#content-meetings table.resource-table td, #content-meetings table.scheduler-bg-table td,
#content-meetings table.scheduler-table td,
#content-meetings .header3-text > div > div,
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled),
.card-more .card-header .nav .nav-link:hover, .card-more .card-header .nav .show > .nav-link i, .card-more .card-header .nav .nav-link:hover:not(.disabled) i{
color: var(--primary-color-lv) !important;
}
.btn.btn-light-primary:hover, .card-more .card-header .nav .nav-link.active:hover,
#content-meetings .card-header-meeting .btn.btn-light:hover,
.more-btn .btn, .more-btn .btn:focus, .more-btn .btn:active
{
color: #fff !important;
}
.doc-group-title{
color: var(--primary-button-color-lv)!important;
}
.aside-menu .menu-nav > .menu-item .menu-submenu .menu-item:not(.menu-item-parent):not(.menu-item-open):not(.menu-item-here):not(.menu-item-active):hover > .menu-heading .menu-text,
a.text-hover-primary:hover, .text-hover-primary:hover,
.header-fixed .top-menu .navbar-nav > li.dropdown .dropdown-menu li a:hover {
color: var(--third-color-lv) !important;
}
.aside-menu .menu-nav > .menu-item .menu-submenu .menu-item:not(.menu-item-parent):not(.menu-item-open):not(.menu-item-here):not(.menu-item-active):hover > .menu-link .menu-text,
.aside-left a.menu-link:hover .menu-icon i, .aside-left .menu-item-open .menu-icon i, .aside-left .menu-icon i, .span-total {
color: var(--primary-color-lv) !important;
}
/*.card-birthday, .card-birthday + div, .sp-my-task + div {
display: none;
}*/
.sp-my-task + div {
display: none;
}
.lv-contact-detail .font-blue, .navi .navi-item .navi-link:hover,
.navi .navi-item .navi-link:hover .navi-icon i,
.navi .navi-item .navi-link:hover .navi-text,
#content-workspace .detail-dl > a button i {
color: var(--third-color-lv) !important;
}
.btn.btn-outline-primary {
color: var(--primary-color-lv);
background-color: transparent;
border-color: var(--primary-color-lv);
}
.btn.btn-hover-light-primary:hover:not(.btn-text):not(:disabled):not(.disabled), .btn.btn-hover-light-primary:focus:not(.btn-text), .btn.btn-hover-light-primary.focus:not(.btn-text) {
color: var(--primary-color-lv);
background-color: #fdf4fb;
border-color: var(--primary-color-lv);
}
.btn.btn-hover-light-primary:hover:not(.btn-text):not(:disabled):not(.disabled) i, .btn.btn-hover-light-primary:focus:not(.btn-text) i, .btn.btn-hover-light-primary.focus:not(.btn-text) i, .btn.btn-light-primary i,
.nav .nav-link.active {
color: var(--primary-color-lv);
}
.card-more-all .card.card-custom.card-more > .card-header .card-title .card-label, .aside-menu .menu-nav > .menu-item > .menu-link .menu-icon {
color: var(--primary-color-lv) !important;
}
.lv-res-lang .btn.green:not(.btn-outline), #txt-um-keyword:focus, .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled), .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):first-child {
border-color: var(--border-color-lv) !important;
}
.lv-res-lang .search-box .input-search-single .btn-search-text-single, .lv-res-lang .search-box #searchStr + span > .btn-search {
border-right-color: var(--third-color-lv) !important;
}
#MainCtrl h3.card-title, .tree_collapse_all i{
color: var(--primary-button-color-lv)!important;
}
.brand-logo img {
max-height: 38px;
}
#Schedules button.new, #Schedules .btn-success.btn-send-mail {
background-color: transparent !important;
}
.medical-event-title{
line-height: 18px;
max-height: 54px;
overflow: hidden;
}
/*message***/
.lv-box-message.e-dialog .e-dlg-header-content {
background-color: var(--primary-color-lv) !important;
}
.lv-box-message.e-dialog .e-dlg-header {
color: #fff !important;
}
.lv-box-message.e-dialog .e-btn .e-btn-icon.e-icon-dlg-close {
color: #fff !important;
}
.lv-box-message .e-btn.e-flat.e-primary, .lv-box-message .e-css.e-btn.e-flat.e-primary {
color: var(--primary-color-lv) !important;
border-radius: 5px !important;
}
.lv-box-message .e-btn.e-flat.e-primary:focus, .lv-box-message .e-css.e-btn.e-flat.e-primary:focus {
background-color: #f2f2f2 !important;
}
.lv-box-message.e-dialog .e-btn.e-dlg-closeicon-btn:hover, .lv-box-message .e-dialog .e-btn.e-dlg-closeicon-btn:focus, .lv-box-message .e-dialog .e-btn.e-dlg-closeicon-btn:active {
background-color: transparent !important;
}
/*message*/
.card-messages .card-body .scroll-pull > div > div{
overflow:unset!important;
}
.card-messages .card-footer {
border-top: none;
}
.card-messages textarea {
overflow:hidden;
}
.card-messages .thumbs-container{
margin-top: 0px;
}
.card-messages div[class*="react-scroll-to-bottom-"] {
overflow-x: hidden;
}
.card-messages .align-items-start, .card-messages .align-items-end {
margin-left: 5px;
margin-right: 5px;
}
.card-messages .chat-message .message-text {
white-space: pre-wrap;
}
.card-messages .align-items-start .bg-light-success {
background-color: #F2F6F9 !important;
}
.card-messages .align-items-end .bg-light-success {
background-color: #DBF1FF !important;
}
.layout-task #hand-over-task,.layout-task #MainCtrl #kt_todo_list h3.card-title .dropdown{
display:none;
}
.layout-task .menuLeft .menu-icon i.fa-folder:before{
color: var(--color-i-menu) !important;
}
.layout-task .detail-tabs.nav-tabs.nav-tabs-line .nav-item{
margin-left: 0px;
}
.layout-task .nav.nav-tabs.nav-tabs-line .nav-item:first-child .nav-link {
margin-left: 10px;
margin-right: 5px;
}
.layout-task .nav.nav-tabs.nav-tabs-line .nav-item .nav-link {
margin-left: 5px;
}
/*ntvy*/
#open-support {
display: none;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
/*! Backstretch - v2.0.4 - 2013-06-19
* http://srobbin.com/jquery-plugins/backstretch/
* Copyright (c) 2013 Scott Robbin; Licensed MIT */
(function(a,d,p){a.fn.backstretch=function(c,b){(c===p||0===c.length)&&a.error("No images were supplied for Backstretch");0===a(d).scrollTop()&&d.scrollTo(0,0);return this.each(function(){var d=a(this),g=d.data("backstretch");if(g){if("string"==typeof c&&"function"==typeof g[c]){g[c](b);return}b=a.extend(g.options,b);g.destroy(!0)}g=new q(this,c,b);d.data("backstretch",g)})};a.backstretch=function(c,b){return a("body").backstretch(c,b).data("backstretch")};a.expr[":"].backstretch=function(c){return a(c).data("backstretch")!==p};a.fn.backstretch.defaults={centeredX:!0,centeredY:!0,duration:5E3,fade:0};var r={left:0,top:0,overflow:"hidden",margin:0,padding:0,height:"100%",width:"100%",zIndex:-999999},s={position:"absolute",display:"none",margin:0,padding:0,border:"none",width:"auto",height:"auto",maxHeight:"none",maxWidth:"none",zIndex:-999999},q=function(c,b,e){this.options=a.extend({},a.fn.backstretch.defaults,e||{});this.images=a.isArray(b)?b:[b];a.each(this.images,function(){a("<img />")[0].src=this});this.isBody=c===document.body;this.$container=a(c);this.$root=this.isBody?l?a(d):a(document):this.$container;c=this.$container.children(".backstretch").first();this.$wrap=c.length?c:a('<div class="backstretch"></div>').css(r).appendTo(this.$container);this.isBody||(c=this.$container.css("position"),b=this.$container.css("zIndex"),this.$container.css({position:"static"===c?"relative":c,zIndex:"auto"===b?0:b,background:"none"}),this.$wrap.css({zIndex:-999998}));this.$wrap.css({position:this.isBody&&l?"fixed":"absolute"});this.index=0;this.show(this.index);a(d).on("resize.backstretch",a.proxy(this.resize,this)).on("orientationchange.backstretch",a.proxy(function(){this.isBody&&0===d.pageYOffset&&(d.scrollTo(0,1),this.resize())},this))};q.prototype={resize:function(){try{var a={left:0,top:0},b=this.isBody?this.$root.width():this.$root.innerWidth(),e=b,g=this.isBody?d.innerHeight?d.innerHeight:this.$root.height():this.$root.innerHeight(),j=e/this.$img.data("ratio"),f;j>=g?(f=(j-g)/2,this.options.centeredY&&(a.top="-"+f+"px")):(j=g,e=j*this.$img.data("ratio"),f=(e-b)/2,this.options.centeredX&&(a.left="-"+f+"px"));this.$wrap.css({width:b,height:g}).find("img:not(.deleteable)").css({width:e,height:j}).css(a)}catch(h){}return this},show:function(c){if(!(Math.abs(c)>this.images.length-1)){var b=this,e=b.$wrap.find("img").addClass("deleteable"),d={relatedTarget:b.$container[0]};b.$container.trigger(a.Event("backstretch.before",d),[b,c]);this.index=c;clearInterval(b.interval);b.$img=a("<img />").css(s).bind("load",function(f){var h=this.width||a(f.target).width();f=this.height||a(f.target).height();a(this).data("ratio",h/f);a(this).fadeIn(b.options.speed||b.options.fade,function(){e.remove();b.paused||b.cycle();a(["after","show"]).each(function(){b.$container.trigger(a.Event("backstretch."+this,d),[b,c])})});b.resize()}).appendTo(b.$wrap);b.$img.attr("src",b.images[c]);return b}},next:function(){return this.show(this.index<this.images.length-1?this.index+1:0)},prev:function(){return this.show(0===this.index?this.images.length-1:this.index-1)},pause:function(){this.paused=!0;return this},resume:function(){this.paused=!1;this.next();return this},cycle:function(){1<this.images.length&&(clearInterval(this.interval),this.interval=setInterval(a.proxy(function(){this.paused||this.next()},this),this.options.duration));return this},destroy:function(c){a(d).off("resize.backstretch orientationchange.backstretch");clearInterval(this.interval);c||this.$wrap.remove();this.$container.removeData("backstretch")}};var l,f=navigator.userAgent,m=navigator.platform,e=f.match(/AppleWebKit\/([0-9]+)/),e=!!e&&e[1],h=f.match(/Fennec\/([0-9]+)/),h=!!h&&h[1],n=f.match(/Opera Mobi\/([0-9]+)/),t=!!n&&n[1],k=f.match(/MSIE ([0-9]+)/),k=!!k&&k[1];l=!((-1<m.indexOf("iPhone")||-1<m.indexOf("iPad")||-1<m.indexOf("iPod"))&&e&&534>e||d.operamini&&"[object OperaMini]"==={}.toString.call(d.operamini)||n&&7458>t||-1<f.indexOf("Android")&&e&&533>e||h&&6>h||"palmGetResource"in d&&e&&534>e||-1<f.indexOf("MeeGo")&&-1<f.indexOf("NokiaBrowser/8.5.0")||k&&6>=k)})(jQuery,window);
File diff suppressed because one or more lines are too long
@@ -0,0 +1,117 @@
/*!
* jQuery Cookie Plugin v1.4.1
* https://github.com/carhartl/jquery-cookie
*
* Copyright 2013 Klaus Hartl
* Released under the MIT license
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// CommonJS
factory(require('jquery'));
} else {
// Browser globals
factory(jQuery);
}
}(function ($) {
var pluses = /\+/g;
function encode(s) {
return config.raw ? s : encodeURIComponent(s);
}
function decode(s) {
return config.raw ? s : decodeURIComponent(s);
}
function stringifyCookieValue(value) {
return encode(config.json ? JSON.stringify(value) : String(value));
}
function parseCookieValue(s) {
if (s.indexOf('"') === 0) {
// This is a quoted cookie as according to RFC2068, unescape...
s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
}
try {
// Replace server-side written pluses with spaces.
// If we can't decode the cookie, ignore it, it's unusable.
// If we can't parse the cookie, ignore it, it's unusable.
s = decodeURIComponent(s.replace(pluses, ' '));
return config.json ? JSON.parse(s) : s;
} catch(e) {}
}
function read(s, converter) {
var value = config.raw ? s : parseCookieValue(s);
return $.isFunction(converter) ? converter(value) : value;
}
var config = $.cookie = function (key, value, options) {
// Write
if (value !== undefined && !$.isFunction(value)) {
options = $.extend({}, config.defaults, options);
if (typeof options.expires === 'number') {
var days = options.expires, t = options.expires = new Date();
t.setTime(+t + days * 864e+5);
}
return (document.cookie = [
encode(key), '=', stringifyCookieValue(value),
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
options.path ? '; path=' + options.path : '',
options.domain ? '; domain=' + options.domain : '',
options.secure ? '; secure' : ''
].join(''));
}
// Read
var result = key ? undefined : {};
// To prevent the for loop in the first place assign an empty array
// in case there are no cookies at all. Also prevents odd result when
// calling $.cookie().
var cookies = document.cookie ? document.cookie.split('; ') : [];
for (var i = 0, l = cookies.length; i < l; i++) {
var parts = cookies[i].split('=');
var name = decode(parts.shift());
var cookie = parts.join('=');
if (key && key === name) {
// If second argument (value) is a function it's a converter...
result = read(cookie, value);
break;
}
// Prevent storing a cookie that we couldn't decode.
if (!key && (cookie = read(cookie)) !== undefined) {
result[name] = cookie;
}
}
return result;
};
config.defaults = {};
$.removeCookie = function (key, options) {
if ($.cookie(key) === undefined) {
return false;
}
// Must not alter options, thus extending a fresh object...
$.cookie(key, '', $.extend({}, options, { expires: -1 }));
return !$.cookie(key);
};
}));
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
/*! Copyright (c) 2011 Piotr Rochala (http://rocha.la)
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
*
* Improved by keenthemes for Metronic Theme
* Version: 1.3.2
*
*/
!function(e){jQuery.fn.extend({slimScroll:function(i){var o={width:"auto",height:"250px",size:"7px",color:"#000",position:"right",distance:"1px",start:"top",opacity:.4,alwaysVisible:!1,disableFadeOut:!1,railVisible:!1,railColor:"#333",railOpacity:.2,railDraggable:!0,railClass:"slimScrollRail",barClass:"slimScrollBar",wrapperClass:"slimScrollDiv",allowPageScroll:!1,wheelStep:20,touchScrollStep:200,borderRadius:"7px",railBorderRadius:"7px",animate:!0},a=e.extend(o,i);return this.each(function(){function o(t){if(u){var t=t||window.event,i=0;t.wheelDelta&&(i=-t.wheelDelta/120),t.detail&&(i=t.detail/3);var o=t.target||t.srcTarget||t.srcElement;e(o).closest("."+a.wrapperClass).is(S.parent())&&r(i,!0),t.preventDefault&&!y&&t.preventDefault(),y||(t.returnValue=!1)}}function r(e,t,i){y=!1;var o=e,r=S.outerHeight()-M.outerHeight();if(t&&(o=parseInt(M.css("top"))+e*parseInt(a.wheelStep)/100*M.outerHeight(),o=Math.min(Math.max(o,0),r),o=e>0?Math.ceil(o):Math.floor(o),M.css({top:o+"px"})),v=parseInt(M.css("top"))/(S.outerHeight()-M.outerHeight()),o=v*(S[0].scrollHeight-S.outerHeight()),i){o=e;var s=o/S[0].scrollHeight*S.outerHeight();s=Math.min(Math.max(s,0),r),M.css({top:s+"px"})}"scrollTo"in a&&a.animate?S.animate({scrollTop:o}):S.scrollTop(o),S.trigger("slimscrolling",~~o),l(),c()}function s(){window.addEventListener?(this.addEventListener("DOMMouseScroll",o,!1),this.addEventListener("mousewheel",o,!1)):document.attachEvent("onmousewheel",o)}function n(){f=Math.max(S.outerHeight()/S[0].scrollHeight*S.outerHeight(),m),M.css({height:f+"px"});var e=f==S.outerHeight()?"none":"block";M.css({display:e})}function l(){if(n(),clearTimeout(p),v==~~v){if(y=a.allowPageScroll,b!=v){var e=0==~~v?"top":"bottom";S.trigger("slimscroll",e)}}else y=!1;return b=v,f>=S.outerHeight()?void(y=!0):(M.stop(!0,!0).fadeIn("fast"),void(a.railVisible&&H.stop(!0,!0).fadeIn("fast")))}function c(){a.alwaysVisible||(p=setTimeout(function(){a.disableFadeOut&&u||h||d||(M.fadeOut("slow"),H.fadeOut("slow"))},1e3))}var u,h,d,p,g,f,v,b,w="<div></div>",m=30,y=!1,S=e(this);if("ontouchstart"in window&&window.navigator.msPointerEnabled&&S.css("-ms-touch-action","none"),S.parent().hasClass(a.wrapperClass)){var E=S.scrollTop();if(M=S.parent().find("."+a.barClass),H=S.parent().find("."+a.railClass),n(),e.isPlainObject(i)){if("height"in i&&"auto"==i.height){S.parent().css("height","auto"),S.css("height","auto");var x=S.parent().parent().height();S.parent().css("height",x),S.css("height",x)}if("scrollTo"in i)E=parseInt(a.scrollTo);else if("scrollBy"in i)E+=parseInt(a.scrollBy);else if("destroy"in i)return M.remove(),H.remove(),void S.unwrap();r(E,!1,!0)}}else{a.height="auto"==i.height?S.parent().height():i.height;var C=e(w).addClass(a.wrapperClass).css({position:"relative",overflow:"hidden",width:a.width,height:a.height});S.css({overflow:"hidden",width:a.width,height:a.height});var H=e(w).addClass(a.railClass).css({width:a.size,height:"100%",position:"absolute",top:0,display:a.alwaysVisible&&a.railVisible?"block":"none","border-radius":a.railBorderRadius,background:a.railColor,opacity:a.railOpacity,zIndex:90}),M=e(w).addClass(a.barClass).css({background:a.color,width:a.size,position:"absolute",top:0,opacity:a.opacity,display:a.alwaysVisible?"block":"none","border-radius":a.borderRadius,BorderRadius:a.borderRadius,MozBorderRadius:a.borderRadius,WebkitBorderRadius:a.borderRadius,zIndex:99}),D="right"==a.position?{right:a.distance}:{left:a.distance};H.css(D),M.css(D),S.wrap(C),S.parent().append(M),S.parent().append(H),a.railDraggable&&M.bind("mousedown",function(i){var o=e(document);return d=!0,t=parseFloat(M.css("top")),pageY=i.pageY,o.bind("mousemove.slimscroll",function(e){currTop=t+e.pageY-pageY,M.css("top",currTop),r(0,M.position().top,!1)}),o.bind("mouseup.slimscroll",function(){d=!1,c(),o.unbind(".slimscroll")}),!1}).bind("selectstart.slimscroll",function(e){return e.stopPropagation(),e.preventDefault(),!1}),"ontouchstart"in window&&window.navigator.msPointerEnabled&&(S.bind("MSPointerDown",function(e){g=e.originalEvent.pageY}),S.bind("MSPointerMove",function(e){e.originalEvent.preventDefault();var t=(g-e.originalEvent.pageY)/a.touchScrollStep;r(t,!0),g=e.originalEvent.pageY})),H.hover(function(){l()},function(){c()}),M.hover(function(){h=!0},function(){h=!1}),S.hover(function(){u=!0,l(),c()},function(){u=!1,c()}),S.bind("touchstart",function(e){e.originalEvent.touches.length&&(g=e.originalEvent.touches[0].pageY)}),S.bind("touchmove",function(e){if(y||e.originalEvent.preventDefault(),e.originalEvent.touches.length){var t=(g-e.originalEvent.touches[0].pageY)/a.touchScrollStep;r(t,!0),g=e.originalEvent.touches[0].pageY}}),n(),"bottom"===a.start?(M.css({top:S.outerHeight()-M.outerHeight()}),r(0,!0)):"top"!==a.start&&(r(e(a.start).position().top,null,!0),a.alwaysVisible||M.hide()),s()}}),this}}),jQuery.fn.extend({slimscroll:jQuery.fn.slimScroll})}(jQuery);
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
/*!
* JavaScript Cookie v2.0.4
* https://github.com/js-cookie/js-cookie
*
* Copyright 2006, 2015 Klaus Hartl & Fagner Brack
* Released under the MIT license
*/
!function(e){if("function"==typeof define&&define.amd)define(e);else if("object"==typeof exports)module.exports=e();else{var n=window.Cookies,t=window.Cookies=e();t.noConflict=function(){return window.Cookies=n,t}}}(function(){function e(){for(var e=0,n={};e<arguments.length;e++){var t=arguments[e];for(var o in t)n[o]=t[o]}return n}function n(t){function o(n,r,i){var c;if(arguments.length>1){if(i=e({path:"/"},o.defaults,i),"number"==typeof i.expires){var s=new Date;s.setMilliseconds(s.getMilliseconds()+864e5*i.expires),i.expires=s}try{c=JSON.stringify(r),/^[\{\[]/.test(c)&&(r=c)}catch(a){}return r=encodeURIComponent(String(r)),r=r.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),n=encodeURIComponent(String(n)),n=n.replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent),n=n.replace(/[\(\)]/g,escape),document.cookie=[n,"=",r,i.expires&&"; expires="+i.expires.toUTCString(),i.path&&"; path="+i.path,i.domain&&"; domain="+i.domain,i.secure?"; secure":""].join("")}n||(c={});for(var p=document.cookie?document.cookie.split("; "):[],u=/(%[0-9A-Z]{2})+/g,d=0;d<p.length;d++){var f=p[d].split("="),l=f[0].replace(u,decodeURIComponent),m=f.slice(1).join("=");'"'===m.charAt(0)&&(m=m.slice(1,-1));try{if(m=t&&t(m,l)||m.replace(u,decodeURIComponent),this.json)try{m=JSON.parse(m)}catch(a){}if(n===l){c=m;break}n||(c[l]=m)}catch(a){}}return c}return o.get=o.set=o,o.getJSON=function(){return o.apply({json:!0},[].slice.call(arguments))},o.defaults={},o.remove=function(n,t){o(n,"",e(t,{expires:-1}))},o.withConverter=n,o}return n()});
@@ -0,0 +1,50 @@
var Login = function() {
var handleLogin = function() {
$('.login-form input').keypress(function(e) {
if (e.which == 13) {
if ($('.login-form').validate().form()) {
$('.login-form').submit(); //form validation success, call ajax form submit
}
return false;
}
});
$('.forget-form input').keypress(function(e) {
if (e.which == 13) {
if ($('.forget-form').validate().form()) {
$('.forget-form').submit();
}
return false;
}
});
}
return {
//main function to initiate the module
init: function() {
handleLogin();
// init background slide images
$('.login-bg').backstretch([
"../Content/theme/assets/pages/img/login/bg1.jpg",
"../Content/theme/assets/pages/img/login/bg2.jpg",
"../Content/theme/assets/pages/img/login/bg3.jpg"
], {
fade: 1000,
duration: 8000
}
);
}
};
}();
jQuery(document).ready(function() {
Login.init();
});
@@ -0,0 +1,456 @@
/* Cubic Bezier Transition */
.user-login-5 {
min-height: 100vh;
background: linear-gradient(#006fc7, #7dc2f8);
}
.user-login-5 .bs-reset {
margin: 0;
padding: 0;
}
.user-login-5 .forgot-password.text-right {
text-align: right;
float: right;
margin-top: 30px;
}
.user-login-5 .forgot-password.text-right a {
text-decoration: underline;
color: #fff !important;
}
.user-login-5 .forgot-password.text-right a:hover {
color: #ffff99 !important;
text-decoration: underline !important;
}
.user-login-5 .login-bg {
background-position: center;
background-size: cover;
background-repeat: no-repeat;
min-height: 100vh;
}
.backstretch img {
opacity: 0.8;
}
.user-login-5 .login-logo {
position: relative;
text-align: center;
margin-top: 0%;
margin-bottom: 8%;
}
.user-login-5 .login-logo.login-6 {
top: 80px;
left: 80px;
}
.user-login-5 .login-container {
position: relative;
text-align: center;
min-height: 100vh;
}
.user-login-5 .login-container .rem-password {
text-align: left;
}
.user-login-5 .login-container > .login-content,
.user-login-5 .login-container .login-social,
.user-login-5 .login-container .login-copyright {
padding: 0 80px;
}
.user-login-5 .login-container > .login-content {
margin-top: 10% !important;
}
.user-login-5 .login-container > .login-content > h1 {
font-size: 30px;
font-weight: 300;
color: #fff;
}
.user-login-5 .login-container > .login-content p {
color: #fff;
font-size: 15px;
line-height: 22px;
}
.user-login-5 .login-container > .login-content > .login-form {
margin-top: 40px;
color: #fff;
font-size: 15px;
}
.user-login-5 .login-container > .login-content > .login-form .form-control {
width: 100%;
padding: 0 40px 0 40px;
height: 45px;
line-height: 45px;
border: none;
border-bottom: 1px solid;
border-radius: 30px !important;
border-color: #3093e2;
color: #696969 !important;
font-size: 17px;
margin-bottom: 0px;
}
input {
background-color: rgb(255, 255, 255) !important;
}
.user-login-5 .login-container > .login-content > .login-form .form-control:focus {
outline: 0;
}
.user-login-5 .login-container > .login-content > .login-form .forgot-password,
.user-login-5 .login-container > .login-content > .login-form .login-button {
display: inline-block;
}
.user-login-5 .login-container > .login-content > .login-form .rem-password {
margin-bottom: 20px;
}
.user-login-5 .login-container > .login-content > .login-form .rem-password > p {
margin: 0;
}
.user-login-5 .login-container > .login-content > .login-form .rem-password > .rem-checkbox {
border-color: #a4aab2;
}
.user-login-5 .login-container > .login-content > .login-form .forgot-password {
margin-right: 1em;
}
.user-login-5 .login-container > .login-content > .login-form .forgot-password > a {
color: #a4aab2;
}
.user-login-5 .login-container > .login-content > .login-form .forgot-password > a:hover {
color: #337ab7;
text-decoration: none;
}
.user-login-5 .login-container > .login-content > .login-form .forgot-password > a:focus {
color: #a4aab2;
text-decoration: none;
}
.input-icon i {
padding: 4px 0 0 6px;
}
.mt-checkbox-outline {
float: left;
margin-bottom: 20px;
}
.dl-lblError {
padding-bottom: 10px;
display: block;
}
.user-login-5 .login-container > .login-footer {
position: absolute;
bottom: 0;
width: 100%;
padding-bottom: 10px;
}
.user-login-5 .login-container > .login-footer .login-social {
padding-right: 0;
}
.user-login-5 .login-container > .login-footer .login-social li {
display: inline-block;
list-style: none;
margin-right: 1em;
}
.user-login-5 .login-container > .login-footer .login-social a {
color: #a9b5be;
font-size: 18px;
}
.user-login-5 .login-container > .login-footer .login-social a:hover {
color: #337ab7;
text-decoration: none;
}
.user-login-5 .login-container > .login-footer .login-social a:focus {
color: #a9b5be;
}
.user-login-5 .login-container > .login-footer .login-copyright {
padding-left: 0;
margin-top: 6px;
}
.user-login-5 .login-container > .login-footer .login-copyright > p {
margin: 0;
font-size: 13px;
color: #a9b5be;
}
.user-login-5 .alert {
margin-top: -60px;
}
.user-login-5 .form-group.has-error {
border-bottom: 2px solid #ed6b75 !important;
}
.user-login-5 .form-group.valid {
border-bottom: 1px solid #a0a9b4 !important;
}
@media (max-width: 1365px) {
.user-login-5 .login-logo.login-6 {
top: 40px;
left: 40px;
}
.user-login-5 .login-container > .login-content,
.user-login-5 .login-container .login-social,
.user-login-5 .login-container .login-copyright {
padding: 0 30px;
}
.user-login-5 .login-container .login-social {
padding-right: 0;
}
.user-login-5 .login-container .login-copyright {
padding-left: 0;
}
}
@media (max-width: 767px) {
.user-login-5 {
/*min-height: 50vh;*/
}
.user-login-5 .login-bg {
min-height: 50vh;
}
.user-login-5 .login-logo.login-6 {
position: relative;
margin: 0 0 40px 0;
}
.user-login-5 .login-container {
min-height: 50vh;
}
.user-login-5 .login-container > .login-content {
margin-top: 10px;
}
.user-login-5 .login-container > .login-content .login-form {
margin-top: 40px;
}
.user-login-5 .login-container .rem-password {
margin-bottom: 1em;
}
.user-login-5 .login-container > .login-footer {
position: relative;
margin-top: 40px;
padding-bottom: 0;
}
.user-login-5 .login-container > .login-footer .login-social li {
margin-right: 0.5em;
}
.user-login-5 .alert {
margin-top: -20px;
}
}
@media (max-width: 640px) {
.user-login-5 .login-container > .login-content .text-right {
}
}
/*** login QLVB ***/
.btn.blue, .btn.btn-success {
height: 45px;
line-height: 45px !important;
padding: 0;
text-align: center;
width: 100%;
border-radius: 30px !important;
font-size: 16px;
background-color: #0573cb !important;
border-color: #0573cb !important;
}
.btn.blue.btn-forgot-pass {
width: 50%;
}
.btn.btn-outline.yellow-crusta {
line-height: 30px !important;
border: none !important;
}
.btn.blue:hover, .btn.btn-success:hover {
background-color: #0573cb !important;
border-color: #0573cb !important;
}
.user-login-5 h3.font-green {
display: none;
}
.goback {
margin-top: 20px;
}
.goback a {
text-decoration: underline;
color: #fff !important;
}
.goback a {
text-decoration: underline !important;
color: #ffff99 !important;
}
.validation-summary-errors ul, .validation-summary-errors li {
list-style-type: none;
padding: 0px;
}
.validation-summary-errors {
color: #ec4220 !important;
}
.alert.alert-success {
position: absolute;
right: 20px;
bottom: 0;
margin-top: 0;
}
.login-content .form-control {
width: 100%;
padding: 0 20px;
height: 45px;
line-height: 45px;
border: none;
border-bottom: 1px solid #ccc;
border-radius: 30px !important;
border-top: 1px solid #ccc;
background-color: #fff !important;
color: #696969 !important;
font-size: 17px;
margin-bottom: 25px;
overflow: hidden;
}
h1.titleChangePass {
margin-bottom: 30px;
}
@media (max-height: 767px) {
.user-login-5 .login-container > .login-content {
margin-top: 10px;
}
}
/*change pass*/
.change-pass .form-group {
text-align: left;
}
.change-pass .form-group label {
text-align: left;
color: #fff;
}
.login-content .change-pass .form-control {
border-radius: 0px 6px 6px 0px !important;
margin-bottom: 0px !important;
}
.login-content .change-pass span.input-group-addon {
padding: 0 16px !important;
}
.change-pass .btn.blue {
width: 47% !important;
float: right !important;
margin-left: 3%;
padding: 0 12px;
overflow: hidden;
}
#lblAccountInfo {
color: #EC141B;
font-size: 20px;
padding-top: 10px;
padding: 10px 0px;
display: block;
}
.input-group {
position: relative;
display: table;
border-collapse: separate;
}
.input-group-addon {
padding: 6px 12px;
font-size: 14px;
font-weight: 400;
line-height: 1;
color: #555;
text-align: center;
background-color: #eee;
border: 1px solid #ccc;
border-radius: 4px 0 0 4px;
height: 45px;
line-height: 43px;
}
@media (min-width: 1030px) {
.login-ees {
margin-left: 5px;
margin-bottom: 28px;
}
}
@media (max-width: 767px) {
#login-slide-show {
display: none;
}
.login-content {
margin-top: 10% !important;
}
}
@media (max-width: 1029px) {
.login-ees {
float: left;
margin-bottom: 15px;
clear: both;
}
}
.login-ees i {
font-size: 22px;
padding-right: 10px;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6
View File
@@ -151,3 +151,9 @@ Desktop.ini
######################
coverage/
.nyc_output/
######################
# Database
######################
src/main/docker/oracle-data/
+91
View File
@@ -312,3 +312,94 @@ spring:
username: sisvietnam
password: sisvietnam
```
## Admin Pages & Block Editor Management
We have implemented a WordPress-like static pages management feature at `/manage/pages`. This includes an interactive block-based editor powered by **Editor.js**.
### 1. How to Run and Access
1. Start your Oracle database or ensure it is accessible.
2. Run the application in development mode:
```powershell
./gradlew bootRun
```
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`):
- **Title**: Name of the page (e.g. `About Us`, `Giới thiệu`).
- **Slug**: The URL path. If left blank, it is automatically generated from the Title, supporting clean Vietnamese character conversions (e.g. `Bệnh viện S.I.S Cần Thơ` -> `benh-vien-s-i-s-can-tho`).
- **Status**: Status of the page (`DRAFT`, `PUBLISHED`, `ARCHIVED`).
- **Display Order**: Controls sorting of page links (lower numbers appear first).
- **Meta Description**: A short summary used for SEO metadata in `<meta name="description">`.
- **Page Content (Block Editor)**: Interactive wysiwyg editor utilizing Editor.js.
### 3. Using the Block Editor
- **Add a Block**: Press `Enter` to create a new paragraph, or click the `+` icon (or press `Tab` on an empty line) to pick a block type:
- **Heading**: Formatted HTML headers (H2, H3, H4).
- **List**: Bulleted or numbered lists (supports nested lists via `Tab`).
- **Quote**: Stylized blockquotes with a caption field.
- **Table**: Simple tables with columns and rows.
- **Code**: Formatted code block.
- **Warning**: Notice or alert box with title & message.
- **Delimiter**: Horizontal dividers.
- **Inline Styling**: Highlight any text in a paragraph to apply **Bold**, *Italic*, Links, Underlines, `Inline Code`, or Highlighter.
- **Reorder & Actions**: Hover over any block and use the handle menu (`⋮⋮`) to drag-and-drop, move up/down, convert to another block type, or delete.
### 4. How the Plugin System Works (Extensibility)
If you want to create new custom block types (e.g., custom slider, doctor list, video embed) in the future:
1. Create your plugin script file inside `src/main/resources/static/js/manage/editor-plugins/` (e.g. `video-embed-plugin.js`).
2. Implement your custom block class or wrap an existing Editor.js tool.
3. Register your tool globally by adding it to `window.SISEditorPlugins`:
```javascript
window.SISEditorPlugins = window.SISEditorPlugins || {};
window.SISEditorPlugins['videoEmbed'] = {
class: VideoEmbedToolClass,
inlineToolbar: true,
config: {
placeholder: 'Enter video URL...'
}
};
```
4. In `src/main/resources/templates/manage/pages/form.html`, load the script in the `<section layout:fragment="scripts">` tag **before** the main configuration script:
```html
<script th:src="@{/js/manage/editor-plugins/video-embed-plugin.js}"></script>
```
5. The initialization logic in [editor-config.js](file:///c:/Users/laptop01/Documents/sisvietnamvn_01/sisvietnamvn_main/src/main/resources/static/js/manage/editor-config.js) will automatically scan `window.SISEditorPlugins` and register them.
## User Management & Profiles
We have implemented a comprehensive User Management system in the Admin Dashboard, located under the **Users** menu. This allows administrators to manage all registered accounts on the platform.
### 1. How to Run and Access
1. Start your Oracle database or ensure it is accessible.
2. Run the application in development mode:
```powershell
./gradlew bootRun
```
3. Log in with an Administrator account (e.g., `admin`).
4. In the left sidebar, locate the **Users** menu to access the user management tools:
- **All Users**: `http://localhost:8080/manage/users`
- **Add New**: `http://localhost:8080/manage/users/new`
### 2. Managing Users (Admin Only)
- **All Users List**: Displays a table of all registered users with their `Login`, `Email`, `Status` (Activated/Deactivated), `Language`, `Profiles` (Roles such as `ROLE_ADMIN`, `ROLE_USER`), and `Created Date`.
- **Add New User**:
- Fill in the required fields: `Login`, `Email`.
- Optionally set `First Name`, `Last Name`, and `Language`.
- Assign Roles by checking the corresponding profile boxes.
- Set the user as **Activated** (if they are ready to log in).
- *Note: New users are created with a randomly generated password and a reset key. They must use the "Forgot your password?" flow to set their initial password before they can log in.*
- **Edit User**: Click the blue Edit icon next to any user in the list to modify their email, name, language, active status, or roles. Note that the `Login` cannot be changed after creation.
- **Delete User**: Click the red Trash icon to permanently remove a user. A confirmation modal will prevent accidental deletions.
### 3. User Profiles (All Authenticated Users)
Every logged-in user (regardless of role) can manage their personal profile by clicking their username in the top right corner and selecting **Profile**, or navigating to `http://localhost:8080/manage/profile`.
The Profile page contains two sections:
- **Personal Information**: Users can update their Email, First Name, Last Name, and Display Language.
- **Change Password**: Users can securely change their password by providing their current password, a new password, and confirming the new password.
+18
View File
@@ -0,0 +1,18 @@
import java.net.URI;
import java.nio.file.Paths;
public class TestFileUri {
public static void main(String[] args) throws Exception {
String uploadPath = "/home/x79/sisvietnamvn_01/sisvietnamvn_main/uploads";
System.out.println("Using file:/ + uploadPath:");
try {
URI uri1 = new URI("file:/" + uploadPath + "/");
System.out.println(" Host: " + uri1.getHost());
System.out.println(" Path: " + uri1.getPath());
} catch(Exception e) { e.printStackTrace(); }
System.out.println("\nUsing Paths.get().toUri():");
URI uri2 = Paths.get(uploadPath).toUri();
System.out.println(" URI: " + uri2.toString());
}
}
+7 -15
View File
@@ -1,20 +1,12 @@
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
public class TestOracleJDBC {
@SuppressWarnings("deprecation")
public static void main(String[] args) {
String jdbcUrl = "jdbc:oracle:thin:@192.168.1.15:1521/sisvietnam";
String username = "sisvietnam";
String password = "sisvietnam";
try {
Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
System.out.println("Connection successful!");
connection.close();
} catch (SQLException e) {
System.err.println("Connection failed!");
e.printStackTrace();
}
MessageDigestPasswordEncoder encoder = new MessageDigestPasswordEncoder("SHA-256");
String hash = encoder.encode("admin");
System.out.println("Encoded hash for admin: " + hash);
boolean matches = encoder.matches("admin", "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918");
System.out.println("Does 'admin' match my hash? " + matches);
}
}
+35
View File
@@ -0,0 +1,35 @@
import os
import re
directory = '/home/x79/Documents/sisvietnamvn_01/sisvietnamvn_main/src/main/resources/templates/manage'
layout = '/home/x79/Documents/sisvietnamvn_01/sisvietnamvn_main/src/main/resources/templates/fragments/manage-layout.html'
csrf_token = '\n <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />'
def process_file(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Check if csrf token is already present
if '_csrf.parameterName' in content:
print(f"Skipping {filepath} - already has CSRF token")
return
# Regex to find <form ... method="post" ...>
# It finds the end of the <form ...> tag that has method="post"
pattern = r'(<form[^>]*?method=["\']post["\'][^>]*?>)'
# We want to replace the match with the match + csrf_token
new_content = re.sub(pattern, r'\1' + csrf_token, content, flags=re.IGNORECASE)
if new_content != content:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"Updated {filepath}")
for root, _, files in os.walk(directory):
for file in files:
if file.endswith('.html'):
process_file(os.path.join(root, file))
process_file(layout)
+2 -1
View File
@@ -50,12 +50,12 @@ idea {
// }
dependencies {
implementation "dev.samstevens.totp:totp-spring-boot-starter:1.7.1"
implementation libs.jhipster.framework
implementation libs.mapstruct
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"
@@ -66,6 +66,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-liquibase"
implementation "org.springframework.boot:spring-boot-starter-mail"
implementation "org.springframework.boot:spring-boot-starter-oauth2-resource-server"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
implementation "org.springframework.boot:spring-boot-starter-security"
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
implementation "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
+49
View File
@@ -0,0 +1,49 @@
# This configuration combines the app and database services for easy execution at the project root
name: sisvietnamvn
services:
oracle:
image: container-registry.oracle.com/database/free:latest
restart: always
ports:
- "1521:1521"
environment:
- ORACLE_PWD=Oracle123!
- ORACLE_PASSWORD=Oracle123!
- ORACLE_PDB=sisvietnam
- ORACLE_DATABASE=sisvietnam
- APP_USER=sisvietnam
- APP_USER_PASSWORD=sisvietnam
volumes:
- oracle_data:/opt/oracle/oradata
networks:
- sisvietnamvn-net
app:
image: git.victorphan.net/basketballcantho/sisvietnamvn:latest
depends_on:
oracle:
condition: service_healthy
environment:
- _JAVA_OPTIONS=-Xmx512m -Xms256m
- SPRING_PROFILES_ACTIVE=prod,api-docs,secret-samples
- MANAGEMENT_PROMETHEUS_METRICS_EXPORT_ENABLED=true
- SPRING_DATASOURCE_URL=jdbc:oracle:thin:@oracle:1521/FREEPDB1
- SPRING_DATASOURCE_USERNAME=SYSTEM
- SPRING_DATASOURCE_PASSWORD=Oracle123!
- SPRING_LIQUIBASE_URL=jdbc:oracle:thin:@oracle:1521/FREEPDB1
ports:
- "8080:8080"
healthcheck:
test: [ 'CMD-SHELL', 'bash -c ''exec 3<>/dev/tcp/127.0.0.1/8080; printf "GET /management/health HTTP/1.0\r\nHost: localhost\r\n\r\n" >&3; grep -q "\"status\":\"UP\"" <&3''' ]
interval: 5s
timeout: 5s
retries: 40
networks:
- sisvietnamvn-net
networks:
sisvietnamvn-net:
driver: bridge
volumes:
oracle_data:
+20
View File
@@ -0,0 +1,20 @@
import java.sql.*;
public class get_users {
public static void main(String[] args) {
String url = "jdbc:oracle:thin:@localhost:1521/FREEPDB1";
String user = "sisvietnam";
String password = "sisvietnam";
try (Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, login, email FROM jhi_user")) {
while (rs.next()) {
System.out.println("ID: " + rs.getLong("id") + ", Login: " + rs.getString("login") + ", Email: " + rs.getString("email"));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Vendored Regular → Executable
View File
+1
View File
@@ -0,0 +1 @@
kill -9 $(lsof -t -i:8080) || true
+60
View File
@@ -0,0 +1,60 @@
import csv
import re
csv_path = '/home/x79/sisvietnamvn_01/Design/List of all WP action hooks and filters -- Adam Brown, BYU Political Science.csv'
java_path = '/home/x79/sisvietnamvn_01/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/hook/WpHooks.java'
def sanitize_constant_name(hook_name):
# Convert {$plugin} to PLUGIN
name = re.sub(r'\{\$([a-zA-Z0-9_]+)\}', r'\1', hook_name)
name = re.sub(r'[^a-zA-Z0-9_]', '_', name).upper()
# Handle multiple underscores or leading numbers
name = re.sub(r'_+', '_', name)
name = name.strip('_')
if name and name[0].isdigit():
name = "HOOK_" + name
return name
with open(csv_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f)
next(reader) # Skip header
hooks = set()
for row in reader:
if len(row) > 1 and row[1].strip():
hooks.add(row[1].strip())
with open(java_path, 'w', encoding='utf-8') as f:
f.write("package com.sisvietnamvn.web.hook;\n\n")
f.write("/**\n")
f.write(" * Automatically generated dictionary of all WordPress Hooks.\n")
f.write(" * Contains " + str(len(hooks)) + " constants.\n")
f.write(" */\n")
f.write("public final class WpHooks {\n\n")
f.write(" private WpHooks() {}\n\n")
# Track written constants to avoid duplicates
written = set()
for hook in sorted(list(hooks)):
const_name = sanitize_constant_name(hook)
if not const_name:
continue
if const_name in written:
const_name = const_name + "_2" # Simple deduplication
written.add(const_name)
# Add comment with original hook name
f.write(f' /**\n')
f.write(f' * Original hook name: {hook}\n')
f.write(f' */\n')
f.write(f' public static final String {const_name} = "{hook}";\n\n')
f.write("}\n")
print(f"Generated {len(written)} hook constants in WpHooks.java")
+5 -5
View File
@@ -2,15 +2,15 @@
name: sisvietnamvn
services:
app:
image: sisvietnamvn
image: git.victorphan.net/basketballcantho/sisvietnamvn:latest
environment:
- _JAVA_OPTIONS=-Xmx512m -Xms256m
- SPRING_PROFILES_ACTIVE=prod,api-docs,secret-samples
- MANAGEMENT_PROMETHEUS_METRICS_EXPORT_ENABLED=true
- SPRING_DATASOURCE_URL=jdbc:oracle:thin:@oracle:1521:sisvietnamvn
- SPRING_DATASOURCE_USERNAME=system
- SPRING_DATASOURCE_PASSWORD=oracle
- SPRING_LIQUIBASE_URL=jdbc:oracle:thin:@oracle:1521:sisvietnamvn
- SPRING_DATASOURCE_URL=jdbc:oracle:thin:@oracle:1521/sisvietnam
- SPRING_DATASOURCE_USERNAME=SYSTEM
- SPRING_DATASOURCE_PASSWORD=Oracle123!
- SPRING_LIQUIBASE_URL=jdbc:oracle:thin:@oracle:1521/sisvietnam
# If you want to expose these ports outside your dev PC,
# remove the "127.0.0.1:" prefix
ports:
+6 -5
View File
@@ -1,16 +1,17 @@
# This configuration is intended for development purpose, it's **your** responsibility to harden it for production
name: sisvietnam-oracle
name: sisvietnamvn
services:
sisvietnam-oracle:
oracle:
image: container-registry.oracle.com/database/free:latest
restart: always
network_mode: "host"
ports:
- "1521:1521"
environment:
# Password for the SYS, SYSTEM, and PDBADMIN administrative accounts (official image uses ORACLE_PWD)
# Password for the SYS, SYSTEM, and PDBADMIN administrative accounts
- ORACLE_PWD=Oracle123!
- ORACLE_PASSWORD=Oracle123!
- ORACLE_PDB=sisvietnam
# Name of the pluggable database (official image default is FREEPDB1)
# Name of the pluggable database
- ORACLE_DATABASE=sisvietnam
# Note: The official Oracle Free image does not automatically create custom app users/databases.
# If needed, create the 'sisvietnam' user manually or use the SYSTEM user to connect.
@@ -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 {
@@ -44,6 +44,10 @@ public class CacheConfiguration {
createCache(cm, com.sisvietnamvn.web.domain.User.class.getName());
createCache(cm, com.sisvietnamvn.web.domain.Authority.class.getName());
createCache(cm, com.sisvietnamvn.web.domain.User.class.getName() + ".authorities");
createCache(cm, com.sisvietnamvn.web.domain.Media.class.getName());
createCache(cm, com.sisvietnamvn.web.domain.Plugin.class.getName());
createCache(cm, com.sisvietnamvn.web.domain.Setting.class.getName());
createCache(cm, "settings");
// jhipster-needle-ehcache-add-entry
};
}
@@ -0,0 +1,44 @@
package com.sisvietnamvn.web.config;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import java.util.LinkedHashMap;
import java.util.Map;
public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private final OAuth2AuthorizationRequestResolver defaultResolver;
public CustomOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository, String authorizationRequestBaseUri) {
this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, authorizationRequestBaseUri);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request);
return customizeAuthorizationRequest(req);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request, clientRegistrationId);
return customizeAuthorizationRequest(req);
}
private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest req) {
if (req == null) {
return null;
}
Map<String, Object> additionalParameters = new LinkedHashMap<>(req.getAdditionalParameters());
additionalParameters.put("prompt", "select_account");
return OAuth2AuthorizationRequest.from(req)
.additionalParameters(additionalParameters)
.build();
}
}
@@ -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);
}
}
@@ -0,0 +1,29 @@
package com.sisvietnamvn.web.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.file.Path;
import java.nio.file.Paths;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
exposeDirectory("uploads", registry);
}
private void exposeDirectory(String dirName, ResourceHandlerRegistry registry) {
Path uploadDir = Paths.get(dirName);
if (dirName.startsWith("../")) {
dirName = dirName.replace("../", "");
}
String uploadPath = uploadDir.toFile().getAbsolutePath();
registry.addResourceHandler("/" + dirName + "/**")
.addResourceLocations("file:" + uploadPath + "/");
}
}
@@ -0,0 +1,40 @@
package com.sisvietnamvn.web.config;
import com.sisvietnamvn.web.domain.Authority;
import com.sisvietnamvn.web.repository.AuthorityRepository;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class RoleSeeder implements CommandLineRunner {
private final AuthorityRepository authorityRepository;
public RoleSeeder(AuthorityRepository authorityRepository) {
this.authorityRepository = authorityRepository;
}
@Override
public void run(String... args) {
List<String> roles = Arrays.asList(
AuthoritiesConstants.EDITOR,
AuthoritiesConstants.AUTHOR,
AuthoritiesConstants.CONTRIBUTOR,
AuthoritiesConstants.SUBSCRIBER,
AuthoritiesConstants.PRE_AUTH_2FA
);
for (String roleName : roles) {
if (authorityRepository.findById(roleName).isEmpty()) {
Authority authority = new Authority();
authority.setName(roleName);
authorityRepository.save(authority);
System.out.println("Seeded role: " + roleName);
}
}
}
}
@@ -3,6 +3,7 @@ package com.sisvietnamvn.web.config;
import static org.springframework.security.config.Customizer.withDefaults;
import com.sisvietnamvn.web.security.*;
import com.sisvietnamvn.web.security.oauth2.CustomOidcUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@@ -11,7 +12,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
@@ -19,6 +20,10 @@ import org.springframework.security.web.SecurityFilterChain;
import tech.jhipster.config.JHipsterConstants;
import tech.jhipster.config.JHipsterProperties;
import com.sisvietnamvn.web.security.CustomAuthenticationSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfiguration {
@@ -26,28 +31,35 @@ public class SecurityConfiguration {
private final Environment env;
private final JHipsterProperties jHipsterProperties;
private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfiguration(Environment env, JHipsterProperties jHipsterProperties) {
public SecurityConfiguration(Environment env, JHipsterProperties jHipsterProperties, CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler, ClientRegistrationRepository clientRegistrationRepository) {
this.env = env;
this.jHipsterProperties = jHipsterProperties;
this.customAuthenticationSuccessHandler = customAuthenticationSuccessHandler;
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Bean
@SuppressWarnings("deprecation")
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
return new MessageDigestPasswordEncoder("SHA-256");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
public SecurityFilterChain filterChain(HttpSecurity http, CustomOidcUserService customOidcUserService) {
http.cors(withDefaults())
.csrf(csrf -> csrf.disable())
.csrf(withDefaults())
.authorizeHttpRequests(authz ->
// prettier-ignore
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/**")
"/manage/login", "/css/**", "/images/**", "/js/**", "/vendor/**", "/fonts/**", "/login-assets/**", "/UMass*/**", "/Undergraduate*/**",
"/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**", "/uploads/**", "/api/manage/snippets/**", "/page/**", "/news/article/**", "/post/**", "/error",
"/about-us", "/specialty", "/doctor", "/service", "/health-library", "/news-and-events", "/patient-support", "/medical-expert", "/umcers", "/bidding", "/contact-us")
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/manage/media/upload").permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/authenticate").permitAll()
.requestMatchers(HttpMethod.GET, "/api/authenticate").permitAll()
@@ -61,11 +73,30 @@ public class SecurityConfiguration {
.requestMatchers("/management/health/**").permitAll()
.requestMatchers("/management/info").permitAll()
.requestMatchers("/management/prometheus").permitAll()
.requestMatchers("/2fa/**").hasAuthority(AuthoritiesConstants.PRE_AUTH_2FA)
.requestMatchers("/manage/**").hasAnyAuthority(AuthoritiesConstants.ADMIN, AuthoritiesConstants.EDITOR, AuthoritiesConstants.AUTHOR, AuthoritiesConstants.CONTRIBUTOR, AuthoritiesConstants.SUBSCRIBER, AuthoritiesConstants.USER)
.requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()))
.formLogin(form -> form
.loginPage("/manage/login")
.successHandler(customAuthenticationSuccessHandler)
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/manage/logout")
.logoutSuccessUrl("/manage/login?logout")
.permitAll()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/manage/login")
.successHandler(customAuthenticationSuccessHandler)
.authorizationEndpoint(authorization -> authorization
.authorizationRequestResolver(new CustomOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI))
)
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(customOidcUserService)
)
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()));
if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) {
http.authorizeHttpRequests(authz -> authz.requestMatchers("/h2-console/**").permitAll())
@@ -13,6 +13,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import com.sisvietnamvn.web.hook.HookManager;
import tech.jhipster.config.JHipsterConstants;
import tech.jhipster.config.JHipsterProperties;
import tech.jhipster.config.h2.H2ConfigurationHelper;
@@ -26,12 +27,13 @@ public class WebConfigurer implements ServletContextInitializer {
private static final Logger LOG = LoggerFactory.getLogger(WebConfigurer.class);
private final Environment env;
private final JHipsterProperties jHipsterProperties;
private final HookManager hookManager;
public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties) {
public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties, HookManager hookManager) {
this.env = env;
this.jHipsterProperties = jHipsterProperties;
this.hookManager = hookManager;
}
@Override
@@ -44,6 +46,7 @@ public class WebConfigurer implements ServletContextInitializer {
initH2Console(servletContext);
}
LOG.info("Web application fully configured");
hookManager.doAction("init");
}
@Bean
@@ -0,0 +1,97 @@
package com.sisvietnamvn.web.controller;
import com.sisvietnamvn.web.service.SettingService;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import java.util.Map;
/**
* Provides global model attributes for all frontend controllers.
*/
@ControllerAdvice(basePackages = "com.sisvietnamvn.web.controller")
public class GlobalControllerAdvice {
private final SettingService settingService;
private final com.sisvietnamvn.web.service.MenuService menuService;
private final com.sisvietnamvn.web.hook.HookManager hookManager;
private final PathMatchingResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
public GlobalControllerAdvice(SettingService settingService, com.sisvietnamvn.web.service.MenuService menuService, com.sisvietnamvn.web.hook.HookManager hookManager) {
this.settingService = settingService;
this.menuService = menuService;
this.hookManager = hookManager;
}
/**
* Makes the 'activeTheme' variable available to all Thymeleaf templates handled by controllers in this package.
*/
@ModelAttribute("activeTheme")
public String getActiveTheme() {
Map<String, String> settings = settingService.getAllAsMap();
String activeTheme = settings.getOrDefault("active_theme", "default");
// Validate that the layout actually exists to prevent Thymeleaf TemplateInputException crashes
try {
Resource[] resources = resourceResolver.getResources("classpath*:templates/themes/" + activeTheme + "/layout.html");
if (resources == null || resources.length == 0) {
return "default"; // Fallback to safe layout
}
} catch (Exception e) {
return "default";
}
return activeTheme;
}
@ModelAttribute("primaryMenu")
public com.sisvietnamvn.web.domain.Menu getPrimaryMenu() {
return menuService.findByLocation("PRIMARY").orElse(null);
}
@ModelAttribute("footerMenu")
public com.sisvietnamvn.web.domain.Menu getFooterMenu() {
return menuService.findByLocation("FOOTER").orElse(null);
}
@ModelAttribute("sidebarWidgets")
public java.util.List<com.sisvietnamvn.web.service.dto.WidgetDto> getSidebarWidgets() {
return getWidgets("sidebar");
}
@ModelAttribute("footerWidgets")
public java.util.List<com.sisvietnamvn.web.service.dto.WidgetDto> getFooterWidgets() {
return getWidgets("footer");
}
@ModelAttribute("hookManager")
public com.sisvietnamvn.web.hook.HookManager getHookManager() {
return hookManager;
}
@ModelAttribute("themeModPrimaryColor")
public String getThemeModPrimaryColor() {
return settingService.getValue("theme_mod_primaryColor", "#007bff");
}
@ModelAttribute("themeModFontFamily")
public String getThemeModFontFamily() {
return settingService.getValue("theme_mod_fontFamily", "sans-serif");
}
@ModelAttribute("themeModCustomCss")
public String getThemeModCustomCss() {
return settingService.getValue("theme_mod_customCss", "");
}
private java.util.List<com.sisvietnamvn.web.service.dto.WidgetDto> getWidgets(String area) {
String json = settingService.getValue("theme_widgets_" + area, "[]");
try {
return new com.fasterxml.jackson.databind.ObjectMapper().readValue(json, new com.fasterxml.jackson.core.type.TypeReference<java.util.List<com.sisvietnamvn.web.service.dto.WidgetDto>>() {});
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
return new java.util.ArrayList<>();
}
}
}
@@ -6,10 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "index";
}
// Trang Flex Finish (MỚI THÊM)
@GetMapping("/flex-finish")
@@ -20,17 +17,17 @@ public class HomeController {
// Trang Giới thiệu Bệnh viện
@GetMapping("/about")
public String about() {
return "about";
return "redirect:/about-us";
}
// Trang Tin tức
@GetMapping("/tin-tuc")
public String tintuc() {
return "tin-tuc";
return "redirect:/news-and-events";
}
@GetMapping("/lien-he")
public String lienhe() {
return "lien-he";
return "redirect:/contact-us";
}
}
@@ -1,16 +0,0 @@
package com.sisvietnamvn.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/manage")
public class ManageController {
@GetMapping({"", "/"})
public String index() {
return "manage/index";
}
}
@@ -0,0 +1,122 @@
package com.sisvietnamvn.web.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sisvietnamvn.web.domain.Page;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.security.SecurityUtils;
import com.sisvietnamvn.web.service.PageService;
import com.sisvietnamvn.web.hook.HookManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.server.ResponseStatusException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Controller for rendering public-facing pages dynamically.
*/
@Controller
public class PageController {
private static final Logger LOG = LoggerFactory.getLogger(PageController.class);
private final PageService pageService;
private final ObjectMapper objectMapper;
private final HookManager hookManager;
public PageController(PageService pageService, ObjectMapper objectMapper, HookManager hookManager) {
this.pageService = pageService;
this.objectMapper = objectMapper;
this.hookManager = hookManager;
}
@GetMapping("/page/{slug}")
public String getPage(@PathVariable("slug") String slug, Model model) {
LOG.debug("REST request to get public Page : {}", slug);
Page page = pageService.findBySlug(slug)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
// Apply WordPress-style Content Filters
page.setTitle(hookManager.applyFilters("the_title", page.getTitle()));
page.setContent(hookManager.applyFilters("the_content", page.getContent()));
return renderPage(Optional.of(page), model);
}
@GetMapping("/")
public String getHome(Model model) {
return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.HOME), model);
}
@GetMapping("/about-us")
public String getAboutUs(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.ABOUT_US), model); }
@GetMapping("/specialty")
public String getSpecialty(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.SPECIALTY), model); }
@GetMapping("/doctor")
public String getDoctor(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.DOCTOR), model); }
@GetMapping("/service")
public String getService(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.SERVICE), model); }
@GetMapping("/health-library")
public String getHealthLibrary(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.HEALTH_LIBRARY), model); }
@GetMapping("/news-and-events")
public String getNewsAndEvents(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.NEWS_AND_EVENTS), model); }
@GetMapping("/patient-support")
public String getPatientSupport(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.PATIENT_SUPPORT), model); }
@GetMapping("/medical-expert")
public String getMedicalExpert(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.MEDICAL_EXPERT), model); }
@GetMapping("/umcers")
public String getUmcers(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.UMCERS), model); }
@GetMapping("/bidding")
public String getBidding(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.BIDDING), model); }
@GetMapping("/contact-us")
public String getContactUs(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.CONTACT_US), model); }
private String renderPage(Optional<Page> pageOpt, Model model) {
if (pageOpt.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Page not found");
}
Page page = pageOpt.get();
// Security Check: If Draft, only Admins can view
if ("DRAFT".equals(page.getStatus()) && !SecurityUtils.hasCurrentUserThisAuthority(AuthoritiesConstants.ADMIN)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Page not found");
}
// Parse Editor.js content JSON to extract blocks
List<Map<String, Object>> blocks = Collections.emptyList();
if (page.getContent() != null && !page.getContent().trim().isEmpty()) {
try {
Map<String, Object> editorData = objectMapper.readValue(page.getContent(), new TypeReference<>() {});
if (editorData.containsKey("blocks")) {
blocks = (List<Map<String, Object>>) editorData.get("blocks");
}
} catch (JsonProcessingException e) {
LOG.error("Failed to parse Editor.js JSON for page ID: {}", page.getId(), e);
}
}
model.addAttribute("page", page);
model.addAttribute("blocks", blocks);
return "page";
}
}
@@ -0,0 +1,64 @@
package com.sisvietnamvn.web.controller;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.repository.PostRepository;
import com.sisvietnamvn.web.hook.HookManager;
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;
private final HookManager hookManager;
public PostController(PostRepository postRepository, HookManager hookManager) {
this.postRepository = postRepository;
this.hookManager = hookManager;
}
@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);
}
// Apply WordPress-style Content Filters
post.setTitle(hookManager.applyFilters("the_title", post.getTitle()));
post.setContent(hookManager.applyFilters("the_content", post.getContent()));
if (post.getExcerpt() != null) {
post.setExcerpt(hookManager.applyFilters("the_excerpt", post.getExcerpt()));
}
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,176 @@
package com.sisvietnamvn.web.controller;
import com.sisvietnamvn.web.domain.Authority;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.repository.UserRepository;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.security.DomainUserDetailsService;
import dev.samstevens.totp.code.CodeGenerator;
import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.code.DefaultCodeGenerator;
import dev.samstevens.totp.code.DefaultCodeVerifier;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.QrGenerator;
import dev.samstevens.totp.qr.ZxingPngQrGenerator;
import dev.samstevens.totp.secret.DefaultSecretGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import dev.samstevens.totp.time.TimeProvider;
import dev.samstevens.totp.util.Utils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Controller
@RequestMapping("/2fa")
public class TwoFactorController {
private final UserRepository userRepository;
public TwoFactorController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping
public String show2FaForm(Authentication authentication) {
if (authentication == null || !authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals(AuthoritiesConstants.PRE_AUTH_2FA))) {
return "redirect:/manage/login";
}
String username = authentication.getName();
Optional<User> userOpt = userRepository.findOneByLogin(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
if (!user.isUsing2FA()) {
return "redirect:/2fa/setup";
}
}
return "manage/2fa/verify";
}
@PostMapping
public String verify2Fa(@RequestParam("code") String code, Authentication authentication, RedirectAttributes redirectAttributes) {
String username = authentication.getName();
Optional<User> userOpt = userRepository.findOneWithAuthoritiesByLogin(username);
if (userOpt.isEmpty()) {
return "redirect:/manage/login";
}
User user = userOpt.get();
if (verifyCode(user.getTotpSecret(), code)) {
upgradeSecurityContext(user, authentication);
return "redirect:/manage";
} else {
redirectAttributes.addFlashAttribute("errorMessage", "Invalid 2FA code. Please try again.");
return "redirect:/2fa";
}
}
@GetMapping("/setup")
public String showSetupForm(Authentication authentication, Model model) {
String username = authentication.getName();
Optional<User> userOpt = userRepository.findOneByLogin(username);
if (userOpt.isEmpty()) {
return "redirect:/manage/login";
}
User user = userOpt.get();
if (user.isUsing2FA()) {
return "redirect:/2fa";
}
if (user.getTotpSecret() == null) {
SecretGenerator secretGenerator = new DefaultSecretGenerator();
user.setTotpSecret(secretGenerator.generate());
userRepository.save(user);
}
try {
QrData data = new QrData.Builder()
.label(user.getEmail() != null ? user.getEmail() : user.getLogin())
.secret(user.getTotpSecret())
.issuer("SIS Vietnam")
.algorithm(dev.samstevens.totp.code.HashingAlgorithm.SHA1)
.digits(6)
.period(30)
.build();
QrGenerator generator = new ZxingPngQrGenerator();
byte[] imageData = generator.generate(data);
String mimeType = generator.getImageMimeType();
String dataUri = Utils.getDataUriForImage(imageData, mimeType);
model.addAttribute("qrCode", dataUri);
model.addAttribute("secret", user.getTotpSecret());
} catch (QrGenerationException e) {
model.addAttribute("errorMessage", "Failed to generate QR Code.");
}
return "manage/2fa/setup";
}
@PostMapping("/setup")
public String verifySetup(@RequestParam("code") String code, Authentication authentication, RedirectAttributes redirectAttributes) {
String username = authentication.getName();
Optional<User> userOpt = userRepository.findOneWithAuthoritiesByLogin(username);
if (userOpt.isEmpty()) {
return "redirect:/manage/login";
}
User user = userOpt.get();
if (verifyCode(user.getTotpSecret(), code)) {
user.setUsing2FA(true);
userRepository.save(user);
upgradeSecurityContext(user, authentication);
return "redirect:/manage";
} else {
redirectAttributes.addFlashAttribute("errorMessage", "Invalid 2FA code. Please try again.");
return "redirect:/2fa/setup";
}
}
private boolean verifyCode(String secret, String code) {
TimeProvider timeProvider = new SystemTimeProvider();
CodeGenerator codeGenerator = new DefaultCodeGenerator();
CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
return verifier.isValidCode(secret, code);
}
private void upgradeSecurityContext(User user, Authentication originalAuthentication) {
List<GrantedAuthority> authorities = user.getAuthorities().stream()
.map(Authority::getName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
DomainUserDetailsService.UserWithId userWithId = new DomainUserDetailsService.UserWithId(
user.getLogin(),
user.getPassword(),
authorities,
user.getId()
);
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(
userWithId, originalAuthentication.getCredentials(), authorities);
SecurityContextHolder.getContext().setAuthentication(newAuth);
}
}
@@ -0,0 +1,144 @@
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;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
/**
* 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")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
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,44 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.SettingService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Controller for the Theme Customizer in the Admin panel.
*/
@Controller
@RequestMapping("/manage/customize")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
public class ManageCustomizeController {
private final SettingService settingService;
public ManageCustomizeController(SettingService settingService) {
this.settingService = settingService;
}
@GetMapping
public String customizerView(Model model) {
// Load current customizer settings
model.addAttribute("primaryColor", settingService.getValue("theme_mod_primaryColor", "#007bff"));
model.addAttribute("fontFamily", settingService.getValue("theme_mod_fontFamily", "sans-serif"));
model.addAttribute("customCss", settingService.getValue("theme_mod_customCss", ""));
return "manage/customize/index";
}
@PostMapping("/save")
public String saveCustomizer(@RequestParam String primaryColor, @RequestParam String fontFamily, @RequestParam String customCss, RedirectAttributes redirectAttributes) {
settingService.setValue("theme_mod_primaryColor", primaryColor);
settingService.setValue("theme_mod_fontFamily", fontFamily);
settingService.setValue("theme_mod_customCss", customCss);
redirectAttributes.addFlashAttribute("successMessage", "Customizations saved securely.");
return "redirect:/manage/customize";
}
}
@@ -0,0 +1,97 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.repository.PostRepository;
import com.sisvietnamvn.web.repository.PageRepository;
import com.sisvietnamvn.web.service.PostService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import java.io.File;
import java.lang.management.ManagementFactory;
import com.sun.management.OperatingSystemMXBean;
import java.util.List;
import java.util.stream.Collectors;
/**
* Controller for the admin dashboard index page.
* Serves the main "/manage" landing page.
*/
@Controller
@RequestMapping("/manage")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\", \"" + AuthoritiesConstants.SUBSCRIBER + "\", \"" + AuthoritiesConstants.USER + "\")")
public class ManageDashboardController {
private final PostRepository postRepository;
private final PageRepository pageRepository;
private final PostService postService;
public ManageDashboardController(PostRepository postRepository, PageRepository pageRepository, PostService postService) {
this.postRepository = postRepository;
this.pageRepository = pageRepository;
this.postService = postService;
}
@GetMapping({"", "/"})
public String index(Model model) {
// At a Glance stats
long postCount = postRepository.count();
long pageCount = pageRepository.count();
model.addAttribute("postCount", postCount);
model.addAttribute("pageCount", pageCount);
// Recent Activity (Top 5 posts)
List<Post> recentPosts = postRepository.findAllByOrderByCreatedDateDesc().stream()
.limit(5)
.collect(Collectors.toList());
model.addAttribute("recentPosts", recentPosts);
// Telemetry Stats
Runtime runtime = Runtime.getRuntime();
long totalRam = runtime.totalMemory();
long freeRam = runtime.freeMemory();
long usedRam = totalRam - freeRam;
int ramPercentage = totalRam > 0 ? (int) ((usedRam * 100.0f) / totalRam) : 0;
model.addAttribute("ramPercentage", ramPercentage);
model.addAttribute("usedRamMb", usedRam / (1024 * 1024));
model.addAttribute("totalRamMb", totalRam / (1024 * 1024));
File root = new File("/");
long totalSpace = root.getTotalSpace();
long freeSpace = root.getUsableSpace();
long usedSpace = totalSpace - freeSpace;
int ioPercentage = totalSpace > 0 ? (int) ((usedSpace * 100.0f) / totalSpace) : 0;
model.addAttribute("ioPercentage", ioPercentage);
model.addAttribute("usedSpaceGb", usedSpace / (1024 * 1024 * 1024));
model.addAttribute("totalSpaceGb", totalSpace / (1024 * 1024 * 1024));
try {
OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
double cpuLoad = osBean.getCpuLoad(); // returns 0.0 to 1.0. Returns < 0 if not available.
int cpuPercentage = cpuLoad >= 0 ? (int) (cpuLoad * 100) : 0;
model.addAttribute("cpuPercentage", cpuPercentage);
} catch (Exception e) {
model.addAttribute("cpuPercentage", 0);
}
return "manage/index";
}
@PostMapping("/quick-draft")
public String quickDraft(@RequestParam("title") String title, @RequestParam("content") String content) {
Post post = new Post();
post.setTitle(title);
post.setContent(content);
post.setStatus(PageStatus.DRAFT);
postService.save(post);
return "redirect:/manage";
}
}
@@ -0,0 +1,66 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.ImportExportService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Controller for Import/Export functionality.
*/
@Controller
@RequestMapping("/manage/tools/import-export")
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
public class ManageImportExportController {
private final ImportExportService importExportService;
public ManageImportExportController(ImportExportService importExportService) {
this.importExportService = importExportService;
}
@GetMapping
public String index() {
return "manage/tools/import-export";
}
@PostMapping("/export")
public ResponseEntity<byte[]> exportData() {
byte[] xmlData = importExportService.exportToXml();
if (xmlData == null) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("application/xml"));
headers.setContentDispositionFormData("attachment", "sisvietnamvn_export.xml");
return new ResponseEntity<>(xmlData, headers, HttpStatus.OK);
}
@PostMapping("/import")
public String importData(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
if (file.isEmpty() || !file.getOriginalFilename().endsWith(".xml")) {
redirectAttributes.addFlashAttribute("errorMessage", "Please provide a valid .xml file.");
return "redirect:/manage/tools/import-export";
}
try {
boolean success = importExportService.importFromXml(file.getInputStream());
if (success) {
redirectAttributes.addFlashAttribute("successMessage", "Data successfully imported.");
} else {
redirectAttributes.addFlashAttribute("errorMessage", "Failed to parse XML file.");
}
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Import error: " + e.getMessage());
}
return "redirect:/manage/tools/import-export";
}
}
@@ -0,0 +1,18 @@
package com.sisvietnamvn.web.controller.manage;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Controller for the admin login page.
*/
@Controller
@RequestMapping("/manage/login")
public class ManageLoginController {
@GetMapping
public String login() {
return "manage/login";
}
}
@@ -0,0 +1,163 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.Media;
import com.sisvietnamvn.web.domain.MediaType;
import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.MediaService;
import java.io.IOException;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Thymeleaf MVC controller for the Media Library admin pages.
* Provides Library view, Add New upload, Detail/Edit, and Delete.
*/
@Controller
@RequestMapping("/manage/media")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\")")
public class ManageMediaController {
private static final Logger LOG = LoggerFactory.getLogger(ManageMediaController.class);
private final MediaService mediaService;
private final HookManager hookManager;
public ManageMediaController(MediaService mediaService, HookManager hookManager) {
this.mediaService = mediaService;
this.hookManager = hookManager;
}
@ModelAttribute
public void adminInit() {
hookManager.doAction("admin_init");
}
/**
* GET /manage/media — Media Library page.
*/
@GetMapping
public String listMedia(@RequestParam(value = "type", required = false) String typeStr,
@RequestParam(value = "keyword", required = false) String keyword,
Model model) {
LOG.debug("Request to list all media (type={}, keyword={})", typeStr, keyword);
MediaType type = null;
if (typeStr != null && !typeStr.isBlank()) {
try {
type = MediaType.valueOf(typeStr);
} catch (IllegalArgumentException e) {
LOG.warn("Invalid media type filter: {}", typeStr);
}
}
model.addAttribute("mediaList", mediaService.findFiltered(type, keyword));
model.addAttribute("mediaTypes", MediaType.values());
model.addAttribute("selectedType", typeStr);
model.addAttribute("keyword", keyword);
return "manage/media/list";
}
/**
* GET /manage/media/new — Add New upload page.
*/
@GetMapping("/new")
public String showUploadPage() {
LOG.debug("Request to show media upload page");
return "manage/media/upload";
}
/**
* POST /manage/media/upload — Handle file upload from the form.
*/
@PostMapping("/upload")
public String uploadMedia(@RequestParam("files") MultipartFile[] files,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to upload {} file(s)", files.length);
int successCount = 0;
int failCount = 0;
for (MultipartFile file : files) {
if (file.isEmpty()) continue;
try {
mediaService.upload(file);
successCount++;
} catch (IOException e) {
LOG.error("Failed to upload file: {}", file.getOriginalFilename(), e);
failCount++;
}
}
if (successCount > 0) {
redirectAttributes.addFlashAttribute("successMessage",
successCount + " file(s) uploaded successfully!");
}
if (failCount > 0) {
redirectAttributes.addFlashAttribute("errorMessage",
failCount + " file(s) failed to upload.");
}
return "redirect:/manage/media";
}
/**
* GET /manage/media/{id} — View/edit detail page for a single media item.
*/
@GetMapping("/{id}")
public String showDetail(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
LOG.debug("Request to show media detail: {}", id);
Optional<Media> mediaOpt = mediaService.findById(id);
if (mediaOpt.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "Media not found.");
return "redirect:/manage/media";
}
Media media = mediaOpt.get();
String fullUrl = org.springframework.web.servlet.support.ServletUriComponentsBuilder
.fromCurrentContextPath()
.path(media.getFileUrl())
.toUriString();
model.addAttribute("media", media);
model.addAttribute("fullUrl", fullUrl);
return "manage/media/detail";
}
/**
* POST /manage/media/{id} — Save edits (e.g. alt text) for a media item.
*/
@PostMapping("/{id}")
public String updateMedia(@PathVariable Long id,
@RequestParam(value = "altText", required = false) String altText,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to update media: {}", id);
Optional<Media> mediaOpt = mediaService.findById(id);
if (mediaOpt.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "Media not found.");
return "redirect:/manage/media";
}
Media media = mediaOpt.get();
media.setAltText(altText);
mediaService.save(media);
redirectAttributes.addFlashAttribute("successMessage", "Media updated successfully!");
return "redirect:/manage/media/" + id;
}
/**
* POST /manage/media/{id}/delete — Delete a media item.
*/
@PostMapping("/{id}/delete")
public String deleteMedia(@PathVariable Long id, RedirectAttributes redirectAttributes) {
LOG.debug("Request to delete media: {}", id);
mediaService.delete(id);
redirectAttributes.addFlashAttribute("successMessage", "Media deleted successfully!");
return "redirect:/manage/media";
}
}
@@ -0,0 +1,72 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.Menu;
import com.sisvietnamvn.web.domain.MenuItem;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.MenuService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Controller for managing Navigation Menus in the Admin panel.
*/
@Controller
@RequestMapping("/manage/menus")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
public class ManageMenuController {
private final MenuService menuService;
public ManageMenuController(MenuService menuService) {
this.menuService = menuService;
}
@GetMapping
public String listMenus(Model model) {
model.addAttribute("menus", menuService.findAll());
return "manage/menus/list";
}
@PostMapping("/create")
public String createMenu(@RequestParam String name, @RequestParam(required = false) String location, RedirectAttributes redirectAttributes) {
Menu menu = new Menu();
menu.setName(name);
menu.setLocation(location);
menuService.save(menu);
redirectAttributes.addFlashAttribute("successMessage", "Menu created successfully.");
return "redirect:/manage/menus";
}
@GetMapping("/{id}/edit")
public String editMenu(@PathVariable Long id, Model model) {
Menu menu = menuService.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid menu Id"));
model.addAttribute("menu", menu);
return "manage/menus/edit";
}
@PostMapping("/{id}/items/add")
public String addMenuItem(@PathVariable Long id, @RequestParam String label, @RequestParam String url, RedirectAttributes redirectAttributes) {
Menu menu = menuService.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid menu Id"));
MenuItem item = new MenuItem();
item.setLabel(label);
item.setUrl(url);
item.setMenu(menu);
item.setDisplayOrder(menu.getItems().size());
menu.getItems().add(item);
menuService.save(menu);
redirectAttributes.addFlashAttribute("successMessage", "Menu item added.");
return "redirect:/manage/menus/" + id + "/edit";
}
@PostMapping("/{id}/delete")
public String deleteMenu(@PathVariable Long id, RedirectAttributes redirectAttributes) {
menuService.deleteById(id);
redirectAttributes.addFlashAttribute("successMessage", "Menu deleted successfully.");
return "redirect:/manage/menus";
}
}
@@ -0,0 +1,145 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.Page;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.service.PageService;
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;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
/**
* Controller for managing CMS Pages in the admin panel.
* Provides CRUD operations for static pages.
*/
@Controller
@RequestMapping("/manage/pages")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
public class ManagePageController {
private static final Logger LOG = LoggerFactory.getLogger(ManagePageController.class);
private final PageService pageService;
private final HookManager hookManager;
public ManagePageController(PageService pageService, HookManager hookManager) {
this.pageService = pageService;
this.hookManager = hookManager;
}
/**
* Triggered before any handler method in this controller.
*/
@ModelAttribute
public void adminInit() {
hookManager.doAction("admin_init");
}
/**
* GET /manage/pages — List all pages.
*/
@GetMapping
public String listPages(Model model) {
LOG.debug("Request to list all pages");
model.addAttribute("pages", pageService.findAll());
return "manage/pages/list";
}
/**
* GET /manage/pages/new — Show the "Add New Page" form.
*/
@GetMapping("/new")
public String showCreateForm(Model model) {
LOG.debug("Request to show create page form");
Page page = new Page();
page.setStatus(PageStatus.DRAFT);
page.setDisplayOrder(0);
model.addAttribute("page", page);
model.addAttribute("statuses", PageStatus.values());
model.addAttribute("pageTypes", com.sisvietnamvn.web.domain.PageType.values());
model.addAttribute("layouts", com.sisvietnamvn.web.domain.PageLayout.values());
model.addAttribute("isNew", true);
return "manage/pages/form";
}
/**
* POST /manage/pages — Save a new page.
*/
@PostMapping
public String createPage(@Valid @ModelAttribute("page") Page page,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to create Page : {}", page);
if (bindingResult.hasErrors()) {
model.addAttribute("statuses", PageStatus.values());
model.addAttribute("pageTypes", com.sisvietnamvn.web.domain.PageType.values());
model.addAttribute("layouts", com.sisvietnamvn.web.domain.PageLayout.values());
model.addAttribute("isNew", true);
return "manage/pages/form";
}
pageService.save(page);
redirectAttributes.addFlashAttribute("successMessage", "Page created successfully!");
return "redirect:/manage/pages";
}
/**
* GET /manage/pages/{id}/edit — Show the "Edit Page" form.
*/
@GetMapping("/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
LOG.debug("Request to show edit form for Page : {}", id);
Optional<Page> pageOptional = pageService.findById(id);
if (pageOptional.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "Page not found.");
return "redirect:/manage/pages";
}
model.addAttribute("page", pageOptional.get());
model.addAttribute("statuses", PageStatus.values());
model.addAttribute("pageTypes", com.sisvietnamvn.web.domain.PageType.values());
model.addAttribute("layouts", com.sisvietnamvn.web.domain.PageLayout.values());
model.addAttribute("isNew", false);
return "manage/pages/form";
}
/**
* POST /manage/pages/{id} — Update an existing page.
*/
@PostMapping("/{id}")
public String updatePage(@PathVariable Long id,
@Valid @ModelAttribute("page") Page page,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to update Page : {}", id);
if (bindingResult.hasErrors()) {
model.addAttribute("statuses", PageStatus.values());
model.addAttribute("pageTypes", com.sisvietnamvn.web.domain.PageType.values());
model.addAttribute("layouts", com.sisvietnamvn.web.domain.PageLayout.values());
model.addAttribute("isNew", false);
return "manage/pages/form";
}
page.setId(id);
pageService.save(page);
redirectAttributes.addFlashAttribute("successMessage", "Page updated successfully!");
return "redirect:/manage/pages";
}
/**
* POST /manage/pages/{id}/delete — Delete a page.
*/
@PostMapping("/{id}/delete")
public String deletePage(@PathVariable Long id, RedirectAttributes redirectAttributes) {
LOG.debug("Request to delete Page : {}", id);
pageService.delete(id);
redirectAttributes.addFlashAttribute("successMessage", "Page deleted successfully!");
return "redirect:/manage/pages";
}
}
@@ -0,0 +1,91 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.PersonalDataService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* Controller for managing Personal Data (GDPR).
*/
@Controller
@RequestMapping("/manage/tools/personal-data")
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
public class ManagePersonalDataController {
private final PersonalDataService personalDataService;
public ManagePersonalDataController(PersonalDataService personalDataService) {
this.personalDataService = personalDataService;
}
@GetMapping
public String personalDataView() {
return "manage/tools/personal-data";
}
@PostMapping("/export")
public ResponseEntity<byte[]> exportData(@RequestParam("email") String email) {
byte[] zipData = personalDataService.exportData(email);
if (zipData == null) {
// If user not found, we redirect back with error.
// Since this returns ResponseEntity, we have to return a redirect status or handle it via a form redirect.
// A better way is to return an error page or redirect response.
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("application/zip"));
headers.setContentDispositionFormData("attachment", "personal_data_export.zip");
return new ResponseEntity<>(zipData, headers, HttpStatus.OK);
}
@PostMapping("/export-form")
public String exportDataForm(@RequestParam("email") String email, RedirectAttributes redirectAttributes) {
byte[] zipData = personalDataService.exportData(email);
if (zipData == null) {
redirectAttributes.addFlashAttribute("errorMessage", "No user found with that email address.");
return "redirect:/manage/tools/personal-data";
}
// If found, redirect to a direct download endpoint to avoid flash attribute issues with file downloads
return "redirect:/manage/tools/personal-data/download?email=" + email;
}
@GetMapping("/download")
public ResponseEntity<byte[]> downloadData(@RequestParam("email") String email) {
byte[] zipData = personalDataService.exportData(email);
if (zipData == null) {
return ResponseEntity.notFound().build();
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("application/zip"));
headers.setContentDispositionFormData("attachment", "personal_data_export.zip");
return new ResponseEntity<>(zipData, headers, HttpStatus.OK);
}
@PostMapping("/erase")
public String eraseData(@RequestParam("email") String email,
@RequestParam(value = "confirm", required = false) String confirm,
RedirectAttributes redirectAttributes) {
if (confirm == null || !confirm.equals("on")) {
redirectAttributes.addFlashAttribute("errorMessage", "You must check the confirmation box to erase data.");
return "redirect:/manage/tools/personal-data";
}
boolean success = personalDataService.eraseData(email);
if (success) {
redirectAttributes.addFlashAttribute("successMessage", "Personal data successfully erased and anonymized for " + email);
} else {
redirectAttributes.addFlashAttribute("errorMessage", "No user found with that email address.");
}
return "redirect:/manage/tools/personal-data";
}
}
@@ -0,0 +1,159 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.Plugin;
import com.sisvietnamvn.web.service.PluginService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
@Controller
@RequestMapping("/manage/plugins")
public class ManagePluginController {
private static final Logger LOG = LoggerFactory.getLogger(ManagePluginController.class);
private final PluginService pluginService;
public ManagePluginController(PluginService pluginService) {
this.pluginService = pluginService;
}
// --- Installed Plugins List ---
@GetMapping
public String listPlugins(Model model) {
LOG.debug("Request to list all plugins");
model.addAttribute("plugins", pluginService.findAll());
return "manage/plugins/list";
}
// --- Activate / Deactivate / Delete ---
@PostMapping("/{id}/activate")
public String activatePlugin(@PathVariable Long id, RedirectAttributes redirectAttributes) {
try {
Plugin plugin = pluginService.activate(id);
redirectAttributes.addFlashAttribute("successMessage", "Plugin '" + plugin.getName() + "' activated successfully. Please restart the application to apply hooks.");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Failed to activate plugin: " + e.getMessage());
}
return "redirect:/manage/plugins";
}
@PostMapping("/{id}/deactivate")
public String deactivatePlugin(@PathVariable Long id, RedirectAttributes redirectAttributes) {
try {
Plugin plugin = pluginService.deactivate(id);
redirectAttributes.addFlashAttribute("successMessage", "Plugin '" + plugin.getName() + "' deactivated successfully. Please restart the application to remove hooks.");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Failed to deactivate plugin: " + e.getMessage());
}
return "redirect:/manage/plugins";
}
@PostMapping("/{id}/delete")
public String deletePlugin(@PathVariable Long id, RedirectAttributes redirectAttributes) {
try {
pluginService.delete(id);
redirectAttributes.addFlashAttribute("successMessage", "Plugin deleted successfully.");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Failed to delete plugin: " + e.getMessage());
}
return "redirect:/manage/plugins";
}
// --- Add New Plugin ---
@GetMapping("/new")
public String showAddPluginPage() {
return "manage/plugins/add-new";
}
@PostMapping("/upload")
public String uploadPlugin(@RequestParam("zipFile") MultipartFile zipFile, RedirectAttributes redirectAttributes) {
if (zipFile.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "Please select a .zip file to upload.");
return "redirect:/manage/plugins/new";
}
try {
pluginService.installFromZip(zipFile);
redirectAttributes.addFlashAttribute("successMessage", "Plugin installed successfully. You can now activate it.");
return "redirect:/manage/plugins";
} catch (IOException e) {
LOG.error("Failed to install plugin", e);
redirectAttributes.addFlashAttribute("errorMessage", "Failed to install plugin: " + e.getMessage());
return "redirect:/manage/plugins/new";
} catch (IllegalArgumentException e) {
redirectAttributes.addFlashAttribute("errorMessage", e.getMessage());
return "redirect:/manage/plugins/new";
}
}
// --- Plugin File Editor ---
@GetMapping("/editor")
public String showEditorSelector(Model model) {
model.addAttribute("plugins", pluginService.findAll());
return "manage/plugins/editor";
}
@GetMapping("/editor/{pluginKey}")
public String showEditor(@PathVariable String pluginKey,
@RequestParam(value = "file", required = false) String file,
Model model, RedirectAttributes redirectAttributes) {
Optional<Plugin> pluginOpt = pluginService.findByKey(pluginKey);
if (pluginOpt.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "Plugin not found.");
return "redirect:/manage/plugins/editor";
}
model.addAttribute("plugins", pluginService.findAll());
model.addAttribute("selectedPlugin", pluginOpt.get());
try {
List<String> files = pluginService.getPluginFiles(pluginKey);
model.addAttribute("pluginFiles", files);
if (file != null && !file.isEmpty() && files.contains(file)) {
String content = pluginService.readPluginFile(pluginKey, file);
model.addAttribute("selectedFile", file);
model.addAttribute("fileContent", content);
} else if (!files.isEmpty()) {
// Select first file by default
String firstFile = files.get(0);
String content = pluginService.readPluginFile(pluginKey, firstFile);
model.addAttribute("selectedFile", firstFile);
model.addAttribute("fileContent", content);
}
} catch (Exception e) {
LOG.error("Error reading plugin files for {}", pluginKey, e);
model.addAttribute("errorMessage", "Error reading plugin files: " + e.getMessage());
}
return "manage/plugins/editor";
}
@PostMapping("/editor/{pluginKey}/save")
public String savePluginFile(@PathVariable String pluginKey,
@RequestParam("file") String file,
@RequestParam("content") String content,
RedirectAttributes redirectAttributes) {
try {
pluginService.writePluginFile(pluginKey, file, content);
redirectAttributes.addFlashAttribute("successMessage", "File '" + file + "' saved successfully. Changes may require a rebuild or restart.");
} catch (Exception e) {
LOG.error("Error saving plugin file {}/{}", pluginKey, file, e);
redirectAttributes.addFlashAttribute("errorMessage", "Failed to save file: " + e.getMessage());
}
return "redirect:/manage/plugins/editor/" + pluginKey + "?file=" + file;
}
}
@@ -0,0 +1,185 @@
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;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
/**
* Controller for managing blog Posts in the admin panel.
* Provides CRUD operations with category and tag management.
*/
@Controller
@RequestMapping("/manage/posts")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\")")
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,111 @@
package com.sisvietnamvn.web.controller.manage;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.service.UserService;
import com.sisvietnamvn.web.service.dto.AdminUserDTO;
import jakarta.validation.Valid;
/**
* Controller for managing the logged-in User's Profile in the admin panel.
*/
@Controller
@RequestMapping("/manage/profile")
@PreAuthorize("isAuthenticated()")
public class ManageProfileController {
private static final Logger LOG = LoggerFactory.getLogger(ManageProfileController.class);
private final UserService userService;
private final HookManager hookManager;
public ManageProfileController(UserService userService, HookManager hookManager) {
this.userService = userService;
this.hookManager = hookManager;
}
@ModelAttribute
public void adminInit() {
hookManager.doAction("admin_init");
}
/**
* GET /manage/profile : view the current user's profile form.
*/
@GetMapping
public String showProfileForm(Model model, RedirectAttributes redirectAttributes) {
LOG.debug("Request to show profile form");
Optional<User> userOptional = userService.getUserWithAuthorities();
if (userOptional.isEmpty()) {
redirectAttributes.addFlashAttribute("errorMessage", "User session not found.");
return "redirect:/manage";
}
model.addAttribute("userDTO", new AdminUserDTO(userOptional.get()));
return "manage/users/profile";
}
/**
* POST /manage/profile/update : update basic information for the current user.
*/
@PostMapping("/update")
public String updateProfile(@Valid @ModelAttribute("userDTO") AdminUserDTO userDTO,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to update current user Profile : {}", userDTO);
if (bindingResult.hasErrors()) {
return "manage/users/profile";
}
try {
userService.updateUser(userDTO.getFirstName(), userDTO.getLastName(), userDTO.getEmail(), userDTO.getLangKey(), userDTO.getImageUrl());
redirectAttributes.addFlashAttribute("successMessage", "Profile updated successfully!");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error updating profile: " + e.getMessage());
}
return "redirect:/manage/profile";
}
/**
* POST /manage/profile/password : change the current user's password.
*/
@PostMapping("/password")
public String changePassword(@RequestParam("currentPassword") String currentPassword,
@RequestParam("newPassword") String newPassword,
@RequestParam("confirmPassword") String confirmPassword,
RedirectAttributes redirectAttributes) {
LOG.debug("Request to change password for current user");
if (!newPassword.equals(confirmPassword)) {
redirectAttributes.addFlashAttribute("errorPasswordMessage", "New password and confirm password do not match!");
return "redirect:/manage/profile";
}
try {
userService.changePassword(currentPassword, newPassword);
redirectAttributes.addFlashAttribute("successPasswordMessage", "Password changed successfully!");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorPasswordMessage", "Incorrect current password or error changing password.");
}
return "redirect:/manage/profile";
}
}
@@ -0,0 +1,142 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.service.SettingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.Map;
@Controller
@RequestMapping("/manage/settings")
public class ManageSettingController {
private static final Logger LOG = LoggerFactory.getLogger(ManageSettingController.class);
private final SettingService settingService;
public ManageSettingController(SettingService settingService) {
this.settingService = settingService;
}
private void addSettingsToModel(Model model) {
model.addAttribute("settings", settingService.getAllAsMap());
}
private String saveSettings(Map<String, String> allParams, RedirectAttributes redirectAttributes, String viewName) {
// Remove CSRF token from params before saving
allParams.remove("_csrf");
try {
settingService.setValues(allParams);
redirectAttributes.addFlashAttribute("successMessage", "Settings saved successfully.");
} catch (Exception e) {
LOG.error("Error saving settings", e);
redirectAttributes.addFlashAttribute("errorMessage", "Failed to save settings: " + e.getMessage());
}
return "redirect:/manage/settings/" + viewName;
}
// --- General Settings ---
@GetMapping("/general")
public String showGeneralSettings(Model model) {
addSettingsToModel(model);
return "manage/settings/general";
}
@PostMapping("/general/save")
public String saveGeneralSettings(@RequestParam Map<String, String> allParams, RedirectAttributes redirectAttributes) {
return saveSettings(allParams, redirectAttributes, "general");
}
// --- Writing Settings ---
@GetMapping("/writing")
public String showWritingSettings(Model model) {
addSettingsToModel(model);
return "manage/settings/writing";
}
@PostMapping("/writing/save")
public String saveWritingSettings(@RequestParam Map<String, String> allParams, RedirectAttributes redirectAttributes) {
return saveSettings(allParams, redirectAttributes, "writing");
}
// --- Reading Settings ---
@GetMapping("/reading")
public String showReadingSettings(Model model) {
addSettingsToModel(model);
return "manage/settings/reading";
}
@PostMapping("/reading/save")
public String saveReadingSettings(@RequestParam Map<String, String> allParams, RedirectAttributes redirectAttributes) {
// Handle un-checked checkboxes which don't send a value
if (!allParams.containsKey("search_engine_visibility")) {
allParams.put("search_engine_visibility", "0");
}
return saveSettings(allParams, redirectAttributes, "reading");
}
// --- Discussion Settings ---
@GetMapping("/discussion")
public String showDiscussionSettings(Model model) {
addSettingsToModel(model);
return "manage/settings/discussion";
}
@PostMapping("/discussion/save")
public String saveDiscussionSettings(@RequestParam Map<String, String> allParams, RedirectAttributes redirectAttributes) {
// Handle checkboxes
if (!allParams.containsKey("allow_comments")) allParams.put("allow_comments", "0");
if (!allParams.containsKey("comment_author_must_fill")) allParams.put("comment_author_must_fill", "0");
if (!allParams.containsKey("comment_manual_approval")) allParams.put("comment_manual_approval", "0");
return saveSettings(allParams, redirectAttributes, "discussion");
}
// --- Media Settings ---
@GetMapping("/media")
public String showMediaSettings(Model model) {
addSettingsToModel(model);
return "manage/settings/media";
}
@PostMapping("/media/save")
public String saveMediaSettings(@RequestParam Map<String, String> allParams, RedirectAttributes redirectAttributes) {
return saveSettings(allParams, redirectAttributes, "media");
}
// --- Permalinks Settings ---
@GetMapping("/permalinks")
public String showPermalinksSettings(Model model) {
addSettingsToModel(model);
return "manage/settings/permalinks";
}
@PostMapping("/permalinks/save")
public String savePermalinksSettings(@RequestParam Map<String, String> allParams, RedirectAttributes redirectAttributes) {
return saveSettings(allParams, redirectAttributes, "permalinks");
}
// --- Privacy Settings ---
@GetMapping("/privacy")
public String showPrivacySettings(Model model) {
addSettingsToModel(model);
return "manage/settings/privacy";
}
@PostMapping("/privacy/save")
public String savePrivacySettings(@RequestParam Map<String, String> allParams, RedirectAttributes redirectAttributes) {
return saveSettings(allParams, redirectAttributes, "privacy");
}
}
@@ -0,0 +1,68 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.sql.DataSource;
import java.sql.Connection;
/**
* Controller for the Site Health diagnostic dashboard.
*/
@Controller
@RequestMapping("/manage/tools/site-health")
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
public class ManageSiteHealthController {
private final DataSource dataSource;
public ManageSiteHealthController(DataSource dataSource) {
this.dataSource = dataSource;
}
@GetMapping
public String siteHealthView(Model model) {
// Gather Java / OS Information
model.addAttribute("javaVersion", System.getProperty("java.version"));
model.addAttribute("javaVendor", System.getProperty("java.vendor"));
model.addAttribute("osName", System.getProperty("os.name"));
model.addAttribute("osVersion", System.getProperty("os.version"));
model.addAttribute("osArch", System.getProperty("os.arch"));
model.addAttribute("timezone", System.getProperty("user.timezone"));
// Gather Memory Information
Runtime runtime = Runtime.getRuntime();
long totalMemory = runtime.totalMemory() / (1024 * 1024);
long freeMemory = runtime.freeMemory() / (1024 * 1024);
long maxMemory = runtime.maxMemory() / (1024 * 1024);
long usedMemory = totalMemory - freeMemory;
model.addAttribute("totalMemory", totalMemory);
model.addAttribute("freeMemory", freeMemory);
model.addAttribute("maxMemory", maxMemory);
model.addAttribute("usedMemory", usedMemory);
// Database Connectivity Check
String dbStatus = "Disconnected";
String dbProduct = "Unknown";
boolean isDbHealthy = false;
try (Connection connection = dataSource.getConnection()) {
if (connection.isValid(2)) {
dbStatus = "Connected";
isDbHealthy = true;
dbProduct = connection.getMetaData().getDatabaseProductName() + " " + connection.getMetaData().getDatabaseProductVersion();
}
} catch (Exception e) {
dbStatus = "Error: " + e.getMessage();
}
model.addAttribute("dbStatus", dbStatus);
model.addAttribute("dbProduct", dbProduct);
model.addAttribute("isDbHealthy", isDbHealthy);
return "manage/tools/site-health";
}
}
@@ -0,0 +1,130 @@
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;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
/**
* 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")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\")")
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);
}
}

Some files were not shown because too many files have changed in this diff Show More