const { useState, useEffect, useRef } = React;
function Logo({ size = 'md' }) {
// Exact lockup from vyovam.com — tall pill V mark + heavy VYOVAM wordmark, all white on navy
return (
V
VYOVAM
TECH SERVICES LLC
);
}
/* ------------ Theme toggle: Day / Night / Auto ------------ */
function ThemeToggle() {
const [mode, setMode] = useState(() => {
try { return localStorage.getItem('vyovam-theme') || 'auto'; } catch (e) { return 'auto'; }
});
useEffect(() => {
try { localStorage.setItem('vyovam-theme', mode); } catch (e) {}
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const apply = () => {
const dark = mode === 'dark' || (mode === 'auto' && mq.matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
};
apply();
mq.addEventListener('change', apply);
return () => mq.removeEventListener('change', apply);
}, [mode]);
const next = { auto: 'light', light: 'dark', dark: 'auto' };
const labels = { auto: 'Auto', light: 'Day', dark: 'Night' };
const icons = {
light: (
),
dark: (
),
auto: (
)
};
return (
setMode(next[mode])}
title={`Theme: ${labels[mode]} — click to switch`}
aria-label={`Theme: ${labels[mode]}. Click to switch theme.`}>
{icons[mode]}
{labels[mode]}
);
}
function Nav({ page, setPage }) {
const [open, setOpen] = useState(false);
const links = [
{ id: 'home', label: 'Home' },
{ id: 'infrastructure', label: 'Algo Trading' },
{ id: 'ecommerce', label: 'E-Commerce', soon: true },
{ id: 'fluentme', label: 'FluentMe' },
{ id: 'contact', label: 'Contact' }
];
const go = (id) => (e) => { e.preventDefault(); setPage(id); setOpen(false); };
return (
setOpen(!open)}
aria-label={open ? 'Close menu' : 'Open menu'} aria-expanded={open}>
{open
?
: }
{open && (
)}
);
}
/* ------------ Live ticker bar (custom scrolling marquee) ------------
Sensex + Nifty (Algomind) scroll together with the global markets
(Yahoo Finance) via one server-side proxy. Fully theme-aware — no
third-party widget, so no day/night colour desync. */
function Ticker() {
const [items, setItems] = useState([]);
useEffect(() => {
let alive = true;
const load = async () => {
try {
const r = await fetch('ticker-data.php', { cache: 'no-store' });
if (!r.ok) return;
const j = await r.json();
if (alive && j && Array.isArray(j.items) && j.items.length) setItems(j.items);
} catch (e) { /* keep last good data */ }
};
load();
const id = setInterval(load, 30000);
return () => { alive = false; clearInterval(id); };
}, []);
const fmt = (p, dp) => Number(p).toLocaleString('en-US', { minimumFractionDigits: dp, maximumFractionDigits: dp });
const renderItem = (it, key) => {
const pct = Number(it.changePct) || 0;
const cls = pct > 0 ? 'up' : pct < 0 ? 'down' : 'flat';
const arrow = pct > 0 ? '▲' : pct < 0 ? '▼' : '·';
return (
{it.name}
{fmt(it.price, it.dp || 2)}
{arrow} {Math.abs(pct).toFixed(2)}%
·
);
};
const loop = [...items, ...items];
const dur = Math.max(28, items.length * 4.5); // seconds — consistent scroll speed
return (
Live
{items.length ? (
{loop.map((it, i) => renderItem(it, i))}
) : (
Loading live market data…
)}
);
}
/* ------------ Sparkline ------------ */
function Sparkline({ seed = 1, up = true }) {
const points = React.useMemo(() => {
const n = 30;
const arr = [];
let v = 50;
let rng = seed;
const rand = () => { rng = (rng * 9301 + 49297) % 233280; return rng / 233280; };
for (let i = 0; i < n; i++) {
const drift = up ? 0.6 : -0.4;
v += (rand() - 0.5) * 6 + drift;
arr.push(Math.max(10, Math.min(90, v)));
}
return arr;
}, [seed, up]);
const w = 200, h = 48;
const path = points.map((p, i) => {
const x = (i / (points.length - 1)) * w;
const y = h - (p / 100) * h;
return `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`;
}).join(' ');
const area = `${path} L ${w} ${h} L 0 ${h} Z`;
const color = up ? '#4ade80' : '#f87171';
const gradId = `sparkG-${seed}`;
return (
);
}
/* ------------ Star rating ------------ */
function Stars({ count = 5 }) {
return (
{Array.from({ length: count }).map((_, i) => (
))}
);
}
function Footer({ setPage }) {
return (
);
}
function HeroChart() {
// generate a synthetic upward chart with noise
const points = React.useMemo(() => {
const n = 80;
const arr = [];
let v = 30;
for (let i = 0; i < n; i++) {
v += Math.sin(i * 0.4) * 1.2 + i / n * 1.4 + (Math.random() - 0.45) * 2.4;
arr.push(Math.max(8, Math.min(92, v)));
}
return arr;
}, []);
const w = 400,h = 240;
const path = points.map((p, i) => {
const x = i / (points.length - 1) * w;
const y = h - p / 100 * h;
return `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`;
}).join(' ');
const area = `${path} L ${w} ${h} L 0 ${h} Z`;
// Drawing animation
const [drawn, setDrawn] = useState(false);
useEffect(() => {const t = setTimeout(() => setDrawn(true), 100);return () => clearTimeout(t);}, []);
return (
{/* marker dots */}
{[20, 40, 60].map((i) => {
const p = points[i];
const x = i / (points.length - 1) * w;
const y = h - p / 100 * h;
return ;
})}
{/* end marker */}
);
}
function Marquee() {
const items = [
'Equities', 'Derivatives', 'FX', 'Crypto', 'Amazon Seller Central',
'Shopify', 'Walmart Marketplace', 'eBay', 'Inventory Sync',
'Order Automation', 'Backtesting', 'Risk Management'];
// duplicate so loop is seamless
const list = [...items, ...items];
return (
{list.map((s, i) =>
◆ {s}
)}
);
}
function Reveal({ children, delay = 0 }) {
const ref = useRef(null);
const [shown, setShown] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const obs = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setTimeout(() => setShown(true), delay);
obs.unobserve(el);
}
}, { threshold: 0.12 });
obs.observe(el);
return () => obs.disconnect();
}, [delay]);
return (
{children}
);
}
Object.assign(window, { Logo, Nav, Footer, HeroChart, Marquee, Reveal, Ticker, Sparkline, Stars });