mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:07:20 +00:00
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:
parent
f7d1d94666
commit
0398f62df6
5 changed files with 631 additions and 4 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
219
client/src/pages/network-profile.tsx
Normal file
219
client/src/pages/network-profile.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
323
client/src/pages/network.tsx
Normal file
323
client/src/pages/network.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue