Spaces:
Running
Running
File size: 8,478 Bytes
1955a0f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | import { useEffect } from "react";
import { motion } from "motion/react";
import { ArrowRight, Boxes, CheckCircle2, XCircle, Zap } from "lucide-react";
import GlassCard from "../components/GlassCard";
import AnimatedLogo from "../components/AnimatedLogo";
import { useApiKey } from "../context/ApiKeyContext";
import { MINT_THEME } from "../context/ThemeContext";
import { useLaunch } from "./LaunchContext";
import { MODULES, MODULE_IDS } from "./registry";
import { preloadModulePages } from "./lazyPages";
const MODULE_CAPS: Record<string, string[]> = {
e: ["Sequence automation", "Unified inbox", "Warmup radar", "Deliverability analytics"],
s: ["Campaign management", "Lead database", "Webhook automation", "Email warmup"],
};
/**
* BRIDGE platform hub — the launcher. Real platform primitives only
* (GlassCard, AnimatedLogo, API-key status). Buttons trigger the module
* launch animation (re-seeded per launch) via LaunchContext.
*/
export default function ModuleHub() {
const { startLaunch } = useLaunch();
const { activeKey } = useApiKey();
useEffect(() => {
// Preload both modules' chunks so the first launch never janks.
const idle = window.requestIdleCallback ?? ((cb: () => void) => window.setTimeout(cb, 200));
const handle = idle(() => {
preloadModulePages("e");
preloadModulePages("s");
});
return () => {
if (typeof window.cancelIdleCallback === "function" && typeof handle === "number") {
window.cancelIdleCallback(handle);
}
};
}, []);
return (
<div className="min-h-screen bg-[#05070a] text-zinc-100 relative overflow-hidden">
{/* Ambient layers — platform primitives */}
<div
className="absolute inset-0 pointer-events-none"
style={{
background:
"radial-gradient(ellipse at top right, rgba(0, 229, 188, 0.10), transparent 60%), radial-gradient(ellipse at bottom left, rgba(99, 102, 241, 0.12), transparent 60%)",
}}
/>
<div className="relative z-10 max-w-5xl mx-auto px-4 md:px-8 py-12 md:py-20">
{/* Header */}
<div className="flex flex-col items-start gap-6 mb-12">
<div className="flex items-center gap-3">
<AnimatedLogo className="w-10 h-10" />
<div>
<div className="text-lg font-bold tracking-[0.08em] text-white">
BRIDGE<span className="text-[#00e5bc]">Modules</span>
</div>
<div className="text-[9px] font-bold tracking-[0.2em] text-zinc-500 uppercase font-mono">
Outbound Systems OS · Platform Hub
</div>
</div>
</div>
<div>
<h1 className="text-2xl md:text-3xl font-bold text-white tracking-tight">
Launch a workspace.
</h1>
<p className="text-sm text-zinc-400 mt-2 max-w-xl">
Two autonomous outbound engines, one command platform. Each module
assembles its own dashboard from live primitives when launched.
</p>
</div>
<div className="flex items-center gap-2">
{activeKey ? (
<div className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider px-2.5 py-1 rounded-full border shadow-sm text-[#00e5bc] bg-[rgba(0,229,188,0.12)] border-[rgba(0,229,188,0.3)]">
<CheckCircle2 className="w-3 h-3" />
API Connected
</div>
) : (
<div className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-rose-300 bg-rose-500/10 border border-rose-500/20 px-2.5 py-1 rounded-full">
<XCircle className="w-3 h-3" />
API Key Missing — set one in a module's Settings
</div>
)}
</div>
</div>
{/* Module launcher cards — real primitives */}
<div className="grid md:grid-cols-2 gap-4 md:gap-6">
{MODULE_IDS.map((id) => {
const def = MODULES[id];
const Icon = def.icon;
const theme = def.theme;
const mint = id === "e" ? MINT_THEME : null;
return (
<motion.div
key={id}
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1], delay: id === "e" ? 0.05 : 0.15 }}
>
<GlassCard className="p-6 md:p-7 h-full flex flex-col group hover:border-white/10 transition-all duration-500">
<div className="flex items-start justify-between mb-5">
<div
className="w-11 h-11 rounded-xl flex items-center justify-center border"
style={{
backgroundColor: `${theme.accent}14`,
borderColor: `${theme.accent}44`,
boxShadow: `0 0 24px ${theme.accent}22`,
}}
>
<Icon className="w-5 h-5" style={{ color: theme.accent }} />
</div>
<span
className="text-[9px] font-bold font-mono uppercase tracking-[0.18em] px-2 py-1 rounded-md border"
style={{
color: theme.accentText,
borderColor: `${theme.accent}44`,
backgroundColor: `${theme.accent}14`,
}}
>
{theme.code}
</span>
</div>
<h2 className="text-xl font-bold text-white tracking-tight">
BRIDGE
<span style={{ color: theme.accent }}>{def.wordmark}</span>
</h2>
<p className="text-[13px] text-zinc-400 mt-2 leading-relaxed flex-1">
{def.blurb}
</p>
<ul className="mt-5 space-y-1.5">
{MODULE_CAPS[id].map((cap) => (
<li key={cap} className="flex items-center gap-2 text-xs text-zinc-300">
<span
className="w-1 h-1 rounded-full shrink-0"
style={{ backgroundColor: theme.accent, boxShadow: `0 0 6px ${theme.accent}` }}
/>
{cap}
</li>
))}
</ul>
<div className="mt-6 flex items-center justify-between gap-3">
<div className="flex items-center gap-1.5 text-[10px] font-mono text-zinc-500 uppercase tracking-wider">
<Boxes className="w-3.5 h-3.5" />
Slow-mo assembly on launch
</div>
<button
type="button"
onClick={() => startLaunch(id)}
onMouseEnter={() => preloadModulePages(id)}
className="flex items-center gap-2 px-4 py-2.5 rounded-lg text-[13px] font-bold transition-all duration-300 active:scale-[0.98] border"
style={{
backgroundColor: theme.buttonBg,
color: theme.buttonText,
borderColor: theme.buttonBg,
boxShadow: `0 0 24px ${theme.accent}33`,
}}
aria-label={`Launch ${def.label}`}
>
{id === "e" ? <Zap className="w-4 h-4" /> : <Boxes className="w-4 h-4" />}
Launch {def.label.replace(/^Module /, "")}
<ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
</button>
</div>
{mint && (
<div className="mt-4 text-[10px] font-mono text-zinc-600 uppercase tracking-[0.16em]">
GreenTheme build · accent {mint.accent}
</div>
)}
</GlassCard>
</motion.div>
);
})}
</div>
<div className="mt-10 text-center text-[10px] font-mono text-zinc-600 uppercase tracking-[0.2em]">
Bridge Modules · Direct URLs — /modules/e · /modules/s
</div>
</div>
</div>
);
}
|