Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 666fe961d0 | |||
| 5099482fde | |||
| 595942f591 |
@@ -205,6 +205,7 @@
|
||||
<script type="text/babel" src="src/timeline-contact.jsx"></script>
|
||||
<script type="text/babel" src="src/command-palette.jsx"></script>
|
||||
<script type="text/babel" src="src/chatbox.jsx"></script>
|
||||
<script type="text/babel" src="src/pages.jsx"></script>
|
||||
<script type="text/babel" src="src/app.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+60
-36
@@ -1,4 +1,4 @@
|
||||
// Root app: theme + ⌘K + Tweaks + scroll reveal
|
||||
// Root app: theme + ⌘K + Tweaks + hash router
|
||||
|
||||
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
|
||||
"accent": "#7c5cff",
|
||||
@@ -8,6 +8,27 @@ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
|
||||
"heroHeadlineSuffix": "automation engineer"
|
||||
}/*EDITMODE-END*/;
|
||||
|
||||
// Simple hash router — reads window.location.hash
|
||||
function useHashRoute() {
|
||||
const [route, setRoute] = React.useState(() => {
|
||||
const h = window.location.hash;
|
||||
return h.startsWith('#/') ? h.slice(2) : '';
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const onHash = () => {
|
||||
const h = window.location.hash;
|
||||
const r = h.startsWith('#/') ? h.slice(2) : '';
|
||||
setRoute(r);
|
||||
window.scrollTo(0, 0);
|
||||
};
|
||||
window.addEventListener('hashchange', onHash);
|
||||
return () => window.removeEventListener('hashchange', onHash);
|
||||
}, []);
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [theme, setTheme] = React.useState(() => {
|
||||
return localStorage.getItem("aura-theme") || "dark";
|
||||
@@ -15,11 +36,11 @@ function App() {
|
||||
const [paletteOpen, setPaletteOpen] = React.useState(false);
|
||||
const [tweaks, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
|
||||
const [lang, setLang] = React.useState(() => localStorage.getItem("aura-lang") || "en");
|
||||
const route = useHashRoute();
|
||||
|
||||
React.useEffect(() => { localStorage.setItem("aura-lang", lang); }, [lang]);
|
||||
const toggleLang = () => setLang(l => l === "en" ? "bn" : "en");
|
||||
|
||||
// theme
|
||||
React.useEffect(() => {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
localStorage.setItem("aura-theme", theme);
|
||||
@@ -33,7 +54,6 @@ function App() {
|
||||
return () => window.removeEventListener("aura:toggle-theme", onToggle);
|
||||
}, []);
|
||||
|
||||
// ⌘K
|
||||
React.useEffect(() => {
|
||||
const onKey = (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
||||
@@ -45,17 +65,13 @@ function App() {
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
|
||||
// tweaks accent vars — apply to :root
|
||||
React.useEffect(() => {
|
||||
document.documentElement.style.setProperty("--accent", tweaks.accent);
|
||||
document.documentElement.style.setProperty("--accent-2", tweaks.accent2);
|
||||
document.documentElement.style.setProperty("--accent-glow", tweaks.accent + "59"); // ~35% alpha
|
||||
document.documentElement.style.setProperty("--accent-glow", tweaks.accent + "59");
|
||||
}, [tweaks.accent, tweaks.accent2]);
|
||||
|
||||
// density
|
||||
const density = tweaks.density;
|
||||
|
||||
// scroll reveal
|
||||
// scroll reveal (re-run on route change)
|
||||
React.useEffect(() => {
|
||||
const els = document.querySelectorAll(".reveal");
|
||||
const io = new IntersectionObserver(entries => {
|
||||
@@ -63,13 +79,32 @@ function App() {
|
||||
}, { threshold: 0.1 });
|
||||
els.forEach(el => io.observe(el));
|
||||
return () => io.disconnect();
|
||||
}, []);
|
||||
}, [route]);
|
||||
|
||||
const density = tweaks.density;
|
||||
const T = window;
|
||||
|
||||
const navProps = {
|
||||
onCmdK: () => setPaletteOpen(true),
|
||||
theme, onToggleTheme: toggleTheme,
|
||||
lang, onToggleLang: toggleLang,
|
||||
route,
|
||||
};
|
||||
|
||||
const renderPage = () => {
|
||||
switch (route) {
|
||||
case 'services':
|
||||
return <T.ServicesPage lang={lang} />;
|
||||
case 'stack':
|
||||
return <T.StackPage />;
|
||||
case 'agents':
|
||||
return <T.AgentsPage />;
|
||||
case 'timeline':
|
||||
return <T.TimelinePage />;
|
||||
case 'contact':
|
||||
return <T.ContactPage lang={lang} />;
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<T.Nav onCmdK={() => setPaletteOpen(true)} theme={theme} onToggleTheme={toggleTheme} lang={lang} onToggleLang={toggleLang} />
|
||||
<main style={density === "compact" ? { fontSize: 14.5 } : undefined}>
|
||||
<T.Hero headlinePrefix={tweaks.heroHeadlinePrefix} headlineSuffix={tweaks.heroHeadlineSuffix} lang={lang} />
|
||||
<T.About />
|
||||
@@ -79,20 +114,21 @@ function App() {
|
||||
<T.Timeline />
|
||||
<T.Contact lang={lang} />
|
||||
</main>
|
||||
<T.Footer />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<T.Nav {...navProps} />
|
||||
{renderPage()}
|
||||
<T.Footer />
|
||||
<T.CommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} />
|
||||
<T.ChatboxWidget />
|
||||
|
||||
<window.TweaksPanel title="Tweaks">
|
||||
<window.TweakSection label="Theme" />
|
||||
<window.TweakRadio
|
||||
label="Mode"
|
||||
value={theme}
|
||||
onChange={v => setTheme(v)}
|
||||
options={["dark", "light"]}
|
||||
/>
|
||||
|
||||
<window.TweakRadio label="Mode" value={theme} onChange={v => setTheme(v)} options={["dark", "light"]} />
|
||||
<window.TweakSection label="Accent" />
|
||||
<window.TweakColor label="Primary" value={tweaks.accent} onChange={v => setTweak("accent", v)} />
|
||||
<window.TweakColor label="Secondary" value={tweaks.accent2} onChange={v => setTweak("accent2", v)} />
|
||||
@@ -105,29 +141,17 @@ function App() {
|
||||
["Mono", "#ffffff", "#a4a8b3"],
|
||||
].map(([name, a, b]) => (
|
||||
<button key={name} onClick={() => setTweak({ accent: a, accent2: b })} style={{
|
||||
padding: "4px 8px",
|
||||
fontSize: 10.5,
|
||||
background: "transparent",
|
||||
border: "1px solid rgba(0,0,0,.12)",
|
||||
borderRadius: 5,
|
||||
color: "#29261b",
|
||||
display: "inline-flex", gap: 5, alignItems: "center",
|
||||
cursor: "pointer",
|
||||
padding: "4px 8px", fontSize: 10.5, background: "transparent",
|
||||
border: "1px solid rgba(0,0,0,.12)", borderRadius: 5, color: "#29261b",
|
||||
display: "inline-flex", gap: 5, alignItems: "center", cursor: "pointer",
|
||||
}}>
|
||||
<span style={{ width: 9, height: 9, background: a, borderRadius: "50%", display: "inline-block" }} />
|
||||
{name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<window.TweakSection label="Density" />
|
||||
<window.TweakRadio
|
||||
label="Layout"
|
||||
value={tweaks.density}
|
||||
onChange={v => setTweak("density", v)}
|
||||
options={["compact", "spacious"]}
|
||||
/>
|
||||
|
||||
<window.TweakRadio label="Layout" value={tweaks.density} onChange={v => setTweak("density", v)} options={["compact", "spacious"]} />
|
||||
<window.TweakSection label="Hero copy" />
|
||||
<window.TweakText label="Prefix" value={tweaks.heroHeadlinePrefix} onChange={v => setTweak("heroHeadlinePrefix", v)} />
|
||||
<window.TweakText label="Suffix" value={tweaks.heroHeadlineSuffix} onChange={v => setTweak("heroHeadlineSuffix", v)} />
|
||||
|
||||
@@ -144,6 +144,26 @@ const PORTFOLIO_DATA = {
|
||||
impact: { primary: "99.98%", secondary: "uptime maintained" },
|
||||
color: "cyan",
|
||||
},
|
||||
{
|
||||
id: "meta-ads-ai",
|
||||
name: "AI-Powered Meta Ads",
|
||||
nameBn: "এআই-চালিত মেটা অ্যাডস",
|
||||
kind: "Growth Service",
|
||||
badge: "NEW · Meta MCP",
|
||||
description:
|
||||
"AI agents connected directly to Meta's official API — optimizing bids, rotating creatives, and reallocating budgets every 15 minutes. Not a human checking ads twice a day. A system that never sleeps.",
|
||||
stack: ["Meta MCP", "Meta Ads API", "n8n", "Claude AI", "Anthropic"],
|
||||
impact: { primary: "5×", secondary: "ROAS vs. manual management" },
|
||||
color: "rose",
|
||||
highlights: [
|
||||
"Real-time bid & budget optimization every 15 min",
|
||||
"100+ ad variants A/B tested simultaneously by AI",
|
||||
"Automated audience expansion & lookalike generation",
|
||||
"Creative fatigue detection — pauses before burnout",
|
||||
"Daily AI-written performance reports to your inbox",
|
||||
"Full Meta API access via official MCP integration",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
agentRun: [
|
||||
|
||||
+19
-11
@@ -1,14 +1,14 @@
|
||||
// Top nav — minimal, sticky, blurred — mobile-responsive
|
||||
const DASHBOARD = 'https://aura.auraajenticai.cloud'
|
||||
const NAV_FALLBACK = [
|
||||
{ id: "work", label: "Services", url: "#work" },
|
||||
{ id: "stack", label: "Stack", url: "#stack" },
|
||||
{ id: "agents", label: "Agents", url: "#agents" },
|
||||
{ id: "timeline", label: "Timeline", url: "#timeline" },
|
||||
{ id: "contact", label: "Contact", url: "#contact" },
|
||||
{ id: "services", label: "Services", url: "#/services" },
|
||||
{ id: "stack", label: "Stack", url: "#/stack" },
|
||||
{ id: "agents", label: "Agents", url: "#/agents" },
|
||||
{ id: "timeline", label: "Timeline", url: "#/timeline" },
|
||||
{ id: "contact", label: "Contact", url: "#/contact" },
|
||||
]
|
||||
|
||||
const Nav = ({ onCmdK, theme, onToggleTheme, accent, lang, onToggleLang }) => {
|
||||
const Nav = ({ onCmdK, theme, onToggleTheme, accent, lang, onToggleLang, route }) => {
|
||||
const [scrolled, setScrolled] = React.useState(false);
|
||||
const [menuOpen, setMenuOpen] = React.useState(false);
|
||||
const [links, setLinks] = React.useState(NAV_FALLBACK);
|
||||
@@ -79,18 +79,26 @@ const Nav = ({ onCmdK, theme, onToggleTheme, accent, lang, onToggleLang }) => {
|
||||
|
||||
{/* Center links — hidden on mobile */}
|
||||
<div style={{ display: "flex", gap: 4, alignItems: "center" }} className="nav-links">
|
||||
{links.map(l => (
|
||||
<a key={l.id} href={l.url || `#${l.id}`} style={{
|
||||
{links.map(l => {
|
||||
const isActive = route === l.id;
|
||||
return (
|
||||
<a key={l.id} href={l.url || `#/${l.id}`} style={{
|
||||
fontSize: 13.5,
|
||||
color: "var(--text-dim)",
|
||||
color: isActive ? "var(--text)" : "var(--text-dim)",
|
||||
background: isActive ? "var(--line)" : "transparent",
|
||||
padding: "6px 12px",
|
||||
borderRadius: 8,
|
||||
transition: "all 0.15s",
|
||||
fontWeight: isActive ? 500 : 400,
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.color = "var(--text)"; e.currentTarget.style.background = "var(--line)"; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.color = "var(--text-dim)"; e.currentTarget.style.background = "transparent"; }}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.color = isActive ? "var(--text)" : "var(--text-dim)";
|
||||
e.currentTarget.style.background = isActive ? "var(--line)" : "transparent";
|
||||
}}
|
||||
>{l.label}</a>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Right — desktop */}
|
||||
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
// Individual page views for hash router
|
||||
|
||||
const COLOR_MAP = {
|
||||
violet: { bg: "rgba(124,92,255,0.08)", border: "rgba(124,92,255,0.22)", text: "#a78bfa" },
|
||||
cyan: { bg: "rgba(0,212,255,0.07)", border: "rgba(0,212,255,0.20)", text: "#67e8f9" },
|
||||
green: { bg: "rgba(34,197,94,0.07)", border: "rgba(34,197,94,0.20)", text: "#86efac" },
|
||||
amber: { bg: "rgba(251,191,36,0.08)", border: "rgba(251,191,36,0.22)", text: "#fde68a" },
|
||||
rose: { bg: "rgba(244,63,94,0.08)", border: "rgba(244,63,94,0.22)", text: "#fda4af" },
|
||||
};
|
||||
|
||||
const PageHero = ({ eyebrow, title, sub, children }) => (
|
||||
<section style={{ padding: "140px 0 80px", borderBottom: "1px solid var(--line)" }}>
|
||||
<div className="container">
|
||||
<a href="#" style={{
|
||||
display: "inline-flex", alignItems: "center", gap: 6,
|
||||
fontSize: 12.5, color: "var(--text-faint)",
|
||||
marginBottom: 40, textDecoration: "none",
|
||||
fontFamily: "var(--font-mono)",
|
||||
transition: "color 0.15s",
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.color = "var(--text-dim)"}
|
||||
onMouseLeave={e => e.currentTarget.style.color = "var(--text-faint)"}
|
||||
>← Home</a>
|
||||
<span className="eyebrow" style={{ marginBottom: 20, display: "flex" }}>{eyebrow}</span>
|
||||
<h1 style={{
|
||||
fontSize: "clamp(36px, 5.5vw, 68px)",
|
||||
fontWeight: 500, letterSpacing: "-0.03em",
|
||||
lineHeight: 1.04, margin: "0 0 20px",
|
||||
}}>{title}</h1>
|
||||
{sub && <p style={{ fontSize: 18, color: "var(--text-dim)", maxWidth: 600, margin: 0, lineHeight: 1.6 }}>{sub}</p>}
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
// ─── SERVICES PAGE ────────────────────────────────────────────────────────────
|
||||
|
||||
const ServicesPage = ({ lang }) => {
|
||||
const D = PORTFOLIO_DATA;
|
||||
const [active, setActive] = React.useState(null);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<PageHero
|
||||
eyebrow="What we build"
|
||||
title={<>Services that <span style={{ fontFamily: "var(--font-serif)", fontStyle: "italic", fontWeight: 400 }}>ship</span> — not decks that pitch</>}
|
||||
sub="Every engagement ends with production code. No retainers for decks. No sprints for prototypes that never deploy."
|
||||
/>
|
||||
|
||||
<section style={{ padding: "80px 0 120px" }}>
|
||||
<div className="container">
|
||||
<div style={{ display: "grid", gap: 24 }}>
|
||||
{D.services.map((svc, i) => {
|
||||
const c = COLOR_MAP[svc.color] || COLOR_MAP.violet;
|
||||
const isOpen = active === svc.id;
|
||||
const isNew = !!svc.badge;
|
||||
return (
|
||||
<div
|
||||
key={svc.id}
|
||||
className="reveal"
|
||||
style={{
|
||||
background: isOpen ? c.bg : "var(--bg-card)",
|
||||
border: `1px solid ${isOpen ? c.border : "var(--line)"}`,
|
||||
borderRadius: "var(--radius)",
|
||||
overflow: "hidden",
|
||||
transition: "all 0.25s ease",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => setActive(isOpen ? null : svc.id)}
|
||||
>
|
||||
{/* Header row */}
|
||||
<div style={{ padding: "28px 32px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 20 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 20, flex: 1, minWidth: 0 }}>
|
||||
<span style={{
|
||||
fontFamily: "var(--font-mono)", fontSize: 11,
|
||||
color: "var(--text-faint)", flexShrink: 0,
|
||||
}}>0{i + 1}</span>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
|
||||
<h3 style={{ margin: 0, fontSize: 18, fontWeight: 500, letterSpacing: "-0.01em" }}>
|
||||
{lang === "bn" && svc.nameBn ? svc.nameBn : svc.name}
|
||||
</h3>
|
||||
{isNew && (
|
||||
<span style={{
|
||||
fontSize: 10.5, fontFamily: "var(--font-mono)",
|
||||
padding: "2px 8px", borderRadius: 999,
|
||||
background: c.bg, border: `1px solid ${c.border}`,
|
||||
color: c.text, fontWeight: 600,
|
||||
}}>{svc.badge}</span>
|
||||
)}
|
||||
</div>
|
||||
<span style={{
|
||||
fontSize: 11, fontFamily: "var(--font-mono)",
|
||||
color: "var(--text-faint)", marginTop: 4, display: "block",
|
||||
}}>{svc.kind}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 24, flexShrink: 0 }}>
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<div style={{ fontSize: 22, fontWeight: 600, color: c.text }}>{svc.impact.primary}</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-faint)", fontFamily: "var(--font-mono)" }}>{svc.impact.secondary}</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: 28, height: 28, borderRadius: 8,
|
||||
border: "1px solid var(--line)",
|
||||
display: "grid", placeItems: "center",
|
||||
color: "var(--text-faint)", fontSize: 16,
|
||||
transform: isOpen ? "rotate(45deg)" : "none",
|
||||
transition: "transform 0.2s",
|
||||
flexShrink: 0,
|
||||
}}>+</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded content */}
|
||||
{isOpen && (
|
||||
<div style={{ padding: "0 32px 32px", borderTop: `1px solid ${c.border}` }}>
|
||||
<div style={{ paddingTop: 24, display: "grid", gridTemplateColumns: svc.highlights ? "1.2fr 1fr" : "1fr 1fr", gap: 32 }} className="svc-detail-grid">
|
||||
<div>
|
||||
<p style={{ margin: "0 0 24px", fontSize: 15.5, lineHeight: 1.65, color: "var(--text-dim)" }}>{svc.description}</p>
|
||||
{svc.highlights && (
|
||||
<ul style={{ margin: 0, padding: 0, listStyle: "none", display: "grid", gap: 10 }}>
|
||||
{svc.highlights.map((h, hi) => (
|
||||
<li key={hi} style={{ display: "flex", gap: 12, fontSize: 14, color: "var(--text)" }}>
|
||||
<span style={{ color: c.text, flexShrink: 0, marginTop: 1 }}>✓</span>
|
||||
{h}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, fontFamily: "var(--font-mono)", color: "var(--text-faint)", marginBottom: 10, textTransform: "uppercase", letterSpacing: "0.1em" }}>Stack</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
||||
{svc.stack.map(s => (
|
||||
<span key={s} style={{
|
||||
fontSize: 12, padding: "4px 10px", borderRadius: 6,
|
||||
background: c.bg, border: `1px solid ${c.border}`,
|
||||
color: c.text, fontFamily: "var(--font-mono)",
|
||||
}}>{s}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="mailto:hello@auraajenticai.cloud?subject=Enquiry: {svc.name}"
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
padding: "10px 20px",
|
||||
background: "var(--text)", color: "var(--bg)",
|
||||
borderRadius: 9, fontSize: 13.5, fontWeight: 500,
|
||||
textDecoration: "none", marginTop: "auto",
|
||||
}}
|
||||
>Get a Quote →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<style>{`
|
||||
@media (max-width: 640px) {
|
||||
.svc-detail-grid { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
`}</style>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── STACK PAGE ──────────────────────────────────────────────────────────────
|
||||
|
||||
const StackPage = () => {
|
||||
const D = PORTFOLIO_DATA;
|
||||
const cats = Object.entries(D.stack);
|
||||
const [activeTab, setActiveTab] = React.useState(cats[0][0]);
|
||||
const skills = cats.find(([k]) => k === activeTab)?.[1] || [];
|
||||
|
||||
return (
|
||||
<main>
|
||||
<PageHero
|
||||
eyebrow="Technology"
|
||||
title={<>Tools we wield <span style={{ fontFamily: "var(--font-serif)", fontStyle: "italic", fontWeight: 400 }}>daily</span></>}
|
||||
sub="Production-tested across 40+ agents, 12 enterprise clients, and 7 years of shipping real systems."
|
||||
/>
|
||||
<section style={{ padding: "80px 0 120px" }}>
|
||||
<div className="container">
|
||||
{/* Tab bar */}
|
||||
<div style={{ display: "flex", gap: 4, flexWrap: "wrap", marginBottom: 48, borderBottom: "1px solid var(--line)", paddingBottom: 0 }}>
|
||||
{cats.map(([cat]) => (
|
||||
<button key={cat} onClick={() => setActiveTab(cat)} style={{
|
||||
padding: "10px 18px", background: "transparent", border: "none",
|
||||
borderBottom: activeTab === cat ? "2px solid var(--accent)" : "2px solid transparent",
|
||||
color: activeTab === cat ? "var(--text)" : "var(--text-dim)",
|
||||
fontSize: 13.5, fontWeight: activeTab === cat ? 500 : 400,
|
||||
cursor: "pointer", marginBottom: -1, transition: "all 0.15s",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}>{cat}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Skill bars */}
|
||||
<div style={{ display: "grid", gap: 20, maxWidth: 720 }}>
|
||||
{skills.map((s, i) => (
|
||||
<div key={s.name} className="reveal" style={{ animationDelay: `${i * 60}ms` }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 14.5, fontWeight: 500 }}>{s.name}</span>
|
||||
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--text-faint)" }}>{s.level}%</span>
|
||||
</div>
|
||||
<div style={{ height: 6, background: "var(--line)", borderRadius: 99, overflow: "hidden" }}>
|
||||
<div style={{
|
||||
height: "100%", width: `${s.level}%`,
|
||||
background: "linear-gradient(90deg, var(--accent), var(--accent-2))",
|
||||
borderRadius: 99,
|
||||
transition: "width 0.8s cubic-bezier(0.2,0.8,0.2,1)",
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── AGENTS PAGE ─────────────────────────────────────────────────────────────
|
||||
|
||||
const AgentsPage = () => (
|
||||
<main>
|
||||
<PageHero
|
||||
eyebrow="AI Agents"
|
||||
title={<>Agents that act — not <span style={{ fontFamily: "var(--font-serif)", fontStyle: "italic", fontWeight: 400 }}>chat</span></>}
|
||||
sub="Every agent we ship has tool access, memory, retry logic, and an audit trail. Live demo below."
|
||||
/>
|
||||
<section style={{ padding: "60px 0 120px" }}>
|
||||
<div className="container">
|
||||
<window.AgentShowcase />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
|
||||
// ─── TIMELINE PAGE ───────────────────────────────────────────────────────────
|
||||
|
||||
const TimelinePage = () => {
|
||||
const D = PORTFOLIO_DATA;
|
||||
return (
|
||||
<main>
|
||||
<PageHero
|
||||
eyebrow="Experience"
|
||||
title="Seven years of shipping"
|
||||
sub="From agency work to fintech infrastructure to founding Aura — the full arc."
|
||||
/>
|
||||
<section style={{ padding: "80px 0 120px" }}>
|
||||
<div className="container">
|
||||
<div style={{ maxWidth: 720, display: "grid", gap: 0 }}>
|
||||
{D.experience.map((exp, i) => (
|
||||
<div key={i} className="reveal" style={{
|
||||
display: "grid", gridTemplateColumns: "160px 1fr",
|
||||
gap: "0 32px", paddingBottom: 40,
|
||||
borderLeft: "1px solid var(--line)",
|
||||
paddingLeft: 32, marginLeft: 160, position: "relative",
|
||||
}}>
|
||||
<div style={{
|
||||
position: "absolute", left: -5, top: 8,
|
||||
width: 9, height: 9, borderRadius: "50%",
|
||||
background: "var(--accent)",
|
||||
boxShadow: "0 0 12px var(--accent-glow)",
|
||||
}} />
|
||||
<div style={{
|
||||
position: "absolute", left: -160, top: 6,
|
||||
fontFamily: "var(--font-mono)", fontSize: 11,
|
||||
color: "var(--text-faint)", whiteSpace: "nowrap",
|
||||
textAlign: "right", paddingRight: 28,
|
||||
}}>{exp.year}</div>
|
||||
<div style={{ paddingTop: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 4, flexWrap: "wrap" }}>
|
||||
<span style={{ fontSize: 15.5, fontWeight: 500 }}>{exp.role}</span>
|
||||
<span style={{
|
||||
fontSize: 10.5, padding: "2px 7px", borderRadius: 999,
|
||||
background: "var(--bg-elev)", border: "1px solid var(--line)",
|
||||
color: "var(--text-faint)", fontFamily: "var(--font-mono)",
|
||||
}}>{exp.kind}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: "var(--accent)", marginBottom: 10, fontWeight: 500 }}>{exp.company}</div>
|
||||
<p style={{ margin: 0, fontSize: 14, lineHeight: 1.6, color: "var(--text-dim)" }}>{exp.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── CONTACT PAGE ─────────────────────────────────────────────────────────────
|
||||
|
||||
const ContactPage = ({ lang }) => {
|
||||
const [sent, setSent] = React.useState(false);
|
||||
const [form, setForm] = React.useState({ name: "", email: "", service: "", message: "" });
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
setSent(true);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const services = PORTFOLIO_DATA.services.map(s => s.name);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<PageHero
|
||||
eyebrow="Contact"
|
||||
title="Let's build something real"
|
||||
sub="Describe what you need — we'll respond within 24 hours with a scoped proposal."
|
||||
/>
|
||||
<section style={{ padding: "80px 0 120px" }}>
|
||||
<div className="container">
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1.3fr", gap: 80, alignItems: "start" }} className="contact-grid">
|
||||
{/* Left info */}
|
||||
<div>
|
||||
<div style={{ display: "grid", gap: 24 }}>
|
||||
{[
|
||||
{ label: "Email", value: "hello@auraajenticai.cloud", href: "mailto:hello@auraajenticai.cloud" },
|
||||
{ label: "Response time", value: "< 24 hours" },
|
||||
{ label: "Location", value: "Dhaka, Bangladesh · Remote" },
|
||||
{ label: "Availability", value: "Open to new projects" },
|
||||
].map(item => (
|
||||
<div key={item.label} style={{
|
||||
padding: "20px 24px", borderRadius: "var(--radius)",
|
||||
border: "1px solid var(--line)", background: "var(--bg-card)",
|
||||
}}>
|
||||
<div style={{ fontSize: 11, fontFamily: "var(--font-mono)", color: "var(--text-faint)", marginBottom: 6, textTransform: "uppercase", letterSpacing: "0.1em" }}>{item.label}</div>
|
||||
{item.href
|
||||
? <a href={item.href} style={{ fontSize: 15, color: "var(--accent)", textDecoration: "none" }}>{item.value}</a>
|
||||
: <div style={{ fontSize: 15, fontWeight: 500 }}>{item.value}</div>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact form */}
|
||||
{sent ? (
|
||||
<div style={{
|
||||
padding: 48, borderRadius: "var(--radius)", border: "1px solid var(--line)",
|
||||
background: "var(--bg-card)", textAlign: "center",
|
||||
}}>
|
||||
<div style={{ fontSize: 40, marginBottom: 16 }}>✓</div>
|
||||
<h3 style={{ margin: "0 0 8px", fontWeight: 500 }}>Message sent!</h3>
|
||||
<p style={{ color: "var(--text-dim)", margin: 0 }}>We'll get back to you within 24 hours.</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} style={{ display: "grid", gap: 16 }}>
|
||||
{[
|
||||
{ key: "name", label: "Your name", type: "text", placeholder: "Jane Smith" },
|
||||
{ key: "email", label: "Email", type: "email", placeholder: "jane@company.com" },
|
||||
].map(f => (
|
||||
<div key={f.key}>
|
||||
<label style={{ display: "block", fontSize: 12.5, color: "var(--text-faint)", marginBottom: 6, fontFamily: "var(--font-mono)" }}>{f.label}</label>
|
||||
<input
|
||||
type={f.type} required placeholder={f.placeholder}
|
||||
value={form[f.key]}
|
||||
onChange={e => setForm(p => ({ ...p, [f.key]: e.target.value }))}
|
||||
style={{
|
||||
width: "100%", padding: "11px 14px",
|
||||
background: "var(--bg-elev)", border: "1px solid var(--line)",
|
||||
borderRadius: 9, color: "var(--text)", fontSize: 14.5,
|
||||
fontFamily: "var(--font-sans)", outline: "none",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<label style={{ display: "block", fontSize: 12.5, color: "var(--text-faint)", marginBottom: 6, fontFamily: "var(--font-mono)" }}>Service interested in</label>
|
||||
<select
|
||||
value={form.service}
|
||||
onChange={e => setForm(p => ({ ...p, service: e.target.value }))}
|
||||
style={{
|
||||
width: "100%", padding: "11px 14px",
|
||||
background: "var(--bg-elev)", border: "1px solid var(--line)",
|
||||
borderRadius: 9, color: form.service ? "var(--text)" : "var(--text-faint)",
|
||||
fontSize: 14.5, fontFamily: "var(--font-sans)", outline: "none",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<option value="">Select a service…</option>
|
||||
{services.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: "block", fontSize: 12.5, color: "var(--text-faint)", marginBottom: 6, fontFamily: "var(--font-mono)" }}>What do you need?</label>
|
||||
<textarea
|
||||
required rows={5} placeholder="Describe your project or goal…"
|
||||
value={form.message}
|
||||
onChange={e => setForm(p => ({ ...p, message: e.target.value }))}
|
||||
style={{
|
||||
width: "100%", padding: "11px 14px",
|
||||
background: "var(--bg-elev)", border: "1px solid var(--line)",
|
||||
borderRadius: 9, color: "var(--text)", fontSize: 14.5,
|
||||
fontFamily: "var(--font-sans)", outline: "none", resize: "vertical",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" disabled={loading} style={{
|
||||
padding: "13px 28px", background: "var(--text)", color: "var(--bg)",
|
||||
border: "none", borderRadius: 10, fontSize: 14.5, fontWeight: 500,
|
||||
cursor: loading ? "wait" : "pointer", opacity: loading ? 0.7 : 1,
|
||||
}}>
|
||||
{loading ? "Sending…" : "Send Message →"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<style>{`
|
||||
@media (max-width: 720px) {
|
||||
.contact-grid { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
`}</style>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
window.ServicesPage = ServicesPage;
|
||||
window.StackPage = StackPage;
|
||||
window.AgentsPage = AgentsPage;
|
||||
window.TimelinePage = TimelinePage;
|
||||
window.ContactPage = ContactPage;
|
||||
+63
-3
@@ -144,14 +144,25 @@ const ProjectCard = ({ p }) => {
|
||||
}}>{p.impact.secondary}</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button style={{
|
||||
{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)",
|
||||
}} title="GitHub"><Icons.Github size={14} /></button>
|
||||
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,
|
||||
@@ -302,8 +313,57 @@ const ProjectVisual = ({ id, tint, hover }) => {
|
||||
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">
|
||||
@@ -319,7 +379,7 @@ const Projects = () => {
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gap: 16,
|
||||
}} className="proj-grid">
|
||||
{D.services.map(p => <ProjectCard key={p.id} p={p} />)}
|
||||
{services.map(p => <ProjectCard key={p.id} p={p} />)}
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
|
||||
Reference in New Issue
Block a user