Files
proyecto-final/tienda/templates/tienda/base.html
T
2026-05-15 12:35:23 +02:00

371 lines
16 KiB
HTML

{% load static %}
{% load cache %}
{% load compress %}
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Sitio web de comercio local Almeriense">
<title>Comercialmeria</title>
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
</noscript>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link rel="preload" href="{% static 'css/custom.css' %}" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{% static 'css/custom.css' %}"></noscript>
<style>
.search-suggestions-container {
position: relative;
flex-grow: 1;
margin: 0 12px;
}
.search-suggestions {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #dee2e6;
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 400px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.search-suggestions.show {
display: block;
}
.search-suggestion-item {
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s;
}
.search-suggestion-item:hover,
.search-suggestion-item.active {
background-color: #f8f9fa;
outline: 2px solid #0d6efd;
outline-offset: -2px;
}
.search-suggestion-item:last-child {
border-bottom: none;
}
.suggestion-name {
font-weight: 500;
color: #212529;
display: block;
margin-bottom: 4px;
}
.suggestion-price {
font-size: 0.875rem;
color: #6c757d;
}
.search-highlight {
font-weight: 600;
color: #0d6efd;
}
</style>
{% block head %}{% endblock %}
</head>
<body class="d-flex flex-column min-vh-100">
<a href="#main-content" class="skip-link">Saltar al contenido</a>
{% cache 500 sidebar request.user.username %}
<nav class="navbar navbar-expand-md header" role="banner">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'home' %}">
<picture>
<source
type="image/webp"
srcset="{% static 'img/logo-40.webp' %} 1x, {% static 'img/logo-80.webp' %} 2x">
<img
src="{% static 'img/logo-40.png' %}"
srcset="{% static 'img/logo-40.png' %} 1x, {% static 'img/logo-80.png' %} 2x"
alt="Logo"
width="40"
height="40"
decoding="async">
</picture>
</a>
<span class="navbar-text fw-semibold site-title-mobile d-md-none">Comercialmeria</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<span class="navbar-text fw-semibold me-3 d-none d-md-inline site-title-desktop" style="color: #ffffff">Comercialmeria</span>
<!-- Barra de búsqueda con sugerencias -->
<form class="search-suggestions-container" method="GET" action="{% url 'search' %}" role="search" id="searchForm">
<div class="input-group">
<input class="form-control" type="search" name="q" id="searchInput" placeholder="Buscar productos..." aria-label="Buscar" autocomplete="off" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-controls="searchSuggestions" aria-activedescendant="" aria-haspopup="listbox">
<button class="btn btn-outline-primary" type="submit" aria-label="Buscar productos">🔍 Buscar</button>
</div>
<div class="search-suggestions" id="searchSuggestions" role="listbox" aria-label="Sugerencias de búsqueda"></div>
</form>
<div class="navbar-nav ms-auto d-flex align-items-md-center gap-2 flex-wrap" role="navigation">
<a href="{% url 'view_cart' %}" class="nav-link position-relative btn btn-outline-primary btn-sm">
🛒 Carrito
{% if cart_count > 0 %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.65rem;">
{{ cart_count }}
</span>
{% endif %}
</a>
{% if user.is_authenticated %}
<a href="{% url 'mis_productos' %}" class="nav-link btn btn-outline-secondary btn-sm">Panel Vendedor</a>
<a href="{% url 'portal_usuario' %}" class="nav-link btn btn-outline-light btn-sm">{{ user.first_name|default:user.username }}</a>
<a href="{% url 'logout' %}" class="nav-link btn btn-primary btn-sm">Cerrar Sesión</a>
{% else %}
<a href="{% url 'login' %}" class="nav-link btn btn-primary btn-sm">Iniciar Sesión</a>
<a href="{% url 'register' %}" class="nav-link btn btn-outline-primary btn-sm">Registrarse</a>
{% endif %}
</div>
</div>
</div>
</nav>
{% endcache %}
<div id="main-content" class="container-fluid flex-grow-1 d-flex flex-column" role="main">
<!-- Mensajes -->
{% if messages %}
<div class="row mt-3">
<div class="col-12">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Contenido-->
{% block content %}{% endblock %}
{% cache 500 footer %}
<!-- Footer-->
<footer id="footer" class="row pt-4 pb-3 mt-auto" role="contentinfo">
<div class="col-12">
<div class="row text-center gy-3">
<div class="col-12 col-md-4">
<h6 class="fw-semibold text-uppercase mb-2">Información Legal</h6>
<ul class="list-unstyled mb-0">
<li><a href="{% url 'privacidad' %}" class="footer-link">Política de Privacidad</a></li>
<li><a href="{% url 'aviso_legal' %}" class="footer-link">Aviso Legal</a></li>
<li><a href="{% url 'terminos' %}" class="footer-link">Términos y Condiciones</a></li>
<li><a href="{% url 'cookies' %}" class="footer-link">Política de Cookies</a></li>
</ul>
</div>
<div class="col-12 col-md-4">
<h6 class="fw-semibold text-uppercase mb-2">Compras</h6>
<ul class="list-unstyled mb-0">
<li><a href="{% url 'devoluciones' %}" class="footer-link">Política de Devoluciones</a></li>
<li><a href="{% url 'ayuda' %}" class="footer-link">Centro de Ayuda</a></li>
<li><a href="{% url 'ayuda' %}#envios" class="footer-link">Información de Envíos</a></li>
</ul>
</div>
<div class="col-12 col-md-4">
<h6 class="fw-semibold text-uppercase mb-2">Comercialmeria</h6>
<ul class="list-unstyled mb-0">
<li><a href="{% url 'sobre_nosotros' %}" class="footer-link">Sobre Nosotros</a></li>
<li><a href="{% url 'mis_productos' %}" class="footer-link">Vende con Nosotros</a></li>
<li><a href="mailto:example@example.com" class="footer-link">Contacto</a></li>
</ul>
</div>
</div>
<hr class="mt-3 mb-2">
<p class="text-center mb-0" style="font-size: 0.85rem;">&copy; 2026 Comercialmeria — Comercio local en Almería, España</p>
</div>
</footer>
{% endcache %}
</div>
{% cache 500 scripts %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script>
// Sistema de sugerencias de búsqueda en tiempo real
const searchInput = document.getElementById('searchInput');
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() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (query.length < 2) {
closeSuggestions();
return;
}
// Debounce: esperar 300ms después de dejar de escribir
searchTimeout = setTimeout(() => {
fetchSuggestions(query);
}, 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)}`)
.then(response => response.json())
.then(data => {
displaySuggestions(data.suggestions, query);
})
.catch(error => {
console.error('Error fetching suggestions:', error);
closeSuggestions();
});
}
// Función para mostrar las sugerencias
function displaySuggestions(suggestions, query) {
currentFocusIndex = -1;
if (suggestions.length === 0) {
searchSuggestions.innerHTML = '<div class="search-suggestion-item text-muted">No se encontraron productos</div>';
openSuggestions();
return;
}
let html = '';
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 += `
<div class="search-suggestion-item" role="option" id="search-option-${index}"
aria-selected="false" tabindex="-1"
data-href="/tienda/producto/${suggestion.id}"
onclick="window.location.href=this.dataset.href">
<span class="suggestion-name">${highlightedName}</span>
<span class="suggestion-price">€${priceWithVAT}</span>
</div>
`;
});
searchSuggestions.innerHTML = html;
openSuggestions();
}
// Función para resaltar el texto que coincide
function highlightMatch(text, query) {
const regex = new RegExp(`(${query})`, 'gi');
return text.replace(regex, '<span class="search-highlight">$1</span>');
}
// Cerrar sugerencias cuando se hace clic fuera
document.addEventListener('click', function(event) {
if (!searchForm.contains(event.target)) {
closeSuggestions();
}
});
// Cerrar sugerencias al enviar el formulario
searchForm.addEventListener('submit', function() {
closeSuggestions();
});
// Mostrar sugerencias al hacer clic en el input (si hay texto)
searchInput.addEventListener('focus', function() {
if (this.value.trim().length >= 2 && searchSuggestions.innerHTML) {
openSuggestions();
}
});
</script>
{% endcache %}
<link href="https://cdn.jsdelivr.net/npm/@n8n/chat/dist/style.css" rel="stylesheet" />
<script type="module">
import { createChat } from 'https://cdn.jsdelivr.net/npm/@n8n/chat/dist/chat.bundle.es.js';
createChat({
webhookUrl: 'https://n8n.elordenador.org/webhook/0e2cbe42-39d2-4e86-be62-c12542e246d4/chat',
initialMessages: [
'¡Hola! 👋',
'Soy el asistente virtual de Comercialmeria. ¿En qué puedo ayudarte?'
],
i18n: {
en: {
title: 'Chat de Soporte',
subtitle: 'Estamos aquí para ayudarte 24/7.',
footer: '',
getStarted: 'Nueva conversación',
inputPlaceholder: 'Escribe tu mensaje...',
},
},
});
</script>
</body>
</html>