Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
Auth & SSO - Wire Authentik (auth.aethex.tech) as OIDC PKCE SSO provider - Server-side only flow with HMAC-signed stateless state token - Account linking via authentik_sub in user metadata - AeThex ID connection card in Dashboard connections tab - Unlink endpoint POST /api/auth/authentik/unlink - Fix node:https helper to bypass undici DNS bug on Node 18 - Fix resolv.conf to use 1.1.1.1/8.8.8.8 in container Schema & types - Regenerate database.types.ts from live Supabase schema (23k lines) - Fix 511 TypeScript errors caused by stale 582-line types file - Fix UserProfile import in aethex-database-adapter.ts - Add notifications migration (title, message, read columns) Server fixes - Remove badge_color from achievements seed/upsert (column doesn't exist) - Rename name→title, add slug field in achievements seed - Remove email from all user_profiles select queries (column doesn't exist) - Fix email-based achievement target lookup via auth.admin.listUsers - Add GET /api/projects/:projectId endpoint - Fix import.meta.dirname → fileURLToPath for Node 18 compatibility - Expose VITE_APP_VERSION from package.json at build time Navigation systems - DevPlatformNav: reorganize into Learn/Build grouped dropdowns with descriptions - Migrate all 11 dev-platform pages from main Layout to DevPlatformLayout - Remove dead isDevMode context nav swap from main Layout - EthosLayout: purple-accented tab bar (Library, Artists, Licensing, Settings) with member-only gating and guest CTA — migrate 4 Ethos pages - GameForgeLayout: orange-branded sidebar with Studio section and lock icons for unauthenticated users — migrate GameForge + GameForgeDashboard - SysBar: live latency ping, status dot (green/yellow/red), real version Layout dropdown - Role-gate Admin (owner/admin/founder only) and Internal Docs (+ staff) - Add Internal section label with separator - Fix settings link from /dashboard?tab=profile#settings to /dashboard?tab=settings Project pages - Add ProjectDetail page at /projects/:projectId - Fix ProfilePassport "View mission" link from /projects/new to /projects/:id Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
593 lines
25 KiB
TypeScript
593 lines
25 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { Link, useLocation } from "react-router-dom";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import { useArmTheme } from "@/contexts/ArmThemeContext";
|
|
import NotificationBell from "@/components/notifications/NotificationBell";
|
|
import { AIChatButton } from "@/components/ai";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import {
|
|
User,
|
|
Settings,
|
|
LogOut,
|
|
Sparkles,
|
|
UserCircle,
|
|
Menu,
|
|
BookOpen,
|
|
Shield,
|
|
X,
|
|
} from "lucide-react";
|
|
|
|
// ─── Brand styles (injected once) ────────────────────────────────────────────
|
|
const AX_STYLES = `
|
|
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;900&family=Share+Tech+Mono&display=swap');
|
|
|
|
@keyframes ax-flicker {
|
|
0%,100%{opacity:1} 92%{opacity:1} 93%{opacity:0.82} 94%{opacity:1} 96%{opacity:0.9} 97%{opacity:1}
|
|
}
|
|
@keyframes ax-glitch {
|
|
0%,89%,100%{transform:translateX(0) skewX(0)}
|
|
90%{transform:translateX(-3px) skewX(-1deg)}
|
|
91%{transform:translateX(3px) skewX(1deg)}
|
|
92%{transform:translateX(0) skewX(0)}
|
|
}
|
|
@keyframes ax-blink { 0%,50%{opacity:1} 51%,100%{opacity:0} }
|
|
@keyframes ax-sweep { 0%{left:-100%} 100%{left:200%} }
|
|
@keyframes ax-pulse-glow {
|
|
0%,100%{box-shadow:0 0 8px rgba(0,255,255,0.08),inset 0 0 8px rgba(0,255,255,0.02)}
|
|
50%{box-shadow:0 0 24px rgba(0,255,255,0.2),inset 0 0 16px rgba(0,255,255,0.05)}
|
|
}
|
|
@keyframes ax-fade-up {
|
|
from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)}
|
|
}
|
|
|
|
.ax-flicker { animation: ax-flicker 12s infinite; }
|
|
.ax-orbitron { font-family: 'Orbitron', monospace !important; }
|
|
.ax-mono { font-family: 'Share Tech Mono', monospace !important; }
|
|
.ax-glitch { animation: ax-glitch 9s infinite; font-family: 'Orbitron', monospace !important; }
|
|
|
|
.ax-card-sweep { position: relative; overflow: hidden; }
|
|
.ax-card-sweep::after {
|
|
content: ''; position: absolute; top: 0; left: -100%;
|
|
width: 50%; height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(0,255,255,0.05), transparent);
|
|
animation: ax-sweep 5s infinite; pointer-events: none;
|
|
}
|
|
.ax-pulse { animation: ax-pulse-glow 4s infinite; }
|
|
.ax-fade-up { animation: ax-fade-up 0.4s ease both; }
|
|
|
|
.ax-corner-bracket { position: relative; }
|
|
.ax-corner-bracket::before, .ax-corner-bracket::after {
|
|
content: ''; position: absolute; width: 16px; height: 16px;
|
|
}
|
|
.ax-corner-bracket::before {
|
|
top: -1px; left: -1px;
|
|
border-top: 2px solid rgba(0,255,255,0.6);
|
|
border-left: 2px solid rgba(0,255,255,0.6);
|
|
}
|
|
.ax-corner-bracket::after {
|
|
bottom: -1px; right: -1px;
|
|
border-bottom: 2px solid rgba(0,255,255,0.6);
|
|
border-right: 2px solid rgba(0,255,255,0.6);
|
|
}
|
|
|
|
.ax-clip {
|
|
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px));
|
|
}
|
|
|
|
body::before {
|
|
content: '';
|
|
position: fixed; inset: 0;
|
|
background: repeating-linear-gradient(
|
|
0deg, transparent, transparent 2px,
|
|
rgba(0,255,255,0.015) 2px, rgba(0,255,255,0.015) 4px
|
|
);
|
|
pointer-events: none; z-index: 9990;
|
|
}
|
|
body::after {
|
|
content: '';
|
|
position: fixed; inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(0,255,255,0.025) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(0,255,255,0.025) 1px, transparent 1px);
|
|
background-size: 50px 50px;
|
|
pointer-events: none; z-index: 9989;
|
|
}
|
|
`;
|
|
|
|
// ─── SysBar ───────────────────────────────────────────────────────────────────
|
|
const APP_VERSION = import.meta.env.VITE_APP_VERSION || "0.1.0";
|
|
|
|
type SysStatus = "ok" | "degraded" | "outage" | "unknown";
|
|
|
|
function SysBar() {
|
|
const [time, setTime] = useState("");
|
|
const [latency, setLatency] = useState<number | null>(null);
|
|
const [status, setStatus] = useState<SysStatus>("unknown");
|
|
const [node, setNode] = useState("vps.aethex.tech");
|
|
|
|
// Live clock
|
|
useEffect(() => {
|
|
const tick = () => setTime(new Date().toLocaleTimeString("en-US", { hour12: false }));
|
|
tick();
|
|
const id = setInterval(tick, 1000);
|
|
return () => clearInterval(id);
|
|
}, []);
|
|
|
|
// Ping /api/health every 30s — measures real round-trip latency
|
|
useEffect(() => {
|
|
const ping = async () => {
|
|
const t0 = performance.now();
|
|
try {
|
|
const res = await fetch("/api/health", { cache: "no-store" });
|
|
const ms = Math.round(performance.now() - t0);
|
|
setLatency(ms);
|
|
if (!res.ok) {
|
|
setStatus("outage");
|
|
} else {
|
|
const body = await res.json().catch(() => ({}));
|
|
const host: string = body?.host ?? "";
|
|
if (host) setNode(new URL(host).hostname.replace("www.", ""));
|
|
setStatus(ms > 800 ? "degraded" : "ok");
|
|
}
|
|
} catch {
|
|
setLatency(null);
|
|
setStatus("outage");
|
|
}
|
|
};
|
|
ping();
|
|
const id = setInterval(ping, 30_000);
|
|
return () => clearInterval(id);
|
|
}, []);
|
|
|
|
const dotColor =
|
|
status === "ok" ? "#00ff41" :
|
|
status === "degraded" ? "#facc15" :
|
|
status === "outage" ? "#f87171" :
|
|
"rgba(0,255,255,0.3)";
|
|
|
|
return (
|
|
<div
|
|
className="ax-mono"
|
|
style={{
|
|
position: "fixed", top: 0, left: 0, right: 0, zIndex: 9995,
|
|
display: "flex", justifyContent: "space-between", alignItems: "center",
|
|
padding: "4px 20px",
|
|
background: "rgba(3,3,3,0.98)",
|
|
borderBottom: "1px solid rgba(0,255,255,0.07)",
|
|
fontSize: 9, letterSpacing: 2, textTransform: "uppercase",
|
|
}}
|
|
>
|
|
<span style={{ color: "rgba(0,255,255,0.35)" }}>
|
|
AeThex.OS v{APP_VERSION} // Forge Terminal
|
|
</span>
|
|
<span style={{ color: "rgba(0,255,255,0.45)" }}>node: {node}</span>
|
|
<span style={{ display: "flex", alignItems: "center", gap: 6, color: "rgba(0,255,255,0.35)" }}>
|
|
<span style={{ color: dotColor, animation: "ax-blink 1s infinite", fontSize: 7 }}>●</span>
|
|
{latency !== null ? `${latency}ms` : "…"}
|
|
<span style={{ marginLeft: 6 }}>{time}</span>
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── HexLogo ──────────────────────────────────────────────────────────────────
|
|
function HexLogo({ size = 30 }: { size?: number }) {
|
|
return (
|
|
<svg viewBox="0 0 100 100" width={size} height={size}
|
|
style={{ filter: "drop-shadow(0 0 6px rgba(0,255,255,0.5))", flexShrink: 0 }}>
|
|
<polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5"
|
|
fill="none" stroke="#00ffff" strokeWidth="1.5" opacity="0.7" />
|
|
<polygon points="50,16 84,33 84,67 50,84 16,67 16,33"
|
|
fill="none" stroke="#00ffff" strokeWidth="1" opacity="0.25" />
|
|
<text x="50" y="58" textAnchor="middle"
|
|
fontFamily="Orbitron" fontSize="23" fontWeight="700" fill="#00ffff">Æ</text>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
// ─── NavLink helper ───────────────────────────────────────────────────────────
|
|
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
|
|
return (
|
|
<Link
|
|
to={to}
|
|
className="ax-mono"
|
|
style={{
|
|
fontSize: 10, letterSpacing: 2, textTransform: "uppercase",
|
|
color: "rgba(0,255,255,0.45)", textDecoration: "none", transition: "color 0.2s",
|
|
}}
|
|
onMouseEnter={e => (e.currentTarget.style.color = "#00ffff")}
|
|
onMouseLeave={e => (e.currentTarget.style.color = "rgba(0,255,255,0.45)")}
|
|
>
|
|
{children}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
// ─── Layout ───────────────────────────────────────────────────────────────────
|
|
interface LayoutProps {
|
|
children: React.ReactNode;
|
|
hideFooter?: boolean;
|
|
}
|
|
|
|
const FOOTER_LINKS = {
|
|
Platform: [
|
|
{ name: "Home", href: "/" },
|
|
{ name: "Realms", href: "/realms" },
|
|
{ name: "Developer Platform", href: "/dev-platform" },
|
|
{ name: "Dashboard", href: "/dashboard" },
|
|
],
|
|
Community: [
|
|
{ name: "Explore", href: "/engage" },
|
|
{ name: "Teams", href: "/teams" },
|
|
{ name: "Squads", href: "/squads" },
|
|
{ name: "Opportunities", href: "/opportunities" },
|
|
],
|
|
Resources: [
|
|
{ name: "Docs", href: "/docs" },
|
|
{ name: "API Reference", href: "/dev-platform/api-reference" },
|
|
{ name: "Quick Start", href: "/dev-platform/quick-start" },
|
|
{ name: "Roadmap", href: "/roadmap" },
|
|
],
|
|
Legal: [
|
|
{ name: "About", href: "/about" },
|
|
{ name: "Contact", href: "/contact" },
|
|
{ name: "Blog", href: "/blog" },
|
|
],
|
|
};
|
|
|
|
export default function CodeLayout({ children, hideFooter }: LayoutProps) {
|
|
const location = useLocation();
|
|
const { user, profile, roles, signOut, loading } = useAuth();
|
|
const isPrivileged = Array.isArray(roles) && roles.some(r => ["owner", "admin", "founder"].includes(r.toLowerCase()));
|
|
const isInternal = Array.isArray(roles) && roles.some(r => ["owner", "admin", "founder", "staff"].includes(r.toLowerCase()));
|
|
useArmTheme(); // keep context alive for downstream consumers
|
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
|
|
const publicNavLinks = [
|
|
{ name: "Realms", href: "/realms" },
|
|
{ name: "Dev Platform", href: "/dev-platform" },
|
|
{ name: "Docs", href: "/docs" },
|
|
{ name: "About", href: "/about" },
|
|
];
|
|
|
|
const userNavLinks = [
|
|
{ name: "Dashboard", href: "/dashboard" },
|
|
{ name: "Realms", href: "/realms" },
|
|
{ name: "Teams", href: "/teams" },
|
|
{ name: "Dev Platform", href: "/dev-platform" },
|
|
{ name: "Engage", href: "/engage" },
|
|
];
|
|
|
|
const navLinks = user ? userNavLinks : publicNavLinks;
|
|
|
|
// Inject global brand styles once
|
|
useEffect(() => {
|
|
const existing = document.getElementById("ax-forge-styles");
|
|
if (!existing) {
|
|
const el = document.createElement("style");
|
|
el.id = "ax-forge-styles";
|
|
el.textContent = AX_STYLES;
|
|
document.head.appendChild(el);
|
|
}
|
|
}, []);
|
|
|
|
const passportHref = profile?.username ? `/passport/${profile.username}` : "/passport/me";
|
|
const userInitials = (profile?.full_name || profile?.username || "U")
|
|
.split(" ").map((n: string) => n[0]).join("").toUpperCase();
|
|
|
|
return (
|
|
<div className="ax-flicker" style={{ minHeight: "100vh", background: "#050505", color: "#e0e0e0" }}>
|
|
<SysBar />
|
|
|
|
{/* ── Navigation ──────────────────────────────────────────────────────── */}
|
|
<header style={{
|
|
position: "fixed", top: 22, left: 0, right: 0, zIndex: 50,
|
|
borderBottom: "1px solid rgba(0,255,255,0.1)",
|
|
background: "rgba(5,5,5,0.95)",
|
|
backdropFilter: "blur(12px)",
|
|
}}>
|
|
{/* TrinityBar */}
|
|
<div style={{ display: "flex", height: 2 }}>
|
|
<div style={{ flex: 1, background: "#ff003c", opacity: 0.45 }} />
|
|
<div style={{ flex: 1, background: "#00d4ff", opacity: 0.45 }} />
|
|
<div style={{ flex: 1, background: "#ffb800", opacity: 0.45 }} />
|
|
</div>
|
|
|
|
<div style={{
|
|
maxWidth: 1200, margin: "0 auto",
|
|
padding: "10px 24px",
|
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
gap: 16,
|
|
}}>
|
|
{/* Logo */}
|
|
<Link to="/" style={{ display: "flex", alignItems: "center", gap: 10, textDecoration: "none" }}>
|
|
<HexLogo size={30} />
|
|
<span
|
|
className="ax-orbitron ax-glitch"
|
|
style={{ fontSize: 13, fontWeight: 700, letterSpacing: 4, color: "#00ffff",
|
|
textShadow: "0 0 10px rgba(0,255,255,0.4)", textDecoration: "none" }}
|
|
>
|
|
AETHEX
|
|
</span>
|
|
</Link>
|
|
|
|
{/* Desktop nav links */}
|
|
<nav className="hidden md:flex" style={{ alignItems: "center", gap: 24, flex: 1, justifyContent: "center" }}>
|
|
{navLinks.map((item) => (
|
|
<NavLink key={item.href} to={item.href}>{item.name}</NavLink>
|
|
))}
|
|
</nav>
|
|
|
|
{/* Auth */}
|
|
<div style={{ display: "flex", alignItems: "center", gap: 12, flexShrink: 0 }}>
|
|
{!loading && (
|
|
<>
|
|
{user ? (
|
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
<NotificationBell />
|
|
{profile ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button style={{
|
|
background: "none", border: "1px solid rgba(0,255,255,0.25)",
|
|
cursor: "pointer", borderRadius: 0, padding: 0,
|
|
}}>
|
|
<Avatar className="h-8 w-8" style={{ borderRadius: 0 }}>
|
|
<AvatarImage src={profile.avatar_url || undefined} alt={profile.full_name || profile.username} />
|
|
<AvatarFallback
|
|
className="ax-mono"
|
|
style={{ background: "rgba(0,255,255,0.08)", color: "#00ffff", fontSize: 11, borderRadius: 0 }}
|
|
>
|
|
{userInitials}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
className="w-52"
|
|
align="end"
|
|
style={{ background: "#080808", border: "1px solid rgba(0,255,255,0.2)", borderRadius: 0, zIndex: 99999 }}
|
|
>
|
|
<div className="ax-mono" style={{ padding: "10px 12px", borderBottom: "1px solid rgba(0,255,255,0.1)", fontSize: 10, color: "rgba(0,255,255,0.5)", letterSpacing: 1 }}>
|
|
{profile.full_name || profile.username}
|
|
</div>
|
|
{/* User items */}
|
|
<DropdownMenuItem asChild>
|
|
<Link to="/dashboard" className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
|
|
<User className="mr-2 h-3.5 w-3.5" />Dashboard
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link to="/profile" className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
|
|
<UserCircle className="mr-2 h-3.5 w-3.5" />My Profile
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link to={passportHref} className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
|
|
<Sparkles className="mr-2 h-3.5 w-3.5" />AeThex Passport
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link to="/dashboard?tab=settings" className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
|
|
<Settings className="mr-2 h-3.5 w-3.5" />Settings
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
|
|
{/* Internal section — staff/admin/owner only */}
|
|
{isInternal && (
|
|
<>
|
|
<DropdownMenuSeparator style={{ background: "rgba(0,255,255,0.08)" }} />
|
|
<div className="ax-mono" style={{ padding: "4px 12px 2px", fontSize: 9, color: "rgba(0,255,255,0.3)", letterSpacing: 2, textTransform: "uppercase" }}>
|
|
Internal
|
|
</div>
|
|
{isPrivileged && (
|
|
<DropdownMenuItem asChild>
|
|
<Link to="/admin" className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
|
|
<Shield className="mr-2 h-3.5 w-3.5" />Admin
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuItem asChild>
|
|
<Link to="/internal-docs" className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
|
|
<BookOpen className="mr-2 h-3.5 w-3.5" />Internal Docs
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
</>
|
|
)}
|
|
|
|
<DropdownMenuSeparator style={{ background: "rgba(0,255,255,0.1)" }} />
|
|
<DropdownMenuItem
|
|
className="cursor-pointer ax-mono"
|
|
style={{ fontSize: 11, color: "rgba(255,80,80,0.7)" }}
|
|
onClick={() => signOut()}
|
|
>
|
|
<LogOut className="mr-2 h-3.5 w-3.5" />Sign out
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : (
|
|
<button
|
|
className="ax-mono"
|
|
onClick={() => signOut()}
|
|
style={{
|
|
background: "none", border: "none", cursor: "pointer",
|
|
fontSize: 10, letterSpacing: 2, textTransform: "uppercase",
|
|
color: "rgba(255,80,80,0.6)", padding: "4px 8px",
|
|
}}
|
|
>
|
|
Sign out
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="hidden sm:flex" style={{ alignItems: "center", gap: 12 }}>
|
|
<NavLink to="/login">Sign In</NavLink>
|
|
<Link
|
|
to="/onboarding"
|
|
className="ax-mono ax-clip"
|
|
style={{
|
|
fontSize: 10, letterSpacing: 2, textTransform: "uppercase",
|
|
border: "1px solid rgba(0,255,255,0.5)", color: "#00ffff",
|
|
padding: "8px 18px", textDecoration: "none",
|
|
background: "rgba(0,255,255,0.04)", transition: "all 0.2s",
|
|
display: "inline-block",
|
|
}}
|
|
onMouseEnter={e => {
|
|
e.currentTarget.style.background = "rgba(0,255,255,0.12)";
|
|
e.currentTarget.style.boxShadow = "0 0 16px rgba(0,255,255,0.2)";
|
|
}}
|
|
onMouseLeave={e => {
|
|
e.currentTarget.style.background = "rgba(0,255,255,0.04)";
|
|
e.currentTarget.style.boxShadow = "none";
|
|
}}
|
|
>
|
|
Join AeThex →
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Mobile hamburger */}
|
|
<button
|
|
className="md:hidden"
|
|
onClick={() => setMobileOpen(o => !o)}
|
|
style={{ background: "none", border: "none", color: "rgba(0,255,255,0.6)", cursor: "pointer", padding: 4 }}
|
|
>
|
|
{mobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile menu */}
|
|
{mobileOpen && (
|
|
<div style={{
|
|
borderTop: "1px solid rgba(0,255,255,0.1)",
|
|
background: "rgba(5,5,5,0.98)",
|
|
padding: "16px 24px",
|
|
display: "flex", flexDirection: "column", gap: 12,
|
|
}}>
|
|
{navLinks.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
to={item.href}
|
|
onClick={() => setMobileOpen(false)}
|
|
className="ax-mono"
|
|
style={{
|
|
fontSize: 11, letterSpacing: 2, textTransform: "uppercase",
|
|
color: "rgba(0,255,255,0.5)", textDecoration: "none", padding: "8px 0",
|
|
borderBottom: "1px solid rgba(0,255,255,0.06)",
|
|
}}
|
|
>
|
|
{item.name}
|
|
</Link>
|
|
))}
|
|
{!user && (
|
|
<Link
|
|
to="/onboarding"
|
|
onClick={() => setMobileOpen(false)}
|
|
className="ax-mono"
|
|
style={{
|
|
marginTop: 4, fontSize: 11, letterSpacing: 2, textTransform: "uppercase",
|
|
border: "1px solid rgba(0,255,255,0.4)", color: "#00ffff",
|
|
padding: "12px 16px", textDecoration: "none", textAlign: "center",
|
|
}}
|
|
>
|
|
Join AeThex →
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)}
|
|
</header>
|
|
|
|
{/* ── Content ─────────────────────────────────────────────────────────── */}
|
|
<main style={{ paddingTop: 80 }}>
|
|
{children}
|
|
</main>
|
|
|
|
{/* ── Footer ──────────────────────────────────────────────────────────── */}
|
|
{!hideFooter && (
|
|
<footer style={{
|
|
borderTop: "1px solid rgba(0,255,255,0.1)",
|
|
background: "rgba(3,3,3,0.95)",
|
|
marginTop: 80,
|
|
}}>
|
|
{/* TrinityBar */}
|
|
<div style={{ display: "flex", height: 2 }}>
|
|
<div style={{ flex: 1, background: "#ff003c", opacity: 0.3 }} />
|
|
<div style={{ flex: 1, background: "#00d4ff", opacity: 0.3 }} />
|
|
<div style={{ flex: 1, background: "#ffb800", opacity: 0.3 }} />
|
|
</div>
|
|
|
|
<div style={{ maxWidth: 1200, margin: "0 auto", padding: "48px 24px 32px" }}>
|
|
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr", gap: 40, marginBottom: 48 }}>
|
|
{/* Brand column */}
|
|
<div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
|
|
<HexLogo size={28} />
|
|
<span className="ax-orbitron" style={{ fontSize: 13, fontWeight: 700, letterSpacing: 4, color: "#00ffff" }}>
|
|
AETHEX
|
|
</span>
|
|
</div>
|
|
<p className="ax-mono" style={{ fontSize: 11, lineHeight: 1.8, color: "rgba(0,255,255,0.35)", letterSpacing: 1, maxWidth: 240 }}>
|
|
The integration layer connecting all metaverse platforms. Six specialized realms. One ecosystem.
|
|
</p>
|
|
<p className="ax-mono" style={{ fontSize: 10, color: "rgba(0,255,255,0.25)", marginTop: 16, letterSpacing: 1 }}>
|
|
Queen Creek, Arizona<br />
|
|
<a href="mailto:info@aethex.biz" style={{ color: "rgba(0,255,255,0.35)", textDecoration: "none" }}>info@aethex.biz</a>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Link columns */}
|
|
{Object.entries(FOOTER_LINKS).map(([section, links]) => (
|
|
<div key={section}>
|
|
<p className="ax-orbitron" style={{ fontSize: 9, fontWeight: 700, letterSpacing: 3, color: "rgba(0,255,255,0.5)", textTransform: "uppercase", marginBottom: 16 }}>
|
|
{section}
|
|
</p>
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
{links.map((link) => (
|
|
<Link
|
|
key={link.href}
|
|
to={link.href}
|
|
className="ax-mono"
|
|
style={{ fontSize: 10, letterSpacing: 1, color: "rgba(0,255,255,0.3)", textDecoration: "none", transition: "color 0.2s" }}
|
|
onMouseEnter={e => (e.currentTarget.style.color = "rgba(0,255,255,0.7)")}
|
|
onMouseLeave={e => (e.currentTarget.style.color = "rgba(0,255,255,0.3)")}
|
|
>
|
|
{link.name}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Bottom bar */}
|
|
<div style={{
|
|
borderTop: "1px solid rgba(0,255,255,0.07)",
|
|
paddingTop: 20,
|
|
display: "flex", justifyContent: "space-between", alignItems: "center",
|
|
flexWrap: "wrap", gap: 12,
|
|
}}>
|
|
<span className="ax-mono" style={{ fontSize: 9, letterSpacing: 2, color: "rgba(0,255,255,0.25)", textTransform: "uppercase" }}>
|
|
© 2026 AeThex Corporation. All rights reserved.
|
|
</span>
|
|
<span className="ax-mono" style={{ fontSize: 9, letterSpacing: 2, color: "rgba(0,255,255,0.2)", textTransform: "uppercase" }}>
|
|
AeThex.OS v3.7.1 // aethex.dev
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
)}
|
|
|
|
<AIChatButton />
|
|
</div>
|
|
);
|
|
}
|