Prettier format pending files

This commit is contained in:
Builder.io 2025-11-15 02:11:47 +00:00
parent db3df1da48
commit aaac82137c
16 changed files with 1043 additions and 763 deletions

View file

@ -59,9 +59,7 @@ export default async function handler(req: any, res: any) {
const { arm_affiliation } = req.body;
if (!arm_affiliation) {
return res
.status(400)
.json({ error: "Missing arm_affiliation" });
return res.status(400).json({ error: "Missing arm_affiliation" });
}
if (!VALID_ARMS.includes(arm_affiliation)) {
@ -115,9 +113,7 @@ export default async function handler(req: any, res: any) {
const { arm_affiliation } = req.body;
if (!arm_affiliation) {
return res
.status(400)
.json({ error: "Missing arm_affiliation" });
return res.status(400).json({ error: "Missing arm_affiliation" });
}
// Unfollow the arm

View file

@ -193,7 +193,10 @@ const App = () => (
<Route path="/trust" element={<Trust />} />
<Route path="/press" element={<PressKit />} />
<Route path="/projects" element={<Projects />} />
<Route path="/projects/admin" element={<ProjectsAdmin />} />
<Route
path="/projects/admin"
element={<ProjectsAdmin />}
/>
<Route path="/directory" element={<Directory />} />
<Route path="/admin" element={<Admin />} />
<Route path="/admin/feed" element={<AdminFeed />} />
@ -256,15 +259,30 @@ const App = () => (
{/* Legacy redirects for backwards compatibility */}
<Route
path="/developers"
element={<Navigate to="/foundation/community/developers" replace />}
element={
<Navigate
to="/foundation/community/developers"
replace
/>
}
/>
<Route
path="/developers/me"
element={<Navigate to="/foundation/community/developers" replace />}
element={
<Navigate
to="/foundation/community/developers"
replace
/>
}
/>
<Route
path="/developers/:id"
element={<Navigate to="/foundation/community/developers" replace />}
element={
<Navigate
to="/foundation/community/developers"
replace
/>
}
/>
<Route
path="/profiles"
@ -283,20 +301,32 @@ const App = () => (
path="/passport"
element={<Navigate to="/passport/me" replace />}
/>
<Route path="/passport/me" element={<ProfilePassport />} />
<Route
path="/passport/me"
element={<ProfilePassport />}
/>
<Route
path="/passport/:username"
element={<ProfilePassport />}
/>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<SignupRedirect />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route
path="/reset-password"
element={<ResetPassword />}
/>
<Route
path="/roblox-callback"
element={<RobloxCallback />}
/>
<Route path="/web3-callback" element={<Web3Callback />} />
<Route path="/discord-verify" element={<DiscordVerify />} />
<Route
path="/web3-callback"
element={<Web3Callback />}
/>
<Route
path="/discord-verify"
element={<DiscordVerify />}
/>
<Route path="/activity" element={<Activity />} />
<Route path="/discord" element={<DiscordActivity />} />
<Route
@ -305,7 +335,10 @@ const App = () => (
/>
{/* Creator Network routes */}
<Route path="/creators" element={<CreatorDirectory />} />
<Route
path="/creators"
element={<CreatorDirectory />}
/>
<Route
path="/creators/:username"
element={<CreatorProfile />}
@ -333,7 +366,10 @@ const App = () => (
path="/consulting"
element={<Navigate to="/corp" replace />}
/>
<Route path="/services" element={<Navigate to="/corp" replace />} />
<Route
path="/services"
element={<Navigate to="/corp" replace />}
/>
<Route
path="/mentorship"
@ -352,7 +388,10 @@ const App = () => (
path="/labs/explore-research"
element={<LabsExploreResearch />}
/>
<Route path="/labs/join-team" element={<LabsJoinTeam />} />
<Route
path="/labs/join-team"
element={<LabsJoinTeam />}
/>
<Route
path="/labs/get-involved"
element={<LabsGetInvolved />}
@ -584,7 +623,10 @@ const App = () => (
>
<Route index element={<DocsOverview />} />
<Route path="tutorials" element={<DocsTutorials />} />
<Route path="curriculum" element={<DocsCurriculum />} />
<Route
path="curriculum"
element={<DocsCurriculum />}
/>
<Route
path="getting-started"
element={<DocsGettingStarted />}
@ -605,15 +647,22 @@ const App = () => (
{/* Legacy /community redirect to /foundation/community */}
<Route
path="/community"
element={<Navigate to="/foundation/community" replace />}
element={
<Navigate to="/foundation/community" replace />
}
/>
<Route
path="/community/:tabId"
element={<Navigate to="/foundation/community" replace />}
element={
<Navigate to="/foundation/community" replace />
}
/>
{/* Ethos Guild Routes */}
<Route path="/ethos/library" element={<TrackLibrary />} />
<Route
path="/ethos/library"
element={<TrackLibrary />}
/>
<Route
path="/ethos/artists/:userId"
element={<ArtistProfile />}
@ -643,7 +692,10 @@ const App = () => (
<Route path="/get-started" element={<GetStarted />} />
<Route path="/explore" element={<Explore />} />
{/* Legacy /services redirect to /corp */}
<Route path="/services" element={<Navigate to="/corp" replace />} />
<Route
path="/services"
element={<Navigate to="/corp" replace />}
/>
<Route path="/careers" element={<Careers />} />
{/* Legal routes */}
@ -736,7 +788,10 @@ const App = () => (
/>
{/* Internal Docs Hub Routes */}
<Route path="/internal-docs" element={<Space1Welcome />} />
<Route
path="/internal-docs"
element={<Space1Welcome />}
/>
<Route
path="/internal-docs/axiom-model"
element={<Space1AxiomModel />}

View file

@ -18,18 +18,85 @@ import { aethexSocialService } from "@/lib/aethex-social-service";
import { cn } from "@/lib/utils";
import { normalizeErrorMessage } from "@/lib/error-utils";
import { communityService, realtimeService } from "@/lib/supabase-service";
import { ArrowUpRight, RotateCcw, TrendingUp, Users, Zap, Gamepad2, Briefcase, BookOpen, Network, Shield, Sparkles } from "lucide-react";
import {
ArrowUpRight,
RotateCcw,
TrendingUp,
Users,
Zap,
Gamepad2,
Briefcase,
BookOpen,
Network,
Shield,
Sparkles,
} from "lucide-react";
export type ArmType = "labs" | "gameforge" | "corp" | "foundation" | "devlink" | "nexus" | "staff";
export type ArmType =
| "labs"
| "gameforge"
| "corp"
| "foundation"
| "devlink"
| "nexus"
| "staff";
const ARMS: { id: ArmType; label: string; icon: any; color: string; description: string }[] = [
{ id: "labs", label: "Labs", icon: Zap, color: "text-yellow-400", description: "Innovation and experimentation" },
{ id: "gameforge", label: "GameForge", icon: Gamepad2, color: "text-green-400", description: "Game development excellence" },
{ id: "corp", label: "Corp", icon: Briefcase, color: "text-blue-400", description: "Commercial partnerships" },
{ id: "foundation", label: "Foundation", icon: BookOpen, color: "text-red-400", description: "Education and mentorship" },
{ id: "devlink", label: "Dev-Link", icon: Network, color: "text-cyan-400", description: "Developer networking" },
{ id: "nexus", label: "Nexus", icon: Sparkles, color: "text-purple-400", description: "Talent marketplace" },
{ id: "staff", label: "Staff", icon: Shield, color: "text-indigo-400", description: "Internal operations" },
const ARMS: {
id: ArmType;
label: string;
icon: any;
color: string;
description: string;
}[] = [
{
id: "labs",
label: "Labs",
icon: Zap,
color: "text-yellow-400",
description: "Innovation and experimentation",
},
{
id: "gameforge",
label: "GameForge",
icon: Gamepad2,
color: "text-green-400",
description: "Game development excellence",
},
{
id: "corp",
label: "Corp",
icon: Briefcase,
color: "text-blue-400",
description: "Commercial partnerships",
},
{
id: "foundation",
label: "Foundation",
icon: BookOpen,
color: "text-red-400",
description: "Education and mentorship",
},
{
id: "devlink",
label: "Dev-Link",
icon: Network,
color: "text-cyan-400",
description: "Developer networking",
},
{
id: "nexus",
label: "Nexus",
icon: Sparkles,
color: "text-purple-400",
description: "Talent marketplace",
},
{
id: "staff",
label: "Staff",
icon: Shield,
color: "text-indigo-400",
description: "Internal operations",
},
];
interface FeedItem {
@ -179,7 +246,8 @@ export default function ArmFeed({ arm }: ArmFeedProps) {
[isFollowingAuthor, user, toast],
);
const handleShare = useCallback(async (id: string) => {
const handleShare = useCallback(
async (id: string) => {
const url = `${location.origin}/${arm}#post-${id}`;
try {
if ((navigator as any).share) {
@ -192,7 +260,9 @@ export default function ArmFeed({ arm }: ArmFeedProps) {
} catch (error) {
console.warn("Share cancelled", error);
}
}, [arm]);
},
[arm],
);
const handleLike = useCallback(
async (postId: string) => {

View file

@ -59,8 +59,7 @@ const ProjectPassport = ({
};
const statusLabel = project.status || "active";
const statusClass =
statusColors[statusLabel] || statusColors["active"];
const statusClass = statusColors[statusLabel] || statusColors["active"];
return (
<Layout>

View file

@ -54,7 +54,7 @@ export const WalletVerification = ({
const normalized = walletInput.trim().toLowerCase();
if (!isValidEthereumAddress(normalized)) {
aethexToast.warning(
"Invalid Ethereum address. Must be 0x followed by 40 hexadecimal characters."
"Invalid Ethereum address. Must be 0x followed by 40 hexadecimal characters.",
);
return;
}
@ -78,7 +78,8 @@ export const WalletVerification = ({
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.error || `HTTP ${response.status}: Failed to connect wallet`
errorData.error ||
`HTTP ${response.status}: Failed to connect wallet`,
);
}
@ -93,7 +94,7 @@ export const WalletVerification = ({
} catch (error: any) {
console.error("[Wallet Verification] Error:", error?.message);
aethexToast.error(
error?.message || "Failed to connect wallet. Please try again."
error?.message || "Failed to connect wallet. Please try again.",
);
} finally {
setIsLoading(false);
@ -116,7 +117,8 @@ export const WalletVerification = ({
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.error || `HTTP ${response.status}: Failed to disconnect wallet`
errorData.error ||
`HTTP ${response.status}: Failed to disconnect wallet`,
);
}
@ -129,7 +131,7 @@ export const WalletVerification = ({
} catch (error: any) {
console.error("[Wallet Verification] Error:", error?.message);
aethexToast.error(
error?.message || "Failed to disconnect wallet. Please try again."
error?.message || "Failed to disconnect wallet. Please try again.",
);
} finally {
setIsLoading(false);
@ -150,7 +152,10 @@ export const WalletVerification = ({
<CardTitle className="flex items-center gap-2">
<span>🔐 Wallet Verification</span>
{connectedWallet && (
<Badge variant="outline" className="ml-auto border-emerald-500/30 bg-emerald-500/10 text-emerald-300">
<Badge
variant="outline"
className="ml-auto border-emerald-500/30 bg-emerald-500/10 text-emerald-300"
>
<CheckCircle className="mr-1 h-3 w-3" />
Connected
</Badge>
@ -199,11 +204,14 @@ export const WalletVerification = ({
Proves you're the owner of this wallet (Web3 identity)
</li>
<li>
Will unlock your <code className="text-aethex-300">.aethex</code> TLD
Will unlock your{" "}
<code className="text-aethex-300">.aethex</code> TLD
verification when the Protocol launches
</li>
<li> No smart contracts or gas fees required right now</li>
<li> Your wallet address is private and only visible to you</li>
<li>
Your wallet address is private and only visible to you
</li>
</ul>
</div>

View file

@ -18,14 +18,52 @@ import { communityService } from "@/lib/supabase-service";
import { Heart, MessageCircle, Share2, Volume2, VolumeX } from "lucide-react";
import type { FeedItem } from "@/pages/Feed";
const ARM_COLORS: Record<string, { bg: string; border: string; badge: string; text: string }> = {
labs: { bg: "bg-yellow-500/10", border: "border-l-4 border-l-yellow-400", badge: "bg-yellow-500/20 text-yellow-200", text: "text-yellow-400" },
gameforge: { bg: "bg-green-500/10", border: "border-l-4 border-l-green-400", badge: "bg-green-500/20 text-green-200", text: "text-green-400" },
corp: { bg: "bg-blue-500/10", border: "border-l-4 border-l-blue-400", badge: "bg-blue-500/20 text-blue-200", text: "text-blue-400" },
foundation: { bg: "bg-red-500/10", border: "border-l-4 border-l-red-400", badge: "bg-red-500/20 text-red-200", text: "text-red-400" },
devlink: { bg: "bg-cyan-500/10", border: "border-l-4 border-l-cyan-400", badge: "bg-cyan-500/20 text-cyan-200", text: "text-cyan-400" },
nexus: { bg: "bg-purple-500/10", border: "border-l-4 border-l-purple-400", badge: "bg-purple-500/20 text-purple-200", text: "text-purple-400" },
staff: { bg: "bg-indigo-500/10", border: "border-l-4 border-l-indigo-400", badge: "bg-indigo-500/20 text-indigo-200", text: "text-indigo-400" },
const ARM_COLORS: Record<
string,
{ bg: string; border: string; badge: string; text: string }
> = {
labs: {
bg: "bg-yellow-500/10",
border: "border-l-4 border-l-yellow-400",
badge: "bg-yellow-500/20 text-yellow-200",
text: "text-yellow-400",
},
gameforge: {
bg: "bg-green-500/10",
border: "border-l-4 border-l-green-400",
badge: "bg-green-500/20 text-green-200",
text: "text-green-400",
},
corp: {
bg: "bg-blue-500/10",
border: "border-l-4 border-l-blue-400",
badge: "bg-blue-500/20 text-blue-200",
text: "text-blue-400",
},
foundation: {
bg: "bg-red-500/10",
border: "border-l-4 border-l-red-400",
badge: "bg-red-500/20 text-red-200",
text: "text-red-400",
},
devlink: {
bg: "bg-cyan-500/10",
border: "border-l-4 border-l-cyan-400",
badge: "bg-cyan-500/20 text-cyan-200",
text: "text-cyan-400",
},
nexus: {
bg: "bg-purple-500/10",
border: "border-l-4 border-l-purple-400",
badge: "bg-purple-500/20 text-purple-200",
text: "text-purple-400",
},
staff: {
bg: "bg-indigo-500/10",
border: "border-l-4 border-l-indigo-400",
badge: "bg-indigo-500/20 text-indigo-200",
text: "text-indigo-400",
},
};
const ARM_LABELS: Record<string, string> = {
@ -112,12 +150,14 @@ export function FeedItemCard({
const armLabel = ARM_LABELS[item.arm || "labs"] || "LABS";
return (
<Card className={cn(
<Card
className={cn(
"overflow-hidden border-border/40 shadow-2xl backdrop-blur-lg",
armColor.border,
armColor.bg,
"bg-background/70"
)}>
"bg-background/70",
)}
>
<CardHeader className="pb-0 p-4 sm:p-5 lg:p-6 !flex !flex-row items-start justify-between gap-3 space-y-0">
<div className="flex flex-1 items-start gap-3">
<Avatar className="h-12 w-12 ring-2 ring-aethex-500/30">
@ -134,7 +174,9 @@ export function FeedItemCard({
<CardTitle className="text-lg font-semibold text-foreground">
{item.authorName}
</CardTitle>
<Badge className={cn("text-xs font-bold uppercase", armColor.badge)}>
<Badge
className={cn("text-xs font-bold uppercase", armColor.badge)}
>
{armLabel}
</Badge>
</div>

View file

@ -13,19 +13,17 @@ interface SubdomainPassportContextType {
error: string | null;
}
const SubdomainPassportContext = createContext<SubdomainPassportContextType>(
{
const SubdomainPassportContext = createContext<SubdomainPassportContextType>({
subdomainInfo: null,
isLoading: true,
error: null,
}
);
});
export const useSubdomainPassport = () => {
const context = useContext(SubdomainPassportContext);
if (!context) {
throw new Error(
"useSubdomainPassport must be used within SubdomainPassportProvider"
"useSubdomainPassport must be used within SubdomainPassportProvider",
);
}
return context;
@ -37,7 +35,7 @@ export const SubdomainPassportProvider = ({
children: React.ReactNode;
}) => {
const [subdomainInfo, setSubdomainInfo] = useState<SubdomainInfo | null>(
null
null,
);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View file

@ -34,7 +34,7 @@ export const aethexSocialService = {
async getFollowing(userId: string): Promise<string[]> {
try {
const resp = await fetch(
`${API_BASE}/api/social/following?userId=${encodeURIComponent(userId)}`
`${API_BASE}/api/social/following?userId=${encodeURIComponent(userId)}`,
);
if (!resp.ok) {
@ -60,7 +60,7 @@ export const aethexSocialService = {
async getFollowers(userId: string): Promise<string[]> {
try {
const resp = await fetch(
`${API_BASE}/api/social/followers?userId=${encodeURIComponent(userId)}`
`${API_BASE}/api/social/followers?userId=${encodeURIComponent(userId)}`,
);
if (!resp.ok) {

View file

@ -158,10 +158,15 @@ export default function AdminFeed() {
{/* Main Form */}
<Card className="border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
<CardHeader className="p-3 sm:p-4 lg:p-6">
<CardTitle className="text-lg sm:text-xl">Create a New Post</CardTitle>
<CardTitle className="text-lg sm:text-xl">
Create a New Post
</CardTitle>
</CardHeader>
<CardContent className="p-3 sm:p-4 lg:p-6">
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-5 lg:space-y-6">
<form
onSubmit={handleSubmit}
className="space-y-4 sm:space-y-5 lg:space-y-6"
>
{/* Title */}
<div className="space-y-1.5 sm:space-y-2">
<label className="block text-xs sm:text-sm font-medium text-foreground">
@ -288,7 +293,9 @@ export default function AdminFeed() {
{/* Quick Reference */}
<Card className="border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
<CardHeader className="p-3 sm:p-4 lg:p-6">
<CardTitle className="text-base sm:text-lg">Arm Color Guide</CardTitle>
<CardTitle className="text-base sm:text-lg">
Arm Color Guide
</CardTitle>
</CardHeader>
<CardContent className="p-3 sm:p-4 lg:p-6">
<div className="grid gap-2 sm:gap-3 grid-cols-2 sm:grid-cols-2 lg:grid-cols-4">
@ -297,7 +304,9 @@ export default function AdminFeed() {
key={arm.id}
className="flex items-center gap-2 rounded-lg border border-border/30 bg-background/60 p-2 sm:p-3"
>
<div className={`h-2 sm:h-3 w-2 sm:w-3 rounded-full ${arm.color}`} />
<div
className={`h-2 sm:h-3 w-2 sm:w-3 rounded-full ${arm.color}`}
/>
<span className="text-xs sm:text-sm font-medium text-foreground">
{arm.label}
</span>
@ -310,7 +319,9 @@ export default function AdminFeed() {
{/* Guidelines */}
<Card className="border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
<CardHeader className="p-3 sm:p-4 lg:p-6">
<CardTitle className="text-base sm:text-lg">Phase 1 Guidelines</CardTitle>
<CardTitle className="text-base sm:text-lg">
Phase 1 Guidelines
</CardTitle>
</CardHeader>
<CardContent className="p-3 sm:p-4 lg:p-6 space-y-2 sm:space-y-3 text-xs sm:text-sm text-muted-foreground">
<p>
@ -323,9 +334,9 @@ export default function AdminFeed() {
(Corp/Labs) separation is real.
</p>
<p>
🤝 <strong>Partnership Showcase:</strong> Use these posts to show
how different Arms collaborate. Example: "Corp hired 3 Architects
from Foundation via Nexus."
🤝 <strong>Partnership Showcase:</strong> Use these posts to
show how different Arms collaborate. Example: "Corp hired 3
Architects from Foundation via Nexus."
</p>
<p>
🚀 <strong>Phase 2:</strong> User-generated posts coming soon.

View file

@ -770,8 +770,7 @@ export default function Dashboard() {
<div className="space-y-3 flex-1">
<div className="flex items-center gap-3">
<h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-aethex-300 via-neon-blue to-aethex-400 bg-clip-text text-transparent">
{activeRealm === "game_developer" &&
"Game Development"}
{activeRealm === "game_developer" && "Game Development"}
{activeRealm === "client" && "Consulting"}
{activeRealm === "community_member" && "Community"}
{activeRealm === "customer" && "Getting Started"}
@ -784,7 +783,11 @@ export default function Dashboard() {
</div>
</div>
<p className="text-base text-muted-foreground max-w-xl">
Welcome back, <span className="text-aethex-300 font-semibold">{profile?.full_name || user.email?.split("@")[0]}</span> {streakLabel}
Welcome back,{" "}
<span className="text-aethex-300 font-semibold">
{profile?.full_name || user.email?.split("@")[0]}
</span>{" "}
{streakLabel}
</p>
{longestStreak > 0 && (
<div className="flex flex-wrap gap-2 pt-2">
@ -941,7 +944,12 @@ export default function Dashboard() {
className="bg-gradient-to-br from-card/60 to-card/30 border border-border/40 hover:border-aethex-400/50 transition-all duration-300 hover-lift animate-scale-in shadow-lg overflow-hidden group"
style={{ animationDelay: `${index * 0.1}s` }}
>
<div className="absolute inset-0 bg-gradient-to-br opacity-0 group-hover:opacity-5 transition-opacity duration-300" style={{background: `linear-gradient(135deg, var(--color-${stat.color.split('-')[1]}), transparent)`}} />
<div
className="absolute inset-0 bg-gradient-to-br opacity-0 group-hover:opacity-5 transition-opacity duration-300"
style={{
background: `linear-gradient(135deg, var(--color-${stat.color.split("-")[1]}), transparent)`,
}}
/>
<CardContent className="p-6 relative">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
@ -953,7 +961,7 @@ export default function Dashboard() {
</p>
</div>
<div
className={`p-3 rounded-xl bg-gradient-to-r ${stat.color} shadow-lg shadow-${stat.color.split('-')[1]}-500/20 group-hover:shadow-xl transition-all duration-300`}
className={`p-3 rounded-xl bg-gradient-to-r ${stat.color} shadow-lg shadow-${stat.color.split("-")[1]}-500/20 group-hover:shadow-xl transition-all duration-300`}
>
<Icon className="h-6 w-6 text-white" />
</div>
@ -1031,10 +1039,7 @@ export default function Dashboard() {
>
Create Team
</Button>
<Button
variant="outline"
onClick={() => navigate("/corp")}
>
<Button variant="outline" onClick={() => navigate("/corp")}>
Consulting Overview
</Button>
</CardContent>

View file

@ -235,7 +235,9 @@ export default function Feed() {
body: JSON.stringify({ arm_affiliation: arm }),
});
setFollowedArms((state) => state.filter((a) => a !== arm));
toast({ description: `Unfollowed ${ARMS.find((a) => a.id === arm)?.label}` });
toast({
description: `Unfollowed ${ARMS.find((a) => a.id === arm)?.label}`,
});
} else {
// Follow
await fetch(`/api/user/arm-follows?user_id=${user.id}`, {
@ -244,11 +246,16 @@ export default function Feed() {
body: JSON.stringify({ arm_affiliation: arm }),
});
setFollowedArms((state) => Array.from(new Set([...state, arm])));
toast({ description: `Following ${ARMS.find((a) => a.id === arm)?.label}!` });
toast({
description: `Following ${ARMS.find((a) => a.id === arm)?.label}!`,
});
}
} catch (error) {
console.error("Failed to update arm follow:", error);
toast({ variant: "destructive", description: "Failed to update preference" });
toast({
variant: "destructive",
description: "Failed to update preference",
});
}
};
@ -497,7 +504,8 @@ export default function Feed() {
onClick={handleManualRefresh}
className="gap-1 sm:gap-2 rounded-full border-border/60 bg-background/80 backdrop-blur text-xs sm:text-sm"
>
<RotateCcw className="h-3 sm:h-4 w-3 sm:w-4" /> <span className="hidden sm:inline">Refresh</span>
<RotateCcw className="h-3 sm:h-4 w-3 sm:w-4" />{" "}
<span className="hidden sm:inline">Refresh</span>
</Button>
</div>
</div>
@ -547,11 +555,15 @@ export default function Feed() {
<div className="space-y-2 sm:space-y-3">
<div className="flex items-center justify-between gap-2 text-xs">
<span className="uppercase text-muted-foreground font-semibold">Filter by Arms</span>
<span className="uppercase text-muted-foreground font-semibold">
Filter by Arms
</span>
<Button
size="sm"
variant="ghost"
onClick={() => setShowArmFollowManager(!showArmFollowManager)}
onClick={() =>
setShowArmFollowManager(!showArmFollowManager)
}
className="text-xs text-aethex-200 hover:text-aethex-100 h-auto p-1"
>
{showArmFollowManager ? "Hide" : "Manage"}
@ -564,7 +576,9 @@ export default function Feed() {
<Button
key={arm.id}
variant={
selectedArms.includes(arm.id) ? "default" : "outline"
selectedArms.includes(arm.id)
? "default"
: "outline"
}
size="sm"
onClick={() =>
@ -582,7 +596,10 @@ export default function Feed() {
)}
>
<arm.icon
className={cn("h-3 sm:h-3.5 w-3 sm:w-3.5", arm.color)}
className={cn(
"h-3 sm:h-3.5 w-3 sm:w-3.5",
arm.color,
)}
/>
<span className="font-medium hidden sm:inline">
{arm.label}
@ -595,14 +612,19 @@ export default function Feed() {
{showArmFollowManager && user?.id && (
<div className="rounded-xl sm:rounded-2xl border border-border/40 bg-background/60 p-3 sm:p-4 space-y-2 sm:space-y-3">
<p className="text-xs text-muted-foreground leading-relaxed">
Follow arms to personalize your feed. Only posts from followed arms will appear in your "Following" tab.
Follow arms to personalize your feed. Only posts from
followed arms will appear in your "Following" tab.
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-1.5 sm:gap-2">
{ARMS.map((arm) => (
<Button
key={arm.id}
size="sm"
variant={followedArms.includes(arm.id) ? "default" : "outline"}
variant={
followedArms.includes(arm.id)
? "default"
: "outline"
}
onClick={() => toggleFollowArm(arm.id)}
className={cn(
"rounded-full text-xs font-medium gap-1",
@ -611,7 +633,8 @@ export default function Feed() {
: "bg-background/60 text-muted-foreground hover:border-border",
)}
>
{followedArms.includes(arm.id) ? "✓" : "+"} {arm.label}
{followedArms.includes(arm.id) ? "✓" : "+"}{" "}
{arm.label}
</Button>
))}
</div>

View file

@ -62,11 +62,11 @@ const SubdomainPassport = () => {
let url = "";
if (subdomainInfo.isCreatorPassport) {
url = `${API_BASE}/api/passport/subdomain/${encodeURIComponent(
subdomainInfo.subdomain
subdomainInfo.subdomain,
)}`;
} else if (subdomainInfo.isProjectPassport) {
url = `${API_BASE}/api/passport/project/${encodeURIComponent(
subdomainInfo.subdomain
subdomainInfo.subdomain,
)}`;
}
@ -82,7 +82,7 @@ const SubdomainPassport = () => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.error || `HTTP ${response.status}: Not found`
errorData.error || `HTTP ${response.status}: Not found`,
);
}

View file

@ -69,8 +69,7 @@ module.exports = {
.insert({
username: "aethex-announcements",
full_name: "AeThex Announcements",
avatar_url:
"https://aethex.dev/logo.png",
avatar_url: "https://aethex.dev/logo.png",
})
.select("id");
@ -108,9 +107,9 @@ module.exports = {
if (imageExtensions.some((ext) => attachmentLower.endsWith(ext))) {
mediaType = "image";
} else if (videoExtensions.some((ext) =>
attachmentLower.endsWith(ext),
)) {
} else if (
videoExtensions.some((ext) => attachmentLower.endsWith(ext))
) {
mediaType = "video";
}
}
@ -149,11 +148,17 @@ module.exports = {
);
if (insertError) {
console.error("[Announcements Sync] Failed to create post:", insertError);
console.error(
"[Announcements Sync] Failed to create post:",
insertError,
);
try {
await message.react("❌");
} catch (reactionError) {
console.warn("[Announcements Sync] Could not add reaction:", reactionError);
console.warn(
"[Announcements Sync] Could not add reaction:",
reactionError,
);
}
return;
}
@ -197,7 +202,10 @@ module.exports = {
}),
});
} catch (webhookError) {
console.warn("[Announcements Sync] Failed to sync to webhook:", webhookError);
console.warn(
"[Announcements Sync] Failed to sync to webhook:",
webhookError,
);
}
}
@ -209,7 +217,10 @@ module.exports = {
try {
await message.react("✅");
} catch (reactionError) {
console.warn("[Announcements Sync] Could not add success reaction:", reactionError);
console.warn(
"[Announcements Sync] Could not add success reaction:",
reactionError,
);
}
} catch (error) {
console.error("[Announcements Sync] Unexpected error:", error);
@ -217,7 +228,10 @@ module.exports = {
try {
await message.react("⚠️");
} catch (reactionError) {
console.warn("[Announcements Sync] Could not add warning reaction:", reactionError);
console.warn(
"[Announcements Sync] Could not add warning reaction:",
reactionError,
);
}
}
},

View file

@ -87,13 +87,17 @@ async function handleAnnouncementSync(message) {
mediaUrl = attachment.url;
const attachmentLower = attachment.name.toLowerCase();
if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) =>
if (
[".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) =>
attachmentLower.endsWith(ext),
)) {
)
) {
mediaType = "image";
} else if ([".mp4", ".webm", ".mov", ".avi"].some((ext) =>
} else if (
[".mp4", ".webm", ".mov", ".avi"].some((ext) =>
attachmentLower.endsWith(ext),
)) {
)
) {
mediaType = "video";
}
}
@ -137,9 +141,7 @@ async function handleAnnouncementSync(message) {
return;
}
console.log(
`[Announcements] ✅ Synced to AeThex (${armAffiliation} arm)`,
);
console.log(`[Announcements] ✅ Synced to AeThex (${armAffiliation} arm)`);
await message.react("✅");
} catch (error) {
@ -160,7 +162,10 @@ module.exports = {
if (!message.content && message.attachments.size === 0) return;
// Check if this is an announcement to sync
if (ANNOUNCEMENT_CHANNELS.length > 0 && ANNOUNCEMENT_CHANNELS.includes(message.channelId)) {
if (
ANNOUNCEMENT_CHANNELS.length > 0 &&
ANNOUNCEMENT_CHANNELS.includes(message.channelId)
) {
return handleAnnouncementSync(message);
}
@ -200,7 +205,10 @@ module.exports = {
.single();
if (profileError || !userProfile) {
console.error("[Feed Sync] Could not fetch user profile:", profileError);
console.error(
"[Feed Sync] Could not fetch user profile:",
profileError,
);
return;
}
@ -215,13 +223,17 @@ module.exports = {
mediaUrl = attachment.url;
const attachmentLower = attachment.name.toLowerCase();
if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) =>
if (
[".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) =>
attachmentLower.endsWith(ext),
)) {
)
) {
mediaType = "image";
} else if ([".mp4", ".webm", ".mov", ".avi"].some((ext) =>
} else if (
[".mp4", ".webm", ".mov", ".avi"].some((ext) =>
attachmentLower.endsWith(ext),
)) {
)
) {
mediaType = "video";
}
}
@ -234,7 +246,8 @@ module.exports = {
const guildNameLower = guild.name.toLowerCase();
if (guildNameLower.includes("gameforge")) armAffiliation = "gameforge";
else if (guildNameLower.includes("corp")) armAffiliation = "corp";
else if (guildNameLower.includes("foundation")) armAffiliation = "foundation";
else if (guildNameLower.includes("foundation"))
armAffiliation = "foundation";
else if (guildNameLower.includes("devlink")) armAffiliation = "devlink";
else if (guildNameLower.includes("nexus")) armAffiliation = "nexus";
else if (guildNameLower.includes("staff")) armAffiliation = "staff";
@ -276,14 +289,15 @@ module.exports = {
return;
}
console.log(
`[Feed Sync] ✅ Posted from ${message.author.tag} to AeThex`,
);
console.log(`[Feed Sync] ✅ Posted from ${message.author.tag} to AeThex`);
try {
await message.react("✅");
} catch (reactionError) {
console.warn("[Feed Sync] Could not add success reaction:", reactionError);
console.warn(
"[Feed Sync] Could not add success reaction:",
reactionError,
);
}
try {

View file

@ -34,7 +34,9 @@ Phase 1 is the **read-only, curated foundation** that proves the Axiom Model wor
## Features Implemented
### 1. **Arm Affiliation Theming**
Every post displays a **color-coded badge** and **left border accent** matching the Arm:
- **LABS** (Yellow): Innovation & experimentation
- **GAMEFORGE** (Green): Game development
- **CORP** (Blue): Commercial partnerships
@ -46,17 +48,22 @@ Every post displays a **color-coded badge** and **left border accent** matching
**Why this matters**: The colors are the **visual proof of the Firewall**. At a glance, you know what type of content you're reading.
### 2. **Arm Follow System**
Users can now:
- Follow specific Arms
- Personalize their feed to show only followed Arms
- Access the "Following" tab to see curated content
**Database**:
- New `arm_follows` table tracks user -> arm relationships
- RLS policies ensure users can only manage their own follows
### 3. **Arm-Specific Feeds**
New routes available:
- `/labs` - Labs feed only
- `/gameforge` - GameForge feed only
- `/corp` - Corp feed only
@ -66,29 +73,35 @@ New routes available:
- `/staff` - Staff feed only
Each has:
- Dedicated header with Arm icon & description
- Content filtered to that Arm only
- Same interaction system (like, comment, share)
### 4. **Admin Feed Manager**
**Route**: `/admin/feed`
Founders/Admins can now create **system announcements** that seed the feed. Features:
- Title & content editor (max 500 & 5000 chars)
- Arm affiliation selector
- Tag management
- One-click publish
**Use cases**:
- Announce new partnerships
- Showcase Arm-to-Arm collaborations
- Prove the "Talent Flywheel" in action
- Demonstrate ethical separation
### 5. **Discord Announcements Sync**
**One-way**: Discord → AeThex Feed
The Discord bot now listens to configured announcement channels and automatically:
1. Posts to the AeThex feed
2. Auto-detects Arm affiliation from channel/guild name
3. Includes media (images, videos)
@ -96,6 +109,7 @@ The Discord bot now listens to configured announcement channels and automaticall
5. Reacts with ✅ when successful
**Configuration**:
```env
DISCORD_ANNOUNCEMENT_CHANNELS=1435667453244866702,your_other_channels
DISCORD_FEED_WEBHOOK_URL=https://discord.com/api/webhooks/...
@ -110,6 +124,7 @@ DISCORD_FEED_CHANNEL_ID=1425114041021497454
### New Tables
#### `arm_follows`
```sql
id BIGSERIAL PRIMARY KEY
user_id UUID REFERENCES auth.users(id)
@ -121,6 +136,7 @@ UNIQUE(user_id, arm_affiliation)
```
#### `community_posts` (Updated)
```sql
-- Already existed, now with validated arm_affiliation
arm_affiliation TEXT NOT NULL CHECK (arm_affiliation IN (...))
@ -136,24 +152,28 @@ CREATE INDEX idx_community_posts_created_at ON community_posts(created_at DESC)
### Feed Management
#### Get Arm Follows
```
GET /api/user/arm-follows?user_id={userId}
Returns: { arms: ["labs", "gameforge", ...] }
```
#### Follow an Arm
```
POST /api/user/arm-follows?user_id={userId}
Body: { arm_affiliation: "labs" }
```
#### Unfollow an Arm
```
DELETE /api/user/arm-follows?user_id={userId}
Body: { arm_affiliation: "labs" }
```
#### Create Post (Admin)
```
POST /api/community/posts
Body: {
@ -169,6 +189,7 @@ Body: {
### Discord Integration
#### Discord Webhook Sync
```
POST /api/discord/feed-sync
Body: {
@ -189,6 +210,7 @@ Body: {
## File Changes Summary
### New Files Created
- `code/client/pages/AdminFeed.tsx` - Admin feed manager UI
- `code/client/components/feed/ArmFeed.tsx` - Reusable Arm feed component
- `code/client/pages/ArmFeeds.tsx` - Individual Arm feed page exports
@ -199,6 +221,7 @@ Body: {
- `code/discord-bot/.env.example` - Environment variable template
### Modified Files
- `code/client/components/social/FeedItemCard.tsx` - Added Arm badges & visual theming
- `code/client/pages/Feed.tsx` - Added arm follow management UI
- `code/discord-bot/bot.js` - Enhanced to load event listeners with correct intents
@ -208,6 +231,7 @@ Body: {
## Deployment Checklist
### 1. Database Migrations
```bash
npx supabase migration up
# OR manually apply:
@ -215,7 +239,9 @@ npx supabase migration up
```
### 2. Environment Variables
Set in your production environment:
```env
DISCORD_ANNOUNCEMENT_CHANNELS=1435667453244866702
DISCORD_FEED_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN
@ -225,7 +251,9 @@ VITE_API_BASE=https://your-api-domain.com
```
### 3. Update App Routing
Add these routes to `code/client/App.tsx`:
```typescript
{
path: "/admin/feed",
@ -262,7 +290,9 @@ Add these routes to `code/client/App.tsx`:
```
### 4. Discord Bot Restart
Restart the Discord bot for it to:
1. Load the new message event listener
2. Subscribe to announcement channels
3. Start syncing posts
@ -272,17 +302,20 @@ Restart the Discord bot for it to:
## Usage Guide
### For Founders/Admins
1. Go to `/admin/feed`
2. Write your announcement
3. Select the appropriate Arm
4. Publish
Example post:
> **Title**: GameForge + Foundation Partnership
> **Content**: We're thrilled to announce that GameForge will hire 3 Artists from Foundation via Nexus. This is the Talent Flywheel in action.
> **Arm**: gameforge
### For Users
1. Go to `/feed` (main unified feed)
2. Manage which Arms you follow using "Manage Follows"
3. Filter the feed with the Arm buttons
@ -294,6 +327,7 @@ Example post:
## Phase 2: User-Generated Posts
Once Phase 1 is proven (admin posts working, Discord sync working), Phase 2 will add:
- User post composer in the `/feed` page
- Moderation queue for new user posts
- Reputation scoring
@ -319,12 +353,14 @@ Once Phase 1 is proven (admin posts working, Discord sync working), Phase 2 will
## Performance Notes
**Indexes Added**:
- `idx_community_posts_arm_affiliation` - Fast Arm filtering
- `idx_community_posts_created_at` - Fast sorting by date
- `idx_arm_follows_user_id` - Fast user follow lookups
- `idx_arm_follows_arm` - Fast arm-based queries
**Caching Recommendations** (Phase 2):
- Cache user's followed Arms for 5 minutes
- Cache trending posts per Arm
- Use Redis for real-time engagement counts
@ -334,6 +370,7 @@ Once Phase 1 is proven (admin posts working, Discord sync working), Phase 2 will
## Contact & Support
For questions on Phase 1 implementation or moving to Phase 2, refer to:
- `/api/community/posts` - Main post creation API
- `/api/user/arm-follows` - Arm follow management
- `code/discord-bot/events/messageCreate.js` - Discord sync logic

View file

@ -309,7 +309,9 @@ export function createServer() {
// Subdomain detection middleware for aethex.me and aethex.space
app.use((req, res, next) => {
const host = (req.headers.host || "").toLowerCase();
const forwarded = ((req.headers["x-forwarded-host"] as string) || "").toLowerCase();
const forwarded = (
(req.headers["x-forwarded-host"] as string) || ""
).toLowerCase();
const hostname = forwarded || host;
// Parse subdomain
@ -358,7 +360,9 @@ export function createServer() {
// API: Creator passport lookup by subdomain (aethex.me)
app.get("/api/passport/subdomain/:username", async (req, res) => {
try {
const username = String(req.params.username || "").toLowerCase().trim();
const username = String(req.params.username || "")
.toLowerCase()
.trim();
if (!username) {
return res.status(400).json({ error: "username required" });
}
@ -366,7 +370,7 @@ export function createServer() {
const { data, error } = await adminSupabase
.from("user_profiles")
.select(
"id, username, full_name, avatar_url, user_type, bio, created_at, email"
"id, username, full_name, avatar_url, user_type, bio, created_at, email",
)
.eq("username", username)
.single();
@ -405,7 +409,7 @@ export function createServer() {
let query = adminSupabase
.from("projects")
.select(
"id, title, slug, description, user_id, created_at, updated_at, status, image_url, website"
"id, title, slug, description, user_id, created_at, updated_at, status, image_url, website",
)
.eq("slug", projectname);
@ -416,7 +420,7 @@ export function createServer() {
query = adminSupabase
.from("projects")
.select(
"id, title, slug, description, user_id, created_at, updated_at, status, image_url, website"
"id, title, slug, description, user_id, created_at, updated_at, status, image_url, website",
)
.ilike("title", projectname);
@ -3252,7 +3256,9 @@ export function createServer() {
app.get("/api/social/following", async (req, res) => {
const userId = req.query.userId as string;
if (!userId) {
return res.status(400).json({ error: "userId query parameter required" });
return res
.status(400)
.json({ error: "userId query parameter required" });
}
try {
const { data, error } = await adminSupabase
@ -3278,7 +3284,9 @@ export function createServer() {
app.get("/api/social/followers", async (req, res) => {
const userId = req.query.userId as string;
if (!userId) {
return res.status(400).json({ error: "userId query parameter required" });
return res
.status(400)
.json({ error: "userId query parameter required" });
}
try {
const { data, error } = await adminSupabase