/* components.jsx — shared across Site A pages */
const { useState, useEffect, useMemo, useRef } = React;
const useS = useState;
window.useS = useS;
/* ───────── NAV ───────── */
function Nav({ current }) {
const [open, setOpen] = useState(false);
const links = [
{ href: 'index.html', label: 'Home', id: 'home' },
{ href: 'indicator.html', label: 'Indicator', id: 'indicator' },
{ href: 'guide.html', label: 'Guide', id: 'guide' },
{ href: 'course.html', label: 'Course', id: 'course' },
{ href: 'tutorials.html', label: 'Tutorials', id: 'tutorials' },
{ href: 'pricing.html', label: 'Pricing', id: 'pricing' },
{ href: 'about.html', label: 'About', id: 'about' },
{ href: 'contact.html', label: 'Contact', id: 'contact' },
];
return (
);
}
/* ───────── FOOTER ───────── */
function Footer() {
return (
);
}
/* ───────── DASHBOARD WIDGET ───────── */
function Dashboard({ push, score, status, signal, signalText }) {
const arrow = (d) => d === 'UP' ? '▲' : d === 'DOWN' ? '▼' : '—';
const pushCls = push === 'UP' ? 'good' : push === 'DOWN' ? 'bad' : '';
const scoreCls = score?.dir === 'UP' ? 'good' : score?.dir === 'DOWN' ? 'bad' : '';
const statusCls = status === 'TRENDING' ? 'good' : status === 'CHOP' ? 'warn' : status === 'LOCKOUT' ? 'bad' : '';
const statusText = status === 'LOCKOUT' ? '🔒 LOCKOUT' : (status || '—');
const sigText = signal === 'BUY' ? '▲ BUY'
: signal === 'SELL' ? '▼ SELL'
: signal === 'BUY_SETUP' ? '▲ BUY SETUP'
: signal === 'SELL_SETUP' ? '▼ SELL SETUP'
: signal === 'NO_TRADE' ? '✕ NO TRADE'
: signal === 'LOCKOUT_REASON' ? (signalText || 'LOCKOUT') : '—';
const sigCls = signal === 'BUY' || signal === 'BUY_SETUP' ? 'good'
: signal === 'SELL' || signal === 'SELL_SETUP' ? 'bad'
: signal === 'NO_TRADE' ? 'bad'
: signal === 'LOCKOUT_REASON' ? 'bad' : '';
return (
Push
{arrow(push)} {push || '—'}
Score
{arrow(score?.dir)} {score?.value ?? '—'}/10
);
}
/* ───────── STORE-LOCK / COMING-SOON OVERLAY ─────────
Parameterised so the same component can gate Pricing, Tutorials, etc.
Pass a `config` prop with: eyebrow, title, deck, points[], actions[], foot.
*/
function StoreLockOverlay({ config }) {
const c = config || {};
const ghostsRef = useRef(null);
const elsRef = useRef([]);
const mouseRef = useRef({ x: -9999, y: -9999, active: false });
const rafRef = useRef(null);
const [, force] = useState(0);
useEffect(() => {
const W = window.innerWidth, H = window.innerHeight;
const sizes = [56, 64, 70, 80, 60, 110, 130];
ghostsRef.current = sizes.map((size, i) => ({
id: i + 1,
size,
x: Math.random() * (W - size - 40) + 20,
y: Math.random() * (H - size - 40) + 20,
vx: (Math.random() - 0.5) * 0.6,
vy: (Math.random() - 0.5) * 0.6,
phase: Math.random() * Math.PI * 2,
phaseSpeed: 0.0006 + Math.random() * 0.0008,
rot: (Math.random() - 0.5) * 20,
rotPhase: Math.random() * Math.PI * 2,
rotSpeed: 0.0004 + Math.random() * 0.0006,
rotAmp: 18 + Math.random() * 10,
}));
force(n => n + 1);
}, []);
useEffect(() => {
const onMove = (e) => { mouseRef.current = { x: e.clientX, y: e.clientY, active: true }; };
const onLeave = () => { mouseRef.current.active = false; };
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseleave', onLeave);
return () => {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseleave', onLeave);
};
}, []);
useEffect(() => {
const tick = () => {
const arr = ghostsRef.current;
if (arr) {
const W = window.innerWidth, H = window.innerHeight;
const t = performance.now();
for (let i = 0; i < arr.length; i++) {
const g = arr[i];
g.phase += g.phaseSpeed * 16;
g.vx += Math.cos(g.phase) * 0.012;
g.vy += Math.sin(g.phase * 0.9) * 0.012;
if (mouseRef.current.active) {
const cx = g.x + g.size / 2, cy = g.y + g.size / 2;
const ddx = cx - mouseRef.current.x, ddy = cy - mouseRef.current.y;
const dist = Math.hypot(ddx, ddy);
const radius = 240;
if (dist < radius && dist > 0.1) {
const f = (1 - dist / radius);
g.vx += (ddx / dist) * f * 1.4;
g.vy += (ddy / dist) * f * 1.4;
}
}
g.vx *= 0.94; g.vy *= 0.94;
const sp = Math.hypot(g.vx, g.vy);
if (sp > 9) { g.vx = (g.vx / sp) * 9; g.vy = (g.vy / sp) * 9; }
g.x += g.vx; g.y += g.vy;
const m = 8;
if (g.x < m) { g.x = m; g.vx = Math.abs(g.vx) * 0.6; }
if (g.x > W - g.size - m) { g.x = W - g.size - m; g.vx = -Math.abs(g.vx) * 0.6; }
if (g.y < m) { g.y = m; g.vy = Math.abs(g.vy) * 0.6; }
if (g.y > H - g.size - m) { g.y = H - g.size - m; g.vy = -Math.abs(g.vy) * 0.6; }
}
// ghost-vs-ghost collisions: treat each as a circle of radius size/2,
// push apart along the contact normal and exchange a bit of velocity.
for (let i = 0; i < arr.length; i++) {
const a = arr[i];
const ar = a.size / 2;
const acx = a.x + ar, acy = a.y + ar;
for (let j = i + 1; j < arr.length; j++) {
const b = arr[j];
const br = b.size / 2;
const bcx = b.x + br, bcy = b.y + br;
const dx = bcx - acx, dy = bcy - acy;
const d = Math.hypot(dx, dy);
const min = ar + br;
if (d > 0 && d < min) {
const nx = dx / d, ny = dy / d;
const overlap = (min - d);
// separate along the normal
a.x -= nx * overlap * 0.5;
a.y -= ny * overlap * 0.5;
b.x += nx * overlap * 0.5;
b.y += ny * overlap * 0.5;
// velocity along normal — exchange with mild damping
const vrel = (b.vx - a.vx) * nx + (b.vy - a.vy) * ny;
if (vrel < 0) {
const e = 0.85; // restitution
const j2 = -(1 + e) * vrel * 0.5;
a.vx -= j2 * nx; a.vy -= j2 * ny;
b.vx += j2 * nx; b.vy += j2 * ny;
}
}
}
}
// pass 2: write transforms
for (let i = 0; i < arr.length; i++) {
const g = arr[i];
g.rotPhase += g.rotSpeed * 16;
const targetRot = Math.sin(g.rotPhase) * Math.min(g.rotAmp, 30);
g.rot += (targetRot - g.rot) * 0.04;
const el = elsRef.current[i];
if (el) {
const bobY = Math.sin(t / 900 + g.id) * 4;
el.style.transform = `translate3d(${g.x.toFixed(1)}px, ${(g.y + bobY).toFixed(1)}px, 0) rotate(${g.rot.toFixed(2)}deg)`;
}
}
}
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => cancelAnimationFrame(rafRef.current);
}, []);
const ghosts = ghostsRef.current || [];
return (
{ghosts.map((g, i) => (
elsRef.current[i] = el}
className="floater"
style={{ width: g.size, height: g.size, fontSize: g.size * 0.85 }}>👻
))}
{c.eyebrow || 'Coming Soon'}
{c.title || 'In Progress.'}
{c.deck}
{c.points && c.points.length > 0 && (
{c.points.map((p, i) => (
- {p}
))}
)}
{c.actions && c.actions.length > 0 && (
)}
{c.foot && (
{c.foot}
)}
);
}
window.Nav = Nav;
window.Footer = Footer;
window.Dashboard = Dashboard;
window.StoreLockOverlay = StoreLockOverlay;