// shared.jsx — common hooks, icons, and atoms const { useState, useEffect, useRef, useMemo, useCallback } = React; // Lucide icon wrapper — uses lucide UMD with createIcons after mount function Icon({ name, size = 18, className = "", style = {}, strokeWidth = 1.75 }) { const ref = useRef(null); useEffect(() => { if (ref.current && window.lucide) { ref.current.innerHTML = ""; const el = document.createElement('i'); el.setAttribute('data-lucide', name); ref.current.appendChild(el); window.lucide.createIcons({ attrs: { width: size, height: size, 'stroke-width': strokeWidth } }); } }, [name, size, strokeWidth]); return ; } // Reveal on scroll function useReveal() { useEffect(() => { const els = document.querySelectorAll('.reveal'); const obs = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); obs.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); els.forEach(el => obs.observe(el)); return () => obs.disconnect(); }); } // Counter function Counter({ to, duration = 1600, prefix = "", suffix = "", className = "", decimals = 0 }) { const [val, setVal] = useState(0); const ref = useRef(null); const started = useRef(false); useEffect(() => { if (!ref.current) return; const obs = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting && !started.current) { started.current = true; const start = performance.now(); const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setVal(to * eased); if (t < 1) requestAnimationFrame(tick); else setVal(to); }; requestAnimationFrame(tick); } }); }, { threshold: 0.4 }); obs.observe(ref.current); return () => obs.disconnect(); }, [to, duration]); return {prefix}{val.toLocaleString('es-AR', { maximumFractionDigits: decimals })}{suffix}; } // Section wrapper function Section({ id, children, className = "", style = {} }) { return (
{children}
); } // Container function Container({ children, className = "" }) { return
{children}
; } // Section header function SectionHeader({ eyebrow, title, subtitle, align = "center" }) { const alignCls = align === "center" ? "text-center mx-auto" : "text-left"; return (
{eyebrow && (
{eyebrow}
)}

{title}

{subtitle && (

{subtitle}

)}
); } // Live market badge function LiveBadge() { return (
Mercado en vivo
); } // Candlestick chart SVG (decorative, animated) function CandlesArt({ className = "" }) { // ====== ANIMATED LIVE CANDLES (rAF-based, no CSS dependency) ====== const VISIBLE = 30; const BUFFER = 2; const TOTAL = VISIBLE + BUFFER; const w = 720; const h = 280; const cw = w / VISIBLE; // 24 const TICK_MS = 1500; // ms per candle slide // Initial seed const buildInitial = () => { const arr = []; let p = 50; let id = 0; const spikes = new Set([10, 20]); const drops = new Set([14, 26]); for (let i = 0; i < TOTAL; i++) { const o = p; let c; if (spikes.has(i)) c = p + 18 + Math.random() * 4; else if (drops.has(i)) c = p - 14 - Math.random() * 3; else c = p + (Math.random() - 0.5) * 5; const hi = Math.max(o, c) + Math.random() * 2 + 0.5; const lo = Math.min(o, c) - Math.random() * 2 - 0.5; arr.push({ o, c, h: hi, l: lo, id: id++ }); p = c; } return arr; }; const generateNext = (prevPrice, id) => { const r = Math.random(); const o = prevPrice; let c; if (r < 0.05) c = prevPrice + 16 + Math.random() * 5; else if (r < 0.09) c = prevPrice - 13 - Math.random() * 4; else c = prevPrice + (Math.random() - 0.5) * 5; if (prevPrice > 90) c -= 8; if (prevPrice < 10) c += 8; const hi = Math.max(o, c) + Math.random() * 2 + 0.5; const lo = Math.min(o, c) - Math.random() * 2 - 0.5; return { o, c, h: hi, l: lo, id }; }; const [data, setData] = useState(buildInitial); const groupRef = useRef(null); const idRef = useRef(TOTAL); const startRef = useRef(null); const rafRef = useRef(null); // Smooth rAF-based slide. Sets transform directly on the SVG via setAttribute. // No CSS keyframes needed → works reliably across browsers and SPA re-renders. useEffect(() => { let mounted = true; const step = (timestamp) => { if (!mounted) return; if (startRef.current === null) startRef.current = timestamp; const elapsed = timestamp - startRef.current; const progress = Math.min(elapsed / TICK_MS, 1); const offset = -cw * progress; if (groupRef.current) { groupRef.current.setAttribute('transform', 'translate(' + offset + ', 0)'); } if (progress >= 1) { // Shift data: drop leftmost, append new candle on the right setData(prev => { const last = prev[prev.length - 1]; return [...prev.slice(1), generateNext(last.c, idRef.current++)]; }); startRef.current = null; } rafRef.current = requestAnimationFrame(step); }; rafRef.current = requestAnimationFrame(step); return () => { mounted = false; if (rafRef.current) cancelAnimationFrame(rafRef.current); }; }, [cw]); // Live formation pulse for the in-buffer candle (CSS-independent) const [pulse, setPulse] = useState(0); useEffect(() => { let raf; const tick = (t) => { setPulse((Math.sin(t / 350) + 1) / 2); // 0..1 raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, []); // Compute scale on each render const min = Math.min(...data.map(d => d.l)) - 4; const max = Math.max(...data.map(d => d.h)) + 4; const scaleY = (v) => h - ((v - min) / (max - min)) * h; const cxAt = (i) => (i - 1) * cw + cw / 2; return ( {/* grid — static, outside the animated group */} {[0.25, 0.5, 0.75].map(p => ( ))} {/* Animated group — transform updated each frame via rAF */} {/* connecting line */} (i === 0 ? 'M' : 'L') + ' ' + cxAt(i) + ' ' + scaleY((d.o + d.c) / 2)).join(' ')} stroke="url(#lineGrad)" strokeWidth="1.2" fill="none" /> {data.map((d, i) => { const up = d.c >= d.o; const cx = cxAt(i); const bodyTop = scaleY(Math.max(d.o, d.c)); const bodyH = Math.abs(scaleY(d.o) - scaleY(d.c)) || 1; const isLive = i === TOTAL - 2; const liveOpacity = isLive ? (0.7 + 0.3 * pulse) : 0.95; return ( ); })} ); } // Mini sparkline function Sparkline({ points = 24, trend = "up", color = "#10b981", className = "", height = 40 }) { const data = useMemo(() => { const arr = []; let v = 50; for (let i = 0; i < points; i++) { const drift = trend === "up" ? 1.2 : trend === "down" ? -1.2 : 0; v += drift + (Math.random() - 0.5) * 6; arr.push(v); } return arr; }, [points, trend]); const min = Math.min(...data) - 2; const max = Math.max(...data) + 2; const w = 120; const sy = (v) => height - ((v - min) / (max - min)) * height; const path = data.map((v, i) => `${i === 0 ? 'M' : 'L'} ${(i / (data.length - 1)) * w} ${sy(v)}`).join(' '); const area = path + ` L ${w} ${height} L 0 ${height} Z`; return ( ); } // Animated particles canvas function Particles() { const ref = useRef(null); useEffect(() => { const c = ref.current; if (!c) return; const ctx = c.getContext('2d'); let raf; const dpi = Math.min(window.devicePixelRatio || 1, 2); const resize = () => { c.width = c.offsetWidth * dpi; c.height = c.offsetHeight * dpi; }; resize(); window.addEventListener('resize', resize); const N = 70; const dots = Array.from({ length: N }, () => ({ x: Math.random() * c.width, y: Math.random() * c.height, vx: (Math.random() - 0.5) * 0.18 * dpi, vy: (Math.random() - 0.5) * 0.18 * dpi, r: (Math.random() * 1.2 + 0.4) * dpi, hue: Math.random() > 0.5 ? 200 : 195, a: Math.random() * 0.5 + 0.2, })); const tick = () => { ctx.clearRect(0, 0, c.width, c.height); // connections for (let i = 0; i < N; i++) { for (let j = i + 1; j < N; j++) { const dx = dots[i].x - dots[j].x; const dy = dots[i].y - dots[j].y; const d = Math.sqrt(dx * dx + dy * dy); if (d < 120 * dpi) { ctx.strokeStyle = `rgba(0,212,255,${(1 - d / (120 * dpi)) * 0.10})`; ctx.lineWidth = 0.6 * dpi; ctx.beginPath(); ctx.moveTo(dots[i].x, dots[i].y); ctx.lineTo(dots[j].x, dots[j].y); ctx.stroke(); } } } dots.forEach(d => { d.x += d.vx; d.y += d.vy; if (d.x < 0 || d.x > c.width) d.vx *= -1; if (d.y < 0 || d.y > c.height) d.vy *= -1; ctx.fillStyle = `hsla(${d.hue}, 100%, 70%, ${d.a})`; ctx.shadowColor = `hsla(${d.hue}, 100%, 70%, 0.8)`; ctx.shadowBlur = 6 * dpi; ctx.beginPath(); ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; }); raf = requestAnimationFrame(tick); }; tick(); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', resize); }; }, []); return ; } // Ticker of market data function MarketTicker() { const items = [ { sym: 'BOOM 1000', price: '8,432.51', chg: '+1.24%', up: true }, { sym: 'CRASH 500', price: '6,128.04', chg: '-0.86%', up: false }, { sym: 'VOL 75', price: '402,118', chg: '+2.41%', up: true }, { sym: 'VOL 100', price: '1,284.92', chg: '+0.55%', up: true }, { sym: 'JUMP 25', price: '7,402.10', chg: '-0.32%', up: false }, { sym: 'STEP IDX', price: '9,128.30', chg: '+0.74%', up: true }, { sym: 'BOOM 500', price: '12,891.66', chg: '+1.92%', up: true }, { sym: 'VOL 25', price: '278,402', chg: '-1.04%', up: false }, { sym: 'VOL 10', price: '8,234.05', chg: '+0.41%', up: true }, ]; const row = [...items, ...items]; return (
{row.map((it, i) => (
{it.sym} {it.price} {it.chg} |
))}
); } // Broker / partner strip function BrokerStrip({ compact = false }) { const brokers = [ { src: 'img/deriv.svg', name: 'Deriv', h: compact ? 22 : 28 }, { src: 'img/weltrade.svg', name: 'Weltrade', h: compact ? 22 : 28 }, { src: 'img/bridgemarkets.svg', name: 'Bridge Markets', h: compact ? 28 : 34 }, ]; return (
Brokers que nos respaldan
Trabajamos con plataformas reguladas
{brokers.map(b => ( {b.name} ))}
); } // ============================================================ // Premium Countdown — animated countdown to the next Premium opening // ============================================================ function PremiumCountdown({ target = '2026-06-16T00:00:00-03:00', supportUrl = 'https://t.me/barbieshark', }) { const targetMs = useMemo(() => new Date(target).getTime(), [target]); const [now, setNow] = useState(() => Date.now()); useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, []); const diff = Math.max(0, targetMs - now); const d = Math.floor(diff / 86400000); const h = Math.floor((diff % 86400000) / 3600000); const m = Math.floor((diff % 3600000) / 60000); const s = Math.floor((diff % 60000) / 1000); const cells = [ { v: d, l: 'Días' }, { v: h, l: 'Horas' }, { v: m, l: 'Min' }, { v: s, l: 'Seg' }, ]; const dateLabel = useMemo(() => { const dt = new Date(targetMs); const months = ['ENE','FEB','MAR','ABR','MAY','JUN','JUL','AGO','SEP','OCT','NOV','DIC']; return `${String(dt.getDate()).padStart(2,'0')} · ${months[dt.getMonth()]} · ${dt.getFullYear()}`; }, [targetMs]); return (
{/* tech grid overlay */}
{/* radial glow */}
{/* sweep shine */}
{/* LEFT — status + date */}
Premium activo · cupo cerrado

Próxima apertura del Premium

Hay un Premium activo en este momento. La próxima ventana para sumarse abre el 16 de junio de 2026. Dejá tu lugar antes y entrá en cuanto se libere el cupo.

{dateLabel}
{/* RIGHT — countdown grid */}
Tiempo restante para el próximo acceso
{cells.map((c, i) => ( ))}
Hora local · cuenta sincronizada al segundo
{/* bottom hairline */}
); } function CountCell({ value, label }) { const v = String(Math.max(0, value)).padStart(2, '0'); return (
{/* glare line in the middle */}
{label}
); } function FlipDigit({ ch }) { const [prev, setPrev] = useState(ch); const [flipping, setFlipping] = useState(false); useEffect(() => { if (ch !== prev) { setFlipping(true); const t = setTimeout(() => { setPrev(ch); setFlipping(false); }, 380); return () => clearTimeout(t); } }, [ch, prev]); return ( {prev} {flipping && {ch}} ); } Object.assign(window, { Icon, useReveal, Counter, Section, Container, SectionHeader, LiveBadge, CandlesArt, Sparkline, Particles, MarketTicker, BrokerStrip, PremiumCountdown, });