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; const { arm_affiliation } = req.body;
if (!arm_affiliation) { if (!arm_affiliation) {
return res return res.status(400).json({ error: "Missing arm_affiliation" });
.status(400)
.json({ error: "Missing arm_affiliation" });
} }
if (!VALID_ARMS.includes(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; const { arm_affiliation } = req.body;
if (!arm_affiliation) { if (!arm_affiliation) {
return res return res.status(400).json({ error: "Missing arm_affiliation" });
.status(400)
.json({ error: "Missing arm_affiliation" });
} }
// Unfollow the arm // Unfollow the arm

File diff suppressed because it is too large Load diff

View file

@ -18,18 +18,85 @@ import { aethexSocialService } from "@/lib/aethex-social-service";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { normalizeErrorMessage } from "@/lib/error-utils"; import { normalizeErrorMessage } from "@/lib/error-utils";
import { communityService, realtimeService } from "@/lib/supabase-service"; 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 }[] = [ const ARMS: {
{ id: "labs", label: "Labs", icon: Zap, color: "text-yellow-400", description: "Innovation and experimentation" }, id: ArmType;
{ id: "gameforge", label: "GameForge", icon: Gamepad2, color: "text-green-400", description: "Game development excellence" }, label: string;
{ id: "corp", label: "Corp", icon: Briefcase, color: "text-blue-400", description: "Commercial partnerships" }, icon: any;
{ id: "foundation", label: "Foundation", icon: BookOpen, color: "text-red-400", description: "Education and mentorship" }, color: string;
{ id: "devlink", label: "Dev-Link", icon: Network, color: "text-cyan-400", description: "Developer networking" }, description: string;
{ 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" }, {
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 { interface FeedItem {
@ -179,20 +246,23 @@ export default function ArmFeed({ arm }: ArmFeedProps) {
[isFollowingAuthor, user, toast], [isFollowingAuthor, user, toast],
); );
const handleShare = useCallback(async (id: string) => { const handleShare = useCallback(
const url = `${location.origin}/${arm}#post-${id}`; async (id: string) => {
try { const url = `${location.origin}/${arm}#post-${id}`;
if ((navigator as any).share) { try {
await (navigator as any).share({ if ((navigator as any).share) {
title: "AeThex", await (navigator as any).share({
text: `Check out this ${ARMS.find((a) => a.id === arm)?.label} post`, title: "AeThex",
url, text: `Check out this ${ARMS.find((a) => a.id === arm)?.label} post`,
}); url,
});
}
} catch (error) {
console.warn("Share cancelled", error);
} }
} catch (error) { },
console.warn("Share cancelled", error); [arm],
} );
}, [arm]);
const handleLike = useCallback( const handleLike = useCallback(
async (postId: string) => { async (postId: string) => {

View file

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

View file

@ -54,7 +54,7 @@ export const WalletVerification = ({
const normalized = walletInput.trim().toLowerCase(); const normalized = walletInput.trim().toLowerCase();
if (!isValidEthereumAddress(normalized)) { if (!isValidEthereumAddress(normalized)) {
aethexToast.warning( 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; return;
} }
@ -78,7 +78,8 @@ export const WalletVerification = ({
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error( 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) { } catch (error: any) {
console.error("[Wallet Verification] Error:", error?.message); console.error("[Wallet Verification] Error:", error?.message);
aethexToast.error( aethexToast.error(
error?.message || "Failed to connect wallet. Please try again." error?.message || "Failed to connect wallet. Please try again.",
); );
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@ -116,7 +117,8 @@ export const WalletVerification = ({
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error( 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) { } catch (error: any) {
console.error("[Wallet Verification] Error:", error?.message); console.error("[Wallet Verification] Error:", error?.message);
aethexToast.error( aethexToast.error(
error?.message || "Failed to disconnect wallet. Please try again." error?.message || "Failed to disconnect wallet. Please try again.",
); );
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@ -150,7 +152,10 @@ export const WalletVerification = ({
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<span>🔐 Wallet Verification</span> <span>🔐 Wallet Verification</span>
{connectedWallet && ( {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" /> <CheckCircle className="mr-1 h-3 w-3" />
Connected Connected
</Badge> </Badge>
@ -199,11 +204,14 @@ export const WalletVerification = ({
Proves you're the owner of this wallet (Web3 identity) Proves you're the owner of this wallet (Web3 identity)
</li> </li>
<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 verification when the Protocol launches
</li> </li>
<li> No smart contracts or gas fees required right now</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> </ul>
</div> </div>

View file

@ -18,14 +18,52 @@ import { communityService } from "@/lib/supabase-service";
import { Heart, MessageCircle, Share2, Volume2, VolumeX } from "lucide-react"; import { Heart, MessageCircle, Share2, Volume2, VolumeX } from "lucide-react";
import type { FeedItem } from "@/pages/Feed"; import type { FeedItem } from "@/pages/Feed";
const ARM_COLORS: Record<string, { bg: string; border: string; badge: string; text: string }> = { const ARM_COLORS: Record<
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" }, string,
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" }, { bg: string; border: string; badge: string; text: string }
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" }, labs: {
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" }, bg: "bg-yellow-500/10",
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" }, border: "border-l-4 border-l-yellow-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" }, 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> = { const ARM_LABELS: Record<string, string> = {
@ -112,46 +150,50 @@ export function FeedItemCard({
const armLabel = ARM_LABELS[item.arm || "labs"] || "LABS"; const armLabel = ARM_LABELS[item.arm || "labs"] || "LABS";
return ( return (
<Card className={cn( <Card
"overflow-hidden border-border/40 shadow-2xl backdrop-blur-lg", className={cn(
armColor.border, "overflow-hidden border-border/40 shadow-2xl backdrop-blur-lg",
armColor.bg, armColor.border,
"bg-background/70" armColor.bg,
)}> "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"> <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"> <div className="flex flex-1 items-start gap-3">
<Avatar className="h-12 w-12 ring-2 ring-aethex-500/30"> <Avatar className="h-12 w-12 ring-2 ring-aethex-500/30">
<AvatarImage <AvatarImage
src={item.authorAvatar || undefined} src={item.authorAvatar || undefined}
alt={item.authorName} alt={item.authorName}
/> />
<AvatarFallback className="bg-aethex-500/10 text-aethex-300"> <AvatarFallback className="bg-aethex-500/10 text-aethex-300">
{item.authorName?.[0]?.toUpperCase() || "U"} {item.authorName?.[0]?.toUpperCase() || "U"}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="min-w-0 flex-1 space-y-1"> <div className="min-w-0 flex-1 space-y-1">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<CardTitle className="text-lg font-semibold text-foreground"> <CardTitle className="text-lg font-semibold text-foreground">
{item.authorName} {item.authorName}
</CardTitle> </CardTitle>
<Badge className={cn("text-xs font-bold uppercase", armColor.badge)}> <Badge
{armLabel} className={cn("text-xs font-bold uppercase", armColor.badge)}
</Badge> >
</div> {armLabel}
</Badge>
</div> </div>
</div>
</div> </div>
<Button <Button
size="sm" size="sm"
variant={isFollowing ? "outline" : "default"} variant={isFollowing ? "outline" : "default"}
onClick={() => onToggleFollow(item.authorId)} onClick={() => onToggleFollow(item.authorId)}
className={cn( className={cn(
"rounded-full border-border/60", "rounded-full border-border/60",
isFollowing isFollowing
? "bg-background/80 text-foreground" ? "bg-background/80 text-foreground"
: "bg-gradient-to-r from-aethex-500 to-neon-blue text-white", : "bg-gradient-to-r from-aethex-500 to-neon-blue text-white",
)} )}
> >
{isFollowing ? "Following" : "Follow"} {isFollowing ? "Following" : "Follow"}
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 sm:space-y-5 lg:space-y-6 p-4 sm:p-5 lg:p-6"> <CardContent className="space-y-4 sm:space-y-5 lg:space-y-6 p-4 sm:p-5 lg:p-6">

View file

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

View file

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

View file

@ -158,10 +158,15 @@ export default function AdminFeed() {
{/* Main Form */} {/* Main Form */}
<Card className="border-border/40 bg-background/70 shadow-xl backdrop-blur-lg"> <Card className="border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
<CardHeader className="p-3 sm:p-4 lg:p-6"> <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> </CardHeader>
<CardContent className="p-3 sm:p-4 lg:p-6"> <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 */} {/* Title */}
<div className="space-y-1.5 sm:space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<label className="block text-xs sm:text-sm font-medium text-foreground"> <label className="block text-xs sm:text-sm font-medium text-foreground">
@ -288,7 +293,9 @@ export default function AdminFeed() {
{/* Quick Reference */} {/* Quick Reference */}
<Card className="border-border/40 bg-background/70 shadow-xl backdrop-blur-lg"> <Card className="border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
<CardHeader className="p-3 sm:p-4 lg:p-6"> <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> </CardHeader>
<CardContent className="p-3 sm:p-4 lg:p-6"> <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"> <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} key={arm.id}
className="flex items-center gap-2 rounded-lg border border-border/30 bg-background/60 p-2 sm:p-3" 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"> <span className="text-xs sm:text-sm font-medium text-foreground">
{arm.label} {arm.label}
</span> </span>
@ -310,7 +319,9 @@ export default function AdminFeed() {
{/* Guidelines */} {/* Guidelines */}
<Card className="border-border/40 bg-background/70 shadow-xl backdrop-blur-lg"> <Card className="border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
<CardHeader className="p-3 sm:p-4 lg:p-6"> <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> </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"> <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> <p>
@ -323,9 +334,9 @@ export default function AdminFeed() {
(Corp/Labs) separation is real. (Corp/Labs) separation is real.
</p> </p>
<p> <p>
🤝 <strong>Partnership Showcase:</strong> Use these posts to show 🤝 <strong>Partnership Showcase:</strong> Use these posts to
how different Arms collaborate. Example: "Corp hired 3 Architects show how different Arms collaborate. Example: "Corp hired 3
from Foundation via Nexus." Architects from Foundation via Nexus."
</p> </p>
<p> <p>
🚀 <strong>Phase 2:</strong> User-generated posts coming soon. 🚀 <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="space-y-3 flex-1">
<div className="flex items-center gap-3"> <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"> <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" && {activeRealm === "game_developer" && "Game Development"}
"Game Development"}
{activeRealm === "client" && "Consulting"} {activeRealm === "client" && "Consulting"}
{activeRealm === "community_member" && "Community"} {activeRealm === "community_member" && "Community"}
{activeRealm === "customer" && "Getting Started"} {activeRealm === "customer" && "Getting Started"}
@ -784,7 +783,11 @@ export default function Dashboard() {
</div> </div>
</div> </div>
<p className="text-base text-muted-foreground max-w-xl"> <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> </p>
{longestStreak > 0 && ( {longestStreak > 0 && (
<div className="flex flex-wrap gap-2 pt-2"> <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" 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` }} 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"> <CardContent className="p-6 relative">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex-1"> <div className="flex-1">
@ -953,7 +961,7 @@ export default function Dashboard() {
</p> </p>
</div> </div>
<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" /> <Icon className="h-6 w-6 text-white" />
</div> </div>
@ -1031,10 +1039,7 @@ export default function Dashboard() {
> >
Create Team Create Team
</Button> </Button>
<Button <Button variant="outline" onClick={() => navigate("/corp")}>
variant="outline"
onClick={() => navigate("/corp")}
>
Consulting Overview Consulting Overview
</Button> </Button>
</CardContent> </CardContent>

View file

@ -235,7 +235,9 @@ export default function Feed() {
body: JSON.stringify({ arm_affiliation: arm }), body: JSON.stringify({ arm_affiliation: arm }),
}); });
setFollowedArms((state) => state.filter((a) => a !== 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 { } else {
// Follow // Follow
await fetch(`/api/user/arm-follows?user_id=${user.id}`, { await fetch(`/api/user/arm-follows?user_id=${user.id}`, {
@ -244,11 +246,16 @@ export default function Feed() {
body: JSON.stringify({ arm_affiliation: arm }), body: JSON.stringify({ arm_affiliation: arm }),
}); });
setFollowedArms((state) => Array.from(new Set([...state, 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) { } catch (error) {
console.error("Failed to update arm follow:", 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} onClick={handleManualRefresh}
className="gap-1 sm:gap-2 rounded-full border-border/60 bg-background/80 backdrop-blur text-xs sm:text-sm" 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> </Button>
</div> </div>
</div> </div>
@ -547,11 +555,15 @@ export default function Feed() {
<div className="space-y-2 sm:space-y-3"> <div className="space-y-2 sm:space-y-3">
<div className="flex items-center justify-between gap-2 text-xs"> <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 <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
onClick={() => setShowArmFollowManager(!showArmFollowManager)} onClick={() =>
setShowArmFollowManager(!showArmFollowManager)
}
className="text-xs text-aethex-200 hover:text-aethex-100 h-auto p-1" className="text-xs text-aethex-200 hover:text-aethex-100 h-auto p-1"
> >
{showArmFollowManager ? "Hide" : "Manage"} {showArmFollowManager ? "Hide" : "Manage"}
@ -564,7 +576,9 @@ export default function Feed() {
<Button <Button
key={arm.id} key={arm.id}
variant={ variant={
selectedArms.includes(arm.id) ? "default" : "outline" selectedArms.includes(arm.id)
? "default"
: "outline"
} }
size="sm" size="sm"
onClick={() => onClick={() =>
@ -582,7 +596,10 @@ export default function Feed() {
)} )}
> >
<arm.icon <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"> <span className="font-medium hidden sm:inline">
{arm.label} {arm.label}
@ -595,14 +612,19 @@ export default function Feed() {
{showArmFollowManager && user?.id && ( {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"> <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"> <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> </p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-1.5 sm:gap-2"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-1.5 sm:gap-2">
{ARMS.map((arm) => ( {ARMS.map((arm) => (
<Button <Button
key={arm.id} key={arm.id}
size="sm" size="sm"
variant={followedArms.includes(arm.id) ? "default" : "outline"} variant={
followedArms.includes(arm.id)
? "default"
: "outline"
}
onClick={() => toggleFollowArm(arm.id)} onClick={() => toggleFollowArm(arm.id)}
className={cn( className={cn(
"rounded-full text-xs font-medium gap-1", "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", : "bg-background/60 text-muted-foreground hover:border-border",
)} )}
> >
{followedArms.includes(arm.id) ? "✓" : "+"} {arm.label} {followedArms.includes(arm.id) ? "✓" : "+"}{" "}
{arm.label}
</Button> </Button>
))} ))}
</div> </div>

View file

@ -62,11 +62,11 @@ const SubdomainPassport = () => {
let url = ""; let url = "";
if (subdomainInfo.isCreatorPassport) { if (subdomainInfo.isCreatorPassport) {
url = `${API_BASE}/api/passport/subdomain/${encodeURIComponent( url = `${API_BASE}/api/passport/subdomain/${encodeURIComponent(
subdomainInfo.subdomain subdomainInfo.subdomain,
)}`; )}`;
} else if (subdomainInfo.isProjectPassport) { } else if (subdomainInfo.isProjectPassport) {
url = `${API_BASE}/api/passport/project/${encodeURIComponent( url = `${API_BASE}/api/passport/project/${encodeURIComponent(
subdomainInfo.subdomain subdomainInfo.subdomain,
)}`; )}`;
} }
@ -82,7 +82,7 @@ const SubdomainPassport = () => {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error( 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({ .insert({
username: "aethex-announcements", username: "aethex-announcements",
full_name: "AeThex Announcements", full_name: "AeThex Announcements",
avatar_url: avatar_url: "https://aethex.dev/logo.png",
"https://aethex.dev/logo.png",
}) })
.select("id"); .select("id");
@ -108,9 +107,9 @@ module.exports = {
if (imageExtensions.some((ext) => attachmentLower.endsWith(ext))) { if (imageExtensions.some((ext) => attachmentLower.endsWith(ext))) {
mediaType = "image"; mediaType = "image";
} else if (videoExtensions.some((ext) => } else if (
attachmentLower.endsWith(ext), videoExtensions.some((ext) => attachmentLower.endsWith(ext))
)) { ) {
mediaType = "video"; mediaType = "video";
} }
} }
@ -149,11 +148,17 @@ module.exports = {
); );
if (insertError) { if (insertError) {
console.error("[Announcements Sync] Failed to create post:", insertError); console.error(
"[Announcements Sync] Failed to create post:",
insertError,
);
try { try {
await message.react("❌"); await message.react("❌");
} catch (reactionError) { } catch (reactionError) {
console.warn("[Announcements Sync] Could not add reaction:", reactionError); console.warn(
"[Announcements Sync] Could not add reaction:",
reactionError,
);
} }
return; return;
} }
@ -197,7 +202,10 @@ module.exports = {
}), }),
}); });
} catch (webhookError) { } 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 { try {
await message.react("✅"); await message.react("✅");
} catch (reactionError) { } catch (reactionError) {
console.warn("[Announcements Sync] Could not add success reaction:", reactionError); console.warn(
"[Announcements Sync] Could not add success reaction:",
reactionError,
);
} }
} catch (error) { } catch (error) {
console.error("[Announcements Sync] Unexpected error:", error); console.error("[Announcements Sync] Unexpected error:", error);
@ -217,7 +228,10 @@ module.exports = {
try { try {
await message.react("⚠️"); await message.react("⚠️");
} catch (reactionError) { } 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; mediaUrl = attachment.url;
const attachmentLower = attachment.name.toLowerCase(); const attachmentLower = attachment.name.toLowerCase();
if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) => if (
attachmentLower.endsWith(ext), [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) =>
)) { attachmentLower.endsWith(ext),
)
) {
mediaType = "image"; mediaType = "image";
} else if ([".mp4", ".webm", ".mov", ".avi"].some((ext) => } else if (
attachmentLower.endsWith(ext), [".mp4", ".webm", ".mov", ".avi"].some((ext) =>
)) { attachmentLower.endsWith(ext),
)
) {
mediaType = "video"; mediaType = "video";
} }
} }
@ -137,9 +141,7 @@ async function handleAnnouncementSync(message) {
return; return;
} }
console.log( console.log(`[Announcements] ✅ Synced to AeThex (${armAffiliation} arm)`);
`[Announcements] ✅ Synced to AeThex (${armAffiliation} arm)`,
);
await message.react("✅"); await message.react("✅");
} catch (error) { } catch (error) {
@ -160,7 +162,10 @@ module.exports = {
if (!message.content && message.attachments.size === 0) return; if (!message.content && message.attachments.size === 0) return;
// Check if this is an announcement to sync // 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); return handleAnnouncementSync(message);
} }
@ -200,7 +205,10 @@ module.exports = {
.single(); .single();
if (profileError || !userProfile) { if (profileError || !userProfile) {
console.error("[Feed Sync] Could not fetch user profile:", profileError); console.error(
"[Feed Sync] Could not fetch user profile:",
profileError,
);
return; return;
} }
@ -215,13 +223,17 @@ module.exports = {
mediaUrl = attachment.url; mediaUrl = attachment.url;
const attachmentLower = attachment.name.toLowerCase(); const attachmentLower = attachment.name.toLowerCase();
if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) => if (
attachmentLower.endsWith(ext), [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) =>
)) { attachmentLower.endsWith(ext),
)
) {
mediaType = "image"; mediaType = "image";
} else if ([".mp4", ".webm", ".mov", ".avi"].some((ext) => } else if (
attachmentLower.endsWith(ext), [".mp4", ".webm", ".mov", ".avi"].some((ext) =>
)) { attachmentLower.endsWith(ext),
)
) {
mediaType = "video"; mediaType = "video";
} }
} }
@ -234,7 +246,8 @@ module.exports = {
const guildNameLower = guild.name.toLowerCase(); const guildNameLower = guild.name.toLowerCase();
if (guildNameLower.includes("gameforge")) armAffiliation = "gameforge"; if (guildNameLower.includes("gameforge")) armAffiliation = "gameforge";
else if (guildNameLower.includes("corp")) armAffiliation = "corp"; 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("devlink")) armAffiliation = "devlink";
else if (guildNameLower.includes("nexus")) armAffiliation = "nexus"; else if (guildNameLower.includes("nexus")) armAffiliation = "nexus";
else if (guildNameLower.includes("staff")) armAffiliation = "staff"; else if (guildNameLower.includes("staff")) armAffiliation = "staff";
@ -276,14 +289,15 @@ module.exports = {
return; return;
} }
console.log( console.log(`[Feed Sync] ✅ Posted from ${message.author.tag} to AeThex`);
`[Feed Sync] ✅ Posted from ${message.author.tag} to AeThex`,
);
try { try {
await message.react("✅"); await message.react("✅");
} catch (reactionError) { } catch (reactionError) {
console.warn("[Feed Sync] Could not add success reaction:", reactionError); console.warn(
"[Feed Sync] Could not add success reaction:",
reactionError,
);
} }
try { try {

View file

@ -34,7 +34,9 @@ Phase 1 is the **read-only, curated foundation** that proves the Axiom Model wor
## Features Implemented ## Features Implemented
### 1. **Arm Affiliation Theming** ### 1. **Arm Affiliation Theming**
Every post displays a **color-coded badge** and **left border accent** matching the Arm: Every post displays a **color-coded badge** and **left border accent** matching the Arm:
- **LABS** (Yellow): Innovation & experimentation - **LABS** (Yellow): Innovation & experimentation
- **GAMEFORGE** (Green): Game development - **GAMEFORGE** (Green): Game development
- **CORP** (Blue): Commercial partnerships - **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. **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** ### 2. **Arm Follow System**
Users can now: Users can now:
- Follow specific Arms - Follow specific Arms
- Personalize their feed to show only followed Arms - Personalize their feed to show only followed Arms
- Access the "Following" tab to see curated content - Access the "Following" tab to see curated content
**Database**: **Database**:
- New `arm_follows` table tracks user -> arm relationships - New `arm_follows` table tracks user -> arm relationships
- RLS policies ensure users can only manage their own follows - RLS policies ensure users can only manage their own follows
### 3. **Arm-Specific Feeds** ### 3. **Arm-Specific Feeds**
New routes available: New routes available:
- `/labs` - Labs feed only - `/labs` - Labs feed only
- `/gameforge` - GameForge feed only - `/gameforge` - GameForge feed only
- `/corp` - Corp feed only - `/corp` - Corp feed only
@ -66,29 +73,35 @@ New routes available:
- `/staff` - Staff feed only - `/staff` - Staff feed only
Each has: Each has:
- Dedicated header with Arm icon & description - Dedicated header with Arm icon & description
- Content filtered to that Arm only - Content filtered to that Arm only
- Same interaction system (like, comment, share) - Same interaction system (like, comment, share)
### 4. **Admin Feed Manager** ### 4. **Admin Feed Manager**
**Route**: `/admin/feed` **Route**: `/admin/feed`
Founders/Admins can now create **system announcements** that seed the feed. Features: Founders/Admins can now create **system announcements** that seed the feed. Features:
- Title & content editor (max 500 & 5000 chars) - Title & content editor (max 500 & 5000 chars)
- Arm affiliation selector - Arm affiliation selector
- Tag management - Tag management
- One-click publish - One-click publish
**Use cases**: **Use cases**:
- Announce new partnerships - Announce new partnerships
- Showcase Arm-to-Arm collaborations - Showcase Arm-to-Arm collaborations
- Prove the "Talent Flywheel" in action - Prove the "Talent Flywheel" in action
- Demonstrate ethical separation - Demonstrate ethical separation
### 5. **Discord Announcements Sync** ### 5. **Discord Announcements Sync**
**One-way**: Discord → AeThex Feed **One-way**: Discord → AeThex Feed
The Discord bot now listens to configured announcement channels and automatically: The Discord bot now listens to configured announcement channels and automatically:
1. Posts to the AeThex feed 1. Posts to the AeThex feed
2. Auto-detects Arm affiliation from channel/guild name 2. Auto-detects Arm affiliation from channel/guild name
3. Includes media (images, videos) 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 5. Reacts with ✅ when successful
**Configuration**: **Configuration**:
```env ```env
DISCORD_ANNOUNCEMENT_CHANNELS=1435667453244866702,your_other_channels DISCORD_ANNOUNCEMENT_CHANNELS=1435667453244866702,your_other_channels
DISCORD_FEED_WEBHOOK_URL=https://discord.com/api/webhooks/... DISCORD_FEED_WEBHOOK_URL=https://discord.com/api/webhooks/...
@ -110,6 +124,7 @@ DISCORD_FEED_CHANNEL_ID=1425114041021497454
### New Tables ### New Tables
#### `arm_follows` #### `arm_follows`
```sql ```sql
id BIGSERIAL PRIMARY KEY id BIGSERIAL PRIMARY KEY
user_id UUID REFERENCES auth.users(id) user_id UUID REFERENCES auth.users(id)
@ -121,6 +136,7 @@ UNIQUE(user_id, arm_affiliation)
``` ```
#### `community_posts` (Updated) #### `community_posts` (Updated)
```sql ```sql
-- Already existed, now with validated arm_affiliation -- Already existed, now with validated arm_affiliation
arm_affiliation TEXT NOT NULL CHECK (arm_affiliation IN (...)) 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 ### Feed Management
#### Get Arm Follows #### Get Arm Follows
``` ```
GET /api/user/arm-follows?user_id={userId} GET /api/user/arm-follows?user_id={userId}
Returns: { arms: ["labs", "gameforge", ...] } Returns: { arms: ["labs", "gameforge", ...] }
``` ```
#### Follow an Arm #### Follow an Arm
``` ```
POST /api/user/arm-follows?user_id={userId} POST /api/user/arm-follows?user_id={userId}
Body: { arm_affiliation: "labs" } Body: { arm_affiliation: "labs" }
``` ```
#### Unfollow an Arm #### Unfollow an Arm
``` ```
DELETE /api/user/arm-follows?user_id={userId} DELETE /api/user/arm-follows?user_id={userId}
Body: { arm_affiliation: "labs" } Body: { arm_affiliation: "labs" }
``` ```
#### Create Post (Admin) #### Create Post (Admin)
``` ```
POST /api/community/posts POST /api/community/posts
Body: { Body: {
@ -169,6 +189,7 @@ Body: {
### Discord Integration ### Discord Integration
#### Discord Webhook Sync #### Discord Webhook Sync
``` ```
POST /api/discord/feed-sync POST /api/discord/feed-sync
Body: { Body: {
@ -189,6 +210,7 @@ Body: {
## File Changes Summary ## File Changes Summary
### New Files Created ### New Files Created
- `code/client/pages/AdminFeed.tsx` - Admin feed manager UI - `code/client/pages/AdminFeed.tsx` - Admin feed manager UI
- `code/client/components/feed/ArmFeed.tsx` - Reusable Arm feed component - `code/client/components/feed/ArmFeed.tsx` - Reusable Arm feed component
- `code/client/pages/ArmFeeds.tsx` - Individual Arm feed page exports - `code/client/pages/ArmFeeds.tsx` - Individual Arm feed page exports
@ -199,6 +221,7 @@ Body: {
- `code/discord-bot/.env.example` - Environment variable template - `code/discord-bot/.env.example` - Environment variable template
### Modified Files ### Modified Files
- `code/client/components/social/FeedItemCard.tsx` - Added Arm badges & visual theming - `code/client/components/social/FeedItemCard.tsx` - Added Arm badges & visual theming
- `code/client/pages/Feed.tsx` - Added arm follow management UI - `code/client/pages/Feed.tsx` - Added arm follow management UI
- `code/discord-bot/bot.js` - Enhanced to load event listeners with correct intents - `code/discord-bot/bot.js` - Enhanced to load event listeners with correct intents
@ -208,6 +231,7 @@ Body: {
## Deployment Checklist ## Deployment Checklist
### 1. Database Migrations ### 1. Database Migrations
```bash ```bash
npx supabase migration up npx supabase migration up
# OR manually apply: # OR manually apply:
@ -215,7 +239,9 @@ npx supabase migration up
``` ```
### 2. Environment Variables ### 2. Environment Variables
Set in your production environment: Set in your production environment:
```env ```env
DISCORD_ANNOUNCEMENT_CHANNELS=1435667453244866702 DISCORD_ANNOUNCEMENT_CHANNELS=1435667453244866702
DISCORD_FEED_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN 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 ### 3. Update App Routing
Add these routes to `code/client/App.tsx`: Add these routes to `code/client/App.tsx`:
```typescript ```typescript
{ {
path: "/admin/feed", path: "/admin/feed",
@ -262,7 +290,9 @@ Add these routes to `code/client/App.tsx`:
``` ```
### 4. Discord Bot Restart ### 4. Discord Bot Restart
Restart the Discord bot for it to: Restart the Discord bot for it to:
1. Load the new message event listener 1. Load the new message event listener
2. Subscribe to announcement channels 2. Subscribe to announcement channels
3. Start syncing posts 3. Start syncing posts
@ -272,17 +302,20 @@ Restart the Discord bot for it to:
## Usage Guide ## Usage Guide
### For Founders/Admins ### For Founders/Admins
1. Go to `/admin/feed` 1. Go to `/admin/feed`
2. Write your announcement 2. Write your announcement
3. Select the appropriate Arm 3. Select the appropriate Arm
4. Publish 4. Publish
Example post: Example post:
> **Title**: GameForge + Foundation Partnership > **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. > **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 > **Arm**: gameforge
### For Users ### For Users
1. Go to `/feed` (main unified feed) 1. Go to `/feed` (main unified feed)
2. Manage which Arms you follow using "Manage Follows" 2. Manage which Arms you follow using "Manage Follows"
3. Filter the feed with the Arm buttons 3. Filter the feed with the Arm buttons
@ -294,6 +327,7 @@ Example post:
## Phase 2: User-Generated Posts ## Phase 2: User-Generated Posts
Once Phase 1 is proven (admin posts working, Discord sync working), Phase 2 will add: Once Phase 1 is proven (admin posts working, Discord sync working), Phase 2 will add:
- User post composer in the `/feed` page - User post composer in the `/feed` page
- Moderation queue for new user posts - Moderation queue for new user posts
- Reputation scoring - Reputation scoring
@ -319,12 +353,14 @@ Once Phase 1 is proven (admin posts working, Discord sync working), Phase 2 will
## Performance Notes ## Performance Notes
**Indexes Added**: **Indexes Added**:
- `idx_community_posts_arm_affiliation` - Fast Arm filtering - `idx_community_posts_arm_affiliation` - Fast Arm filtering
- `idx_community_posts_created_at` - Fast sorting by date - `idx_community_posts_created_at` - Fast sorting by date
- `idx_arm_follows_user_id` - Fast user follow lookups - `idx_arm_follows_user_id` - Fast user follow lookups
- `idx_arm_follows_arm` - Fast arm-based queries - `idx_arm_follows_arm` - Fast arm-based queries
**Caching Recommendations** (Phase 2): **Caching Recommendations** (Phase 2):
- Cache user's followed Arms for 5 minutes - Cache user's followed Arms for 5 minutes
- Cache trending posts per Arm - Cache trending posts per Arm
- Use Redis for real-time engagement counts - 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 ## Contact & Support
For questions on Phase 1 implementation or moving to Phase 2, refer to: For questions on Phase 1 implementation or moving to Phase 2, refer to:
- `/api/community/posts` - Main post creation API - `/api/community/posts` - Main post creation API
- `/api/user/arm-follows` - Arm follow management - `/api/user/arm-follows` - Arm follow management
- `code/discord-bot/events/messageCreate.js` - Discord sync logic - `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 // Subdomain detection middleware for aethex.me and aethex.space
app.use((req, res, next) => { app.use((req, res, next) => {
const host = (req.headers.host || "").toLowerCase(); 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; const hostname = forwarded || host;
// Parse subdomain // Parse subdomain
@ -358,7 +360,9 @@ export function createServer() {
// API: Creator passport lookup by subdomain (aethex.me) // API: Creator passport lookup by subdomain (aethex.me)
app.get("/api/passport/subdomain/:username", async (req, res) => { app.get("/api/passport/subdomain/:username", async (req, res) => {
try { try {
const username = String(req.params.username || "").toLowerCase().trim(); const username = String(req.params.username || "")
.toLowerCase()
.trim();
if (!username) { if (!username) {
return res.status(400).json({ error: "username required" }); return res.status(400).json({ error: "username required" });
} }
@ -366,7 +370,7 @@ export function createServer() {
const { data, error } = await adminSupabase const { data, error } = await adminSupabase
.from("user_profiles") .from("user_profiles")
.select( .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) .eq("username", username)
.single(); .single();
@ -405,7 +409,7 @@ export function createServer() {
let query = adminSupabase let query = adminSupabase
.from("projects") .from("projects")
.select( .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); .eq("slug", projectname);
@ -416,7 +420,7 @@ export function createServer() {
query = adminSupabase query = adminSupabase
.from("projects") .from("projects")
.select( .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); .ilike("title", projectname);
@ -3252,7 +3256,9 @@ export function createServer() {
app.get("/api/social/following", async (req, res) => { app.get("/api/social/following", async (req, res) => {
const userId = req.query.userId as string; const userId = req.query.userId as string;
if (!userId) { if (!userId) {
return res.status(400).json({ error: "userId query parameter required" }); return res
.status(400)
.json({ error: "userId query parameter required" });
} }
try { try {
const { data, error } = await adminSupabase const { data, error } = await adminSupabase
@ -3278,7 +3284,9 @@ export function createServer() {
app.get("/api/social/followers", async (req, res) => { app.get("/api/social/followers", async (req, res) => {
const userId = req.query.userId as string; const userId = req.query.userId as string;
if (!userId) { if (!userId) {
return res.status(400).json({ error: "userId query parameter required" }); return res
.status(400)
.json({ error: "userId query parameter required" });
} }
try { try {
const { data, error } = await adminSupabase const { data, error } = await adminSupabase