/* global React */
// Shared building blocks for the Waterlemon site
const { useState, useEffect, useRef, useLayoutEffect } = React;
/* ─────────── WATERLEMON LOGO MARK (lemon + W) ─────────── */
// Geometry from official Watermelon_logo_editable-01.svg
function LemonMark({ size = 24, color = "#FF2D87", inner = "#FFFCF5", outline = false }) {
return (
);
}
/* Full wordmark — lemon + WATERLEMON STUDIO */
function LemonWordmark({ height = 36, color = "var(--wl-ink)", accent = "var(--wl-accent)" }) {
return (
WATERLEMON
studio
);
}
/* Decorative lemon — a floating illustrated lemon (silhouette + W inside + leaf) */
function FloatingLemon({ size = 80, color = "var(--wl-accent)", style = {}, rotate = 0 }) {
return (
);
}
/* ─────────── CUSTOM CURSOR (limón sticker) ─────────── */
function WaterlemonCursor() { return null; }
function _UnusedCursor({ enabled = true }) {
const ref = useRef(null);
const [hover, setHover] = useState(false);
const [hidden, setHidden] = useState(true);
useEffect(() => {
if (!enabled) {
document.body.classList.remove("has-wl-cursor");
return;
}
document.body.classList.add("has-wl-cursor");
let raf = null;
let tx = 0, ty = 0, cx = 0, cy = 0;
const onMove = (e) => {
tx = e.clientX; ty = e.clientY;
setHidden(false);
if (!raf) {
raf = requestAnimationFrame(loop);
}
};
const loop = () => {
cx += (tx - cx) * 0.18;
cy += (ty - cy) * 0.18;
if (ref.current) {
ref.current.style.transform = `translate(${cx}px, ${cy}px) translate(-50%, -50%)`;
}
if (Math.abs(tx - cx) > 0.4 || Math.abs(ty - cy) > 0.4) {
raf = requestAnimationFrame(loop);
} else {
raf = null;
}
};
const onOver = (e) => {
const t = e.target;
if (t.closest && t.closest("a, button, .service-row, .case-card, .testi-card, .polaroid, [data-cursor='hover']")) {
setHover(true);
}
};
const onOut = (e) => {
const t = e.target;
if (t.closest && t.closest("a, button, .service-row, .case-card, .testi-card, .polaroid, [data-cursor='hover']")) {
setHover(false);
}
};
const onLeave = () => setHidden(true);
const onEnter = () => setHidden(false);
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseover", onOver);
window.addEventListener("mouseout", onOut);
document.addEventListener("mouseleave", onLeave);
document.addEventListener("mouseenter", onEnter);
return () => {
document.body.classList.remove("has-wl-cursor");
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseover", onOver);
window.removeEventListener("mouseout", onOut);
document.removeEventListener("mouseleave", onLeave);
document.removeEventListener("mouseenter", onEnter);
if (raf) cancelAnimationFrame(raf);
};
}, [enabled]);
if (!enabled) return null;
return (
);
}
/* ─────────── NAV ─────────── */
const NAV_SERVICES = [
{ n: "01", slug: "redes-sociales", name: "Redes Sociales" },
{ n: "02", slug: "contenido", name: "Creación de Contenido" },
{ n: "03", slug: "ads-meta", name: "Paid Media (Meta)" },
{ n: "04", slug: "seo", name: "SEO Orgánico" },
{ n: "05", slug: "google-ads", name: "Google Ads" },
{ n: "06", slug: "web", name: "Diseño Web" },
{ n: "07", slug: "estrategia", name: "Estrategia 360" },
{ n: "08", slug: "video", name: "Producción Audiovisual" },
{ n: "09", slug: "consultoria", name: "Consultoría 1:1" },
];
function Nav({ active = "home", onPink = false }) {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const [ddOpen, setDdOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const links = [
{ k: "home", label: "Inicio", href: "index.html" },
{ k: "servicios", label: "Servicios", href: "servicios.html" },
{ k: "casos", label: "Proyectos", href: "casos.html" },
{ k: "equipo", label: "Equipo", href: "equipo.html" },
{ k: "contacto", label: "Contacto", href: "contacto.html" },
];
return (
{links.map(l =>
{l.label}{l.k === "servicios" && })}
contacto
info@waterlemonstudio.com
+34 698 38 65 56
);
}
/* ─────────── MARQUEE BAND ─────────── */
function MarqueeBand({ items, variant = "ink", speed = 38 }) {
const list = (
{items.map((it, i) => (
{it.label}
))}
);
return (
{list}
{list}
);
}
/* ─────────── CLIENT TICKER ─────────── */
function ClientTicker() {
const clients = [
{ name: "Club Pilates", badge: "+25" },
{ name: "O'Retorniño" },
{ name: "Spandex" },
{ name: "Orangetheory Valencia" },
{ name: "ZEISS Vision Center" },
{ name: "Flippo" },
{ name: "The Digital Brands House" },
{ name: "Centros de Artes Marciales" },
{ name: "Ayres del Sur" },
{ name: "Anita Gelato" },
{ name: "CM Ibérica" },
{ name: "Casa Carbó" },
{ name: "Berga & Berga" },
];
const row = (
{clients.concat(clients).map((c, i) => (
{c.name}
{c.badge && {c.badge}}
♥
))}
);
return (
CLIENTES QUE NOS HAN FICHADO COMO PARTNERS
{row}
);
}
/* ─────────── FOOTER PHRASES ─────────── */
// Each phrase is an array of lines; each line is { text, color }
// color: "ink" (default cream/bone), "accent" (pink), "out" (outlined)
const FOOTER_PHRASES = {
"hablemos": [
[{ t: "HABLEMOS " }, { t: "SIN PRISA, ", c: "accent" }, { t: "PERO CON GANAS.", c: "out" }],
],
"water": [
[{ t: "SOMOS EL " }, { t: "WATER ", c: "accent" }, { t: "DE TU " }, { t: "LEMON.", c: "out" }],
],
"obsesion": [
[{ t: "TU MARCA, " }],
[{ t: "NUESTRA ", c: "out" }, { t: "OBSESIÓN.", c: "accent" }],
],
"leads": [
[{ t: "MÁS LEADS. " }],
[{ t: "MEJOR ", c: "accent" }, { t: "MARCA." }],
[{ t: "EMPECEMOS.", c: "out" }],
],
"ideas": [
[{ t: "IDEAS " }, { t: "REALES,", c: "accent" }],
[{ t: "PARA MARCAS QUE ", c: "out" }, { t: "CRECEN." }],
],
"llevemos": [
[{ t: "PROBABLEMENTE " }],
[{ t: "NOS LLEVEMOS ", c: "accent" }, { t: "BIEN.", c: "out" }],
],
"24h": [
[{ t: "¿HABLAMOS? " }],
[{ t: "TE LEEMOS EN ", c: "out" }, { t: "24H.", c: "accent" }],
],
};
function FooterPhrase({ phraseKey = "hablemos" }) {
const lines = FOOTER_PHRASES[phraseKey] || FOOTER_PHRASES.hablemos;
return (
{lines.map((line, i) => (
{line.map((seg, j) => (
{seg.t}
))}
))}
);
}
function Footer({ phrase = "hablemos" }) {
return (
);
}
/* ─────────── IN-VIEW HOOK ─────────── */
function useInView(options = {}) {
const ref = useRef(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const obs = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setInView(true);
obs.disconnect();
}
},
{ threshold: 0.18, ...options }
);
obs.observe(el);
return () => obs.disconnect();
}, []);
return [ref, inView];
}
/* ─────────── COUNTER ─────────── */
function Counter({ to, suffix = "", duration = 1600, decimals = 0 }) {
const [ref, inView] = useInView();
const [val, setVal] = useState(0);
useEffect(() => {
if (!inView) return;
const start = performance.now();
let raf;
const tick = (t) => {
const p = Math.min(1, (t - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setVal(eased * to);
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [inView, to, duration]);
const display = decimals > 0 ? val.toFixed(decimals) : Math.round(val).toLocaleString("es-ES");
return {display}{suffix};
}
/* ─────────── SCRIBBLES & DOODLES ─────────── */
function CircleScribble({ style }) {
const [ref, inView] = useInView();
return (
);
}
function DrawArrow({ style, color = "var(--wl-accent)" }) {
const ref = useRef(null);
const [ref2, inView] = useInView();
return (
);
}
function Sparkle({ style, size = 22, color = "currentColor" }) {
return (
);
}
function HeartScribble({ style, size = 28 }) {
return (
);
}
function StarDoodle({ style, size = 26 }) {
return (
);
}
function SmileyDoodle({ style, size = 28 }) {
return (
);
}
/* expose globally for sibling babel scripts */
Object.assign(window, {
LemonMark, LemonWordmark, FloatingLemon, WaterlemonCursor, Nav, MarqueeBand, ClientTicker, Footer, FooterPhrase, FOOTER_PHRASES,
useInView, Counter, CircleScribble, DrawArrow, Sparkle, HeartScribble, StarDoodle, SmileyDoodle
});