|
class JVBAdmin {
|
constructor(){
|
this.queue = window.jvbQueue;
|
this.loading = window.jvbLoading;
|
this.cache = window.jvbCache;
|
this.a11y = window.jvbA11y;
|
this.error = window.jvbError;
|
this.activeTab = 'artist';
|
this.reset = false;
|
this.observer = null;
|
|
this.form = {};
|
this.isSaving = false;
|
|
|
this.hasChanges = false;
|
this.trackedChanges = new Map();
|
this.items = new Map();
|
|
this.isLoading = false;
|
|
this.tabNav = localStorage.getItem('jvbTabNav') === 'vertical';
|
|
this.template = new Map();
|
|
this.endpoints = 'myster';
|
|
this.resetFilters();
|
this.hasMore = true;
|
this.maxPages = 1;
|
this.totalItems = 0;
|
|
this.initElements();
|
this.initEvents();
|
|
this.firstLoad = false;
|
if(!this.firstLoad){
|
this.resetTable();
|
this.firstLoad = true;
|
}
|
|
// Define all your tabs in an array
|
let tabs = document.querySelectorAll('button.tab');
|
|
let tabsConfig = {};
|
this.modals = {};
|
tabs.forEach(tab =>{
|
let name = tab.dataset.tab;
|
tabsConfig[name] = () => {
|
if (tab.classList.contains('active')) {
|
this.activeTab = name;
|
}
|
this.loading.setContent([this.activeTab]);
|
this.resetTable();
|
this.resetFilters();
|
this.filters.content = name;
|
localStorage.setItem('jvbAdminTab', name);
|
this.loadItems(true).then(()=>{});
|
}
|
|
this.modals[name] = new window.jvbModal(document.querySelector(`dialog.edit-modal.${name}`, {
|
open: false,
|
onSave: () => this.saveEditModal.bind(this)
|
}));
|
|
this.items.set(name, new Map());
|
});
|
|
// Initialize tabs with the generated config
|
this.tabs = new window.jvbTabs(document.querySelector('.replace'), tabsConfig);
|
|
this.loading.setContent([this.activeTab]);
|
this.tabs.switchTab(this.activeTab);
|
this.loadItems();
|
|
this.saveTimeout = null;
|
this.SAVE_DELAY = 5000; // 5 seconds
|
|
// Bind the method to preserve 'this' context
|
this.debouncedSave = this.debouncedSave.bind(this);
|
}
|
|
resetFilters(){
|
this.filters = {
|
page: 1,
|
order: 'DESC',
|
orderby: 'name',
|
content: this.activeTab
|
}
|
}
|
|
resetTable(){
|
removeChildren(this.grid);
|
let table = window.getTemplate(`${this.activeTab}Table`).cloneNode(true);
|
|
this.row = `${this.activeTab}Row`;
|
|
let head = table.querySelector('thead').cloneNode(true);
|
let foot = table.querySelector('tfoot');
|
Array.from(head.children).forEach(child => {
|
foot.appendChild(child.cloneNode(true));
|
});
|
this.grid.append(table);
|
this.body = this.grid.querySelector('tbody');
|
|
this.grid.removeEventListener('change', this.boundChanges);
|
this.boundChanges = this.trackChanges.bind(this);
|
this.grid.addEventListener('change', this.boundChanges);
|
}
|
|
initElements(){
|
this.container = document.querySelector('.replace');
|
this.grid = this.container.querySelector('.items-container');
|
this.tabToggle = this.container.querySelector('input#vertical');
|
this.tabToggle.checked = this.tabNav;
|
|
// this.filterForm = this.container.querySelector('form');
|
// this.dateRangeFilter = new window.jvbModal(
|
// this.container.querySelector('dialog.date-range'),
|
// {
|
// open: false,
|
// });
|
// this.clearFilters = this.container.querySelector('.clear-filters');
|
//
|
// this.replyModal = new window.jvbModal(
|
// this.container.querySelector('.create-response'),
|
// {
|
// open: false,
|
// content: 'response',
|
// openMessage: 'Opened Response modal',
|
// onSave: this.saveCreatedResponse.bind(this)
|
// });
|
}
|
trackChanges(e) {
|
this.hasChanges = true;
|
let id = e.target.closest('tr').id;
|
if(!this.trackedChanges.has(id)){
|
this.trackedChanges.set(id, new Map());
|
}
|
let name = e.target.name;
|
let value = e.target.value;
|
this.trackedChanges.get(id).set(name, value);
|
|
// Schedule the save with debouncing
|
this.debouncedSave();
|
}
|
|
// Add this method to handle the debounced save
|
debouncedSave() {
|
// Clear any existing timeout
|
if (this.saveTimeout) {
|
clearTimeout(this.saveTimeout);
|
}
|
|
// Set a new timeout
|
this.saveTimeout = setTimeout(() => {
|
this.processChanges();
|
}, this.SAVE_DELAY);
|
}
|
|
async processChanges(){
|
if (this.trackedChanges.size === 0) return;
|
|
try {
|
|
this.loading.showLoading();
|
|
let changes = mapToObj(this.trackedChanges);
|
|
console.log("Saving changes:", changes);
|
|
// Send to server
|
const response = await fetch(`${jvbSettings.api}${this.endpoints}`, {
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': jvbSettings.nonce,
|
'action_nonce': jvbAdmin.nonce
|
},
|
body: JSON.stringify({
|
user: jvbSettings.currentUser,
|
data: changes,
|
content: this.activeTab
|
})
|
});
|
|
if (!response.ok) {
|
throw new Error(`Server returned ${response.status}`);
|
}
|
|
const data = await response.json();
|
|
|
if (data.success) {
|
this.reset = true;
|
// Clear tracked changes after successful save
|
this.trackedChanges = new Map();
|
this.hasChanges = false;
|
|
this.loadItems();
|
|
// Notify user of successful save
|
this.a11y.announce(`Changes saved successfully`);
|
|
} else {
|
throw new Error(data.message || 'Unknown error');
|
}
|
} catch (error) {
|
this.handleError(error, 'saving changes');
|
} finally {
|
this.loading.hideLoading();
|
}
|
}
|
|
initEvents(){
|
this.tabToggle.addEventListener('change', (e)=>{
|
this.tabNav = e.target.checked;
|
let value = e.target.checked ? 'vertical' : 'horizontal';
|
localStorage.setItem('jvbTabNav', value);
|
window.jvbA11y.announce((this.tabNav)?'Changed to vertical navigation':'Changed to horizontal navigation');
|
});
|
this.grid.addEventListener('keydown', (e)=>{
|
if (e.key === 'Tab' && this.tabNav) {
|
let current = e.target.closest('td').dataset.id;
|
let row = e.target.closest('tr');
|
let rows = Array.from(this.body.querySelectorAll('tr'));
|
let index = rows.indexOf(row);
|
|
let total = rows.length;
|
if (index !== -1 && index < total) {
|
e.preventDefault(); // Prevent default tab behavior
|
|
//down if just tab, up if shift key
|
let next = (e.shiftKey) ? index-1 : index+1;
|
rows[next].scrollIntoView({behavior: 'smooth',block: 'center', inline: 'center'});
|
rows[next].querySelector(`[data-id="${current}"] input`).focus();
|
}
|
if(index === total - 5 && this.hasMore){
|
this.loadItems(false);
|
}
|
}
|
});
|
|
this.clickListener = this.handleClick.bind(this);
|
this.container.addEventListener('click', this.clickListener);
|
}
|
|
handleClick(e){
|
if(e.target !== 'button' && !e.target.closest('button[data-action="edit"]')){
|
return;
|
}
|
let id = (e.target === 'button') ? e.target.dataset.id : e.target.closest('button').dataset.id;
|
|
let item = this.items.get(this.activeTab).get(parseInt(id));
|
|
let modal = this.container.querySelector(`dialog.edit-modal.${this.activeTab}`);
|
for(let[name, value] of Object.entries(item)){
|
let field = modal.querySelector(`[name="${name}"]`);
|
if(field){
|
field.value =value;
|
}
|
}
|
if(this.form.instance){
|
this.form.instance = null;
|
}
|
this.form.instance = this.handleForm(modal.querySelector('form'), id);
|
this.modals[this.activeTab].modal.dataset.id = item.id;
|
this.modals[this.activeTab].handleOpen();
|
}
|
|
handleForm(form, id){
|
return window.jvbForm.addForm(form, {
|
onSave: this.saveEditModal.bind(this),
|
itemID: id,
|
});
|
}
|
|
async saveEditModal(){
|
if(this.isSaving){
|
return;
|
}
|
this.isSaving = true;
|
let modal = this.modals[this.activeTab];
|
let form = modal.modal.querySelector('form');
|
let id = modal.modal.dataset.id;
|
|
|
let original = this.items.get(this.activeTab).get(parseInt(id));
|
let newData = new FormData(form);
|
console.log(newData);
|
|
let changes = {};
|
for(const [name, value] of newData.entries()){
|
if(name.includes(':')){
|
let split = name.split(':');
|
if(!changes[split[0]]){
|
changes[split[0]] = {};
|
}
|
changes[split[0]][split[1]] = value;
|
}else{
|
changes[name] = value;
|
}
|
|
}
|
console.log(changes);
|
newData = {};
|
newData[id] = changes;
|
|
try {
|
const response = await fetch(`${jvbSettings.api}${this.endpoints}`, {
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': jvbSettings.nonce,
|
'action_nonce': jvbAdmin.nonce
|
},
|
body: JSON.stringify({
|
user: jvbSettings.currentUser,
|
data: newData,
|
content: this.activeTab
|
})
|
});
|
|
} catch (error) {
|
|
} finally {
|
this.modals[this.activeTab].handleClose();
|
this.isSaving = false;
|
}
|
|
form.reset();
|
|
}
|
|
setupInfiniteScroll() {
|
// If we already have an observer, disconnect it first
|
if (this.observer) {
|
this.observer.disconnect();
|
}
|
|
// Find the last row in the tbody
|
const lastRow = this.body.lastElementChild;
|
if (!lastRow) return; // No rows to observe yet
|
|
// Create and set up the observer
|
this.observer = new IntersectionObserver((entries) => {
|
entries.forEach(entry => {
|
if (entry.isIntersecting && this.hasMore && !this.isLoading) {
|
console.log('Last row visible, loading more items');
|
this.loadItems(false);
|
}
|
});
|
}, {
|
rootMargin: '200px 0px', // Start loading when within 200px of the viewport
|
threshold: 0.1 // Trigger when at least 10% of the element is visible
|
});
|
|
// Start observing the last row
|
this.observer.observe(lastRow);
|
console.log('Observing last row:', lastRow);
|
}
|
|
/**
|
* Load favourites from the server
|
* @returns {Promise<Object>} Response data
|
*/
|
async loadItems(reset = true) {
|
|
if(this.isLoading || !this.hasMore) return;
|
|
try {
|
this.isLoading = true;
|
this.loading.showLoading();
|
|
if(reset){
|
this.filters.page = 1;
|
this.grid.classList.remove('empty');
|
}
|
const params = this.buildFilters();
|
console.log(this.filters);
|
console.log('Reset? ',this.reset);
|
const data = await this.cache.fetchWithCache(
|
`${jvbSettings.api}${this.endpoints}?${params.toString()}`,
|
{
|
method: 'GET',
|
headers: {
|
'X-WP-Nonce': jvbSettings.nonce,
|
'action_nonce': jvbAdmin.nonce,
|
}
|
},{
|
context: 'admin',
|
forceRefresh: true
|
}
|
);
|
|
console.log(data, 'Fetched Data:');
|
|
// Process and render the favourites
|
this.renderItems(data.items || [], this.filters.page > 1);
|
[
|
this.hasMore,
|
this.totalItems,
|
this.maxPages
|
] = [
|
data.has_more,
|
data.total_items,
|
data.total_pages
|
];
|
if(this.hasMore){
|
this.filters.page++;
|
}
|
|
this.setupInfiniteScroll();
|
|
return data;
|
} catch (error) {
|
this.handleError(error, 'loading news');
|
throw error;
|
} finally {
|
this.isLoading = false;
|
this.loading.hideLoading();
|
}
|
|
}
|
|
|
buildFilters(){
|
//Clone to avoid modifying original
|
const filters = JSON.parse(JSON.stringify(this.filters));
|
|
let temp = {};
|
for(var[name, value] of Object.entries(filters)){
|
if(value !== false && value !== null){
|
temp[name] = value;
|
}
|
}
|
|
return new URLSearchParams(temp);
|
}
|
|
renderItems(items, append = false){
|
// if(!append && this.body.children.length>0){
|
// removeChildren(this.body);
|
// }
|
|
// Use DocumentFragment for better performance
|
const fragment = document.createDocumentFragment();
|
|
// Process items in batches for better performance
|
const batchSize = 10;
|
const processBatch = (startIndex) => {
|
const endIndex = Math.min(startIndex + batchSize, items.length);
|
|
// Process this batch
|
for (let i = startIndex; i < endIndex; i++) {
|
const item = items[i];
|
const element = this.createItemElement(item);
|
this.items.get(this.activeTab).set(item.id, item);
|
fragment.appendChild(element);
|
}
|
|
// If we have more items, process next batch in next frame
|
if (endIndex < items.length) {
|
requestAnimationFrame(() => {
|
processBatch(endIndex);
|
});
|
} else {
|
// All batches processed, append fragment
|
this.body.appendChild(fragment);
|
this.a11y.makeNavigable(this.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
|
this.a11y.announceItems(items.length, append, this.hasMore);
|
|
// Set up the observer for the new last row
|
this.setupInfiniteScroll();
|
}
|
};
|
|
// Start processing the first batch
|
if (items.length > 0) {
|
processBatch(0);
|
|
} else {
|
this.a11y.announceItems(0, append);
|
}
|
}
|
|
createItemElement(item){
|
let row = window.getTemplate(this.row);
|
row.id = item.id;
|
|
let fields = row.querySelectorAll('td');
|
fields.forEach(field => {
|
let name = field.dataset.id;
|
let input = field.querySelector('input[type="text"]');
|
if(input){
|
input.value = item[name];
|
field.querySelector('label').remove();
|
field.querySelector('.description').remove();
|
}
|
});
|
|
let [
|
input,
|
inputLabel,
|
editButton
|
] = [
|
row.querySelector('[data-id="actions"] input'),
|
row.querySelector('[data-id="actions"] label'),
|
row.querySelector('[data-id="actions"] button'),
|
];
|
|
[
|
input.checked,
|
input.id,
|
inputLabel.htmlFor,
|
editButton.dataset.id
|
] = [
|
item.public,
|
`public-${item.id}`,
|
`public-${item.id}`,
|
item.id
|
];
|
|
return row;
|
}
|
createShopElement(item){
|
let row = window.getTemplate(this.row);
|
row.id = item.id;
|
|
let [
|
input,
|
inputLabel,
|
editButton,
|
shopName,
|
owner,
|
managers,
|
city,
|
address,
|
opened,
|
phone,
|
email,
|
admin,
|
publicContact,
|
links,
|
rate,
|
languages,
|
keywords,
|
tagline,
|
instagram,
|
socialFollowers
|
] = [
|
row.querySelector('[data-id="actions"] input'),
|
row.querySelector('[data-id="actions"] label'),
|
row.querySelector('[data-id="actions"] button'),
|
row.querySelector('[data-id="term_name"] input'),
|
row.querySelector('[data-id="owner"] input'),
|
row.querySelector('[data-id="managers"] input'),
|
row.querySelector('[data-id="city"] input'),
|
row.querySelector('[data-id="location"] input'),
|
row.querySelector('[data-id="established"] input'),
|
row.querySelector('[data-id="phone"] input'),
|
row.querySelector('[data-id="email"] input'),
|
row.querySelector('[data-id="admin_contact"] input'),
|
row.querySelector('[data-id="public_contact"] input'),
|
row.querySelector('[data-id="links"] input'),
|
row.querySelector('[data-id="rate"] input'),
|
row.querySelector('[data-id="languages"] input'),
|
row.querySelector('[data-id="keywords"] input'),
|
row.querySelector('[data-id="slogan"] input'),
|
row.querySelector('[data-id="insta_handle"] input'),
|
row.querySelector('[data-id="followers"] input'),
|
];
|
|
[
|
input.checked,
|
input.id,
|
inputLabel.htmlFor,
|
editButton.dataset.id,
|
shopName.value,
|
owner.value,
|
managers.value,
|
city.value,
|
address.value,
|
opened.value,
|
phone.value,
|
email.value,
|
admin.value,
|
publicContact.value,
|
rate.value,
|
tagline.value,
|
instagram.value
|
] = [
|
item.public,
|
`public-${item.id}`,
|
`public-${item.id}`,
|
item.id,
|
item.term_name,
|
item.owner,
|
item.managers,
|
item.city,
|
item.location.address,
|
item.established,
|
item.phone,
|
item.email,
|
item.admin_contact,
|
item.public_contact,
|
item.rate,
|
item.slogan,
|
item.insta_handle,
|
];
|
|
if(item.links.length>0){
|
let l = '';
|
item.links.forEach(li => {
|
l += `[${li.url}, ${li.title}, ${li.tracker}]`;
|
});
|
links.value = l.trim();
|
}
|
|
if(item.followers.length>0){
|
let l = '';
|
item.followers.forEach(f=>{
|
l+= `[${f.count}, ${f.source}, ${formatDate(f.checked)}]`;
|
});
|
socialFollowers.value = l;
|
}
|
|
|
if(item.keywords.length > 0){
|
let l = [];
|
item.keywords.forEach(keyword => {
|
l.push(keyword.keyword);
|
});
|
keywords.value =l.join(', ');
|
}
|
if(item.languages.length > 0){
|
let l = [];
|
item.languages.forEach(language => {
|
l.push(language.language);
|
});
|
languages.value =l.join(', ');
|
}
|
|
return row;
|
}
|
|
createPartnerElement(item){
|
let row = window.getTemplate(this.row);
|
row.id = item.id;
|
|
let [
|
input,
|
inputLabel,
|
editButton
|
] = [
|
row.querySelector('[data-id="actions"] input'),
|
row.querySelector('[data-id="actions"] label'),
|
row.querySelector('[data-id="actions"] button'),
|
];
|
|
[
|
input.checked,
|
input.id,
|
inputLabel.htmlFor,
|
editButton.dataset.id
|
] = [
|
item.public,
|
`public-${item.id}`,
|
`public-${item.id}`,
|
item.id
|
];
|
|
return row;
|
}
|
|
|
createStyleElement(item){
|
let row = window.getTemplate(this.row);
|
row.id = item.id;
|
|
let fields = row.querySelectorAll('td');
|
fields.forEach(field => {
|
let name = field.dataset.id;
|
// field.label.remove();
|
// field.querySelector('.description').remove();
|
let input = field.querySelector('input[type="text"]');
|
if(input){
|
input.value = item[name];
|
}
|
});
|
|
let [
|
input,
|
inputLabel,
|
editButton
|
] = [
|
row.querySelector('[data-id="actions"] input'),
|
row.querySelector('[data-id="actions"] label'),
|
row.querySelector('[data-id="actions"] button'),
|
];
|
|
[
|
input.checked,
|
input.id,
|
inputLabel.htmlFor,
|
editButton.dataset.id
|
] = [
|
item.public,
|
`public-${item.id}`,
|
`public-${item.id}`,
|
item.id
|
];
|
|
return row;
|
}
|
createArtistElement(item){
|
|
let row = window.getTemplate(this.row);
|
row.id = item.id;
|
let [
|
input,
|
inputLabel,
|
editButton,
|
display,
|
first,
|
phone,
|
email,
|
links,
|
admin,
|
publicContact,
|
socialFollowers,
|
instagram,
|
type,
|
city,
|
shop,
|
rate,
|
language,
|
keywords,
|
credential
|
] = [
|
row.querySelector('[data-id="actions"] input'),
|
row.querySelector('[data-id="actions"] label'),
|
row.querySelector('[data-id="actions"] button'),
|
row.querySelector('[data-id="display_name"] input'),
|
row.querySelector('[data-id="first_name"] input'),
|
row.querySelector('[data-id="phone"] input'),
|
row.querySelector('[data-id="email"] input'),
|
row.querySelector('[data-id="links"] input'),
|
row.querySelector('[data-id="admin_contact"] input'),
|
row.querySelector('[data-id="public_contact"] input'),
|
row.querySelector('[data-id="followers"] input'),
|
row.querySelector('[data-id="insta_handle"] input'),
|
row.querySelector('[data-id="type"] input'),
|
row.querySelector('[data-id="city"] input'),
|
row.querySelector('[data-id="shop"] input'),
|
row.querySelector('[data-id="rate"] input'),
|
row.querySelector('[data-id="languages"] input'),
|
row.querySelector('[data-id="keywords"] input'),
|
row.querySelector('[data-id="credentials"] input')
|
|
];
|
|
[
|
input.checked,
|
input.id,
|
inputLabel.htmlFor,
|
editButton.dataset.id,
|
display.value,
|
first.value,
|
phone.value,
|
email.value,
|
admin.value,
|
publicContact.value,
|
type.value,
|
city.value,
|
shop.value,
|
instagram.value,
|
rate.value,
|
] = [
|
item.public,
|
`public-${item.id}`,
|
`public-${item.id}`,
|
item.id,
|
item['display_name'],
|
item['first_name'],
|
item.phone,
|
item.email,
|
item.admin_contact,
|
item.public_contact,
|
item.type,
|
item.city,
|
item.shop,
|
item['insta_handle'],
|
item.rate,
|
];
|
|
if(item.links.length>0){
|
let l = '';
|
item.links.forEach(li => {
|
l += `[${li.url}, ${li.title}, ${li.tracker}]`;
|
});
|
links.value = l.trim();
|
}
|
|
if(item.followers.length>0){
|
let l = '';
|
item.followers.forEach(f=>{
|
l+= `[${f.count}, ${f.source}, ${formatDate(f.checked)}]`;
|
});
|
socialFollowers.value = l;
|
}
|
|
if(item.keywords.length > 0){
|
let l = [];
|
item.keywords.forEach(keyword => {
|
l.push(keyword.keyword);
|
});
|
keywords.value =l.join(', ');
|
}
|
if(item.languages.length > 0){
|
let l = [];
|
item.languages.forEach(language => {
|
l.push(language.language);
|
});
|
language.value =l.join(', ');
|
}
|
return row;
|
}
|
|
/**
|
* Handle errors
|
* @param {Error} error - Error object
|
* @param {string} action - Action being performed when error occurred
|
*/
|
handleError(error, action) {
|
console.error(`News error (${action}):`, error);
|
|
// Log with error handler if available
|
if (window.jvbError) {
|
window.jvbError.log(error, {
|
component: 'Admin',
|
action: action
|
});
|
}
|
|
// Announce to screen readers
|
if (window.jvbA11y) {
|
window.jvbA11y.announce(`Error ${action}. ${error.message || 'Please try again.'}`);
|
}
|
}
|
|
|
}
|
// Initialize when DOM is ready
|
document.addEventListener('DOMContentLoaded', () => {
|
new JVBAdmin();
|
});
|
|
function mapToObj(map){
|
return Array.from(map).reduce((obj, [key, value]) => {
|
if(value instanceof Map){
|
value = mapToObj(value);
|
}
|
obj[key] = value;
|
return obj;
|
}, {});
|
}
|