5099482fde
- Hash router (#/services, #/stack, #/agents, #/timeline, #/contact) - New pages.jsx: ServicesPage, StackPage, AgentsPage, TimelinePage, ContactPage - AI-Powered Meta Ads service added (Meta MCP, 5x ROAS, 15-min bid optimization) - Nav links updated to hash routes with active-state highlighting - index.html loads pages.jsx before app.jsx Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
397 lines
15 KiB
React
397 lines
15 KiB
React
// Projects — SaaS-card grid with featured first
|
|
|
|
const colorMap = {
|
|
violet: { bg: "rgba(124, 92, 255, 0.08)", border: "rgba(124, 92, 255, 0.25)", text: "#a18bff" },
|
|
cyan: { bg: "rgba(0, 212, 255, 0.08)", border: "rgba(0, 212, 255, 0.25)", text: "#5ee0ff" },
|
|
green: { bg: "rgba(74, 222, 128, 0.08)", border: "rgba(74, 222, 128, 0.25)", text: "#7ee8a3" },
|
|
amber: { bg: "rgba(251, 191, 36, 0.08)", border: "rgba(251, 191, 36, 0.25)", text: "#fcd34d" },
|
|
};
|
|
|
|
const ProjectCard = ({ p }) => {
|
|
const tint = colorMap[p.color] || colorMap.violet;
|
|
const [hover, setHover] = React.useState(false);
|
|
|
|
return (
|
|
<article
|
|
onMouseEnter={() => setHover(true)}
|
|
onMouseLeave={() => setHover(false)}
|
|
style={{
|
|
position: "relative",
|
|
gridColumn: "span 1",
|
|
background: "var(--bg-card)",
|
|
border: "1px solid var(--line)",
|
|
borderRadius: 16,
|
|
overflow: "hidden",
|
|
cursor: "pointer",
|
|
transition: "all 0.25s ease",
|
|
transform: hover ? "translateY(-2px)" : "none",
|
|
boxShadow: hover ? `0 24px 60px -20px ${tint.border}` : "none",
|
|
}}>
|
|
{/* hover sheen */}
|
|
<div style={{
|
|
position: "absolute",
|
|
inset: 0,
|
|
background: `radial-gradient(circle at 30% 0%, ${tint.bg}, transparent 60%)`,
|
|
opacity: hover ? 1 : 0.5,
|
|
transition: "opacity 0.25s",
|
|
pointerEvents: "none",
|
|
}} />
|
|
|
|
{/* Decorative top — small visual specific to service type */}
|
|
<div style={{
|
|
position: "relative",
|
|
height: 160,
|
|
borderBottom: "1px solid var(--line)",
|
|
overflow: "hidden",
|
|
background: `linear-gradient(180deg, color-mix(in srgb, ${tint.text} 8%, var(--bg-card)), var(--bg-card))`,
|
|
}}>
|
|
<ProjectVisual id={p.id} tint={tint} hover={hover} />
|
|
<div style={{
|
|
position: "absolute",
|
|
top: 16, left: 16,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
padding: "5px 10px",
|
|
background: "color-mix(in srgb, var(--bg) 60%, transparent)",
|
|
backdropFilter: "blur(6px)",
|
|
border: `1px solid ${tint.border}`,
|
|
borderRadius: 999,
|
|
fontSize: 11,
|
|
fontFamily: "var(--font-mono)",
|
|
color: tint.text,
|
|
textTransform: "uppercase",
|
|
letterSpacing: "0.06em",
|
|
}}>
|
|
<span style={{ width: 6, height: 6, borderRadius: "50%", background: tint.text }} />
|
|
{p.kind}
|
|
</div>
|
|
<div style={{
|
|
position: "absolute",
|
|
top: 16, right: 16,
|
|
fontFamily: "var(--font-mono)",
|
|
fontSize: 11,
|
|
color: tint.text,
|
|
padding: "2px 8px",
|
|
background: tint.bg,
|
|
border: `1px solid ${tint.border}`,
|
|
borderRadius: 4,
|
|
}}>{p.kind}</div>
|
|
</div>
|
|
|
|
<div style={{ padding: 24, position: "relative" }}>
|
|
<h3 style={{
|
|
margin: 0,
|
|
fontSize: 20,
|
|
fontWeight: 500,
|
|
letterSpacing: "-0.02em",
|
|
}}>{p.name}</h3>
|
|
{p.nameBn && (
|
|
<div style={{
|
|
marginTop: 4,
|
|
fontSize: 12,
|
|
color: "var(--text-faint)",
|
|
fontFamily: "var(--font-sans)",
|
|
}}>{p.nameBn}</div>
|
|
)}
|
|
|
|
<p style={{
|
|
marginTop: 12,
|
|
marginBottom: 20,
|
|
fontSize: 14,
|
|
lineHeight: 1.55,
|
|
color: "var(--text-dim)",
|
|
}}>{p.description}</p>
|
|
|
|
{/* Stack badges */}
|
|
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 20 }}>
|
|
{p.stack.map(s => (
|
|
<span key={s} style={{
|
|
padding: "4px 10px",
|
|
background: "var(--bg-elev)",
|
|
border: "1px solid var(--line)",
|
|
borderRadius: 6,
|
|
fontFamily: "var(--font-mono)",
|
|
fontSize: 11,
|
|
color: "var(--text-dim)",
|
|
}}>{s}</span>
|
|
))}
|
|
</div>
|
|
|
|
{/* Footer: impact + actions */}
|
|
<div style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
paddingTop: 18,
|
|
borderTop: "1px solid var(--line)",
|
|
gap: 12,
|
|
}}>
|
|
<div>
|
|
<div style={{
|
|
fontSize: 18,
|
|
fontWeight: 500,
|
|
letterSpacing: "-0.01em",
|
|
color: tint.text,
|
|
}}>{p.impact.primary}</div>
|
|
<div style={{
|
|
marginTop: 2,
|
|
fontSize: 11,
|
|
fontFamily: "var(--font-mono)",
|
|
color: "var(--text-faint)",
|
|
textTransform: "uppercase",
|
|
letterSpacing: "0.06em",
|
|
}}>{p.impact.secondary}</div>
|
|
</div>
|
|
<div style={{ display: "flex", gap: 6 }}>
|
|
{p.gitRepo ? (
|
|
<a href={p.gitRepo} target="_blank" rel="noopener noreferrer" style={{
|
|
width: 34, height: 34,
|
|
display: "grid", placeItems: "center",
|
|
background: "transparent",
|
|
border: "1px solid var(--line)",
|
|
borderRadius: 8,
|
|
color: "var(--text-dim)",
|
|
textDecoration: "none",
|
|
}} title="View repository"><Icons.Github size={14} /></a>
|
|
) : (
|
|
<span style={{
|
|
width: 34, height: 34,
|
|
display: "grid", placeItems: "center",
|
|
border: "1px solid var(--line)",
|
|
borderRadius: 8,
|
|
color: "var(--line-strong)",
|
|
}} title="No repository linked"><Icons.Github size={14} /></span>
|
|
)}
|
|
{p.demo && (
|
|
<a href={p.demo} target="_blank" rel="noopener noreferrer" style={{
|
|
padding: "0 14px", height: 34,
|
|
display: "inline-flex", alignItems: "center", gap: 6,
|
|
background: "var(--text)",
|
|
color: "var(--bg)",
|
|
border: "none",
|
|
borderRadius: 8,
|
|
fontSize: 13,
|
|
fontWeight: 500,
|
|
textDecoration: "none",
|
|
cursor: "pointer",
|
|
}}>Demo <Icons.ArrowUpRight size={12} /></a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
};
|
|
|
|
// Tiny SVG visual per service — gives each card a distinct "product preview"
|
|
const ProjectVisual = ({ id, tint, hover }) => {
|
|
if (id === "ai-agent-automation") {
|
|
return (
|
|
<svg viewBox="0 0 600 220" style={{ width: "100%", height: "100%", position: "absolute", inset: 0 }}>
|
|
<defs>
|
|
<linearGradient id="aura-g" x1="0" x2="1">
|
|
<stop stopColor={tint.text} stopOpacity="0.6"/>
|
|
<stop offset="1" stopColor={tint.text} stopOpacity="0.05"/>
|
|
</linearGradient>
|
|
</defs>
|
|
{/* nodes */}
|
|
{[
|
|
[120, 60], [220, 110], [320, 70], [420, 130], [500, 90],
|
|
[180, 160], [280, 180], [380, 50], [460, 180],
|
|
].map(([x, y], i) => (
|
|
<g key={i}>
|
|
<circle cx={x} cy={y} r="4" fill={tint.text} opacity="0.9" />
|
|
<circle cx={x} cy={y} r="14" fill={tint.text} opacity={hover ? 0.18 : 0.08}>
|
|
<animate attributeName="r" from="4" to="22" dur={`${2 + i*0.3}s`} repeatCount="indefinite" />
|
|
<animate attributeName="opacity" from="0.25" to="0" dur={`${2 + i*0.3}s`} repeatCount="indefinite" />
|
|
</circle>
|
|
</g>
|
|
))}
|
|
{/* connectors */}
|
|
<g stroke={tint.text} strokeOpacity="0.35" strokeWidth="1" fill="none">
|
|
<path d="M120,60 L220,110 L320,70 L420,130 L500,90"/>
|
|
<path d="M180,160 L280,180 L380,50 L460,180"/>
|
|
<path d="M120,60 L180,160 M220,110 L280,180 M320,70 L380,50 M420,130 L460,180"/>
|
|
</g>
|
|
</svg>
|
|
);
|
|
}
|
|
if (id === "scraping-data-pipeline") {
|
|
// bar chart
|
|
const bars = [40, 64, 52, 78, 92, 70, 110, 95, 130, 118, 142];
|
|
return (
|
|
<svg viewBox="0 0 600 160" style={{ width: "100%", height: "100%", position: "absolute", inset: 0 }} preserveAspectRatio="xMidYMid meet">
|
|
{bars.map((h, i) => (
|
|
<rect key={i} x={50 + i*45} y={150 - h} width="22" height={h} rx="3" fill={tint.text} opacity={0.4 + i*0.05}>
|
|
<animate attributeName="height" from="0" to={h} dur="0.8s" begin={`${i*0.05}s`} fill="freeze" />
|
|
<animate attributeName="y" from="150" to={150-h} dur="0.8s" begin={`${i*0.05}s`} fill="freeze" />
|
|
</rect>
|
|
))}
|
|
<line x1="40" y1="150" x2="580" y2="150" stroke={tint.border} />
|
|
</svg>
|
|
);
|
|
}
|
|
if (id === "web3-blockchain") {
|
|
// chain dots
|
|
const chains = ["ETH","ARB","OP","BASE","SOL","ZK","POL","BNB","AVAX","CELO","XLM","TRX"];
|
|
return (
|
|
<div style={{ position: "absolute", inset: 0, display: "grid", gridTemplateColumns: "repeat(6, 1fr)", padding: 24, gap: 8, alignContent: "center" }}>
|
|
{chains.map((c, i) => (
|
|
<div key={c} style={{
|
|
padding: "8px 0",
|
|
textAlign: "center",
|
|
border: `1px solid ${tint.border}`,
|
|
borderRadius: 6,
|
|
fontFamily: "var(--font-mono)",
|
|
fontSize: 11,
|
|
color: tint.text,
|
|
background: tint.bg,
|
|
animation: `float-up 0.5s ${i*0.04}s both`,
|
|
}}>{c}</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
if (id === "mt5-ea-trading") {
|
|
// line chart
|
|
return (
|
|
<svg viewBox="0 0 600 160" style={{ width: "100%", height: "100%", position: "absolute", inset: 0 }}>
|
|
<defs>
|
|
<linearGradient id="ea-g" x1="0" y1="0" x2="0" y2="1">
|
|
<stop stopColor={tint.text} stopOpacity="0.4"/>
|
|
<stop offset="1" stopColor={tint.text} stopOpacity="0"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<path d="M0,110 L60,90 L120,100 L180,70 L240,80 L300,55 L360,70 L420,40 L480,55 L540,30 L600,45 L600,160 L0,160 Z" fill="url(#ea-g)" />
|
|
<path d="M0,110 L60,90 L120,100 L180,70 L240,80 L300,55 L360,70 L420,40 L480,55 L540,30 L600,45" stroke={tint.text} strokeWidth="1.5" fill="none" />
|
|
{[60,180,300,420,540].map((x, i) => (
|
|
<circle key={i} cx={x} cy={[90,70,55,40,30][i]} r="3" fill={tint.text} />
|
|
))}
|
|
</svg>
|
|
);
|
|
}
|
|
if (id === "web-app-dev") {
|
|
// schema → grid morph
|
|
return (
|
|
<svg viewBox="0 0 600 200" style={{ width: "100%", height: "100%", position: "absolute", inset: 0 }}>
|
|
{/* schema box */}
|
|
<g transform="translate(60, 50)">
|
|
<rect width="160" height="100" rx="8" fill="none" stroke={tint.border} />
|
|
{[20, 40, 60, 80].map(y => <line key={y} x1="12" y1={y} x2="148" y2={y} stroke={tint.border} strokeOpacity="0.5" strokeDasharray="2 2" />)}
|
|
{[20, 40, 60, 80].map(y => <rect key={y} x="14" y={y-4} width="60" height="6" fill={tint.text} opacity="0.4" rx="1" />)}
|
|
</g>
|
|
{/* arrow */}
|
|
<g transform="translate(240, 90)" stroke={tint.text} fill="none" strokeWidth="1.5">
|
|
<line x1="0" y1="10" x2="80" y2="10" />
|
|
<polyline points="70,4 80,10 70,16" />
|
|
</g>
|
|
{/* dashboard grid */}
|
|
<g transform="translate(360, 30)">
|
|
<rect width="200" height="140" rx="8" fill={tint.bg} stroke={tint.border} />
|
|
<rect x="12" y="12" width="80" height="36" rx="4" fill={tint.text} opacity="0.5" />
|
|
<rect x="100" y="12" width="88" height="36" rx="4" fill={tint.text} opacity="0.3" />
|
|
<rect x="12" y="56" width="176" height="72" rx="4" fill={tint.text} opacity="0.18" />
|
|
</g>
|
|
</svg>
|
|
);
|
|
}
|
|
if (id === "infra-devops") {
|
|
// chat bubbles
|
|
return (
|
|
<div style={{ position: "absolute", inset: 0, padding: "20px 28px", display: "flex", flexDirection: "column", gap: 8, justifyContent: "center", fontFamily: "var(--font-mono)", fontSize: 11 }}>
|
|
<div style={{ alignSelf: "flex-start", padding: "8px 12px", background: "var(--bg-elev)", border: "1px solid var(--line)", borderRadius: 12, color: "var(--text-dim)", maxWidth: "70%" }}>How do I rotate my API key?</div>
|
|
<div style={{ alignSelf: "flex-end", padding: "8px 12px", background: tint.bg, border: `1px solid ${tint.border}`, borderRadius: 12, color: tint.text, maxWidth: "70%" }}>Settings → API → Rotate. New key live in <5s.</div>
|
|
<div style={{ alignSelf: "flex-start", padding: "8px 12px", background: "var(--bg-elev)", border: "1px solid var(--line)", borderRadius: 12, color: "var(--text-dim)", maxWidth: "70%", display: "flex", gap: 4, alignItems: "center" }}>
|
|
<span style={{ width: 4, height: 4, borderRadius: "50%", background: "var(--text-dim)", animation: "pulse-dot 1.2s infinite" }} />
|
|
<span style={{ width: 4, height: 4, borderRadius: "50%", background: "var(--text-dim)", animation: "pulse-dot 1.2s 0.2s infinite" }} />
|
|
<span style={{ width: 4, height: 4, borderRadius: "50%", background: "var(--text-dim)", animation: "pulse-dot 1.2s 0.4s infinite" }} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Merge Dashboard API data (demo/git links, active state) into local static data
|
|
function mergeServices(local, apiData) {
|
|
const byId = {};
|
|
apiData.forEach(s => { byId[s.serviceId] = s; });
|
|
return local
|
|
.filter(s => !byId[s.id] || byId[s.id].isActive !== false)
|
|
.map(s => {
|
|
const api = byId[s.id];
|
|
if (!api) return s;
|
|
return {
|
|
...s,
|
|
demo: api.demoUrl || s.demo || null,
|
|
gitRepo: api.gitRepo || null,
|
|
name: api.name || s.name,
|
|
nameBn: api.nameBn || s.nameBn,
|
|
impact: {
|
|
primary: api.impactPrimary || s.impact.primary,
|
|
secondary: api.impactSecondary || s.impact.secondary,
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
const Projects = () => {
|
|
const D = PORTFOLIO_DATA;
|
|
const [services, setServices] = React.useState(D.services);
|
|
|
|
React.useEffect(() => {
|
|
const CACHE_KEY = 'aura_services_v2';
|
|
const CACHE_TTL = 60 * 1000;
|
|
const cached = sessionStorage.getItem(CACHE_KEY);
|
|
if (cached) {
|
|
try {
|
|
const { data, ts } = JSON.parse(cached);
|
|
if (Date.now() - ts < CACHE_TTL && data.length > 0) {
|
|
setServices(mergeServices(D.services, data));
|
|
return;
|
|
}
|
|
} catch {}
|
|
}
|
|
fetch('https://aura.auraajenticai.cloud/api/public/services', { signal: AbortSignal.timeout(3000) })
|
|
.then(r => r.ok ? r.json() : null)
|
|
.then(data => {
|
|
if (Array.isArray(data) && data.length > 0) {
|
|
sessionStorage.setItem(CACHE_KEY, JSON.stringify({ data, ts: Date.now() }));
|
|
setServices(mergeServices(D.services, data));
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
return (
|
|
<section id="work" style={{ padding: "120px 0", borderTop: "1px solid var(--line)" }}>
|
|
<div className="container">
|
|
<SectionHeader
|
|
eyebrow="What we build"
|
|
num="03 / 06"
|
|
title={<>Six services. One team. All <span style={{ fontFamily: "var(--font-serif)", fontStyle: "italic", fontWeight: 400 }}>production-grade</span>.</>}
|
|
sub="From landing pages to AI agent runtimes — we take your idea from spec to live deployment."
|
|
/>
|
|
|
|
<div style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "repeat(2, 1fr)",
|
|
gap: 16,
|
|
}} className="proj-grid">
|
|
{services.map(p => <ProjectCard key={p.id} p={p} />)}
|
|
</div>
|
|
|
|
<style>{`
|
|
@media (max-width: 880px) {
|
|
.proj-grid { grid-template-columns: 1fr !important; }
|
|
.proj-grid > article { grid-column: span 1 !important; }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
window.Projects = Projects;
|