// ============================================================ // Qestylo — shared components // ============================================================ const { useState, useEffect, useRef, useCallback } = React; // ---- Lucide icon (renders SVG inline so it survives re-renders) ---- function pascal(name) { return name.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(''); } function Icon({ name, size = 22, strokeWidth = 1.6, className = '', style = {} }) { const data = (window.lucide && window.lucide.icons && window.lucide.icons[pascal(name)]) || null; let inner = ''; if (data && Array.isArray(data[2])) { inner = data[2].map(([tag, attrs]) => { const a = Object.entries(attrs).map(([k, v]) => `${k}="${v}"`).join(' '); return `<${tag} ${a} />`; }).join(''); } return ( ); } // ---- AnimatedHeading: per-character entrance ---- function AnimatedHeading({ text, className = '', lineClassName = () => '', startDelay = 200 }) { const [shown, setShown] = useState(false); useEffect(() => { const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (reduce) { setShown(true); return; } const t = setTimeout(() => setShown(true), 20); return () => clearTimeout(t); }, []); const lines = text.split('\n'); return ( {lines.map((line, li) => { const lineLength = line.length; const chars = line.split(''); return ( {chars.map((ch, ci) => { const delay = startDelay + (li * lineLength * 30) + (ci * 30); return ( {ch === ' ' ? '\u00A0' : ch} ); })} ); })} ); } // ---- FadeIn ---- function FadeIn({ children, delay = 0, duration = 800, className = '', style = {}, as = 'div' }) { const [shown, setShown] = useState(false); useEffect(() => { const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (reduce) { setShown(true); return; } const t = setTimeout(() => setShown(true), delay); return () => clearTimeout(t); }, [delay]); const Tag = as; return ( {children} ); } // ---- Reveal on scroll (IntersectionObserver, once at 20%) ---- function Reveal({ children, className = '', delay = 0, as = 'div', style = {} }) { const ref = useRef(null); const [seen, setSeen] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (reduce) { setSeen(true); return; } const obs = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { setSeen(true); obs.unobserve(el); } }); }, { threshold: 0.2 }); obs.observe(el); return () => obs.disconnect(); }, []); const Tag = as; return ( {children} ); } // ---- Video transition to static sunset background (single play + smooth cross-fade) ---- function PingPongVideo() { const videoRef = useRef(null); const wrapRef = useRef(null); const [ready, setReady] = useState(false); const [ended, setEnded] = useState(false); useEffect(() => { const v = videoRef.current; if (!v) return; const onPlay = () => { setReady(true); }; const onEnded = () => { setEnded(true); }; // If video is already playing or ready if (v.readyState >= 3) { setReady(true); } v.addEventListener('playing', onPlay); v.addEventListener('canplay', onPlay); v.addEventListener('canplaythrough', onPlay); v.addEventListener('ended', onEnded); // Fallback: in case events don't fire but video is loaded const t = setTimeout(() => { if (v.readyState >= 2) { setReady(true); } }, 1500); return () => { v.removeEventListener('playing', onPlay); v.removeEventListener('canplay', onPlay); v.removeEventListener('canplaythrough', onPlay); v.removeEventListener('ended', onEnded); clearTimeout(t); }; }, []); // subtle scroll zoom 1 -> 1.08 over first viewport useEffect(() => { const onScroll = () => { const w = wrapRef.current; if (!w) return; const p = Math.min(1, Math.max(0, window.scrollY / window.innerHeight)); w.style.transform = `scale(${1 + p * 0.08})`; }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); return ( ); } // ---- Buttons ---- function PrimaryCTA({ href, children, className = '', onClick }) { return ( {children} ); } function GlassCTA({ href, children, className = '', target, rel, onClick }) { return ( {children} ); } // ---- WhatsApp Widget ---- function WhatsAppWidget() { const [isOpen, setIsOpen] = useState(false); return (
{/* Chat Card */} {isOpen && (
{/* Header */}
{/* WhatsApp Icon */}

Javier (Qestylo)

Asesor Técnico de Kömmerling · En línea
{/* Close Button */}
{/* Body */}
{/* Very subtle chat background pattern */}
{/* Message bubble */}

Contacta con nosotros sin compromiso

{/* timestamp */} Justo ahora
{/* CTA Button */} Abrir chat
)} {/* Floating Button */}
); } // ---- CookieBanner ---- function CookieBanner({ onOpenCookies }) { const [visible, setVisible] = useState(false); const [showConfig, setShowConfig] = useState(false); const [analytics, setAnalytics] = useState(true); const [marketing, setMarketing] = useState(false); useEffect(() => { const consent = localStorage.getItem('qestylo-cookie-consent'); if (!consent) { const timer = setTimeout(() => { setVisible(true); }, 1000); // 1s delay return () => clearTimeout(timer); } }, []); const handleAcceptAll = () => { localStorage.setItem('qestylo-cookie-consent', JSON.stringify({ necessary: true, analytics: true, marketing: true })); setVisible(false); }; const handleRejectAll = () => { localStorage.setItem('qestylo-cookie-consent', JSON.stringify({ necessary: true, analytics: false, marketing: false })); setVisible(false); }; const handleSaveSelection = () => { localStorage.setItem('qestylo-cookie-consent', JSON.stringify({ necessary: true, analytics, marketing })); setVisible(false); }; if (!visible) return null; return (

Control de cookies

Utilizamos cookies para personalizar el contenido, analizar el tráfico y mejorar tu experiencia. Puedes configurar tus preferencias o aceptar todas. Consulta nuestra .

{showConfig && (
Cookies Técnicas Siempre activas
Cookies de Análisis
Cookies de Marketing
)}
{showConfig ? ( <> ) : ( <> )}
); } // ---- LegalModal ---- function LegalModal({ activeTab, onClose }) { if (!activeTab) return null; // Handle ESC key to close useEffect(() => { const handleKeyDown = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); const tabs = [ { id: 'legal', label: 'Aviso Legal' }, { id: 'privacidad', label: 'Privacidad' }, { id: 'cookies', label: 'Política de Cookies' }, ]; return ReactDOM.createPortal(
e.stopPropagation()} style={{ boxShadow: '0 0 35px 2px rgba(233, 122, 56, 0.1), 0 25px 50px -12px rgba(0, 0, 0, 0.5)' }} > {/* Header with Title & Tabs */}
Textos Legales

Información y Políticas

{/* Tab Navigator */}
{tabs.map((tab) => ( ))}
{/* Content Box (scrollable) */} {/* Footer of modal */}
, document.body ); } Object.assign(window, { Icon, AnimatedHeading, FadeIn, Reveal, PingPongVideo, PrimaryCTA, GlassCTA, WhatsAppWidget, CookieBanner, LegalModal, });