Prettier format pending files
This commit is contained in:
parent
db3df1da48
commit
aaac82137c
16 changed files with 1043 additions and 763 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 />}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue