Files
may/static/index.html
T
2026-06-18 17:08:26 +02:00

697 lines
30 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>May HairCare — Peluquería en Barcelona</title>
<meta name="description" content="May HairCare, peluquería en Horta, Barcelona. Cortes, color, peinados y tratamientos para señoras, caballeros y niños.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<!-- NAV -->
<nav class="nav">
<div class="container nav-inner">
<a href="#" class="logo">May <span>HairCare</span></a>
<ul class="nav-links">
<li><a href="#servicios">Servicios</a></li>
<li><a href="#galeria">Galería</a></li>
<li><a href="#profesional">La profesional</a></li>
<li><a href="#horario">Horario</a></li>
<li><a href="#reservar">Reservar</a></li>
<li><a href="#contacto">Contacto</a></li>
</ul>
<a href="tel:+34933333333" class="btn btn-primary btn-nav">Llamar</a>
</div>
</nav>
<!-- HERO -->
<header class="hero">
<div class="hero-bg">
<img src="/assets/hero.jpg" alt="Interior del salón May HairCare">
</div>
<div class="container hero-content">
<h1>May HairCare<br>Tu peluquería en Horta</h1>
<p class="hero-sub">Cortes, color, peinados y tratamientos capilares para toda la familia. Un espacio pensado para ti.</p>
<div class="hero-actions">
<a href="#reservar" class="btn btn-primary">Reservar cita</a>
<a href="#servicios" class="btn btn-secondary">Ver servicios</a>
</div>
</div>
</header>
<!-- SERVICIOS -->
<section id="servicios" class="section">
<div class="container">
<h2 class="section-title">Nuestros servicios</h2>
<p class="section-subtitle">Cuidamos de tu pelo con los mejores productos y técnicas</p>
<div class="services-grid" id="services-grid">
<p class="services-loading">Cargando servicios…</p>
</div>
</div>
</section>
<!-- GALERÍA -->
<section id="galeria" class="section section-alt">
<div class="container">
<h2 class="section-title">Nuestro trabajo</h2>
<p class="section-subtitle">Algunos de los looks que hemos creado para nuestras clientas</p>
<div class="gallery-grid">
<div class="gallery-item"><img src="/assets/galeria-1.jpg" alt="Trabajo de peluquería"></div>
<div class="gallery-item"><img src="/assets/galeria-2.jpg" alt="Trabajo de peluquería"></div>
<div class="gallery-item"><img src="/assets/galeria-3.jpg" alt="Trabajo de peluquería"></div>
<div class="gallery-item"><img src="/assets/galeria-4.jpg" alt="Trabajo de peluquería"></div>
<div class="gallery-item"><img src="/assets/galeria-5.jpg" alt="Trabajo de peluquería"></div>
<div class="gallery-item"><img src="/assets/galeria-6.jpg" alt="Trabajo de peluquería"></div>
</div>
</div>
</section>
<!-- PROFESIONAL -->
<section id="profesional" class="section">
<div class="container">
<div class="profesional-grid">
<div class="profesional-img">
<img src="/assets/may.jpg" alt="May, peluquera profesional en May HairCare">
</div>
<div class="profesional-info">
<h3>May</h3>
<p class="profesional-role">Peluquera &amp; fundadora</p>
<p>Con más de 15 años de experiencia en peluquería profesional, May ha hecho del cuidado del cabello su vocación. Especializada en coloración, corte y tratamientos capilares, trabaja con técnicas actuales y productos de alta calidad para ofrecer resultados duraderos. Su trato cercano y la atención personalizada a cada cliente son la base de May HairCare.</p>
</div>
</div>
</div>
</section>
<!-- HORARIO -->
<section id="horario" class="section section-alt">
<div class="container">
<h2 class="section-title">Horario</h2>
<div class="schedule">
<div class="schedule-row">
<span>Lunes a Viernes</span>
<span>9:30 — 13:30 / 16:30 — 20:30</span>
</div>
<div class="schedule-row">
<span>Sábados</span>
<span>9:00 — 14:00</span>
</div>
<div class="schedule-row closed">
<span>Domingos y festivos</span>
<span>Cerrado</span>
</div>
</div>
<p class="schedule-note">Llámanos o reserva online. Te atendemos con o sin cita.</p>
</div>
</section>
<!-- RESERVAR -->
<section id="reservar" class="section">
<div class="container">
<h2 class="section-title">Reservar cita</h2>
<p class="section-subtitle">Elige uno o varios servicios, día y hora para tu cita</p>
<!-- Service selector -->
<div class="service-selector" id="service-selector">
<p class="services-loading">Cargando servicios…</p>
</div>
<div class="service-summary-bar" id="service-summary">
<span id="summary-names"></span>
<span class="summary-sep">·</span>
<span>Duración: <strong id="summary-dur"></strong></span>
</div>
<div class="booking-calendar">
<!-- Left: calendar -->
<div class="cal-left">
<div class="cal-header">
<button type="button" class="cal-nav" id="cal-prev">&larr;</button>
<span class="cal-month" id="cal-month"></span>
<button type="button" class="cal-nav" id="cal-next">&rarr;</button>
</div>
<div class="cal-weekdays">
<span>Lu</span><span>Ma</span><span>Mi</span><span>Ju</span><span>Vi</span><span></span><span>Do</span>
</div>
<div class="cal-grid" id="cal-grid"></div>
</div>
<!-- Right: slots + contact form -->
<div class="cal-right">
<div id="slots-placeholder" class="slots-placeholder">Selecciona primero el servicio que deseas</div>
<div id="slots-container" style="display:none">
<h3 class="slots-title" id="slots-title"></h3>
<div class="slots-grid" id="slots-grid"></div>
</div>
<form id="booking-form" class="booking-mini-form" style="display:none" novalidate>
<div class="form-row">
<div class="form-group">
<label for="nombre">Nombre *</label>
<input type="text" id="nombre" name="nombre" autocomplete="name" required>
</div>
<div class="form-group">
<label for="email">Email *</label>
<input type="email" id="email" name="email" autocomplete="email" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="telefono">Teléfono *</label>
<input type="tel" id="telefono" name="telefono" autocomplete="tel" required>
</div>
</div>
<div class="form-group">
<label for="mensaje">Mensaje (opcional)</label>
<textarea id="mensaje" name="mensaje" rows="2" placeholder="Algo que debamos saber…"></textarea>
</div>
<button type="submit" class="btn btn-primary" id="booking-submit">Confirmar reserva</button>
</form>
</div>
</div>
<div id="booking-result" class="booking-result" style="display:none"></div>
</div>
</section>
<!-- CONTACTO -->
<section id="contacto" class="section section-alt">
<div class="container">
<h2 class="section-title">Encuéntranos</h2>
<div class="contact-grid">
<div class="contact-info">
<div class="contact-item">
<strong>Dirección</strong>
<p>Carrer d'Horta, 12<br>08032 Barcelona (Horta)</p>
</div>
<div class="contact-item">
<strong>Teléfono</strong>
<p><a href="tel:+34933333333">933 333 333</a></p>
</div>
<div class="contact-item">
<strong>Cómo llegar</strong>
<p>Metro L5 — Horta<br>Bus V19, 45, 102</p>
</div>
</div>
<div class="contact-map">
<iframe
src="https://www.openstreetmap.org/export/embed.html?bbox=2.1550%2C41.4380%2C2.1650%2C41.4430&layer=mapnik&marker=41.4405%2C2.1600"
width="100%" height="300" style="border:0; border-radius: 12px;"
loading="lazy" title="Mapa May HairCare">
</iframe>
</div>
</div>
</div>
</section>
<!-- FOOTER -->
<footer class="footer">
<div class="container footer-inner">
<span class="logo">May <span>HairCare</span></span>
<p>&copy; 2026 May HairCare. Tu peluquería en Horta.</p>
</div>
</footer>
<script>
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// === EasyAppointments config ===
// IMPORTANTE: Después de configurar EasyAppointments, ve al admin (http://localhost:8888)
// y crea al menos un Provider. Anota su ID y ponlo aquí.
const PROVIDER_ID = 1;
// ---- Service selector state ----
let selectedCategory = null;
let selectedServices = []; // [{id, nombre, duracion}]
let totalDuration = 0;
let allServices = []; // cache de servicios EA
// ---- Services (ahora desde EasyAppointments) ----
async function loadServices() {
const grid = document.getElementById('services-grid');
try {
const res = await fetch('/ea-api/v1/services');
if (!res.ok) throw new Error('HTTP ' + res.status);
const services = await res.json();
allServices = services;
// Agrupamos en una categoría simple (puedes crear categorías en EA y extender esto)
const fakeCategorias = [{
nombre: "Servicios",
imagen: "",
servicios: services.map(s => ({
id: s.id,
nombre: s.name,
precio: (parseFloat(s.price) || 0) + " €",
duracion: s.duration || 30
}))
}];
renderServices(fakeCategorias, grid);
} catch (e) {
console.error(e);
grid.innerHTML = '<p class="services-loading">No se pudieron cargar los servicios desde EasyAppointments.</p>';
}
}
function renderServices(categorias, grid) {
grid.innerHTML = categorias.map(cat => `
<div class="service-card">
<div class="service-img">
<img src="${esc(cat.imagen)}" alt="${esc(cat.nombre)}">
</div>
<div class="service-body">
<h3>${esc(cat.nombre)}</h3>
<ul class="service-list">
${cat.servicios.map(s => `
<li><span>${esc(s.nombre)}</span><span class="price">${esc(s.precio)}</span></li>
`).join('')}
</ul>
</div>
</div>
`).join('');
renderServiceSelector(categorias);
}
function renderServiceSelector(categorias) {
const container = document.getElementById('service-selector');
container.innerHTML = categorias.map(cat => `
<div class="svc-cat-card" data-cat="${esc(cat.nombre)}">
<h4 class="svc-cat-title">${esc(cat.nombre)}</h4>
<div class="svc-options">
${cat.servicios.map(s => `
<label class="svc-option">
<input type="checkbox"
value="${esc(s.nombre)}"
data-cat="${esc(cat.nombre)}"
data-duracion="${s.duracion || 30}"
data-id="${s.id || ''}">
<span class="svc-name">${esc(s.nombre)}</span>
<span class="svc-meta">${s.duracion || 30}&nbsp;min · ${esc(s.precio)}</span>
</label>
`).join('')}
</div>
</div>
`).join('');
// Listener se añade una vez desde loadServices
container.addEventListener('change', handleServiceChange);
}
function handleServiceChange(e) {
if (e.target.type !== 'checkbox') return;
// Rebuild selected services from all checked boxes (incluye id de EA).
const checked = document.querySelectorAll('#service-selector input[type=checkbox]:checked');
selectedServices = [];
totalDuration = 0;
checked.forEach(cb => {
selectedServices.push({
id: parseInt(cb.dataset.id) || null,
nombre: cb.value,
duracion: parseInt(cb.dataset.duracion) || 30
});
totalDuration += parseInt(cb.dataset.duracion) || 30;
});
// Lock to one category
const first = document.querySelector('#service-selector input[type=checkbox]:checked');
selectedCategory = first ? first.dataset.cat : null;
updateServiceDisabledState();
updateServiceSummary();
if (selectedDate) {
selectedHora = null;
document.getElementById('booking-form').style.display = 'none';
const parts = selectedDate.split('-');
const dow = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])).getDay();
renderSlots(selectedDate, dow);
} else {
const ph = document.getElementById('slots-placeholder');
ph.textContent = selectedServices.length === 0
? 'Selecciona primero el servicio que deseas'
: 'Selecciona un día en el calendario';
}
}
function updateServiceDisabledState() {
document.querySelectorAll('#service-selector .svc-cat-card').forEach(card => {
const locked = selectedCategory !== null && card.dataset.cat !== selectedCategory;
card.classList.toggle('svc-cat-locked', locked);
card.querySelectorAll('input[type=checkbox]').forEach(cb => { cb.disabled = locked; });
});
}
function updateServiceSummary() {
const bar = document.getElementById('service-summary');
if (selectedServices.length === 0) { bar.style.display = 'none'; return; }
bar.style.display = 'flex';
document.getElementById('summary-names').textContent = selectedServices.map(s => s.nombre).join(' + ');
const h = Math.floor(totalDuration / 60);
const m = totalDuration % 60;
document.getElementById('summary-dur').textContent =
h > 0 ? (m > 0 ? h + ' h ' + m + ' min' : h + ' h') : m + ' min';
}
// ---- Calendar state (usando availabilities de EasyAppointments) ----
let calYear, calMonth;
let selectedDate = null;
let selectedHora = null;
let availableSlotsForDate = []; // array de "HH:mm" devueltos por EA
const MONTH_NAMES = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'];
function pad(n) { return n < 10 ? '0' + n : '' + n; }
function fmtDate(y, m, d) { return y + '-' + pad(m + 1) + '-' + pad(d); }
function timeToMinutes(hora) {
const [h, m] = hora.split(':').map(Number);
return h * 60 + m;
}
function minutesToTime(mins) {
const h = Math.floor(mins / 60);
const m = mins % 60;
return pad(h) + ':' + pad(m);
}
// Comprueba si desde 'startHora' hay un bloque consecutivo libre de 'neededMinutes'
// usando la lista de starts devueltos por EA.
function canFitFullBlock(startHora, neededMinutes, availableList) {
if (!startHora || neededMinutes <= 0 || !availableList || availableList.length === 0) return false;
const startM = timeToMinutes(startHora);
const step = 30;
const numSteps = Math.max(1, Math.ceil(neededMinutes / step));
for (let i = 0; i < numSteps; i++) {
const checkM = startM + i * step;
const checkStr = minutesToTime(checkM);
if (!availableList.includes(checkStr)) {
return false;
}
}
return true;
}
// Obtiene el serviceId principal para consultar disponibilidad (el de mayor duración o el primero)
function getPrimaryServiceId() {
if (!selectedServices.length) return null;
// Prioriza el de mayor duración
const sorted = [...selectedServices].sort((a, b) => (b.duracion || 0) - (a.duracion || 0));
return sorted[0].id;
}
function renderCalendar() {
document.getElementById('cal-month').textContent = MONTH_NAMES[calMonth] + ' ' + calYear;
const grid = document.getElementById('cal-grid');
grid.innerHTML = '';
const firstDay = new Date(calYear, calMonth, 1);
let startDow = firstDay.getDay();
startDow = startDow === 0 ? 6 : startDow - 1;
const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate();
const today = new Date();
today.setHours(0,0,0,0);
for (let i = 0; i < startDow; i++) {
const empty = document.createElement('span');
empty.className = 'cal-day cal-empty';
grid.appendChild(empty);
}
for (let d = 1; d <= daysInMonth; d++) {
const date = new Date(calYear, calMonth, d);
const dateStr = fmtDate(calYear, calMonth, d);
const dow = date.getDay();
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = d;
btn.className = 'cal-day';
if (date < today || dow === 0) { btn.disabled = true; btn.classList.add('cal-disabled'); }
if (dateStr === selectedDate) btn.classList.add('cal-selected');
btn.addEventListener('click', () => selectDay(dateStr, dow));
grid.appendChild(btn);
}
}
function selectDay(dateStr, dow) {
selectedDate = dateStr;
selectedHora = null;
document.getElementById('booking-form').style.display = 'none';
renderCalendar();
renderSlots(dateStr, dow);
}
// Versión simplificada usando EasyAppointments
async function renderSlots(dateStr, dow) {
const container = document.getElementById('slots-container');
const placeholder = document.getElementById('slots-placeholder');
const grid = document.getElementById('slots-grid');
const title = document.getElementById('slots-title');
if (selectedServices.length === 0) {
placeholder.textContent = 'Selecciona primero el servicio que deseas';
placeholder.style.display = '';
container.style.display = 'none';
return;
}
const primaryServiceId = getPrimaryServiceId();
if (!primaryServiceId) {
placeholder.textContent = 'Error seleccionando servicio principal';
placeholder.style.display = '';
container.style.display = 'none';
return;
}
placeholder.style.display = 'none';
container.style.display = '';
const [y, m, d] = dateStr.split('-');
const dayNames = ['Domingo','Lunes','Martes','Miércoles','Jueves','Viernes','Sábado'];
const dateObj = new Date(parseInt(y), parseInt(m) - 1, parseInt(d));
title.textContent = dayNames[dateObj.getDay()] + ' ' + parseInt(d) + ' de ' + MONTH_NAMES[parseInt(m) - 1];
try {
const url = `/ea-api/v1/availabilities?providerId=${PROVIDER_ID}&serviceId=${primaryServiceId}&date=${dateStr}`;
const res = await fetch(url);
if (!res.ok) throw new Error(await res.text());
availableSlotsForDate = await res.json();
} catch (e) {
console.error('Error al obtener availabilities de EA:', e);
availableSlotsForDate = [];
}
if (!availableSlotsForDate || availableSlotsForDate.length === 0) {
placeholder.textContent = 'No hay disponibilidad este día para el servicio seleccionado';
placeholder.style.display = '';
container.style.display = 'none';
return;
}
// Filtrado clave para multiservicio:
// Usamos la lista de EA (basada en el servicio más largo) pero solo mostramos
// aquellos starts desde los que cabe un bloque completo de 'totalDuration' minutos.
const filteredSlots = availableSlotsForDate.filter(hora =>
canFitFullBlock(hora, totalDuration, availableSlotsForDate)
);
if (filteredSlots.length === 0) {
placeholder.textContent = 'No hay bloques libres de ' + totalDuration + ' minutos';
placeholder.style.display = '';
container.style.display = 'none';
return;
}
grid.innerHTML = filteredSlots.map(hora => {
const isSel = hora === selectedHora;
const cls = 'slot-btn' + (isSel ? ' slot-selected' : '');
return `<button type="button" class="${cls}" data-hora="${esc(hora)}">${esc(hora)}</button>`;
}).join('');
grid.querySelectorAll('.slot-btn').forEach(btn => {
btn.addEventListener('click', () => {
selectedHora = btn.dataset.hora;
renderSlots(dateStr, dow);
document.getElementById('booking-form').style.display = '';
});
});
}
function initCalendar() {
const now = new Date();
calYear = now.getFullYear();
calMonth = now.getMonth();
document.getElementById('cal-prev').addEventListener('click', () => {
const now = new Date();
if (calYear === now.getFullYear() && calMonth === now.getMonth()) return;
calMonth--;
if (calMonth < 0) { calMonth = 11; calYear--; }
renderCalendar();
hideSlots();
});
document.getElementById('cal-next').addEventListener('click', () => {
calMonth++;
if (calMonth > 11) { calMonth = 0; calYear++; }
renderCalendar();
hideSlots();
});
renderCalendar();
}
function hideSlots() {
selectedDate = null;
selectedHora = null;
document.getElementById('slots-container').style.display = 'none';
document.getElementById('slots-placeholder').style.display = '';
document.getElementById('slots-placeholder').textContent = selectedServices.length === 0
? 'Selecciona primero el servicio que deseas'
: 'Selecciona un día en el calendario';
document.getElementById('booking-form').style.display = 'none';
}
// Busca cliente por email o lo crea en EasyAppointments
async function findOrCreateCustomer(nombre, email, telefono) {
// Buscar
try {
const searchRes = await fetch(`/ea-api/v1/customers?q=${encodeURIComponent(email)}`);
if (searchRes.ok) {
const list = await searchRes.json();
if (Array.isArray(list) && list.length > 0) {
return list[0].id;
}
}
} catch (e) { console.warn('Búsqueda de cliente falló', e); }
// Crear nuevo
const parts = nombre.trim().split(/\s+/);
const firstName = parts[0] || nombre;
const lastName = parts.slice(1).join(' ') || '';
const createRes = await fetch('/ea-api/v1/customers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
firstName: firstName,
lastName: lastName,
email: email,
phone: telefono || '',
notes: 'Creado desde web May HairCare'
})
});
if (!createRes.ok) {
const errText = await createRes.text().catch(() => '');
throw new Error('No se pudo crear el cliente en EasyAppointments: ' + errText);
}
const created = await createRes.json();
return created.id;
}
function initBookingForm() {
document.getElementById('booking-form').addEventListener('submit', async function(e) {
e.preventDefault();
const form = e.target;
const submit = document.getElementById('booking-submit');
const result = document.getElementById('booking-result');
if (selectedServices.length === 0) { alert('Selecciona al menos un servicio'); return; }
if (!selectedDate || !selectedHora) { alert('Selecciona día y hora'); return; }
const email = form.email.value.trim();
if (!email) { alert('El email es obligatorio'); return; }
submit.disabled = true;
submit.textContent = 'Enviando…';
result.style.display = 'none';
try {
// 1. Cliente (por email)
const customerId = await findOrCreateCustomer(
form.nombre.value.trim(),
email,
form.telefono.value.trim()
);
// 2. Preparar appointment (usamos el servicio principal - el de mayor duración - + notas con todos)
const primaryServiceId = getPrimaryServiceId();
const primaryService = selectedServices.find(s => s.id === primaryServiceId) || selectedServices[0];
if (!primaryService || !primaryService.id) {
throw new Error('No se encontró ID de servicio válido');
}
const start = `${selectedDate} ${selectedHora}:00`;
// Duración total (EA usará la del servicio, pero ponemos nota)
const endDate = new Date(`${selectedDate}T${selectedHora}`);
endDate.setMinutes(endDate.getMinutes() + totalDuration);
const endHour = pad(endDate.getHours()) + ':' + pad(endDate.getMinutes());
const notes = [
`Nombre: ${form.nombre.value.trim()}`,
`Email: ${email}`,
`Teléfono: ${form.telefono.value.trim()}`,
`Servicios: ${selectedServices.map(s => s.nombre).join(' + ')}`,
`Duración total: ${totalDuration} min`,
form.mensaje.value.trim() ? `Mensaje: ${form.mensaje.value.trim()}` : ''
].filter(Boolean).join('\n');
const appointment = {
start: start,
end: `${selectedDate} ${endHour}:00`,
serviceId: primaryService.id,
providerId: PROVIDER_ID,
customerId: customerId,
notes: notes,
status: 'booked'
};
const res = await fetch('/ea-api/v1/appointments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(appointment),
});
if (res.ok) {
form.reset();
selectedHora = null;
// Refrescar slots
const savedDate = selectedDate;
const savedDow = new Date(parseInt(savedDate.split('-')[0]), parseInt(savedDate.split('-')[1])-1, parseInt(savedDate.split('-')[2])).getDay();
renderSlots(savedDate, savedDow);
form.style.display = 'none';
result.className = 'booking-result booking-success';
result.textContent = '¡Reserva confirmada! Te esperamos. Gestionada con EasyAppointments.';
} else {
const err = await res.json().catch(() => ({}));
result.className = 'booking-result booking-error';
result.textContent = (err.message || err.error || JSON.stringify(err)) || 'Error creando la cita en EasyAppointments.';
}
} catch (err) {
console.error(err);
result.className = 'booking-result booking-error';
result.textContent = err.message || 'No se pudo conectar con EasyAppointments.';
}
result.style.display = 'block';
submit.disabled = false;
submit.textContent = 'Confirmar reserva';
});
}
document.addEventListener('DOMContentLoaded', function() {
loadServices();
initCalendar();
initBookingForm();
});
</script>
</body>
</html>