// 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 (
);
}
// 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 => (
))}
);
}
// ============================================================
// 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 */}
);
}
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,
});