866 lines
44 KiB
React
866 lines
44 KiB
React
|
|
/* global React */
|
|||
|
|
const { useState, useEffect, useRef, Fragment } = React;
|
|||
|
|
|
|||
|
|
// ─── Icons ───────────────────────────────────────────────────────────────────
|
|||
|
|
const Icon = ({ size = 16, sw = 1.75, className = "", children }) => (
|
|||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24"
|
|||
|
|
fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|||
|
|
{children}
|
|||
|
|
</svg>
|
|||
|
|
);
|
|||
|
|
const ChevronDown = (p) => <Icon size={16} {...p}><polyline points="6 9 12 15 18 9"/></Icon>;
|
|||
|
|
const Menu = (p) => <Icon size={24} {...p}><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="18" y2="18"/></Icon>;
|
|||
|
|
const X = (p) => <Icon size={24} {...p}><path d="M18 6 6 18"/><path d="m6 6 12 12"/></Icon>;
|
|||
|
|
const ArrowRight = (p) => <Icon size={16} {...p}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></Icon>;
|
|||
|
|
const ArrowLeft = (p) => <Icon size={16} {...p}><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></Icon>;
|
|||
|
|
const ArrowUpRight = (p) => <Icon size={14} {...p}><path d="M7 17 17 7"/><path d="M7 7h10v10"/></Icon>;
|
|||
|
|
const Mail = (p) => <Icon size={16} {...p}><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></Icon>;
|
|||
|
|
const MapPin = (p) => <Icon size={16} {...p}><path d="M20 10c0 7-8 13-8 13s-8-6-8-13a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></Icon>;
|
|||
|
|
const Linkedin = (p) => <Icon size={16} {...p}><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect width="4" height="12" x="2" y="9"/><circle cx="4" cy="4" r="2"/></Icon>;
|
|||
|
|
const ExternalLink = (p) => <Icon size={13} {...p}><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></Icon>;
|
|||
|
|
const Radio = (p) => <Icon {...p}><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.2 19.1 19.1"/></Icon>;
|
|||
|
|
const Tv = (p) => <Icon {...p}><rect width="20" height="15" x="2" y="7" rx="2" ry="2"/><polyline points="17 2 12 7 7 2"/></Icon>;
|
|||
|
|
const Cloud = (p) => <Icon {...p}><path d="M17.5 19a4.5 4.5 0 1 0-1.41-8.775A6.5 6.5 0 1 0 7 19h10.5z"/></Icon>;
|
|||
|
|
const Cable = (p) => <Icon {...p}><path d="M4 9a2 2 0 0 1-2-2V5h6v2a2 2 0 0 1-2 2Z"/><path d="M3 5V3"/><path d="M7 5V3"/><path d="M19 15V6.5a3.5 3.5 0 0 0-7 0v11a3.5 3.5 0 0 1-7 0V9"/><path d="M17 21v-2"/><path d="M21 21v-2"/><path d="M22 19v-2a2 2 0 0 0-2-2h-2v4Z"/></Icon>;
|
|||
|
|
const Ruler = (p) => <Icon {...p}><path d="M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z"/><path d="m14.5 12.5 2-2"/><path d="m11.5 9.5 2-2"/><path d="m8.5 6.5 2-2"/><path d="m17.5 15.5 2-2"/></Icon>;
|
|||
|
|
const Wrench = (p) => <Icon {...p}><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></Icon>;
|
|||
|
|
const MonitorCheck = (p) => <Icon {...p}><path d="m9 10 2 2 4-4"/><rect width="20" height="14" x="2" y="3" rx="2"/><path d="M12 17v4"/><path d="M8 21h8"/></Icon>;
|
|||
|
|
const Headset = (p) => <Icon {...p}><path d="M3 11h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H3v-7a9 9 0 0 1 18 0v7h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3"/></Icon>;
|
|||
|
|
const Camera = (p) => <Icon {...p}><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></Icon>;
|
|||
|
|
const Film = (p) => <Icon {...p}><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 3v18"/><path d="M3 7.5h4"/><path d="M3 12h18"/><path d="M3 16.5h4"/><path d="M17 3v18"/><path d="M17 7.5h4"/><path d="M17 16.5h4"/></Icon>;
|
|||
|
|
const Package = (p) => <Icon {...p}><path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></Icon>;
|
|||
|
|
const Monitor = (p) => <Icon {...p}><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></Icon>;
|
|||
|
|
|
|||
|
|
// ─── Reveal wrapper ──────────────────────────────────────────────────────────
|
|||
|
|
function Reveal({ children, delay = 0, className = "" }) {
|
|||
|
|
const ref = useRef(null);
|
|||
|
|
const [visible, setVisible] = useState(false);
|
|||
|
|
useEffect(() => {
|
|||
|
|
const obs = new IntersectionObserver(([e]) => {
|
|||
|
|
if (e.isIntersecting) {
|
|||
|
|
setTimeout(() => setVisible(true), delay);
|
|||
|
|
obs.unobserve(e.target);
|
|||
|
|
}
|
|||
|
|
}, { threshold: 0.1, rootMargin: "0px 0px -60px 0px" });
|
|||
|
|
if (ref.current) obs.observe(ref.current);
|
|||
|
|
return () => obs.disconnect();
|
|||
|
|
}, [delay]);
|
|||
|
|
return <div ref={ref} className={`reveal ${visible ? "visible" : ""} ${className}`}>{children}</div>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Cursor follower ─────────────────────────────────────────────────────────
|
|||
|
|
function CursorDot() {
|
|||
|
|
const ref = useRef(null);
|
|||
|
|
useEffect(() => {
|
|||
|
|
const onMove = (e) => {
|
|||
|
|
if (ref.current) {
|
|||
|
|
ref.current.style.transform = `translate(${e.clientX}px, ${e.clientY}px) translate(-50%, -50%)`;
|
|||
|
|
}
|
|||
|
|
document.body.dataset.cursor = "on";
|
|||
|
|
};
|
|||
|
|
window.addEventListener("mousemove", onMove);
|
|||
|
|
return () => window.removeEventListener("mousemove", onMove);
|
|||
|
|
}, []);
|
|||
|
|
return <div ref={ref} className="cursor-dot"></div>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Page-load curtain ───────────────────────────────────────────────────────
|
|||
|
|
function Curtain() {
|
|||
|
|
const [gone, setGone] = useState(false);
|
|||
|
|
useEffect(() => { const t = setTimeout(() => setGone(true), 1300); return () => clearTimeout(t); }, []);
|
|||
|
|
if (gone) return null;
|
|||
|
|
return <div className="curtain"></div>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── NAVIGATION ──────────────────────────────────────────────────────────────
|
|||
|
|
function Navigation() {
|
|||
|
|
const [scrolled, setScrolled] = useState(false);
|
|||
|
|
const [open, setOpen] = useState(false);
|
|||
|
|
useEffect(() => {
|
|||
|
|
const onScroll = () => setScrolled(window.scrollY > 50);
|
|||
|
|
window.addEventListener("scroll", onScroll);
|
|||
|
|
return () => window.removeEventListener("scroll", onScroll);
|
|||
|
|
}, []);
|
|||
|
|
const links = [{href:"#about",label:"About"},{href:"#projects",label:"Projects"},{href:"#on-set",label:"On Set"},{href:"#contact",label:"Contact"}];
|
|||
|
|
return (
|
|||
|
|
<nav className={`nav ${scrolled ? "scrolled" : ""}`}>
|
|||
|
|
<div className="nav-inner">
|
|||
|
|
<a href="#" className="nav-logo">
|
|||
|
|
<img src={__R("images/dragon-mark.png")} alt="Wild Dragon"/>
|
|||
|
|
<span>Wild Dragon</span>
|
|||
|
|
</a>
|
|||
|
|
<div className="nav-links">
|
|||
|
|
{links.map(l => <a key={l.href} href={l.href}>{l.label}</a>)}
|
|||
|
|
</div>
|
|||
|
|
<button className="nav-toggle" onClick={() => setOpen(!open)} aria-label="Toggle menu">
|
|||
|
|
{open ? <X/> : <Menu/>}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className={`nav-mobile ${open ? "open" : ""}`}>
|
|||
|
|
{links.map(l => <a key={l.href} href={l.href} onClick={()=>setOpen(false)}>{l.label}</a>)}
|
|||
|
|
</div>
|
|||
|
|
</nav>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── HERO ────────────────────────────────────────────────────────────────────
|
|||
|
|
const HERO_ROLES = [
|
|||
|
|
"Broadcast Systems Integration",
|
|||
|
|
"Production Facility Design",
|
|||
|
|
"IP & Cloud Production",
|
|||
|
|
"Extended Reality Stages",
|
|||
|
|
];
|
|||
|
|
function Hero({ ctaLabel }) {
|
|||
|
|
const [loaded, setLoaded] = useState(false);
|
|||
|
|
const [idx, setIdx] = useState(0);
|
|||
|
|
const [text, setText] = useState("");
|
|||
|
|
const [typing, setTyping] = useState(true);
|
|||
|
|
useEffect(() => { const t = setTimeout(() => setLoaded(true), 100); return () => clearTimeout(t); }, []);
|
|||
|
|
useEffect(() => {
|
|||
|
|
const role = HERO_ROLES[idx];
|
|||
|
|
if (typing) {
|
|||
|
|
if (text.length < role.length) {
|
|||
|
|
const t = setTimeout(() => setText(role.slice(0, text.length + 1)), 50);
|
|||
|
|
return () => clearTimeout(t);
|
|||
|
|
} else {
|
|||
|
|
const t = setTimeout(() => setTyping(false), 2500);
|
|||
|
|
return () => clearTimeout(t);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if (text.length > 0) {
|
|||
|
|
const t = setTimeout(() => setText(text.slice(0, -1)), 25);
|
|||
|
|
return () => clearTimeout(t);
|
|||
|
|
} else {
|
|||
|
|
setIdx((i) => (i + 1) % HERO_ROLES.length);
|
|||
|
|
setTyping(true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, [text, typing, idx]);
|
|||
|
|
|
|||
|
|
const aniBase = (d) => ({transition:`opacity .9s ${d}s, transform .9s ${d}s`, opacity: loaded?1:0, transform: loaded?"translateY(0)":"translateY(24px)"});
|
|||
|
|
return (
|
|||
|
|
<section className="hero grain">
|
|||
|
|
<div className="hero-bg"><img src={__R("images/photos/production-switcher.jpg")} alt="Production switcher in a broadcast control room"/></div>
|
|||
|
|
<div className="hero-grid"></div>
|
|||
|
|
<div className="hero-fade-v"></div>
|
|||
|
|
<div className="hero-fade-h"></div>
|
|||
|
|
<div className="hero-corner tl">// 38.9°N · 77.0°W</div>
|
|||
|
|
<div className="hero-corner tr">EST. 2024 · DC</div>
|
|||
|
|
<div className="hero-corner bl">REC · LIVE</div>
|
|||
|
|
<div className="hero-corner br">● 01 / 09</div>
|
|||
|
|
<div className="hero-content">
|
|||
|
|
<div className="hero-logo" style={aniBase(0)}>
|
|||
|
|
<img src={__R("images/wild-dragon-logo.png")} alt="Wild Dragon — Zachary Gaetano"/>
|
|||
|
|
</div>
|
|||
|
|
<div className="hero-tag" style={aniBase(.15)}>
|
|||
|
|
{text}<span className="caret">|</span>
|
|||
|
|
</div>
|
|||
|
|
<h1 style={aniBase(.3)}>
|
|||
|
|
Zachary<br/><span className="last">Gaetano</span>
|
|||
|
|
</h1>
|
|||
|
|
<div className="hero-rule" style={{transition:"opacity 1s .4s, width 1s .4s", opacity: loaded?1:0, width: loaded?"6rem":0}}></div>
|
|||
|
|
<p className="hero-desc" style={aniBase(.5)}>
|
|||
|
|
Designing and integrating broadcast production facilities for sports, corporate, aerospace, and financial organizations.
|
|||
|
|
</p>
|
|||
|
|
<div className="hero-ctas" style={aniBase(.65)}>
|
|||
|
|
<a href="#projects" className="btn btn-primary">{ctaLabel || "View Projects"}</a>
|
|||
|
|
<a href="mailto:zgaetano@wilddragon.net" className="btn btn-ghost-dark">Get in Touch</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<a href="#clients" className="scroll-indicator" style={{transition:"opacity 1s .8s", opacity: loaded?1:0}}>
|
|||
|
|
<span>Scroll</span>
|
|||
|
|
<ChevronDown/>
|
|||
|
|
</a>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── CLIENTS — marquee or static ─────────────────────────────────────────────
|
|||
|
|
const CLIENTS = [
|
|||
|
|
{ name:"Broadcast Management Group", logo:__R("images/clients/bmg.png") },
|
|||
|
|
{ name:"AVI-SPL", logo:__R("images/clients/avispl.png") },
|
|||
|
|
{ name:"NBC Sports", logo:__R("images/clients/nbc-sports.png") },
|
|||
|
|
{ name:"Washington Commanders", logo:__R("images/clients/commanders.png") },
|
|||
|
|
{ name:"CVS / Aetna", logo:__R("images/clients/cvs.png") },
|
|||
|
|
{ name:"UBS", logo:__R("images/clients/ubs.png") },
|
|||
|
|
{ name:"BetMGM", logo:__R("images/clients/betmgm.svg") },
|
|||
|
|
{ name:"Intuit", logo:__R("images/clients/intuit.png") },
|
|||
|
|
{ name:"Monumental Sports", logo:__R("images/clients/monumental.svg") },
|
|||
|
|
{ name:"COSM", logo:__R("images/clients/cosm.svg") },
|
|||
|
|
{ name:"Red Sands", logo:__R("images/clients/redsands.svg") },
|
|||
|
|
];
|
|||
|
|
function Clients({ marquee }) {
|
|||
|
|
return (
|
|||
|
|
<section id="clients" className="clients" aria-label="Clients">
|
|||
|
|
<div className="container">
|
|||
|
|
<Reveal>
|
|||
|
|
<p className="clients-label">— Trusted by —</p>
|
|||
|
|
</Reveal>
|
|||
|
|
</div>
|
|||
|
|
{marquee ? (
|
|||
|
|
<div className="marquee">
|
|||
|
|
<div className="marquee-track">
|
|||
|
|
{[...CLIENTS, ...CLIENTS].map((c, i) => (
|
|||
|
|
<div className="marquee-item" key={i} title={c.name}>
|
|||
|
|
<img src={c.logo} alt={c.name}/>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="container">
|
|||
|
|
<Reveal>
|
|||
|
|
<div className="clients-static">
|
|||
|
|
{CLIENTS.map((c) => (
|
|||
|
|
<div className="marquee-item" key={c.name} title={c.name}>
|
|||
|
|
<img src={c.logo} alt={c.name}/>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── PARTNERS ────────────────────────────────────────────────────────────────
|
|||
|
|
const PARTNERS = [
|
|||
|
|
{ name:"THOR Broadcast", logo:__R("images/clients/thor-logo.png") },
|
|||
|
|
{ name:"Ross Video", logo:__R("images/clients/ross-video.png") },
|
|||
|
|
{ name:"Filmtools", logo:__R("images/clients/filmtools.png") },
|
|||
|
|
{ name:"Forecast Consoles", logo:__R("images/clients/forecast.png") },
|
|||
|
|
{ name:"vMix by Studio Coast", logo:__R("images/clients/vmix.png") },
|
|||
|
|
{ name:"RED Digital Cinema", logo:__R("images/partners/red-digital.png") },
|
|||
|
|
];
|
|||
|
|
function Partners() {
|
|||
|
|
return (
|
|||
|
|
<section className="partners">
|
|||
|
|
<div className="container">
|
|||
|
|
<Reveal>
|
|||
|
|
<p>// Technology Partners</p>
|
|||
|
|
<div className="partners-grid">
|
|||
|
|
{PARTNERS.map(p => (
|
|||
|
|
<div className="partner-logo" title={p.name} key={p.name}>
|
|||
|
|
<img src={p.logo} alt={p.name}/>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── ABOUT ───────────────────────────────────────────────────────────────────
|
|||
|
|
const CAPS = [
|
|||
|
|
{ icon: Radio, title:"Systems Design", desc:"End-to-end broadcast facility design including signal flow engineering, equipment specification, and architectural documentation." },
|
|||
|
|
{ icon: Cable, title:"Integration", desc:"Full system integration from rack builds and wiring to commissioning, testing, and operator training." },
|
|||
|
|
{ icon: Cloud, title:"Cloud & IP", desc:"Cloud-native production architectures, SMPTE ST 2110, NDI, SRT, and hybrid on-prem/cloud workflows." },
|
|||
|
|
{ icon: Tv, title:"Broadcast Engineering", desc:"Live production engineering, RF systems, video routing, and real-time broadcast operations." },
|
|||
|
|
];
|
|||
|
|
function About() {
|
|||
|
|
return (
|
|||
|
|
<section id="about" className="about">
|
|||
|
|
<div className="container">
|
|||
|
|
<Reveal>
|
|||
|
|
<div className="about-intro">
|
|||
|
|
<div className="section-counter"><span className="num">01</span><span className="bar"></span><span>About</span></div>
|
|||
|
|
<h2 className="section-title">From live production to<br/>systems architecture.</h2>
|
|||
|
|
<div className="body" style={{marginTop:"2rem"}}>
|
|||
|
|
<p>I'm a broadcast engineer and systems integrator based in the Washington DC area, with a career that spans both sides of the industry — production and infrastructure.</p>
|
|||
|
|
<p>My background in live production as a 1st AC, DIT, Camera Operator, and Trinity Operator gives me an operator's perspective that most systems designers don't have. I build facilities that work the way production teams actually need them to — not just the way engineers imagine they will.</p>
|
|||
|
|
<p>Today, my focus is broadcast systems integration. As a principal systems designer with <a className="inline" href="https://broadcastmgmt.com" target="_blank" rel="noopener noreferrer">Broadcast Management Group</a>, I design production facilities for organizations including the <strong>Washington Commanders</strong>, <strong>CVS/Aetna</strong>, <strong>UBS</strong>, <strong>BetMGM</strong>, <strong>Intuit</strong>, and <strong>Monumental Sports</strong> — engineering control rooms, studios, XR stages, and IP-based workflows across sports, corporate, financial services, aerospace, and defense.</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
<div className="cap-grid">
|
|||
|
|
{CAPS.map((c, i) => (
|
|||
|
|
<Reveal key={c.title} delay={i*100}>
|
|||
|
|
<div className="cap">
|
|||
|
|
<div className="cap-icon"><c.icon size={20} sw={1.5}/></div>
|
|||
|
|
<h3>{c.title}</h3>
|
|||
|
|
<p>{c.desc}</p>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── STATS — dark band ───────────────────────────────────────────────────────
|
|||
|
|
const STATS = [
|
|||
|
|
{ v:10, suf:"+", label:"Years in Broadcast", desc:"Production & engineering" },
|
|||
|
|
{ v:8, suf:"+", label:"Major Facilities", desc:"Designed & integrated" },
|
|||
|
|
{ v:5900, suf:"+", label:"End Users Served", desc:"Global content delivery" },
|
|||
|
|
{ v:5, suf:"", label:"Industry Verticals", desc:"Sports, corporate, financial, aerospace, defense" },
|
|||
|
|
];
|
|||
|
|
function AnimatedNumber({ value, suffix }) {
|
|||
|
|
const [n, setN] = useState(0);
|
|||
|
|
const ref = useRef(null);
|
|||
|
|
const done = useRef(false);
|
|||
|
|
useEffect(() => {
|
|||
|
|
const obs = new IntersectionObserver(([e]) => {
|
|||
|
|
if (e.isIntersecting && !done.current) {
|
|||
|
|
done.current = true;
|
|||
|
|
const dur = 2000, steps = 60, inc = value/steps;
|
|||
|
|
let cur = 0;
|
|||
|
|
const t = setInterval(() => {
|
|||
|
|
cur += inc;
|
|||
|
|
if (cur >= value) { setN(value); clearInterval(t); }
|
|||
|
|
else setN(Math.floor(cur));
|
|||
|
|
}, dur/steps);
|
|||
|
|
}
|
|||
|
|
}, { threshold: .3 });
|
|||
|
|
if (ref.current) obs.observe(ref.current);
|
|||
|
|
return () => obs.disconnect();
|
|||
|
|
}, [value]);
|
|||
|
|
const display = n >= 1000 && value >= 1000 ? `${(n/1000).toFixed(1).replace(/\.0$/,"")}k` : n;
|
|||
|
|
return <span ref={ref} className="tabular-nums">{display}<span className="suf">{suffix}</span></span>;
|
|||
|
|
}
|
|||
|
|
function Stats() {
|
|||
|
|
return (
|
|||
|
|
<section className="stats grain dark-section">
|
|||
|
|
<div className="container">
|
|||
|
|
<div className="stats-grid">
|
|||
|
|
{STATS.map((s, i) => (
|
|||
|
|
<Reveal key={s.label} delay={i*80}>
|
|||
|
|
<div>
|
|||
|
|
<div className="stat-value"><AnimatedNumber value={s.v} suffix={s.suf}/></div>
|
|||
|
|
<p className="stat-label">{s.label}</p>
|
|||
|
|
<p className="stat-desc">{s.desc}</p>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── SERVICES — timeline ─────────────────────────────────────────────────────
|
|||
|
|
const SERVICES = [
|
|||
|
|
{ icon: Ruler, phase:"01", title:"Design", desc:"Signal flow engineering, system architecture, and equipment specification. Every facility starts with documentation that defines how signals move and how operators interact." },
|
|||
|
|
{ icon: Wrench, phase:"02", title:"Integrate", desc:"Full system builds from rack fabrication and cable infrastructure through to final termination and labeling — clean builds technicians can troubleshoot and maintain for years." },
|
|||
|
|
{ icon: MonitorCheck, phase:"03", title:"Commission", desc:"End-to-end system testing, calibration, and performance verification. Every signal path tested, every failover validated, every workflow documented before handoff." },
|
|||
|
|
{ icon: Headset, phase:"04", title:"Support", desc:"Operator training, documentation packages, and ongoing technical support. Systems designed with the people who use them in mind — not just the engineers who build them." },
|
|||
|
|
];
|
|||
|
|
function Services() {
|
|||
|
|
return (
|
|||
|
|
<section id="services" className="services">
|
|||
|
|
<div className="container">
|
|||
|
|
<Reveal>
|
|||
|
|
<div style={{maxWidth:"48rem", marginBottom:"4rem"}}>
|
|||
|
|
<div className="section-counter"><span className="num">02</span><span className="bar"></span><span>Process</span></div>
|
|||
|
|
<h2 className="section-title" style={{marginBottom:"1.25rem"}}>From blueprint to broadcast.</h2>
|
|||
|
|
<p className="section-lede">A complete lifecycle approach to broadcast facility design and systems integration. Every project follows a disciplined process so systems work flawlessly when the red light goes on.</p>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
<div className="svc-timeline">
|
|||
|
|
<div className="svc-line"></div>
|
|||
|
|
<div className="svc-grid">
|
|||
|
|
{SERVICES.map((s, i) => (
|
|||
|
|
<Reveal key={s.title} delay={i*100}>
|
|||
|
|
<div className="svc">
|
|||
|
|
<div className="svc-node">{s.phase}</div>
|
|||
|
|
<div className="svc-icon"><s.icon size={20} sw={1.5}/></div>
|
|||
|
|
<h3>{s.title}</h3>
|
|||
|
|
<p>{s.desc}</p>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── PROJECTS — featured + grid ──────────────────────────────────────────────
|
|||
|
|
function pickScopeChips(scopes) {
|
|||
|
|
return (scopes || []).slice(0, 3);
|
|||
|
|
}
|
|||
|
|
function Projects({ onOpen }) {
|
|||
|
|
const list = window.PROJECTS || [];
|
|||
|
|
const [featured, ...rest] = list;
|
|||
|
|
return (
|
|||
|
|
<section id="projects" className="projects" aria-label="Selected projects">
|
|||
|
|
<div className="container">
|
|||
|
|
<Reveal>
|
|||
|
|
<div className="projects-head">
|
|||
|
|
<div>
|
|||
|
|
<div className="section-counter"><span className="num">03</span><span className="bar"></span><span>Projects</span></div>
|
|||
|
|
<h2 className="section-title">Selected work.</h2>
|
|||
|
|
</div>
|
|||
|
|
<p className="lede">Broadcast facility design and systems integration for enterprise, sports, aerospace, and financial services clients.</p>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
{featured && (
|
|||
|
|
<Reveal>
|
|||
|
|
<a className="proj-featured" href={`#/projects/${featured.slug}`} onClick={(e)=>{e.preventDefault(); onOpen(featured.slug);}}>
|
|||
|
|
<img src={featured.thumbnail} alt={`${featured.client} — ${featured.category}`}/>
|
|||
|
|
<div className="proj-featured-body">
|
|||
|
|
<div className="proj-featured-meta">
|
|||
|
|
<span className="badge">Featured</span>
|
|||
|
|
<span className="yr">{featured.category} · {featured.year}</span>
|
|||
|
|
</div>
|
|||
|
|
<h3>{featured.client}</h3>
|
|||
|
|
<p>{featured.summary}</p>
|
|||
|
|
<span className="cta">View case study <ArrowRight/></span>
|
|||
|
|
</div>
|
|||
|
|
</a>
|
|||
|
|
</Reveal>
|
|||
|
|
)}
|
|||
|
|
<div className="proj-grid">
|
|||
|
|
{rest.map((p, i) => (
|
|||
|
|
<Reveal key={p.slug} delay={i*60}>
|
|||
|
|
<a className="proj-card"
|
|||
|
|
href={`#/projects/${p.slug}`}
|
|||
|
|
onClick={(e)=>{e.preventDefault(); onOpen(p.slug);}}>
|
|||
|
|
<img src={p.thumbnail} alt={`${p.client} — ${p.category}`}/>
|
|||
|
|
<div className="proj-card-overlay"></div>
|
|||
|
|
<div className="proj-arrow"><ArrowUpRight/></div>
|
|||
|
|
<div className="proj-card-body">
|
|||
|
|
<div className="proj-card-meta">
|
|||
|
|
<span className="cat">{p.category}</span>
|
|||
|
|
<span className="sep">/</span>
|
|||
|
|
<span className="yr">{p.year}</span>
|
|||
|
|
</div>
|
|||
|
|
<h3>{p.client}</h3>
|
|||
|
|
<p className="summary">{p.summary}</p>
|
|||
|
|
<div className="scope-chips">
|
|||
|
|
{pickScopeChips(p.scope).map(s => <span key={s}>{s}</span>)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</a>
|
|||
|
|
</Reveal>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── TECH — spec sheet ───────────────────────────────────────────────────────
|
|||
|
|
const TECH_CATS = [
|
|||
|
|
{ name:"Routing & Switching", items:["Ross Ultrix","Blackmagic ATEM","Evertz EQX","Panasonic Kairos"] },
|
|||
|
|
{ name:"All-In-One Production", items:["vMix","Vizrt","AMPP"] },
|
|||
|
|
{ name:"Signal Transport", items:["SMPTE 2110","NDI","SRT","SDI","Dante","MADI","JPEG XS"] },
|
|||
|
|
{ name:"Infrastructure", items:["IP Networking","Fiber Infrastructure","KVM Systems","UPS & Power","Cooling & Ventilation"] },
|
|||
|
|
{ name:"Display & XR", items:["LED Processing","Unreal Engine","Camera Tracking","Projection","Color Management"] },
|
|||
|
|
];
|
|||
|
|
function TechStack() {
|
|||
|
|
return (
|
|||
|
|
<section className="tech grain dark-section">
|
|||
|
|
<div className="container">
|
|||
|
|
<Reveal>
|
|||
|
|
<div style={{maxWidth:"48rem", marginBottom:"3rem"}}>
|
|||
|
|
<div className="section-counter"><span className="num">04</span><span className="bar"></span><span>Technology</span></div>
|
|||
|
|
<h2 className="section-title" style={{marginBottom:"1.25rem"}}>Tools of the trade.</h2>
|
|||
|
|
<p className="section-lede">Deep experience across the full broadcast technology stack — from traditional SDI infrastructure to modern IP and cloud-native production platforms.</p>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
<div className="tech-table">
|
|||
|
|
{TECH_CATS.map((c, i) => (
|
|||
|
|
<Reveal key={c.name} delay={i*60}>
|
|||
|
|
<div className="tech-row">
|
|||
|
|
<h3>{c.name}</h3>
|
|||
|
|
<div className="items">
|
|||
|
|
{c.items.map(it => <span key={it}>{it}</span>)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── ON SET ──────────────────────────────────────────────────────────────────
|
|||
|
|
const REEL_URL = "https://player.vimeo.com/video/225920311?h=0&badge=0&autopause=0&player_id=0&app_id=58479";
|
|||
|
|
const CREDITS = [
|
|||
|
|
{ t:"Bethesda Fallout Day", r:"1st AC", f:"Corporate / Event", y:"2025" },
|
|||
|
|
{ t:"ZeniMax Elder Scrolls Online Update", r:"1st AC", f:"Corporate", y:"2025" },
|
|||
|
|
{ t:"Palo Alto Ad", r:"Arri Trinity Operator", f:"Commercial", y:"2025" },
|
|||
|
|
{ t:"Street Fighter DLC Ad", r:"Arri Trinity Operator", f:"Commercial", y:"2025" },
|
|||
|
|
{ t:"Chase Small Business Promo", r:"Arri Trinity Operator", f:"Commercial", y:"2025" },
|
|||
|
|
{ t:"ZeniMax Elder Scrolls Online Update", r:"1st AC", f:"Corporate", y:"2024" },
|
|||
|
|
{ t:"Joe Vogel For Congress", r:"Camera Op / Trinity Op", f:"Political", y:"2024" },
|
|||
|
|
{ t:"I Am a Champion", r:"Camera Op / Trinity Op", f:"Sports", y:"2023" },
|
|||
|
|
{ t:"Washington Commanders Schedule Release", r:"Director of Photography", f:"Sports", y:"2023" },
|
|||
|
|
{ t:"Stephen Sharer – MIDNIGHT", r:"DP / Trinity Operator", f:"Narrative", y:"2023" },
|
|||
|
|
{ t:"Stephen Sharer – YOU and I", r:"DP / Trinity Operator", f:"Narrative", y:"2023" },
|
|||
|
|
{ t:"A Chocolate Lens", r:"Camera Operator", f:"Narrative", y:"2023" },
|
|||
|
|
{ t:"Capital One: Pride Month", r:"Director of Photography", f:"Corporate", y:"2021" },
|
|||
|
|
{ t:"Tafon Nchukwi: My Goal Is to Be UFC Champion", r:"Camera Operator", f:"Sports Doc", y:"2020" },
|
|||
|
|
{ t:"Tinder", r:"Director of Photography", f:"Commercial", y:"2020" },
|
|||
|
|
{ t:"Conewago", r:"DP / Editor", f:"Documentary", y:"2020" },
|
|||
|
|
{ t:"In Memoriam", r:"Director of Photography", f:"Documentary", y:"2019" },
|
|||
|
|
];
|
|||
|
|
const GEAR = [
|
|||
|
|
{ cat:"Cinema Cameras", icon: Camera, items:["RED Komodo-X","RED DSMC3 V-Raptor-X 8K VV + 6K S35 Dual-Format (Canon RF)"] },
|
|||
|
|
{ cat:"Lenses", icon: Film, items:["Mamiya 645 Sekor C PL 7 Lens Set","Mamiya 645 Sekor C 35mm","Mamiya 645 Sekor C 110mm","Mamiya 645 Sekor C 150mm","16-30mm / 28-75mm / 75-180mm T2.9 Zoom Set"] },
|
|||
|
|
{ cat:"Monitoring & Transmission", icon: Monitor, items:["SmallHD DSMC3 7\"","SmallHD Cine7 with Bolt 6 1500RX","Teradek Bolt 6 Max XT Kit (TX + RX)","DJI SDR Transmission"] },
|
|||
|
|
{ cat:"Camera Support", icon: Package, items:["Arri Trinity — Certified Owner & Operator","Dana Dolly Kit (8 / 4ft Speedrail, 2× Matthews Stands)","DJI Focus Pro All In One"] },
|
|||
|
|
];
|
|||
|
|
function OnSet() {
|
|||
|
|
return (
|
|||
|
|
<section id="on-set" className="onset">
|
|||
|
|
<div className="container onset-stack">
|
|||
|
|
<Reveal>
|
|||
|
|
<div className="onset-intro">
|
|||
|
|
<div className="section-counter"><span className="num">05</span><span className="bar"></span><span>On Set</span></div>
|
|||
|
|
<h2 className="section-title">Trinity Operator & DIT, <span className="light">Washington DC.</span></h2>
|
|||
|
|
<p className="section-lede" style={{marginTop:"1.25rem"}}>Certified Arri Trinity owner and operator with RED camera packages and specialized live production gear — from run-and-gun documentary to multi-camera sports and political campaigns. Available for commercial, corporate, sports, and narrative work throughout the DMV.</p>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
<Reveal>
|
|||
|
|
<div>
|
|||
|
|
<p className="eyebrow">Reel</p>
|
|||
|
|
<div className="reel-frame">
|
|||
|
|
<iframe src={REEL_URL} allow="autoplay; fullscreen; picture-in-picture" allowFullScreen title="Zachary Gaetano — Showreel"></iframe>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
<Reveal>
|
|||
|
|
<div>
|
|||
|
|
<p className="eyebrow">Selected Credits</p>
|
|||
|
|
<div className="credits-list">
|
|||
|
|
{CREDITS.map((c, i) => (
|
|||
|
|
<div key={i} className="credit-row">
|
|||
|
|
<span className="t">{c.t}</span>
|
|||
|
|
<span className="r">{c.r}</span>
|
|||
|
|
<span className="f">{c.f}</span>
|
|||
|
|
<span className="y">{c.y}</span>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
<div>
|
|||
|
|
<Reveal><p className="eyebrow">Kit</p></Reveal>
|
|||
|
|
<div className="gear-grid">
|
|||
|
|
{GEAR.map((g, i) => (
|
|||
|
|
<Reveal key={g.cat} delay={i*100}>
|
|||
|
|
<div className="gear-card">
|
|||
|
|
<div className="cap-icon" style={{marginBottom:"1rem"}}><g.icon size={20} sw={1.5}/></div>
|
|||
|
|
<h3>{g.cat}</h3>
|
|||
|
|
<ul>{g.items.map(it => <li key={it}>{it}</li>)}</ul>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── CONTACT ─────────────────────────────────────────────────────────────────
|
|||
|
|
function Contact() {
|
|||
|
|
return (
|
|||
|
|
<section id="contact" className="contact">
|
|||
|
|
<div className="container">
|
|||
|
|
<Reveal>
|
|||
|
|
<div className="contact-head">
|
|||
|
|
<div className="section-rule center"></div>
|
|||
|
|
<p className="eyebrow center">// Contact</p>
|
|||
|
|
<h2 className="section-title">Let's create something.</h2>
|
|||
|
|
<p className="lede" style={{marginTop:"1.25rem"}}>Whether you're planning a new broadcast facility, upgrading existing infrastructure, or exploring cloud and IP-based production workflows, I'd love to discuss your project.</p>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
<div className="contact-grid">
|
|||
|
|
<Reveal delay={0}><a className="contact-card" href="mailto:zgaetano@wilddragon.net">
|
|||
|
|
<div className="icon"><Mail size={16} sw={1.5}/></div>
|
|||
|
|
<span className="label">Email</span>
|
|||
|
|
<span className="val">zgaetano@wilddragon.net</span>
|
|||
|
|
</a></Reveal>
|
|||
|
|
<Reveal delay={80}><a className="contact-card" href="https://www.linkedin.com/in/zachary-gaetano-05962386/" target="_blank" rel="noopener noreferrer">
|
|||
|
|
<div className="icon"><Linkedin size={16} sw={1.5}/></div>
|
|||
|
|
<span className="label">LinkedIn</span>
|
|||
|
|
<span className="val">Zachary Gaetano</span>
|
|||
|
|
</a></Reveal>
|
|||
|
|
<Reveal delay={160}><div className="contact-card">
|
|||
|
|
<div className="icon"><MapPin size={16} sw={1.5}/></div>
|
|||
|
|
<span className="label">Location</span>
|
|||
|
|
<span className="val">Washington, DC Area</span>
|
|||
|
|
</div></Reveal>
|
|||
|
|
</div>
|
|||
|
|
<Reveal delay={200}>
|
|||
|
|
<div className="cta-banner grain">
|
|||
|
|
<div className="cta-banner-grid"></div>
|
|||
|
|
<div className="inner">
|
|||
|
|
<h3>Ready to start your next project?</h3>
|
|||
|
|
<p>From initial concept through commissioning and training, I bring a complete lifecycle approach to every facility I design.</p>
|
|||
|
|
<a href="mailto:zgaetano@wilddragon.net" className="btn btn-primary">Start a Conversation <ArrowRight/></a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Reveal>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── FOOTER ──────────────────────────────────────────────────────────────────
|
|||
|
|
function Footer() {
|
|||
|
|
const sections = [
|
|||
|
|
{ title:"Navigation", links:[{l:"About",h:"#about"},{l:"Process",h:"#services"},{l:"Projects",h:"#projects"},{l:"On Set",h:"#on-set"},{l:"Contact",h:"#contact"}] },
|
|||
|
|
{ title:"Expertise", links:[{l:"Systems Design",h:"#about"},{l:"IP & Cloud Production",h:"#about"},{l:"Broadcast Integration",h:"#about"},{l:"XR / Virtual Production",h:"#projects"}] },
|
|||
|
|
];
|
|||
|
|
return (
|
|||
|
|
<footer className="footer">
|
|||
|
|
<div className="container">
|
|||
|
|
<div className="footer-top">
|
|||
|
|
<div>
|
|||
|
|
<div className="footer-brand">
|
|||
|
|
<img src={__R("images/dragon-mark.png")} alt="Wild Dragon"/>
|
|||
|
|
<span>Wild Dragon</span>
|
|||
|
|
</div>
|
|||
|
|
<p className="about-line">Broadcast systems integration and production facility design for sports, corporate, financial services, and defense organizations.</p>
|
|||
|
|
<div className="footer-socials">
|
|||
|
|
<a href="https://www.linkedin.com/in/zachary-gaetano-05962386/" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn"><Linkedin size={14}/></a>
|
|||
|
|
<a href="mailto:zgaetano@wilddragon.net" aria-label="Email"><Mail size={14}/></a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
{sections.map(s => (
|
|||
|
|
<div className="footer-col" key={s.title}>
|
|||
|
|
<h4>{s.title}</h4>
|
|||
|
|
<ul>{s.links.map(l => <li key={l.l}><a href={l.h}>{l.l}</a></li>)}</ul>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
<div className="footer-bottom">
|
|||
|
|
<p>© {new Date().getFullYear()} Zachary Gaetano. All rights reserved.</p>
|
|||
|
|
<p>Washington, DC Area</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</footer>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── PROJECT OVERLAY — editorial case study ──────────────────────────────────
|
|||
|
|
function ProjectOverlay({ slug, onClose, onOpen }) {
|
|||
|
|
const projects = window.PROJECTS;
|
|||
|
|
const project = projects.find(p => p.slug === slug);
|
|||
|
|
useEffect(() => {
|
|||
|
|
const onKey = (e) => { if (e.key === "Escape") onClose(); };
|
|||
|
|
window.addEventListener("keydown", onKey);
|
|||
|
|
document.body.style.overflow = "hidden";
|
|||
|
|
return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = ""; };
|
|||
|
|
}, [onClose]);
|
|||
|
|
|
|||
|
|
const maskRef = useRef(null);
|
|||
|
|
useEffect(() => { if (maskRef.current) maskRef.current.scrollTop = 0; }, [slug]);
|
|||
|
|
|
|||
|
|
if (!project) return null;
|
|||
|
|
const idx = projects.findIndex(p => p.slug === slug);
|
|||
|
|
const total = projects.length;
|
|||
|
|
const prev = idx > 0 ? projects[idx-1] : projects[total-1];
|
|||
|
|
const next = idx < total - 1 ? projects[idx+1] : projects[0];
|
|||
|
|
const num = String(idx+1).padStart(2,"0");
|
|||
|
|
const totalStr = String(total).padStart(2,"0");
|
|||
|
|
|
|||
|
|
// First description paragraph used as a lede, rest as body
|
|||
|
|
const [lede, ...rest] = project.description;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="po-mask" ref={maskRef} onClick={(e)=>{ if(e.target===maskRef.current) onClose(); }}>
|
|||
|
|
<button className="po-close" onClick={onClose} aria-label="Close"><X size={20}/></button>
|
|||
|
|
<div className="po">
|
|||
|
|
<nav className="po-nav">
|
|||
|
|
<div className="po-nav-inner">
|
|||
|
|
<a className="po-back" href="#projects" onClick={(e)=>{e.preventDefault(); onClose();}}>
|
|||
|
|
<ArrowLeft size={15}/> All projects
|
|||
|
|
</a>
|
|||
|
|
<span className="po-counter"><span className="n">{num}</span><span className="d">/</span><span className="t">{totalStr}</span></span>
|
|||
|
|
<span className="po-brand">Wild Dragon</span>
|
|||
|
|
</div>
|
|||
|
|
</nav>
|
|||
|
|
|
|||
|
|
{/* === HERO === */}
|
|||
|
|
<section className="po-hero">
|
|||
|
|
<div className="po-hero-bg">
|
|||
|
|
<img src={project.thumbnail} alt=""/>
|
|||
|
|
<div className="po-hero-grid"></div>
|
|||
|
|
<div className="po-hero-fade"></div>
|
|||
|
|
</div>
|
|||
|
|
<div className="container po-hero-inner">
|
|||
|
|
<div className="po-hero-meta">
|
|||
|
|
<span className="po-cat">{project.category}</span>
|
|||
|
|
<span className="po-dot"></span>
|
|||
|
|
<span className="po-yr">{project.year}</span>
|
|||
|
|
</div>
|
|||
|
|
<h1 className="po-title">{project.client}</h1>
|
|||
|
|
<p className="po-lede">{project.summary}</p>
|
|||
|
|
<div className="po-hero-rule"></div>
|
|||
|
|
<div className="po-hero-foot">
|
|||
|
|
<span className="po-corner-l">// {project.title || project.client}</span>
|
|||
|
|
<span className="po-corner-r">Case Study · {num}/{totalStr}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
{/* === FACTS BAR === */}
|
|||
|
|
<section className="po-facts">
|
|||
|
|
<div className="container">
|
|||
|
|
<div className="po-facts-grid">
|
|||
|
|
<div><span className="lbl">Client</span><span className="val">{project.client}</span></div>
|
|||
|
|
<div><span className="lbl">Category</span><span className="val">{project.category}</span></div>
|
|||
|
|
<div><span className="lbl">Year</span><span className="val">{project.year}</span></div>
|
|||
|
|
<div><span className="lbl">Role</span><span className="val">{(project.scope && project.scope[0]) || "Systems Design"}</span></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
{/* === LEDE / OPENING === */}
|
|||
|
|
{lede && (
|
|||
|
|
<section className="po-section po-opening">
|
|||
|
|
<div className="container po-opening-grid">
|
|||
|
|
<div className="po-opening-label">
|
|||
|
|
<span className="num">01</span>
|
|||
|
|
<span className="bar"></span>
|
|||
|
|
<span>Overview</span>
|
|||
|
|
</div>
|
|||
|
|
<p className="po-opening-text">{lede}</p>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* === FEATURE IMAGE === */}
|
|||
|
|
<div className="po-feature">
|
|||
|
|
<div className="po-feature-inner">
|
|||
|
|
<img src={project.thumbnail} alt={`${project.client} — ${project.category}`}/>
|
|||
|
|
<div className="po-feature-cap">
|
|||
|
|
<span>{project.client} · {project.category}</span>
|
|||
|
|
<span>{project.year}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* === BODY + SIDECAR === */}
|
|||
|
|
<section className="po-section po-body">
|
|||
|
|
<div className="container po-body-grid">
|
|||
|
|
<div className="po-body-main">
|
|||
|
|
<div className="po-body-label">
|
|||
|
|
<span className="num">02</span>
|
|||
|
|
<span className="bar"></span>
|
|||
|
|
<span>The build</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="po-body-prose">
|
|||
|
|
{rest.map((p, i) => <p key={i}>{p}</p>)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{project.highlights && project.highlights.length > 0 && (
|
|||
|
|
<>
|
|||
|
|
<div className="po-body-label" style={{marginTop:"4.5rem"}}>
|
|||
|
|
<span className="num">03</span>
|
|||
|
|
<span className="bar"></span>
|
|||
|
|
<span>Highlights</span>
|
|||
|
|
</div>
|
|||
|
|
<ul className="po-hl">
|
|||
|
|
{project.highlights.map((h, i) => (
|
|||
|
|
<li key={i}>
|
|||
|
|
<span className="hl-n">{String(i+1).padStart(2,"0")}</span>
|
|||
|
|
<span className="hl-t">{h}</span>
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<aside className="po-sidecar">
|
|||
|
|
<div className="po-spec">
|
|||
|
|
<div className="po-spec-row">
|
|||
|
|
<span className="k">Scope</span>
|
|||
|
|
<div className="v">
|
|||
|
|
{project.scope.map(s => <span key={s} className="chip">{s}</span>)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="po-spec-row">
|
|||
|
|
<span className="k">Technology</span>
|
|||
|
|
<div className="v">
|
|||
|
|
{project.technologies.map(t => <span key={t} className="chip chip-accent">{t}</span>)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</aside>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
{/* === CLOSING CTA === */}
|
|||
|
|
<section className="po-cta">
|
|||
|
|
<div className="container">
|
|||
|
|
<div className="po-cta-inner grain">
|
|||
|
|
<div>
|
|||
|
|
<p className="po-cta-eyebrow">// Next step</p>
|
|||
|
|
<h3>Planning a similar build?</h3>
|
|||
|
|
<p className="po-cta-desc">Same engineering discipline, end to end — design, integration, commissioning, training, support.</p>
|
|||
|
|
</div>
|
|||
|
|
<a href="mailto:zgaetano@wilddragon.net" className="btn btn-primary">Start a conversation <ArrowRight/></a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
{/* === RELATED + PREV/NEXT === */}
|
|||
|
|
<section className="po-related">
|
|||
|
|
<div className="container">
|
|||
|
|
<div className="po-related-head">
|
|||
|
|
<span className="po-related-label">// Other case studies</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="po-related-grid">
|
|||
|
|
{projects.filter(p => p.slug !== project.slug).slice(0, 3).map(p => (
|
|||
|
|
<a key={p.slug} className="po-related-card"
|
|||
|
|
href={`#/projects/${p.slug}`}
|
|||
|
|
onClick={(e)=>{e.preventDefault(); onOpen(p.slug);}}>
|
|||
|
|
<div className="img"><img src={p.thumbnail} alt={p.client}/></div>
|
|||
|
|
<div className="meta">
|
|||
|
|
<span className="cat">{p.category}</span>
|
|||
|
|
<span className="yr">{p.year}</span>
|
|||
|
|
</div>
|
|||
|
|
<h4>{p.client}</h4>
|
|||
|
|
<span className="go">View case study <ArrowRight size={13}/></span>
|
|||
|
|
</a>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section className="po-prevnext">
|
|||
|
|
<div className="po-prevnext-grid">
|
|||
|
|
<a className="prev" href={`#/projects/${prev.slug}`} onClick={(e)=>{e.preventDefault(); onOpen(prev.slug);}}>
|
|||
|
|
<ArrowLeft size={20}/>
|
|||
|
|
<div className="text">
|
|||
|
|
<div className="label">Previous</div>
|
|||
|
|
<div className="name">{prev.client}</div>
|
|||
|
|
</div>
|
|||
|
|
</a>
|
|||
|
|
<a className="next" href={`#/projects/${next.slug}`} onClick={(e)=>{e.preventDefault(); onOpen(next.slug);}}>
|
|||
|
|
<div className="text">
|
|||
|
|
<div className="label">Next</div>
|
|||
|
|
<div className="name">{next.client}</div>
|
|||
|
|
</div>
|
|||
|
|
<ArrowRight size={20}/>
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<footer className="po-foot">
|
|||
|
|
<div className="po-foot-inner">
|
|||
|
|
<a className="brand" href="#" onClick={(e)=>{e.preventDefault(); onClose();}}>Wild Dragon</a>
|
|||
|
|
<p>© {new Date().getFullYear()} Zachary Gaetano</p>
|
|||
|
|
</div>
|
|||
|
|
</footer>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Object.assign(window, {
|
|||
|
|
Navigation, Hero, Clients, Partners, About, Stats, Services, Projects,
|
|||
|
|
TechStack, OnSet, Contact, Footer, ProjectOverlay, CursorDot, Curtain,
|
|||
|
|
});
|