Files
lynkeduppro-crm/public/ui.jsx
T
tanweer919 a459fefb7f feat(landing): port the full marketing site into the CRM (static)
Copies the goutam-pages landing (React marketing home + /page/1..19 + assets)
into /public and adds beforeFiles rewrites so '/' serves the marketing home and
'/1'..'/19' + '/thanks' serve the static pages — reproducing the landing's
vercel.json clean URLs. /portal/* (login/register) is untouched.
2026-07-18 00:06:53 +05:30

432 lines
18 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* global React */
// Shared UI primitives for LynkedUp Pro
const { useEffect, useRef, useState } = React;
// ---------- Hex logo (matches flyer mark) ----------
function Logo({ size = 90 }) {
return (
<div className="row gap-12" style={{ alignItems: "center" }}>
<img
src="uploads/lynkedup-logo.png"
alt="LynkedUp Pro"
width={150}
height={30}
style={{ objectFit: "contain", filter: "drop-shadow(0 0 8px rgba(0,209,255,0.6))" }}
/>
</div>
);
}
// ---------- Counter that animates on view ----------
function Counter({ to, suffix = "", prefix = "", duration = 1800, decimals = 0 }) {
const ref = useRef(null);
const [val, setVal] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
const start = performance.now();
const tick = (now) => {
const t = Math.min(1, (now - start) / duration);
const ease = 1 - Math.pow(1 - t, 3);
setVal(to * ease);
if (t < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
obs.disconnect();
}
});
}, { threshold: 0.3 });
obs.observe(el);
return () => obs.disconnect();
}, [to]);
const formatted = decimals
? val.toFixed(decimals)
: Math.round(val).toLocaleString();
return <span ref={ref} className="tabular">{prefix}{formatted}{suffix}</span>;
}
// ---------- Reveal-on-scroll wrapper ----------
function Reveal({ children, delay = 0, className = "", as: As = "div", style = {} }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
el.classList.add("in");
obs.unobserve(el);
}
});
}, { threshold: 0.12, rootMargin: "0px 0px -60px 0px" });
obs.observe(el);
return () => obs.disconnect();
}, []);
const delayCls = delay ? ` delay-${delay}` : "";
return (
<As ref={ref} className={`reveal${delayCls} ${className}`} style={style}>
{children}
</As>
);
}
// ---------- Card with mouse-tracking glow ----------
function GlowCard({ children, className = "", style = {}, onClick }) {
const ref = useRef(null);
const handle = (e) => {
const el = ref.current;
if (!el) return;
const r = el.getBoundingClientRect();
el.style.setProperty("--mx", `${e.clientX - r.left}px`);
el.style.setProperty("--my", `${e.clientY - r.top}px`);
};
return (
<div
ref={ref}
className={`card ${className}`}
style={style}
onMouseMove={handle}
onClick={onClick}
>
<span className="corner tl"></span>
<span className="corner tr"></span>
<span className="corner bl"></span>
<span className="corner br"></span>
{children}
</div>
);
}
// ---------- Wireframe Blueprint House (SVG, animated drawing) ----------
function BlueprintHouse({ size = 600, animate = true, withMeasurements = true, withScan = true }) {
// viewBox 600x460
const dash = animate ? { strokeDasharray: 1400, strokeDashoffset: 1400, animation: "drawIn 4s ease-out forwards" } : {};
return (
<svg viewBox="0 0 600 460" width={size} height={size * 460 / 600} style={{ display: "block", overflow: "visible" }}>
<defs>
<linearGradient id="hglow" x1="0" x2="0" y1="0" y2="1">
<stop offset="0" stopColor="#7be8ff"/>
<stop offset="1" stopColor="#0090c0"/>
</linearGradient>
<filter id="bglow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<pattern id="floorgrid" width="32" height="32" patternUnits="userSpaceOnUse" patternTransform="skewX(-30) skewY(0)">
<path d="M32 0 L0 0 L0 32" fill="none" stroke="rgba(0,209,255,0.22)" strokeWidth="0.5"/>
</pattern>
</defs>
{/* Floor grid */}
<g transform="translate(0,380)">
<rect x="-60" y="0" width="720" height="80" fill="url(#floorgrid)" opacity="0.7"/>
</g>
{/* House outline - two-gable */}
<g stroke="url(#hglow)" strokeWidth="1.5" fill="none" filter="url(#bglow)" style={dash}>
{/* Main body */}
<path d="M60 380 L60 200 L210 90 L360 200 L360 380 Z" />
{/* Right side smaller gable */}
<path d="M360 380 L360 240 L450 180 L540 240 L540 380 Z" />
{/* Roof ridge */}
<line x1="210" y1="90" x2="210" y2="200" opacity="0.4"/>
<line x1="450" y1="180" x2="450" y2="240" opacity="0.4"/>
{/* Chimney */}
<rect x="100" y="135" width="22" height="48"/>
{/* Door */}
<rect x="180" y="290" width="60" height="90"/>
<circle cx="232" cy="335" r="2.5" fill="#7be8ff"/>
{/* Windows */}
<rect x="90" y="240" width="60" height="50"/>
<line x1="120" y1="240" x2="120" y2="290" opacity="0.5"/>
<line x1="90" y1="265" x2="150" y2="265" opacity="0.5"/>
<rect x="270" y="240" width="60" height="50"/>
<line x1="300" y1="240" x2="300" y2="290" opacity="0.5"/>
<line x1="270" y1="265" x2="330" y2="265" opacity="0.5"/>
<rect x="410" y="280" width="80" height="60"/>
<line x1="450" y1="280" x2="450" y2="340" opacity="0.5"/>
<line x1="410" y1="310" x2="490" y2="310" opacity="0.5"/>
{/* Roof structure lines */}
<line x1="60" y1="200" x2="360" y2="200" opacity="0.35"/>
<line x1="360" y1="240" x2="540" y2="240" opacity="0.35"/>
</g>
{/* Measurements */}
{withMeasurements && (
<g fontFamily="var(--font-mono)" fontSize="13" fill="#7be8ff" filter="url(#bglow)">
<g>
<line x1="60" y1="60" x2="210" y2="60" stroke="#7be8ff" strokeWidth="0.8" strokeDasharray="3 3" opacity="0.7"/>
<text x="115" y="52">18-7</text>
</g>
<g>
<line x1="210" y1="60" x2="360" y2="60" stroke="#7be8ff" strokeWidth="0.8" strokeDasharray="3 3" opacity="0.7"/>
<text x="265" y="52">12-3</text>
</g>
<g>
<line x1="360" y1="160" x2="540" y2="160" stroke="#7be8ff" strokeWidth="0.8" strokeDasharray="3 3" opacity="0.7"/>
<text x="430" y="153">15-4</text>
</g>
<g>
<line x1="560" y1="240" x2="560" y2="380" stroke="#7be8ff" strokeWidth="0.8" strokeDasharray="3 3" opacity="0.7"/>
<text x="566" y="315">8-0</text>
</g>
</g>
)}
{/* Corner markers */}
<g fill="#00d1ff">
{[[60,200],[210,90],[360,200],[450,180],[540,240]].map(([x,y],i) => (
<g key={i}>
<circle cx={x} cy={y} r="3.5" />
<circle cx={x} cy={y} r="9" fill="none" stroke="#00d1ff" opacity="0.5">
<animate attributeName="r" values="6;14;6" dur="2.4s" begin={`${i*0.2}s`} repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.7;0;0.7" dur="2.4s" begin={`${i*0.2}s`} repeatCount="indefinite"/>
</circle>
</g>
))}
</g>
{/* AI scan line */}
{withScan && (
<rect x="0" y="80" width="600" height="3" fill="rgba(0,209,255,0.7)" style={{ filter: "blur(2px)" }}>
<animate attributeName="y" values="80;380;80" dur="6s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0;1;0" dur="6s" repeatCount="indefinite" />
</rect>
)}
</svg>
);
}
// ---------- Tiny chart components ----------
function Sparkline({ data, color = "#00d1ff", height = 36, width = 120 }) {
const max = Math.max(...data), min = Math.min(...data);
const range = max - min || 1;
const stepX = width / (data.length - 1);
const pts = data.map((v, i) => `${i * stepX},${height - ((v - min) / range) * height}`).join(" ");
const area = `0,${height} ${pts} ${width},${height}`;
return (
<svg width={width} height={height} style={{ display: "block" }}>
<defs>
<linearGradient id={`sg-${color}`} x1="0" x2="0" y1="0" y2="1">
<stop offset="0" stopColor={color} stopOpacity="0.4"/>
<stop offset="1" stopColor={color} stopOpacity="0"/>
</linearGradient>
</defs>
<polygon points={area} fill={`url(#sg-${color})`} />
<polyline points={pts} fill="none" stroke={color} strokeWidth="1.6"
style={{ filter: `drop-shadow(0 0 6px ${color})` }}/>
</svg>
);
}
function DonutChart({ size = 120, stroke = 12, segments }) {
// segments: [{value, color}]
const r = (size - stroke) / 2;
const c = 2 * Math.PI * r;
const total = segments.reduce((s, x) => s + x.value, 0);
let offset = 0;
return (
<svg width={size} height={size} style={{ transform: "rotate(-90deg)" }}>
<circle cx={size/2} cy={size/2} r={r} fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth={stroke}/>
{segments.map((s, i) => {
const len = (s.value / total) * c;
const el = (
<circle key={i} cx={size/2} cy={size/2} r={r}
fill="none"
stroke={s.color}
strokeWidth={stroke}
strokeDasharray={`${len} ${c - len}`}
strokeDashoffset={-offset}
style={{ filter: `drop-shadow(0 0 4px ${s.color})` }}
/>
);
offset += len;
return el;
})}
</svg>
);
}
// ---------- Icon set (line, glowing) ----------
const Icon = ({ name, size = 22, stroke = "#7be8ff" }) => {
const props = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke, strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", style: { filter: `drop-shadow(0 0 6px ${stroke}55)` } };
switch (name) {
case "ai":
return <svg {...props}><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/><circle cx="12" cy="12" r="4"/><path d="M10 12h4M12 10v4"/></svg>;
case "calendar":
return <svg {...props}><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/><rect x="7" y="13" width="4" height="3" rx="0.5"/></svg>;
case "crew":
return <svg {...props}><circle cx="9" cy="8" r="3"/><circle cx="17" cy="10" r="2.2"/><path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6M14 20c0-2 1.2-3.6 3-4.4"/></svg>;
case "fleet":
return <svg {...props}><circle cx="12" cy="11" r="9"/><path d="M3 11h18M12 2c3 3 3 15 0 18M12 2c-3 3-3 15 0 18"/><circle cx="12" cy="11" r="2" fill={stroke}/></svg>;
case "budget":
return <svg {...props}><path d="M3 17l5-5 4 3 8-9"/><path d="M14 6h6v6"/><path d="M3 21h18"/></svg>;
case "report":
return <svg {...props}><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h8M8 16h5"/></svg>;
case "chat":
return <svg {...props}><path d="M21 12a8 8 0 1 1-3.4-6.5L21 4l-1.4 3.5A8 8 0 0 1 21 12z"/><path d="M8 11h.01M12 11h.01M16 11h.01"/></svg>;
case "field":
return <svg {...props}><path d="M3 12l9-8 9 8M5 10v10h14V10"/><path d="M10 20v-6h4v6"/></svg>;
case "shield":
return <svg {...props}><path d="M12 3l8 3v6c0 5-4 8-8 9-4-1-8-4-8-9V6l8-3z"/><path d="M9 12l2 2 4-4"/></svg>;
case "clock":
return <svg {...props}><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>;
case "target":
return <svg {...props}><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill={stroke}/></svg>;
case "growth":
return <svg {...props}><path d="M3 20V8M9 20V13M15 20v-9M21 20V5"/></svg>;
case "bell":
return <svg {...props}><path d="M18 16v-5a6 6 0 0 0-12 0v5l-2 3h16l-2-3z"/><path d="M10 21a2 2 0 0 0 4 0"/></svg>;
case "pin":
return <svg {...props}><path d="M12 21s7-6 7-12a7 7 0 0 0-14 0c0 6 7 12 7 12z"/><circle cx="12" cy="9" r="2.5"/></svg>;
case "sync":
return <svg {...props}><path d="M21 12a9 9 0 0 1-15.5 6.3M3 12a9 9 0 0 1 15.5-6.3"/><path d="M21 4v5h-5M3 20v-5h5"/></svg>;
case "brain":
return <svg {...props}><path d="M9 4a3 3 0 0 0-3 3v0a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3v0a3 3 0 0 0 3 3"/><path d="M15 4a3 3 0 0 1 3 3v0a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v0a3 3 0 0 1-3 3"/><path d="M12 4v16M9 9h6M9 14h6"/></svg>;
case "doc":
return <svg {...props}><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6M8 13h8M8 17h5"/></svg>;
case "camera":
return <svg {...props}><path d="M4 8h3l2-3h6l2 3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z"/><circle cx="12" cy="13" r="4"/></svg>;
case "hammer":
return <svg {...props}><path d="M15 5l4 4-3 3-4-4 3-3z"/><path d="M12 8L4 16l3 3 8-8"/></svg>;
case "lead":
return <svg {...props}><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/><path d="M16 4l2 2-4 4"/></svg>;
case "money":
return <svg {...props}><rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M5 9v.01M19 15v.01"/></svg>;
case "spark":
return <svg {...props}><path d="M12 3l1.7 5.3L19 10l-5.3 1.7L12 17l-1.7-5.3L5 10l5.3-1.7L12 3z"/></svg>;
case "arrow":
return <svg {...props}><path d="M5 12h14M13 5l7 7-7 7"/></svg>;
case "play":
return <svg {...props}><polygon points="6,4 20,12 6,20" fill={stroke} opacity="0.2"/><polygon points="6,4 20,12 6,20"/></svg>;
default:
return <svg {...props}><circle cx="12" cy="12" r="9"/></svg>;
}
};
// ---------- Dimension corner brackets (blueprint chrome) ----------
function DimCorner({ pos = "tl" }) {
return (
<div className={`dim-corner ${pos}`}>
<svg viewBox="0 0 36 36">
<path d="M0 14 L0 0 L14 0" fill="none" stroke="#00d1ff" strokeWidth="1.2"
style={{ filter: "drop-shadow(0 0 4px #00d1ff)" }}/>
<line x1="0" y1="6" x2="6" y2="6" stroke="#00d1ff" strokeWidth="0.8" opacity="0.6"/>
<line x1="6" y1="0" x2="6" y2="6" stroke="#00d1ff" strokeWidth="0.8" opacity="0.6"/>
<circle cx="0" cy="0" r="2" fill="#00d1ff"/>
</svg>
</div>
);
}
// ---------- Dimension line (engineering measurement) ----------
function DimLine({ orientation = "h", label, length = "100%", offset = 0, side = "top" }) {
const isH = orientation === "h";
return (
<div style={{
position: "absolute",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
[side]: offset,
...(isH ? { left: "10%", right: "10%" } : { top: "10%", bottom: "10%", width: 20 }),
pointerEvents: "none",
}}>
<svg viewBox={isH ? "0 0 200 12" : "0 0 12 200"}
preserveAspectRatio="none"
style={{ width: isH ? "100%" : 12, height: isH ? 12 : "100%" }}>
{isH ? (
<>
<line x1="0" y1="6" x2="200" y2="6" stroke="#7be8ff" strokeWidth="0.6" strokeDasharray="3 3" opacity="0.6"/>
<line x1="0" y1="2" x2="0" y2="10" stroke="#7be8ff" strokeWidth="0.8"/>
<line x1="200" y1="2" x2="200" y2="10" stroke="#7be8ff" strokeWidth="0.8"/>
</>
) : (
<>
<line x1="6" y1="0" x2="6" y2="200" stroke="#7be8ff" strokeWidth="0.6" strokeDasharray="3 3" opacity="0.6"/>
<line x1="2" y1="0" x2="10" y2="0" stroke="#7be8ff" strokeWidth="0.8"/>
<line x1="2" y1="200" x2="10" y2="200" stroke="#7be8ff" strokeWidth="0.8"/>
</>
)}
</svg>
{label && (
<span style={{
position: "absolute",
padding: "2px 6px",
background: "rgba(2,6,14,0.9)",
border: "1px solid rgba(0,209,255,0.3)",
borderRadius: 3,
fontFamily: "var(--font-mono)",
fontSize: 10,
letterSpacing: "0.16em",
color: "var(--cyan-2)",
}}>{label}</span>
)}
</div>
);
}
// ---------- SectionFrame: standard architectural-framed section ----------
function SectionFrame({ children, number, tag, vlabel, id, className = "", style = {} }) {
return (
<section id={id} className={`section ${className}`} style={style}>
<div className="shell" style={{ position: "relative" }}>
{vlabel && (
<>
<div className="vrail"/>
<div className="vrail-label">{vlabel}</div>
</>
)}
{(number || tag) && (
<Reveal>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 40 }}>
{number && (
<span className="section-number">
<span className="num">§{number}</span>
<span>{tag}</span>
</span>
)}
<span className="dim-tag">// {vlabel || "BUILD MK.07"}</span>
</div>
</Reveal>
)}
{children}
</div>
</section>
);
}
// ---------- Section head with rail (number + tag + ticks) ----------
function SectionHead({ number, tag, title, sub, align = "left" }) {
return (
<div className="shead">
<Reveal>
<div className="rail">
<span className="num-big">{number}</span>
<span className="tag">{tag}</span>
<div className="ticks"/>
</div>
</Reveal>
<div>
<Reveal delay={1}>
{typeof title === "string" ? <h2 className="h-section">{title}</h2> : title}
</Reveal>
{sub && (
<Reveal delay={2}>
<p className="sub" style={{ marginTop: 20 }}>{sub}</p>
</Reveal>
)}
</div>
</div>
);
}
// Export to window for cross-script access
Object.assign(window, { Logo, Counter, Reveal, GlowCard, BlueprintHouse, Sparkline, DonutChart, Icon, DimCorner, DimLine, SectionFrame, SectionHead });