diff --git a/tienda/static/css/custom.css b/tienda/static/css/custom.css index eaac49c..56a7bce 100644 --- a/tienda/static/css/custom.css +++ b/tienda/static/css/custom.css @@ -1,3 +1,28 @@ +.skip-link { + position: fixed; + top: -100%; + left: 50%; + transform: translateX(-50%); + background: #fff; + color: #513CB0; + padding: 8px 24px; + font-weight: 700; + font-size: 0.9rem; + z-index: 10001; + text-decoration: none; + border-radius: 0 0 8px 8px; + border: 2px solid #513CB0; + border-top: none; + box-shadow: 0 4px 12px rgba(81, 60, 176, 0.25); + transition: top 0.2s ease; + outline: none; + white-space: nowrap; +} +.skip-link:focus, +.skip-link:focus-visible { + top: 0; +} + @media (min-width: 1250px) { .grid { display: grid; diff --git a/tienda/templates/tienda/base.html b/tienda/templates/tienda/base.html index f4d6708..8cc1491 100644 --- a/tienda/templates/tienda/base.html +++ b/tienda/templates/tienda/base.html @@ -50,8 +50,11 @@ transition: background-color 0.2s; } - .search-suggestion-item:hover { + .search-suggestion-item:hover, + .search-suggestion-item.active { background-color: #f8f9fa; + outline: 2px solid #0d6efd; + outline-offset: -2px; } .search-suggestion-item:last-child { @@ -78,6 +81,7 @@ {% block head %}{% endblock %} + {% cache 500 sidebar request.user.username %} {% endcache %} -
+
{% if messages %}
@@ -201,6 +205,35 @@ const searchSuggestions = document.getElementById('searchSuggestions'); const searchForm = document.getElementById('searchForm'); let searchTimeout; + let currentFocusIndex = -1; + + // Helpers para gestionar el estado ARIA del combobox + function openSuggestions() { + searchSuggestions.classList.add('show'); + searchInput.setAttribute('aria-expanded', 'true'); + } + + function closeSuggestions() { + searchSuggestions.classList.remove('show'); + searchInput.setAttribute('aria-expanded', 'false'); + searchInput.setAttribute('aria-activedescendant', ''); + currentFocusIndex = -1; + } + + function updateFocus(options) { + options.forEach((option, index) => { + const active = index === currentFocusIndex; + option.classList.toggle('active', active); + option.setAttribute('aria-selected', active ? 'true' : 'false'); + }); + if (currentFocusIndex >= 0) { + const activeOption = options[currentFocusIndex]; + searchInput.setAttribute('aria-activedescendant', activeOption.id); + activeOption.scrollIntoView({ block: 'nearest' }); + } else { + searchInput.setAttribute('aria-activedescendant', ''); + } + } // Escuchar cambios en el input searchInput.addEventListener('input', function() { @@ -208,7 +241,7 @@ const query = this.value.trim(); if (query.length < 2) { - searchSuggestions.classList.remove('show'); + closeSuggestions(); return; } @@ -218,6 +251,31 @@ }, 300); }); + // Navegación por teclado (ArrowDown/ArrowUp/Enter/Escape) + searchInput.addEventListener('keydown', function(event) { + const options = searchSuggestions.querySelectorAll('[role="option"]'); + if (!options.length || !searchSuggestions.classList.contains('show')) { + return; + } + + if (event.key === 'ArrowDown') { + event.preventDefault(); + currentFocusIndex = Math.min(currentFocusIndex + 1, options.length - 1); + updateFocus(options); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + currentFocusIndex = Math.max(currentFocusIndex - 1, -1); + updateFocus(options); + } else if (event.key === 'Enter' && currentFocusIndex >= 0) { + event.preventDefault(); + const selected = options[currentFocusIndex]; + window.location.href = selected.dataset.href; + } else if (event.key === 'Escape') { + closeSuggestions(); + searchInput.focus(); + } + }); + // Función para obtener sugerencias del servidor function fetchSuggestions(query) { fetch(`{% url 'search_suggestions' %}?q=${encodeURIComponent(query)}`) @@ -227,33 +285,37 @@ }) .catch(error => { console.error('Error fetching suggestions:', error); - searchSuggestions.classList.remove('show'); + closeSuggestions(); }); } // Función para mostrar las sugerencias function displaySuggestions(suggestions, query) { + currentFocusIndex = -1; if (suggestions.length === 0) { searchSuggestions.innerHTML = '
No se encontraron productos
'; - searchSuggestions.classList.add('show'); + openSuggestions(); return; } let html = ''; - suggestions.forEach(suggestion => { + suggestions.forEach((suggestion, index) => { // Resaltar la coincidencia en el nombre const highlightedName = highlightMatch(suggestion.name, query); const priceWithVAT = (suggestion.price * 1.21).toFixed(2); html += ` - + `; }); searchSuggestions.innerHTML = html; - searchSuggestions.classList.add('show'); + openSuggestions(); } // Función para resaltar el texto que coincide @@ -265,19 +327,19 @@ // Cerrar sugerencias cuando se hace clic fuera document.addEventListener('click', function(event) { if (!searchForm.contains(event.target)) { - searchSuggestions.classList.remove('show'); + closeSuggestions(); } }); // Cerrar sugerencias al enviar el formulario searchForm.addEventListener('submit', function() { - searchSuggestions.classList.remove('show'); + closeSuggestions(); }); // Mostrar sugerencias al hacer clic en el input (si hay texto) searchInput.addEventListener('focus', function() { if (this.value.trim().length >= 2 && searchSuggestions.innerHTML) { - searchSuggestions.classList.add('show'); + openSuggestions(); } }); diff --git a/tienda/templates/tienda/cart.html b/tienda/templates/tienda/cart.html index bcde235..5f2bd7b 100644 --- a/tienda/templates/tienda/cart.html +++ b/tienda/templates/tienda/cart.html @@ -51,7 +51,7 @@
{% csrf_token %} - +
@@ -59,7 +59,7 @@ {% if item.product.stock > 0 %} {{ item.product.stock }} {% else %} - 0 + Sin stock {% endif %} {{ item.get_subtotal_with_vat|format_price }} € @@ -89,7 +89,7 @@
IVA (21%) - {{ cart.get_vat_amount|format_price }} € + {{ cart.get_vat_amount|format_price }} €
Envío diff --git a/tienda/templates/tienda/checkout.html b/tienda/templates/tienda/checkout.html index 1b88ee9..b1135e5 100644 --- a/tienda/templates/tienda/checkout.html +++ b/tienda/templates/tienda/checkout.html @@ -127,22 +127,25 @@ -
+
{% if saved_cards %} -
-

Tarjetas guardadas:

+
+ Selección de tarjeta {% for card in saved_cards %}
@@ -156,7 +159,7 @@
-
+ {% endif %}
@@ -183,7 +186,8 @@
-
+
{% if saved_paypal %}
Cuenta PayPal guardada: @@ -196,6 +200,7 @@ Guardar esta cuenta de PayPal para futuras compras
+
{% if not addresses or stock_issues %}
Selecciona una dirección de envío válida para activar el pago.
@@ -221,12 +226,42 @@ const HAS_STOCK_ISSUES = {{ stock_issues|yesno:"true,false" }}; const HAS_ADDRESS = {{ addresses|yesno:"true,false" }}; // ---- Tab switching ---- -document.querySelectorAll('#paymentTabs .nav-link').forEach(btn => { - btn.addEventListener('click', () => { - document.querySelectorAll('#paymentTabs .nav-link').forEach(b => b.classList.remove('active')); - document.querySelectorAll('.payment-tab-content').forEach(p => p.classList.remove('active')); - btn.classList.add('active'); - document.getElementById(btn.dataset.tab).classList.add('active'); +const paymentTabs = Array.from(document.querySelectorAll('#paymentTabs .nav-link[role="tab"]')); + +function activateTab(tab) { + paymentTabs.forEach(b => { + const isSelected = b === tab; + b.classList.toggle('active', isSelected); + b.setAttribute('aria-selected', isSelected ? 'true' : 'false'); + b.setAttribute('tabindex', isSelected ? '0' : '-1'); + }); + document.querySelectorAll('.payment-tab-content').forEach(p => p.classList.remove('active')); + document.getElementById(tab.dataset.tab).classList.add('active'); +} + +paymentTabs.forEach(btn => { + btn.addEventListener('click', () => activateTab(btn)); + btn.addEventListener('keydown', e => { + const idx = paymentTabs.indexOf(e.currentTarget); + if (e.key === 'ArrowRight') { + e.preventDefault(); + const next = paymentTabs[(idx + 1) % paymentTabs.length]; + activateTab(next); + next.focus(); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + const prev = paymentTabs[(idx - 1 + paymentTabs.length) % paymentTabs.length]; + activateTab(prev); + prev.focus(); + } else if (e.key === 'Home') { + e.preventDefault(); + activateTab(paymentTabs[0]); + paymentTabs[0].focus(); + } else if (e.key === 'End') { + e.preventDefault(); + activateTab(paymentTabs[paymentTabs.length - 1]); + paymentTabs[paymentTabs.length - 1].focus(); + } }); }); @@ -254,10 +289,13 @@ document.getElementById('pay-card-btn').addEventListener('click', async () => { const addressId = document.getElementById('shipping-address').value; if (!addressId) { - alert('Selecciona una dirección de envío para continuar.'); + const cardErrors = document.getElementById('card-errors'); + if (cardErrors) cardErrors.textContent = 'Selecciona una dirección de envío para continuar.'; return; } + const cardErrorsEl = document.getElementById('card-errors'); + if (cardErrorsEl) cardErrorsEl.textContent = ''; const btn = document.getElementById('pay-card-btn'); const spinner = document.getElementById('card-spinner'); btn.disabled = true; @@ -335,9 +373,15 @@ paypal.Buttons({ createOrder: async () => { const addressId = document.getElementById('shipping-address').value; if (!addressId) { - alert('Selecciona una dirección de envío para continuar.'); + const paypalErrors = document.getElementById('paypal-errors'); + if (paypalErrors) { + paypalErrors.textContent = 'Selecciona una dirección de envío para continuar.'; + paypalErrors.classList.remove('d-none'); + } return Promise.reject(new Error('Sin dirección')); } + const paypalErrorsEl = document.getElementById('paypal-errors'); + if (paypalErrorsEl) paypalErrorsEl.classList.add('d-none'); const resp = await fetch('{% url "crear_orden_paypal" %}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN }, @@ -359,7 +403,11 @@ paypal.Buttons({ showSuccess(result.transaction_code); }, onError: (err) => { - alert('Error en el pago con PayPal: ' + err); + const paypalErrors = document.getElementById('paypal-errors'); + if (paypalErrors) { + paypalErrors.textContent = 'Error en el pago con PayPal: ' + err; + paypalErrors.classList.remove('d-none'); + } }, }).render('#paypal-button-container'); {% endif %} diff --git a/tienda/templates/tienda/home.html b/tienda/templates/tienda/home.html index 4465721..3e28fb3 100644 --- a/tienda/templates/tienda/home.html +++ b/tienda/templates/tienda/home.html @@ -196,9 +196,11 @@ Ver detalles - - 🛒 - +
+ {% csrf_token %} + + +
diff --git a/tienda/templates/tienda/producto.html b/tienda/templates/tienda/producto.html index fca8dd8..117aa9f 100644 --- a/tienda/templates/tienda/producto.html +++ b/tienda/templates/tienda/producto.html @@ -34,16 +34,16 @@
Precio total (IVA incluido):
€{{ product.get_price_with_vat|format_price }} -
IVA: €{{ product.get_vat_amount|format_price }}
+
IVA incluido: €{{ product.get_vat_amount|format_price }}
{{ product.briefdesc }}
{% if product.stock > 0 %} - Stock disponible: {{ product.stock }} + Stock disponible: {{ product.stock }} {% else %} - Sin stock + Sin stock {% endif %}
diff --git a/tienda/templates/tienda/register.html b/tienda/templates/tienda/register.html index ce794d8..c648827 100644 --- a/tienda/templates/tienda/register.html +++ b/tienda/templates/tienda/register.html @@ -36,7 +36,7 @@ diff --git a/tienda/templates/tienda/search.html b/tienda/templates/tienda/search.html index 3641bdd..7fc8385 100644 --- a/tienda/templates/tienda/search.html +++ b/tienda/templates/tienda/search.html @@ -79,9 +79,11 @@ Ver detalles - - 🛒 - +
+ {% csrf_token %} + + +
diff --git a/tienda/views.py b/tienda/views.py index a44db6b..b5af383 100644 --- a/tienda/views.py +++ b/tienda/views.py @@ -16,6 +16,7 @@ from .vars import ( ) from django.conf import settings from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST from django.urls import reverse from django.utils import timezone from decimal import Decimal, ROUND_HALF_UP @@ -704,6 +705,7 @@ def create_order_from_cart(request, payment_method, payment_reference="", shippi return order, "" +@require_POST def add_to_cart(request: HttpRequest, product_id: int): """Agrega un producto al carrito""" try: