Add a network directory for viewing architects and their profiles

Introduce new routes and API endpoints for a public network directory, allowing users to view architect profiles and associated data.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 947fb3f7-a8df-488c-a0dd-73d268844a6f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/qyCtfDO
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-21 02:37:14 +00:00
parent f7d1d94666
commit 0398f62df6
5 changed files with 631 additions and 4 deletions

View file

@ -25,6 +25,8 @@ import AdminApplications from "@/pages/admin-applications";
import AdminActivity from "@/pages/admin-activity";
import AdminNotifications from "@/pages/admin-notifications";
import AeThexOS from "@/pages/os";
import Network from "@/pages/network";
import NetworkProfile from "@/pages/network-profile";
import { Chatbot } from "@/components/Chatbot";
function Router() {
@ -49,6 +51,8 @@ function Router() {
<Route path="/admin/notifications">{() => <ProtectedRoute><AdminNotifications /></ProtectedRoute>}</Route>
<Route path="/pitch" component={Pitch} />
<Route path="/os" component={AeThexOS} />
<Route path="/network" component={Network} />
<Route path="/network/:slug" component={NetworkProfile} />
<Route component={NotFound} />
</Switch>
);

View file

@ -95,11 +95,11 @@ export default function Home() {
Learn More <ArrowRight className="w-4 h-4" />
</button>
</Link>
<a href="https://aethex.foundation" target="_blank" rel="noopener noreferrer">
<button className="border border-white/20 text-white px-8 py-4 font-bold uppercase tracking-wider hover:bg-white/5 transition-colors flex items-center gap-2">
Join Foundation <ExternalLink className="w-4 h-4" />
<Link href="/network">
<button className="border border-cyan-500/30 text-cyan-500 px-8 py-4 font-bold uppercase tracking-wider hover:bg-cyan-500/10 transition-colors flex items-center gap-2" data-testid="button-view-network">
View The Network <Network className="w-4 h-4" />
</button>
</a>
</Link>
</div>
</motion.div>

View file

@ -0,0 +1,219 @@
import { motion } from "framer-motion";
import { Link, useParams } from "wouter";
import { useQuery } from "@tanstack/react-query";
import {
ArrowLeft, Shield, Zap, ExternalLink, Github, Twitter,
Globe, Award, Code, User, Loader2, AlertCircle
} from "lucide-react";
interface ArchitectProfile {
id: string;
name: string;
role: string;
bio: string | null;
level: number;
xp: number;
passportId: string | null;
skills: string[] | null;
isVerified: boolean;
avatarUrl: string | null;
github: string | null;
twitter: string | null;
website: string | null;
}
export default function NetworkProfile() {
const params = useParams<{ slug: string }>();
const slug = params.slug;
const { data: profile, isLoading, error } = useQuery<ArchitectProfile>({
queryKey: ['architect', slug],
queryFn: async () => {
const res = await fetch(`/api/directory/architects/${slug}`);
if (!res.ok) {
throw new Error('Architect not found');
}
return res.json();
},
enabled: !!slug,
});
if (isLoading) {
return (
<div className="min-h-screen bg-black text-white font-mono flex items-center justify-center">
<Loader2 className="w-8 h-8 text-cyan-500 animate-spin" />
</div>
);
}
if (error || !profile) {
return (
<div className="min-h-screen bg-black text-white font-mono flex flex-col items-center justify-center gap-4">
<AlertCircle className="w-12 h-12 text-red-500" />
<h1 className="text-2xl font-bold">Architect Not Found</h1>
<p className="text-cyan-500/60">The requested profile does not exist in our directory.</p>
<Link href="/network">
<button className="mt-4 flex items-center gap-2 text-cyan-500 hover:text-cyan-400 transition-colors">
<ArrowLeft className="w-4 h-4" />
Back to Network
</button>
</Link>
</div>
);
}
return (
<div className="min-h-screen bg-black text-white font-mono selection:bg-cyan-500 selection:text-black">
<div className="fixed inset-0 opacity-5 pointer-events-none">
<div className="absolute inset-0" style={{
backgroundImage: `repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,255,255,0.03) 1px, rgba(0,255,255,0.03) 2px)`,
backgroundSize: '100% 4px'
}} />
</div>
<nav className="relative z-20 flex justify-between items-center px-6 py-4 border-b border-cyan-500/20">
<Link href="/network">
<button className="flex items-center gap-2 text-cyan-500/60 hover:text-cyan-500 transition-colors" data-testid="link-back-network">
<ArrowLeft className="w-4 h-4" />
<span className="text-xs uppercase tracking-wider">Network</span>
</button>
</Link>
<div className="text-cyan-500 font-bold tracking-widest uppercase text-sm">
Architect Profile
</div>
<div className="w-20" />
</nav>
<main className="relative z-10 max-w-2xl mx-auto px-6 py-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-8"
>
<div className="text-center space-y-4">
<div className="w-24 h-24 mx-auto rounded-full bg-gradient-to-br from-cyan-500 to-purple-600 flex items-center justify-center">
{profile.avatarUrl ? (
<img
src={profile.avatarUrl}
alt={profile.name}
className="w-full h-full rounded-full object-cover"
/>
) : (
<User className="w-10 h-10 text-white" />
)}
</div>
<div>
<h1 className="text-3xl font-bold text-white flex items-center justify-center gap-2">
{profile.name}
{profile.isVerified && (
<Shield className="w-5 h-5 text-cyan-500" />
)}
</h1>
<p className="text-cyan-500/60 uppercase tracking-wider text-sm">
{profile.role}
</p>
</div>
{profile.passportId && (
<div className="inline-block border border-cyan-500/30 bg-cyan-500/10 px-4 py-2">
<span className="text-cyan-500/60 text-xs uppercase tracking-wider">Passport ID</span>
<p className="text-cyan-500 font-bold">{profile.passportId}</p>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="border border-purple-500/30 bg-purple-500/10 p-4 text-center">
<div className="flex items-center justify-center gap-1 text-purple-500 mb-1">
<Award className="w-4 h-4" />
<span className="text-xs uppercase tracking-wider">Level</span>
</div>
<p className="text-2xl font-bold text-white">{profile.level || 1}</p>
</div>
<div className="border border-yellow-500/30 bg-yellow-500/10 p-4 text-center">
<div className="flex items-center justify-center gap-1 text-yellow-500 mb-1">
<Zap className="w-4 h-4" />
<span className="text-xs uppercase tracking-wider">XP</span>
</div>
<p className="text-2xl font-bold text-white">{profile.xp?.toLocaleString() || 0}</p>
</div>
</div>
{profile.bio && (
<div className="border border-cyan-500/20 bg-cyan-500/5 p-6">
<h3 className="text-cyan-500/60 text-xs uppercase tracking-wider mb-2">Bio</h3>
<p className="text-white/80">{profile.bio}</p>
</div>
)}
{profile.skills && profile.skills.length > 0 && (
<div>
<h3 className="text-cyan-500/60 text-xs uppercase tracking-wider mb-3 flex items-center gap-2">
<Code className="w-4 h-4" />
Skills
</h3>
<div className="flex flex-wrap gap-2">
{profile.skills.map((skill) => (
<span
key={skill}
className="px-3 py-1 border border-cyan-500/30 bg-cyan-500/10 text-cyan-500 text-sm"
>
{skill}
</span>
))}
</div>
</div>
)}
{(profile.github || profile.twitter || profile.website) && (
<div className="flex justify-center gap-4 pt-4 border-t border-cyan-500/20">
{profile.github && (
<a
href={profile.github}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-cyan-500/60 hover:text-cyan-500 transition-colors"
data-testid="link-github"
>
<Github className="w-5 h-5" />
<span className="text-sm">GitHub</span>
</a>
)}
{profile.twitter && (
<a
href={profile.twitter}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-cyan-500/60 hover:text-cyan-500 transition-colors"
data-testid="link-twitter"
>
<Twitter className="w-5 h-5" />
<span className="text-sm">Twitter</span>
</a>
)}
{profile.website && (
<a
href={profile.website}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-cyan-500/60 hover:text-cyan-500 transition-colors"
data-testid="link-website"
>
<Globe className="w-5 h-5" />
<span className="text-sm">Website</span>
</a>
)}
</div>
)}
</motion.div>
</main>
<footer className="relative z-10 border-t border-cyan-500/10 py-8 px-6 text-center">
<div className="text-cyan-500/30 text-xs uppercase tracking-wider">
AeThex Network // Architect Profile
</div>
</footer>
</div>
);
}

View file

@ -0,0 +1,323 @@
import { motion } from "framer-motion";
import { Link } from "wouter";
import { useQuery } from "@tanstack/react-query";
import {
Terminal, Users, Cpu, Globe, ExternalLink, Shield,
ArrowLeft, Zap, Lock, Code, Sparkles
} from "lucide-react";
interface ApiArchitect {
id: string;
name: string;
role: string;
bio: string | null;
level: number;
xp: number;
passportId: string | null;
skills: string[] | null;
}
interface DisplayArchitect {
id: string;
index: number;
name: string;
role: string;
slug?: string;
isReserved?: boolean;
isLive?: boolean;
}
interface Protocol {
id: string;
name: string;
description: string;
link?: string;
}
interface Domain {
name: string;
purpose: string;
}
const NUM_RESERVED_SLOTS = 10;
const PROTOCOLS: Protocol[] = [
{
id: "aegis",
name: "AEGIS",
description: "Identity & Security Layer",
link: "https://github.com"
},
{
id: "warden",
name: "WARDEN",
description: "Browser Security Extension",
link: "https://chrome.google.com/webstore"
},
{
id: "lonestar",
name: "LONE STAR",
description: "Simulation Engine",
link: "https://roblox.com"
},
];
const DOMAINS: Domain[] = [
{ name: ".foundation", purpose: "Governance & Policy" },
{ name: ".studio", purpose: "Labs & Education" },
{ name: ".dev", purpose: "Developer Tools" },
{ name: ".network", purpose: "Public Directory" },
];
export default function Network() {
const { data: metrics } = useQuery({
queryKey: ["metrics"],
queryFn: async () => {
const res = await fetch("/api/metrics");
return res.json();
},
});
const { data: liveArchitects = [] } = useQuery<ApiArchitect[]>({
queryKey: ["directory-architects"],
queryFn: async () => {
const res = await fetch("/api/directory/architects");
if (!res.ok) return [];
return res.json();
},
});
// Combine live architects with reserved slots
const displayArchitects: DisplayArchitect[] = [
...liveArchitects.map((a, index) => ({
id: String(index + 1).padStart(3, "0"),
index: index + 1,
name: a.name,
role: a.role || "Architect",
slug: a.passportId || a.name.toLowerCase().replace(/\s+/g, '-'),
isLive: true,
})),
...Array.from({ length: Math.max(0, NUM_RESERVED_SLOTS - liveArchitects.length) }, (_, i) => ({
id: String(liveArchitects.length + i + 1).padStart(3, "0"),
index: liveArchitects.length + i + 1,
name: "[RESERVED FOR FOUNDRY]",
role: "Available Slot",
isReserved: true,
})),
];
const activeNodes = liveArchitects.length;
return (
<div className="min-h-screen bg-black text-white font-mono selection:bg-cyan-500 selection:text-black">
<div className="fixed inset-0 opacity-5 pointer-events-none">
<div className="absolute inset-0" style={{
backgroundImage: `repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,255,255,0.03) 1px, rgba(0,255,255,0.03) 2px)`,
backgroundSize: '100% 4px'
}} />
</div>
<nav className="relative z-20 flex justify-between items-center px-6 py-4 border-b border-cyan-500/20">
<Link href="/">
<button className="flex items-center gap-2 text-cyan-500/60 hover:text-cyan-500 transition-colors" data-testid="link-back-home">
<ArrowLeft className="w-4 h-4" />
<span className="text-xs uppercase tracking-wider">Back</span>
</button>
</Link>
<div className="text-cyan-500 font-bold tracking-widest uppercase text-sm">
AeThex.Network
</div>
<div className="text-cyan-500/40 text-xs">
v1.0.0
</div>
</nav>
<header className="relative z-10 border-b border-cyan-500/20 py-12 px-6">
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-4"
>
<div className="flex items-center gap-3">
<Terminal className="w-8 h-8 text-cyan-500" />
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-white uppercase">
The Network
</h1>
</div>
<p className="text-cyan-500/60 text-lg max-w-2xl">
A directory of verified entities in the AeThex ecosystem.
</p>
<div className="flex items-center gap-6 pt-4">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
<span className="text-green-500 text-sm uppercase tracking-wider">
Nodes Detected: <span className="font-bold">{activeNodes}</span>
</span>
</div>
<div className="text-cyan-500/40 text-sm">
Last Sync: {new Date().toLocaleDateString()}
</div>
</div>
</motion.div>
</div>
</header>
<main className="relative z-10 max-w-4xl mx-auto px-6 py-12 space-y-16">
<section>
<div className="flex items-center gap-3 mb-6">
<Users className="w-5 h-5 text-cyan-500" />
<h2 className="text-xl font-bold uppercase tracking-wider text-white">
The Architects
</h2>
<span className="text-cyan-500/40 text-sm">// Humans</span>
</div>
<div className="space-y-1">
{displayArchitects.map((architect: DisplayArchitect, idx: number) => (
<motion.div
key={architect.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.05 }}
className={`group flex items-center justify-between py-3 px-4 border-l-2 transition-all ${
architect.isReserved
? "border-yellow-500/30 bg-yellow-500/5 hover:bg-yellow-500/10"
: "border-cyan-500/30 bg-cyan-500/5 hover:bg-cyan-500/10"
}`}
data-testid={`architect-row-${architect.id}`}
>
<div className="flex items-center gap-4">
<span className={`font-mono text-sm ${architect.isReserved ? "text-yellow-500/60" : "text-cyan-500/60"}`}>
[{architect.id}]
</span>
<div>
<span className={`font-bold ${architect.isReserved ? "text-yellow-500/80" : "text-white"}`}>
{architect.name}
</span>
<span className="text-cyan-500/40 text-sm ml-2">
{architect.role}
</span>
</div>
</div>
{architect.isReserved ? (
<a
href="https://aethex.studio"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-yellow-500 hover:text-yellow-400 transition-colors uppercase tracking-wider"
data-testid={`link-join-foundry-${architect.id}`}
>
Join Foundry <ExternalLink className="w-3 h-3" />
</a>
) : architect.isLive && architect.slug ? (
<Link href={`/network/${architect.slug}`}>
<span className="flex items-center gap-1 text-xs text-cyan-500/60 hover:text-cyan-500 transition-colors uppercase tracking-wider cursor-pointer">
View Profile <ArrowLeft className="w-3 h-3 rotate-180" />
</span>
</Link>
) : null}
</motion.div>
))}
</div>
</section>
<section>
<div className="flex items-center gap-3 mb-6">
<Cpu className="w-5 h-5 text-purple-500" />
<h2 className="text-xl font-bold uppercase tracking-wider text-white">
The Protocols
</h2>
<span className="text-purple-500/40 text-sm">// Technology</span>
</div>
<div className="grid gap-4">
{PROTOCOLS.map((protocol, idx) => (
<motion.a
key={protocol.id}
href={protocol.link}
target="_blank"
rel="noopener noreferrer"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.1 }}
className="group flex items-center justify-between py-4 px-4 border-l-2 border-purple-500/30 bg-purple-500/5 hover:bg-purple-500/10 transition-all"
data-testid={`protocol-row-${protocol.id}`}
>
<div className="flex items-center gap-4">
{protocol.id === "aegis" && <Shield className="w-5 h-5 text-purple-500" />}
{protocol.id === "warden" && <Lock className="w-5 h-5 text-purple-500" />}
{protocol.id === "lonestar" && <Sparkles className="w-5 h-5 text-purple-500" />}
<div>
<span className="font-bold text-white">{protocol.name}</span>
<span className="text-purple-500/60 text-sm ml-2"> {protocol.description}</span>
</div>
</div>
<ExternalLink className="w-4 h-4 text-purple-500/40 group-hover:text-purple-500 transition-colors" />
</motion.a>
))}
</div>
</section>
<section>
<div className="flex items-center gap-3 mb-6">
<Globe className="w-5 h-5 text-green-500" />
<h2 className="text-xl font-bold uppercase tracking-wider text-white">
The Domains
</h2>
<span className="text-green-500/40 text-sm">// Real Estate</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{DOMAINS.map((domain, idx) => (
<motion.div
key={domain.name}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.1 }}
className="p-4 border border-green-500/20 bg-green-500/5 hover:bg-green-500/10 transition-all text-center"
data-testid={`domain-card-${domain.name}`}
>
<div className="font-bold text-green-500 text-lg">{domain.name}</div>
<div className="text-green-500/60 text-xs uppercase tracking-wider mt-1">{domain.purpose}</div>
</motion.div>
))}
</div>
</section>
<section className="border-t border-cyan-500/20 pt-12">
<div className="text-center space-y-6">
<div className="flex items-center justify-center gap-2">
<Zap className="w-5 h-5 text-yellow-500" />
<h3 className="text-lg font-bold uppercase tracking-wider text-white">
Want Your Name on This List?
</h3>
</div>
<p className="text-cyan-500/60 max-w-md mx-auto">
Join The Foundry and become a verified Architect in the AeThex ecosystem.
Your name gets hardcoded into the Network Genesis Block.
</p>
<a
href="https://aethex.studio"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 bg-gradient-to-r from-yellow-500 to-orange-500 text-black px-8 py-3 font-bold uppercase tracking-wider hover:from-yellow-400 hover:to-orange-400 transition-all"
data-testid="link-join-foundry-cta"
>
Join The Foundry <ExternalLink className="w-4 h-4" />
</a>
</div>
</section>
</main>
<footer className="relative z-10 border-t border-cyan-500/10 py-8 px-6 text-center">
<div className="text-cyan-500/30 text-xs uppercase tracking-wider">
AeThex Network // The Public Square
</div>
</footer>
</div>
);
}

View file

@ -185,6 +185,87 @@ export async function registerRoutes(
}
});
// ========== PUBLIC DIRECTORY ROUTES ==========
// Get public directory of verified architects
app.get("/api/directory/architects", async (req, res) => {
try {
const profiles = await storage.getProfiles();
// Filter and map to public-safe fields
const publicProfiles = profiles
.filter(p => p.is_verified || ['admin', 'oversee', 'employee'].includes(p.role || ''))
.map((p, index) => ({
id: String(index + 1).padStart(3, '0'),
name: p.full_name || p.username || p.email?.split('@')[0] || 'Architect',
role: p.role || 'member',
bio: p.bio,
level: p.level,
xp: p.total_xp,
passportId: p.aethex_passport_id,
skills: p.skills,
}));
res.json(publicProfiles);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// Get public directory of projects
app.get("/api/directory/projects", async (req, res) => {
try {
const projects = await storage.getProjects();
// Map to public-safe fields
const publicProjects = projects.map(p => ({
id: p.id,
name: p.title,
description: p.description,
techStack: p.technologies,
status: p.status,
}));
res.json(publicProjects);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// Get single architect profile by username/slug
app.get("/api/directory/architects/:slug", async (req, res) => {
try {
const { slug } = req.params;
const profiles = await storage.getProfiles();
const profile = profiles.find(p =>
p.aethex_passport_id?.toLowerCase() === slug.toLowerCase() ||
p.full_name?.toLowerCase() === slug.toLowerCase() ||
p.username?.toLowerCase() === slug.toLowerCase() ||
p.email?.split('@')[0].toLowerCase() === slug.toLowerCase()
);
if (!profile) {
return res.status(404).json({ error: "Architect not found" });
}
// Return public-safe fields only
const socialLinks = profile.social_links || {};
res.json({
id: profile.id,
name: profile.full_name || profile.username || profile.email?.split('@')[0] || 'Architect',
role: profile.role,
bio: profile.bio,
level: profile.level,
xp: profile.total_xp,
passportId: profile.aethex_passport_id,
skills: profile.skills,
isVerified: profile.is_verified,
avatarUrl: profile.avatar_url,
github: socialLinks.github,
twitter: socialLinks.twitter,
website: socialLinks.website,
});
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// ========== ADMIN-PROTECTED API ROUTES ==========
// Get all profiles (admin only)