forked from Goutam/lynkeduppro-crm
a459fefb7f
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.
432 lines
18 KiB
React
432 lines
18 KiB
React
/* 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 });
|