All checks were successful
Build and Push Docker Image / build (push) Successful in 51s
- Implemented `Favorite` model with unique user-item constraint and timestamped records - Added `favorites:index` and `favorites:toggle` views with AJAX support for toggling favorites - Built templates and front-end assets for the favorites page and integration with item grids - Extended header with favorites link and dynamic counter - Added tests covering models, views, and templates for the favorites functionality - Updated admin to enable management of favorites via Django admin interface
115 lines
3.9 KiB
JavaScript
115 lines
3.9 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function () {
|
||
const favoriteForms = document.querySelectorAll('[data-favorite-form]');
|
||
|
||
const pluralizeFavorites = function (count) {
|
||
const value = Math.abs(Number(count) || 0);
|
||
const lastTwo = value % 100;
|
||
if (lastTwo >= 11 && lastTwo <= 14) {
|
||
return 'товаров';
|
||
}
|
||
|
||
switch (value % 10) {
|
||
case 1:
|
||
return 'товар';
|
||
case 2:
|
||
case 3:
|
||
case 4:
|
||
return 'товара';
|
||
default:
|
||
return 'товаров';
|
||
}
|
||
};
|
||
|
||
const updateFavoritesCounter = function (favoriteCount) {
|
||
const valueNode = document.querySelector('[data-favorites-count-value]');
|
||
const labelNode = document.querySelector('[data-favorites-count-label]');
|
||
|
||
if (valueNode) {
|
||
valueNode.textContent = String(favoriteCount);
|
||
}
|
||
|
||
if (labelNode) {
|
||
labelNode.textContent = pluralizeFavorites(favoriteCount);
|
||
}
|
||
};
|
||
|
||
const ensureEmptyFavoritesState = function () {
|
||
const grid = document.querySelector('[data-favorites-grid]');
|
||
if (!grid) {
|
||
return;
|
||
}
|
||
|
||
const cards = grid.querySelectorAll('[data-item-card]');
|
||
if (cards.length > 0) {
|
||
return;
|
||
}
|
||
|
||
const emptyMessage = grid.dataset.emptyMessage || 'В избранном пока нет товаров.';
|
||
grid.innerHTML = '<p class="item-list-empty">' + emptyMessage + '</p>';
|
||
};
|
||
|
||
const syncButtonState = function (button, payload) {
|
||
const isFavorite = Boolean(payload.is_favorite);
|
||
const addLabel = button.dataset.labelAdd || 'Добавить в избранное';
|
||
const removeLabel = button.dataset.labelRemove || 'Удалить из избранного';
|
||
const label = isFavorite ? removeLabel : addLabel;
|
||
|
||
button.classList.toggle('is-active', isFavorite);
|
||
button.setAttribute('title', label);
|
||
button.setAttribute('aria-label', label);
|
||
};
|
||
|
||
favoriteForms.forEach(function (form) {
|
||
form.addEventListener('submit', async function (event) {
|
||
event.preventDefault();
|
||
|
||
const button = form.querySelector('[data-favorite-button]');
|
||
if (!button || form.dataset.loading === '1') {
|
||
return;
|
||
}
|
||
|
||
form.dataset.loading = '1';
|
||
button.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch(form.action, {
|
||
method: 'POST',
|
||
body: new FormData(form),
|
||
headers: {
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: 'same-origin',
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to update favorites');
|
||
}
|
||
|
||
const payload = await response.json();
|
||
const card = form.closest('[data-item-card]');
|
||
const favoritesPage = Boolean(document.querySelector('[data-favorites-page]'));
|
||
|
||
syncButtonState(button, payload);
|
||
updateFavoritesCounter(payload.favorite_count);
|
||
|
||
if (card) {
|
||
card.classList.toggle('is-favorite', Boolean(payload.is_favorite));
|
||
}
|
||
|
||
if (favoritesPage && !payload.is_favorite && card) {
|
||
card.remove();
|
||
ensureEmptyFavoritesState();
|
||
}
|
||
} catch (error) {
|
||
console.error(error);
|
||
HTMLFormElement.prototype.submit.call(form);
|
||
return;
|
||
} finally {
|
||
form.dataset.loading = '0';
|
||
button.disabled = false;
|
||
}
|
||
});
|
||
});
|
||
});
|