web server
This commit is contained in:
1489
web-server/public/css/styles.css
Normal file
1489
web-server/public/css/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
719
web-server/public/js/app.js
Normal file
719
web-server/public/js/app.js
Normal file
@@ -0,0 +1,719 @@
|
||||
(function () {
|
||||
const body = document.body;
|
||||
const menuButton = document.getElementById('mobileMenuBtn');
|
||||
const sidebarBackdrop = document.getElementById('sidebarBackdrop');
|
||||
|
||||
function initNotiflix() {
|
||||
if (!window.Notiflix) return;
|
||||
|
||||
window.Notiflix.Notify.init({
|
||||
width: '320px',
|
||||
position: 'right-top',
|
||||
distance: '16px',
|
||||
timeout: 2600,
|
||||
borderRadius: '8px',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
fontSize: '13px',
|
||||
messageMaxLength: 180,
|
||||
clickToClose: true,
|
||||
pauseOnHover: true,
|
||||
cssAnimationStyle: 'from-right',
|
||||
useIcon: true,
|
||||
zindex: 5000,
|
||||
success: {
|
||||
background: '#067647',
|
||||
textColor: '#ffffff'
|
||||
},
|
||||
failure: {
|
||||
background: '#b42318',
|
||||
textColor: '#ffffff'
|
||||
},
|
||||
warning: {
|
||||
background: '#b54708',
|
||||
textColor: '#ffffff'
|
||||
},
|
||||
info: {
|
||||
background: '#3755c3',
|
||||
textColor: '#ffffff'
|
||||
}
|
||||
});
|
||||
|
||||
window.Notiflix.Confirm.init({
|
||||
width: '360px',
|
||||
borderRadius: '8px',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
titleColor: '#111827',
|
||||
titleFontSize: '16px',
|
||||
messageColor: '#475569',
|
||||
messageFontSize: '13px',
|
||||
okButtonBackground: '#3755c3',
|
||||
okButtonColor: '#ffffff',
|
||||
cancelButtonBackground: '#e2e8f0',
|
||||
cancelButtonColor: '#334155',
|
||||
backOverlayColor: 'rgba(15, 23, 42, 0.42)',
|
||||
zindex: 5001,
|
||||
cssAnimationStyle: 'zoom'
|
||||
});
|
||||
}
|
||||
|
||||
function notify(type, message) {
|
||||
if (!message) return;
|
||||
|
||||
if (!window.Notiflix) {
|
||||
console.info(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const Notify = window.Notiflix.Notify;
|
||||
|
||||
if (type === 'success') {
|
||||
Notify.success(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'failure') {
|
||||
Notify.failure(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'warning') {
|
||||
Notify.warning(message);
|
||||
return;
|
||||
}
|
||||
|
||||
Notify.info(message);
|
||||
}
|
||||
|
||||
function confirmAction(message, onConfirm) {
|
||||
if (!window.Notiflix) {
|
||||
if (window.confirm(message || 'Xác nhận thao tác?') && typeof onConfirm === 'function') {
|
||||
onConfirm();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
window.Notiflix.Confirm.show(
|
||||
'Xác nhận thao tác',
|
||||
message,
|
||||
'Xác nhận',
|
||||
'Hủy',
|
||||
() => {
|
||||
if (typeof onConfirm === 'function') {
|
||||
onConfirm();
|
||||
return;
|
||||
}
|
||||
notify('success', 'Đã xác nhận thao tác');
|
||||
},
|
||||
() => {
|
||||
notify('info', 'Đã hủy thao tác');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function setMobileNav(open) {
|
||||
body.classList.toggle('mobile-nav-open', open);
|
||||
if (menuButton) {
|
||||
menuButton.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function openModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (!modal) return;
|
||||
|
||||
modal.classList.add('open');
|
||||
const focusTarget = modal.querySelector('input, select, textarea, button');
|
||||
if (focusTarget) {
|
||||
window.setTimeout(() => focusTarget.focus(), 60);
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(modal) {
|
||||
if (!modal) return;
|
||||
modal.classList.remove('open');
|
||||
}
|
||||
|
||||
function applyTableFilters(tableId) {
|
||||
const table = document.getElementById(tableId);
|
||||
if (!table) return;
|
||||
|
||||
const searchInput = document.querySelector(`[data-table-search="${tableId}"]`);
|
||||
const query = searchInput ? searchInput.value.trim().toLowerCase() : '';
|
||||
const selects = document.querySelectorAll(`[data-filter-select][data-filter-table="${tableId}"]`);
|
||||
|
||||
table.querySelectorAll('tbody tr').forEach((row) => {
|
||||
const matchesSearch = !query || (row.dataset.search || '').includes(query);
|
||||
let matchesSelects = true;
|
||||
|
||||
selects.forEach((select) => {
|
||||
const column = select.dataset.filterColumn;
|
||||
const value = select.value;
|
||||
if (value && row.dataset[column] !== value) {
|
||||
matchesSelects = false;
|
||||
}
|
||||
});
|
||||
|
||||
row.hidden = !(matchesSearch && matchesSelects);
|
||||
});
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||
const value = bytes / Math.pow(1024, index);
|
||||
|
||||
return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
|
||||
}
|
||||
|
||||
function isAllowedPackageFile(file) {
|
||||
if (!file) return false;
|
||||
|
||||
const name = file.name.toLowerCase();
|
||||
const allowedExtensions = ['.deb', '.tar', '.tar.gz', '.tgz', '.zip', '.gz'];
|
||||
|
||||
return allowedExtensions.some((extension) => name.endsWith(extension));
|
||||
}
|
||||
|
||||
function renderSelectedFile(zone, file) {
|
||||
const preview = zone.querySelector('[data-file-preview]');
|
||||
const fileName = zone.querySelector('[data-file-name]');
|
||||
const fileMeta = zone.querySelector('[data-file-meta]');
|
||||
|
||||
if (!preview || !fileName || !fileMeta) return;
|
||||
|
||||
if (!file) {
|
||||
preview.hidden = true;
|
||||
fileName.textContent = 'Chưa chọn file';
|
||||
fileMeta.textContent = '';
|
||||
zone.classList.remove('has-file');
|
||||
return;
|
||||
}
|
||||
|
||||
fileName.textContent = file.name;
|
||||
fileMeta.textContent = `${formatFileSize(file.size)} • ${file.type || 'package file'}`;
|
||||
preview.hidden = false;
|
||||
zone.classList.add('has-file');
|
||||
notify('success', `Đã chọn file: ${file.name}`);
|
||||
}
|
||||
|
||||
function setInputFiles(input, files) {
|
||||
try {
|
||||
input.files = files;
|
||||
} catch (error) {
|
||||
console.info('Browser does not allow assigning dropped files to input.files.', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePackageFiles(zone, files) {
|
||||
const input = zone.querySelector('[data-file-input]');
|
||||
const file = files && files[0];
|
||||
|
||||
if (!input || !file) return;
|
||||
|
||||
if (!isAllowedPackageFile(file)) {
|
||||
notify('warning', 'File chưa đúng định dạng. Vui lòng chọn .deb, .tar, .tgz, .zip hoặc .gz.');
|
||||
return;
|
||||
}
|
||||
|
||||
setInputFiles(input, files);
|
||||
renderSelectedFile(zone, file);
|
||||
}
|
||||
|
||||
function initFileDropzones() {
|
||||
document.querySelectorAll('[data-file-dropzone]').forEach((zone) => {
|
||||
const input = zone.querySelector('[data-file-input]');
|
||||
const browseButton = zone.querySelector('[data-file-browse]');
|
||||
const clearButton = zone.querySelector('[data-file-clear]');
|
||||
|
||||
if (!input) return;
|
||||
|
||||
if (browseButton) {
|
||||
browseButton.addEventListener('click', () => input.click());
|
||||
}
|
||||
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener('click', () => {
|
||||
input.value = '';
|
||||
renderSelectedFile(zone, null);
|
||||
notify('info', 'Đã bỏ file đã chọn');
|
||||
});
|
||||
}
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
handlePackageFiles(zone, input.files);
|
||||
});
|
||||
|
||||
['dragenter', 'dragover'].forEach((eventName) => {
|
||||
zone.addEventListener(eventName, (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
zone.classList.add('dragover');
|
||||
});
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach((eventName) => {
|
||||
zone.addEventListener(eventName, (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
zone.classList.remove('dragover');
|
||||
});
|
||||
});
|
||||
|
||||
zone.addEventListener('drop', (event) => {
|
||||
handlePackageFiles(zone, event.dataTransfer.files);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateRegisterSubmit(form) {
|
||||
const submitButton = form.querySelector('[data-register-submit]');
|
||||
if (!submitButton) return;
|
||||
|
||||
const uniqueInputs = form.querySelectorAll('[data-unique-check]');
|
||||
const isBlocked = Array.from(uniqueInputs).some((input) => (
|
||||
input.dataset.uniqueStatus === 'error' || input.dataset.uniqueStatus === 'checking'
|
||||
));
|
||||
|
||||
submitButton.disabled = isBlocked;
|
||||
}
|
||||
|
||||
function setUniqueState(input, state, message) {
|
||||
const field = input.closest('.form-field');
|
||||
const form = input.closest('[data-register-form]');
|
||||
const feedback = form
|
||||
? form.querySelector(`[data-unique-feedback="${input.dataset.uniqueCheck}"]`)
|
||||
: null;
|
||||
|
||||
input.dataset.uniqueStatus = state;
|
||||
|
||||
if (field) {
|
||||
field.classList.toggle('has-error', state === 'error');
|
||||
field.classList.toggle('has-success', state === 'success');
|
||||
}
|
||||
|
||||
if (feedback) {
|
||||
feedback.textContent = message || '';
|
||||
feedback.style.display = message ? 'block' : '';
|
||||
}
|
||||
|
||||
input.setCustomValidity(state === 'error' ? message : '');
|
||||
|
||||
if (form) {
|
||||
updateRegisterSubmit(form);
|
||||
}
|
||||
}
|
||||
|
||||
function checkUniqueInput(input) {
|
||||
const field = input.dataset.uniqueCheck;
|
||||
const value = input.value.trim();
|
||||
|
||||
if (!field || !value) {
|
||||
setUniqueState(input, 'idle', '');
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === 'email' && !input.validity.valid) {
|
||||
setUniqueState(input, 'idle', '');
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = String(Date.now());
|
||||
input.dataset.uniqueRequestId = requestId;
|
||||
setUniqueState(input, 'checking', 'Đang kiểm tra...');
|
||||
|
||||
fetch(`/register/check?field=${encodeURIComponent(field)}&value=${encodeURIComponent(value)}`, {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => response.ok ? response.json() : Promise.reject(new Error('Cannot check field')))
|
||||
.then((data) => {
|
||||
if (input.dataset.uniqueRequestId !== requestId) return;
|
||||
|
||||
setUniqueState(
|
||||
input,
|
||||
data.available ? 'success' : 'error',
|
||||
data.message || (data.available ? 'Có thể sử dụng.' : 'Đã tồn tại.')
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.info('Cannot check registration field:', error);
|
||||
if (input.dataset.uniqueRequestId === requestId) {
|
||||
setUniqueState(input, 'idle', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initRegistrationUniqueChecks() {
|
||||
document.querySelectorAll('[data-register-form]').forEach((form) => {
|
||||
const timers = new Map();
|
||||
|
||||
form.querySelectorAll('[data-unique-check]').forEach((input) => {
|
||||
input.addEventListener('input', () => {
|
||||
window.clearTimeout(timers.get(input));
|
||||
setUniqueState(input, 'idle', '');
|
||||
timers.set(input, window.setTimeout(() => checkUniqueInput(input), 450));
|
||||
});
|
||||
|
||||
input.addEventListener('blur', () => {
|
||||
window.clearTimeout(timers.get(input));
|
||||
checkUniqueInput(input);
|
||||
});
|
||||
|
||||
if (input.value.trim()) {
|
||||
checkUniqueInput(input);
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('submit', (event) => {
|
||||
const blockedInput = form.querySelector('[data-unique-status="error"], [data-unique-status="checking"]');
|
||||
if (!blockedInput) return;
|
||||
|
||||
event.preventDefault();
|
||||
notify('warning', blockedInput.dataset.uniqueStatus === 'checking'
|
||||
? 'Đang kiểm tra username/email, vui lòng chờ một chút.'
|
||||
: 'Vui lòng đổi username/email đang bị trùng.');
|
||||
blockedInput.focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getUserDataFromRow(row) {
|
||||
return {
|
||||
id: row.dataset.userId || '',
|
||||
name: row.dataset.userName || '',
|
||||
username: row.dataset.userUsername || '',
|
||||
email: row.dataset.userEmail || '',
|
||||
fullName: row.dataset.userFullName || '',
|
||||
role: row.dataset.userRole || 'User',
|
||||
status: row.dataset.userStatus || '',
|
||||
isActive: row.dataset.userActive === 'true',
|
||||
createdAt: row.dataset.userCreatedAt || '',
|
||||
updatedAt: row.dataset.userUpdatedAt || '',
|
||||
packageCount: row.dataset.userPackageCount || '0',
|
||||
applicationCount: row.dataset.userApplicationCount || '0'
|
||||
};
|
||||
}
|
||||
|
||||
function parseJsonAttribute(value, fallback) {
|
||||
try {
|
||||
return JSON.parse(value || '');
|
||||
} catch (error) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function getAppDataFromTrigger(trigger) {
|
||||
return {
|
||||
id: trigger.dataset.appId || '',
|
||||
code: trigger.dataset.appCode || '',
|
||||
name: trigger.dataset.appName || '',
|
||||
version: trigger.dataset.appVersion || '',
|
||||
status: trigger.dataset.appStatus || 'Draft',
|
||||
notes: trigger.dataset.appNotes || '',
|
||||
packages: parseJsonAttribute(trigger.dataset.appPackages, [])
|
||||
};
|
||||
}
|
||||
|
||||
function findEditAppVersionSelect(form, packageId) {
|
||||
return Array.from(form.querySelectorAll('[data-edit-app-version]'))
|
||||
.find((select) => select.dataset.editAppVersion === packageId);
|
||||
}
|
||||
|
||||
function setEditAppPackageEnabled(form, checkbox) {
|
||||
const select = findEditAppVersionSelect(form, checkbox.dataset.editAppPackage);
|
||||
if (select) {
|
||||
select.disabled = !checkbox.checked;
|
||||
}
|
||||
}
|
||||
|
||||
function setEditAppField(form, field, value) {
|
||||
const input = form.querySelector(`[data-edit-app-field="${field}"]`);
|
||||
if (input) {
|
||||
input.value = value || '';
|
||||
}
|
||||
}
|
||||
|
||||
function openAppEdit(trigger) {
|
||||
const app = getAppDataFromTrigger(trigger);
|
||||
const form = document.getElementById('editAppForm');
|
||||
if (!form || !app.id) return;
|
||||
|
||||
form.action = `/applications/${encodeURIComponent(app.id)}/edit`;
|
||||
setEditAppField(form, 'appCode', app.code);
|
||||
setEditAppField(form, 'appName', app.name);
|
||||
setEditAppField(form, 'appVersion', app.version);
|
||||
setEditAppField(form, 'status', app.status);
|
||||
setEditAppField(form, 'notes', app.notes);
|
||||
|
||||
form.querySelectorAll('[data-edit-app-package]').forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
setEditAppPackageEnabled(form, checkbox);
|
||||
});
|
||||
|
||||
app.packages.forEach((packageItem) => {
|
||||
const packageId = packageItem.packageId || '';
|
||||
const checkbox = Array.from(form.querySelectorAll('[data-edit-app-package]'))
|
||||
.find((input) => input.dataset.editAppPackage === packageId);
|
||||
const select = findEditAppVersionSelect(form, packageId);
|
||||
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
|
||||
if (select) {
|
||||
select.disabled = false;
|
||||
select.value = packageItem.selectedVersionId || '';
|
||||
}
|
||||
});
|
||||
|
||||
openModal('editAppModal');
|
||||
}
|
||||
|
||||
function openPackageUpdate(packageId) {
|
||||
const modalId = 'updatePackageModal';
|
||||
const modal = document.getElementById(modalId);
|
||||
const packageSelect = modal ? modal.querySelector('select[name="packageId"]') : null;
|
||||
|
||||
if (packageSelect && packageId) {
|
||||
packageSelect.value = packageId;
|
||||
}
|
||||
|
||||
openModal(modalId);
|
||||
}
|
||||
|
||||
function setText(selector, value) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.textContent = value || '';
|
||||
}
|
||||
}
|
||||
|
||||
function openUserDetail(row) {
|
||||
const user = getUserDataFromRow(row);
|
||||
|
||||
setText('[data-user-detail="name"]', user.name);
|
||||
setText('[data-user-detail="username"]', user.username);
|
||||
setText('[data-user-detail="email"]', user.email);
|
||||
setText('[data-user-detail="role"]', user.role);
|
||||
setText('[data-user-detail="status"]', user.status);
|
||||
setText('[data-user-detail="createdAt"]', user.createdAt);
|
||||
setText('[data-user-detail="updatedAt"]', user.updatedAt || 'Chưa cập nhật');
|
||||
setText('[data-user-detail="ownedData"]', `${user.packageCount} packages, ${user.applicationCount} apps`);
|
||||
|
||||
openModal('userDetailModal');
|
||||
}
|
||||
|
||||
function openUserEdit(row) {
|
||||
const user = getUserDataFromRow(row);
|
||||
const form = document.getElementById('editUserForm');
|
||||
if (!form) return;
|
||||
|
||||
form.action = `/users/${encodeURIComponent(user.id)}/edit`;
|
||||
form.querySelector('[data-edit-user-field="username"]').value = user.username;
|
||||
form.querySelector('[data-edit-user-field="fullName"]').value = user.fullName;
|
||||
form.querySelector('[data-edit-user-field="email"]').value = user.email;
|
||||
form.querySelector('[data-edit-user-field="role"]').value = user.role;
|
||||
form.querySelector('[data-edit-user-field="isActive"]').checked = user.isActive;
|
||||
form.querySelector('[data-edit-user-field="newPassword"]').value = '';
|
||||
form.querySelector('[data-edit-user-field="confirmPassword"]').value = '';
|
||||
|
||||
openModal('editUserModal');
|
||||
}
|
||||
|
||||
function validateProfileForm(form, shouldNotify) {
|
||||
const email = form.querySelector('[data-profile-email]');
|
||||
const confirmEmail = form.querySelector('[data-profile-confirm-email]');
|
||||
const newPassword = form.querySelector('[data-profile-new-password]');
|
||||
const confirmPassword = form.querySelector('[data-profile-confirm-password]');
|
||||
const emailFeedback = form.querySelector('[data-profile-feedback="email"]');
|
||||
const passwordFeedback = form.querySelector('[data-profile-feedback="password"]');
|
||||
|
||||
const emailMismatch = Boolean(email && confirmEmail
|
||||
&& email.value.trim().toLowerCase() !== confirmEmail.value.trim().toLowerCase());
|
||||
const passwordMismatch = Boolean(newPassword && confirmPassword
|
||||
&& (newPassword.value || confirmPassword.value)
|
||||
&& newPassword.value !== confirmPassword.value);
|
||||
|
||||
if (confirmEmail) {
|
||||
confirmEmail.setCustomValidity(emailMismatch ? 'Confirm email mới chưa khớp.' : '');
|
||||
const field = confirmEmail.closest('.form-field');
|
||||
if (field) {
|
||||
field.classList.toggle('has-error', emailMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
if (emailFeedback) {
|
||||
emailFeedback.textContent = emailMismatch ? 'Confirm email mới chưa khớp.' : '';
|
||||
emailFeedback.style.display = emailMismatch ? 'block' : '';
|
||||
}
|
||||
|
||||
if (confirmPassword) {
|
||||
confirmPassword.setCustomValidity(passwordMismatch ? 'Xác nhận mật khẩu mới chưa khớp.' : '');
|
||||
const field = confirmPassword.closest('.form-field');
|
||||
if (field) {
|
||||
field.classList.toggle('has-error', passwordMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
if (passwordFeedback) {
|
||||
passwordFeedback.textContent = passwordMismatch ? 'Xác nhận mật khẩu mới chưa khớp.' : '';
|
||||
passwordFeedback.style.display = passwordMismatch ? 'block' : '';
|
||||
}
|
||||
|
||||
if (shouldNotify && emailMismatch) {
|
||||
notify('warning', 'Confirm email mới chưa khớp.');
|
||||
} else if (shouldNotify && passwordMismatch) {
|
||||
notify('warning', 'Xác nhận mật khẩu mới chưa khớp.');
|
||||
}
|
||||
|
||||
return !emailMismatch && !passwordMismatch;
|
||||
}
|
||||
|
||||
function initProfileForms() {
|
||||
document.querySelectorAll('[data-profile-form]').forEach((form) => {
|
||||
form.querySelectorAll('input').forEach((input) => {
|
||||
input.addEventListener('input', () => validateProfileForm(form, false));
|
||||
});
|
||||
|
||||
form.addEventListener('submit', (event) => {
|
||||
if (!validateProfileForm(form, true)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initEditAppForms() {
|
||||
document.querySelectorAll('#editAppForm [data-edit-app-package]').forEach((checkbox) => {
|
||||
const form = checkbox.closest('form');
|
||||
setEditAppPackageEnabled(form, checkbox);
|
||||
checkbox.addEventListener('change', () => setEditAppPackageEnabled(form, checkbox));
|
||||
});
|
||||
}
|
||||
|
||||
initNotiflix();
|
||||
initFileDropzones();
|
||||
initRegistrationUniqueChecks();
|
||||
initProfileForms();
|
||||
initEditAppForms();
|
||||
|
||||
if (body.dataset.notice) {
|
||||
notify(body.dataset.noticeType || 'info', body.dataset.notice);
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.has('notice') || url.searchParams.has('noticeType')) {
|
||||
url.searchParams.delete('notice');
|
||||
url.searchParams.delete('noticeType');
|
||||
window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`);
|
||||
}
|
||||
delete body.dataset.notice;
|
||||
delete body.dataset.noticeType;
|
||||
}
|
||||
|
||||
if (menuButton) {
|
||||
menuButton.addEventListener('click', () => {
|
||||
setMobileNav(!body.classList.contains('mobile-nav-open'));
|
||||
});
|
||||
}
|
||||
|
||||
if (sidebarBackdrop) {
|
||||
sidebarBackdrop.addEventListener('click', () => setMobileNav(false));
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-table-search]').forEach((input) => {
|
||||
input.addEventListener('input', () => applyTableFilters(input.dataset.tableSearch));
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-filter-select]').forEach((select) => {
|
||||
select.addEventListener('change', () => applyTableFilters(select.dataset.filterTable));
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const appEditButton = event.target.closest('[data-app-edit]');
|
||||
if (appEditButton) {
|
||||
openAppEdit(appEditButton);
|
||||
return;
|
||||
}
|
||||
|
||||
const packageUpdateButton = event.target.closest('[data-package-update]');
|
||||
if (packageUpdateButton) {
|
||||
openPackageUpdate(packageUpdateButton.dataset.packageUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshButton = event.target.closest('[data-refresh-page]');
|
||||
if (refreshButton) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const userViewButton = event.target.closest('[data-user-view]');
|
||||
if (userViewButton) {
|
||||
const row = userViewButton.closest('tr');
|
||||
if (row) {
|
||||
openUserDetail(row);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const userEditButton = event.target.closest('[data-user-edit]');
|
||||
if (userEditButton) {
|
||||
const row = userEditButton.closest('tr');
|
||||
if (row) {
|
||||
openUserEdit(row);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const modalOpenButton = event.target.closest('[data-modal-open]');
|
||||
if (modalOpenButton) {
|
||||
openModal(modalOpenButton.dataset.modalOpen);
|
||||
return;
|
||||
}
|
||||
|
||||
const modalCloseButton = event.target.closest('[data-modal-close]');
|
||||
if (modalCloseButton) {
|
||||
closeModal(modalCloseButton.closest('.modal-backdrop'));
|
||||
}
|
||||
|
||||
const modalBackdrop = event.target.classList.contains('modal-backdrop') ? event.target : null;
|
||||
if (modalBackdrop) {
|
||||
closeModal(modalBackdrop);
|
||||
}
|
||||
|
||||
const toastButton = event.target.closest('[data-toast]');
|
||||
if (toastButton) {
|
||||
notify(toastButton.dataset.toastType || 'info', toastButton.dataset.toast);
|
||||
}
|
||||
|
||||
const confirmButton = event.target.closest('[data-confirm]');
|
||||
if (confirmButton) {
|
||||
confirmAction(confirmButton.dataset.confirm);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('submit', (event) => {
|
||||
const form = event.target.closest('form[data-confirm-submit]');
|
||||
if (!form || form.dataset.confirmed === 'true') return;
|
||||
|
||||
event.preventDefault();
|
||||
confirmAction(form.dataset.confirmSubmit, () => {
|
||||
form.dataset.confirmed = 'true';
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Escape') return;
|
||||
|
||||
const openModalNode = document.querySelector('.modal-backdrop.open');
|
||||
if (openModalNode) {
|
||||
closeModal(openModalNode);
|
||||
return;
|
||||
}
|
||||
|
||||
setMobileNav(false);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user