initialize project documentation and setup instructions
This commit is contained in:
355
static/js/main.js
Normal file
355
static/js/main.js
Normal file
@@ -0,0 +1,355 @@
|
||||
/**
|
||||
* © 2025-2026 Skoci Admin
|
||||
* Designed and Developed by Abimanyu
|
||||
* Contact : abimanyuag12@gmail.com
|
||||
* All Rights Reserved
|
||||
*/
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
// 1. Logika Preloader
|
||||
const preloader = document.getElementById('bz-preloader');
|
||||
if (preloader) {
|
||||
preloader.classList.add('hide');
|
||||
setTimeout(() => {
|
||||
preloader.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 2. Logika penyimpanan tab perhalaman
|
||||
// Memilih semua elemen toggle
|
||||
const tabTriggers = document.querySelectorAll('[data-bs-toggle="pill"], [data-bs-toggle="tab"]');
|
||||
|
||||
tabTriggers.forEach(trigger => {
|
||||
// Membuat key unik berdasarkan path DAN tipe toggle (pill atau tab)
|
||||
const toggleType = trigger.getAttribute('data-bs-toggle');
|
||||
const storageKey = `activeTab_${location.pathname}_${toggleType}`;
|
||||
|
||||
// Event saat tab ditampilkan
|
||||
trigger.addEventListener('shown.bs.tab', event => {
|
||||
const targetIdentifier = event.target.getAttribute('data-bs-target');
|
||||
localStorage.setItem(storageKey, targetIdentifier);
|
||||
});
|
||||
|
||||
// Cek storage saat halaman dimuat (hanya jalankan sekali untuk masing-masing tipe)
|
||||
const savedTarget = localStorage.getItem(storageKey);
|
||||
if (savedTarget) {
|
||||
const targetElement = document.querySelector(`[data-bs-target="${savedTarget}"]`);
|
||||
if (targetElement) {
|
||||
const tabInstance = new bootstrap.Tab(targetElement);
|
||||
tabInstance.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Logika sidebar Toggle
|
||||
|
||||
const sidebar = document.querySelector('.sidebar-right');
|
||||
const topbar = document.querySelector('.topbar-right');
|
||||
const toggleBtn = document.getElementById('toggleSidebarRight');
|
||||
const mobileToggle = document.getElementById('mobileToogle');
|
||||
const hoverTargets = [sidebar, topbar];
|
||||
|
||||
// Ambil status awal dari localStorage
|
||||
const isCollapsedSaved = localStorage.getItem('sidebar-collapsed') === 'true';
|
||||
|
||||
// Terapkan hanya jika di desktop (window width > 992)
|
||||
if (isCollapsedSaved && window.innerWidth >= 992) {
|
||||
sidebar.classList.add('collapsed');
|
||||
}
|
||||
|
||||
hoverTargets.forEach(target => {
|
||||
if (!target) return;
|
||||
|
||||
target.addEventListener('mouseenter', () => {
|
||||
// Hanya aktifkan temp-expanded jika sidebar memang sedang collapsed
|
||||
if (sidebar.classList.contains('collapsed')) {
|
||||
sidebar.classList.remove('collapsed');
|
||||
sidebar.classList.add('temp-expanded');
|
||||
}
|
||||
});
|
||||
|
||||
target.addEventListener('mouseleave', () => {
|
||||
// Kembalikan ke collapsed hanya jika statusnya temp-expanded
|
||||
if (sidebar.classList.contains('temp-expanded')) {
|
||||
sidebar.classList.add('collapsed');
|
||||
sidebar.classList.remove('temp-expanded');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (toggleBtn && sidebar) {
|
||||
toggleBtn.addEventListener('click', (e) => {
|
||||
// Mencegah bubbling agar tidak terpicu dua kali
|
||||
e.stopPropagation();
|
||||
|
||||
if (sidebar.classList.contains('temp-expanded')) {
|
||||
// Jika sedang di-hover lalu diklik, hapus status 'temp'
|
||||
// agar sidebar tetap terbuka saat kursor keluar
|
||||
sidebar.classList.remove('temp-expanded');
|
||||
} else {
|
||||
// Kondisi klik normal (buka/tutup)
|
||||
sidebar.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
// --- SIMPAN KE LOCALSTORAGE ---
|
||||
// Kita simpan true jika class 'collapsed' ada, false jika tidak ada
|
||||
const isCurrentlyCollapsed = sidebar.classList.contains('collapsed');
|
||||
localStorage.setItem('sidebar-collapsed', isCurrentlyCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
if (mobileToggle && sidebar) {
|
||||
mobileToggle.addEventListener('click', function(e) {
|
||||
e.stopPropagation(); // Mencegah klik menyebar
|
||||
sidebar.classList.toggle('mobile-show');
|
||||
});
|
||||
|
||||
// Opsional: Klik di mana saja di luar sidebar untuk menutupnya
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!sidebar.contains(e.target) && sidebar.classList.contains('mobile-show')) {
|
||||
sidebar.classList.remove('mobile-show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fungsi untuk bersihkan class collapsed di mobile
|
||||
const cleanMobileSidebar = () => {
|
||||
if (!sidebar) return;
|
||||
|
||||
if (window.innerWidth < 992) {
|
||||
sidebar.classList.remove('collapsed');
|
||||
}
|
||||
};
|
||||
|
||||
// Jalankan saat pertama kali load
|
||||
cleanMobileSidebar();
|
||||
|
||||
// Jalankan juga saat layar di-resize (penting!)
|
||||
window.addEventListener('resize', cleanMobileSidebar);
|
||||
|
||||
// Hide Alert
|
||||
const alerts = document.querySelectorAll('.bz-alert');
|
||||
|
||||
alerts.forEach(alert => {
|
||||
setTimeout(() => {
|
||||
alert.classList.remove('show'); // fade out
|
||||
|
||||
alert.addEventListener('transitionend', () => {
|
||||
alert.remove(); // hapus dari DOM setelah animasi selesai
|
||||
});
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
// Tab pada mobile mode
|
||||
document.querySelectorAll('.js-tab-trigger').forEach(el => {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = this.dataset.tabTarget;
|
||||
|
||||
const desktopTrigger = document.querySelector(
|
||||
`[data-bs-toggle="tab"][data-bs-target="#${targetId}"]`
|
||||
);
|
||||
|
||||
if (desktopTrigger) {
|
||||
bootstrap.Tab.getOrCreateInstance(desktopTrigger).show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function adjustQuantity(button, change) {
|
||||
const container = button.closest('.quantity-control'); // cari parent terdekat
|
||||
if (!container) return;
|
||||
|
||||
const input = container.querySelector('.qty-input'); // ambil input di dalam container
|
||||
if (!input) return;
|
||||
|
||||
// ambil value, min, max dari HTML langsung
|
||||
let value = +input.value; // unary + → convert string ke number
|
||||
const min = +input.min; // ambil min dari HTML
|
||||
const max = +input.max; // ambil max dari HTML
|
||||
|
||||
value += change;
|
||||
|
||||
// jaga value tetap dalam batas min & max
|
||||
if (value < min) value = min;
|
||||
if (value > max) value = max;
|
||||
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
// File upload handling
|
||||
function handleFileUpload(input, previewId, inputId) {
|
||||
const previewContainer = document.getElementById(previewId);
|
||||
previewContainer.innerHTML = '';
|
||||
|
||||
if (input.files.length > 0) {
|
||||
previewContainer.style.display = 'block';
|
||||
|
||||
for (let i = 0; i < input.files.length; i++) {
|
||||
const file = input.files[i];
|
||||
const fileSize = formatFileSize(file.size);
|
||||
const fileExtension = getFileExtension(file.name);
|
||||
const fileIcon = getFileIcon(fileExtension);
|
||||
|
||||
const fileItem = document.createElement('div');
|
||||
fileItem.className = 'file-item';
|
||||
|
||||
fileItem.innerHTML = `
|
||||
<div class="file-info d-flex align-items-center p-2">
|
||||
<div class="icon-wrapper rounded-circle bg-dark bg-opacity-10
|
||||
d-flex justify-content-center align-items-center">
|
||||
<i class="${fileIcon} text-dark"></i>
|
||||
</div>
|
||||
|
||||
<div class="file-details">
|
||||
<div class="file-name fw-bold ">${file.name}</div>
|
||||
|
||||
<div class="file-size text-muted small">${fileSize}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="file-remove" onclick="showDeleteConfirmation('${inputId}', '${previewId}', ${i})">
|
||||
<i class="ri-close-line"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
previewContainer.appendChild(fileItem);
|
||||
}
|
||||
} else {
|
||||
previewContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function removeFile(inputId, previewId, index) {
|
||||
const input = document.getElementById(inputId);
|
||||
const dt = new DataTransfer();
|
||||
|
||||
for (let i = 0; i < input.files.length; i++) {
|
||||
if (i !== index) {
|
||||
dt.items.add(input.files[i]);
|
||||
}
|
||||
}
|
||||
|
||||
input.files = dt.files;
|
||||
|
||||
handleFileUpload(input, previewId, inputId);
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getFileExtension(filename) {
|
||||
return filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
||||
}
|
||||
|
||||
function getFileIcon(extension) {
|
||||
const icons = {
|
||||
'pdf': 'bi bi-file-earmark-pdf',
|
||||
'doc': 'bi bi-file-earmark-word',
|
||||
'docx': 'bi bi-file-earmark-word',
|
||||
'xls': 'bi bi-file-earmark-excel',
|
||||
'xlsx': 'bi bi-file-earmark-excel',
|
||||
'jpg': 'bi bi-file-earmark-image',
|
||||
'jpeg': 'bi bi-file-earmark-image',
|
||||
'png': 'bi bi-file-earmark-image',
|
||||
'gif': 'bi bi-file-earmark-image',
|
||||
'zip': 'bi bi-file-earmark-zip',
|
||||
'rar': 'bi bi-file-earmark-zip',
|
||||
'txt': 'bi bi-file-earmark-text',
|
||||
'csv': 'bi bi-filetype-csv'
|
||||
};
|
||||
|
||||
return icons[extension] || 'bi bi-file-earmark';
|
||||
}
|
||||
|
||||
// generate icon untuk file lama
|
||||
document.querySelectorAll('.icon-wrapper[data-extension]').forEach(wrapper => {
|
||||
const ext = wrapper.dataset.extension.toLowerCase();
|
||||
const iconClass = getFileIcon(ext);
|
||||
wrapper.innerHTML = `<i class="${iconClass} text-dark"></i>`;
|
||||
});
|
||||
|
||||
function showDeleteConfirmation(
|
||||
inputId,
|
||||
previewId,
|
||||
index,
|
||||
) {
|
||||
Swal.fire({
|
||||
html: '<p class="mb-0">Delete this file?</p>',
|
||||
showCancelButton: true,
|
||||
cancelButtonText: 'Cancel',
|
||||
confirmButtonText: 'Delete',
|
||||
reverseButtons: true,
|
||||
customClass: { popup: 'swal-minimal' }
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
/* 1. Ubah nilai hidden input agar server tahu harus hapus database
|
||||
const deleteInput = document.getElementById('delete_existing_file');
|
||||
if (deleteInput) {
|
||||
deleteInput.value = "true";
|
||||
} */
|
||||
|
||||
// 2. Jalankan fungsi visual yang sudah Anda buat
|
||||
removeFile(inputId, previewId, index);
|
||||
|
||||
/* 3. Hapus preview secara visual (front-end)
|
||||
const previewContainer = document.getElementById(previewId);
|
||||
if (previewContainer) {
|
||||
previewContainer.style.display = 'none';
|
||||
} */
|
||||
// hapus sessionStorage
|
||||
sessionStorage.removeItem('file_draft');
|
||||
|
||||
// BERSIHKAN URL (Mencegah "Resend" saat Reload)
|
||||
if (window.history.replaceState) {
|
||||
window.history.replaceState(null, null, window.location.href);
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
html: '<p class="mb-0">File has been removed.</p>',
|
||||
showConfirmButton: true,
|
||||
customClass: {
|
||||
popup: 'swal-minimal'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof flatpickr !== 'undefined' && $('.date').length) {
|
||||
flatpickr(".date", {
|
||||
// dateFormat: "d-m-Y",
|
||||
dateFormat: "d/m/Y H:i", // Ditambah H:i untuk jam dan menit
|
||||
enableTime: true,
|
||||
allowInput: true,
|
||||
// locale: "id",
|
||||
|
||||
onDayCreate: function(_, __, ___, dayElem) {
|
||||
const d = dayElem.dateObj;
|
||||
|
||||
if (
|
||||
dayElem.classList.contains("prevMonthDay") ||
|
||||
dayElem.classList.contains("nextMonthDay")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sunday (0)
|
||||
if (d.getDay() === 0) {
|
||||
dayElem.classList.add("sunday");
|
||||
}
|
||||
|
||||
// Friday (5)
|
||||
if (d.getDay() === 5) {
|
||||
dayElem.classList.add("friday");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user