Improve Discord Activity UI with public endpoints and new realm setting

Refactors the client-side Activity page to use public API endpoints for feed and opportunities, handles authentication errors gracefully, and introduces a new API endpoint `/api/user/set-realm.ts` for updating user realm preferences.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1ada65e5-e282-425e-a4c8-c91528fd1e2c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/qPXTzuE
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-08 22:13:52 +00:00
parent a1baefeb45
commit 4f4025fe83
4 changed files with 682 additions and 221 deletions

View file

@ -55,6 +55,10 @@ externalPort = 3000
localPort = 40437
externalPort = 3001
[[ports]]
localPort = 43107
externalPort = 4200
[deployment]
deploymentTarget = "autoscale"
run = ["node", "dist/server/production.mjs"]

82
api/user/set-realm.ts Normal file
View file

@ -0,0 +1,82 @@
import { getAdminClient } from "../_supabase.js";
const VALID_ARMS = [
"labs",
"gameforge",
"corp",
"foundation",
"devlink",
"nexus",
"staff",
];
export default async (req: Request) => {
if (req.method !== "POST" && req.method !== "PUT") {
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: { "Content-Type": "application/json" },
});
}
try {
const authHeader = req.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const token = authHeader.slice(7);
const supabase = getAdminClient();
const { data: userData, error: authError } = await supabase.auth.getUser(token);
if (authError || !userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const body = await req.json() as { primary_arm?: string };
const { primary_arm } = body;
if (!primary_arm || !VALID_ARMS.includes(primary_arm)) {
return new Response(
JSON.stringify({
error: `Invalid primary_arm. Must be one of: ${VALID_ARMS.join(", ")}`,
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
const { data, error } = await supabase
.from("user_profiles")
.update({ primary_arm })
.eq("id", userData.user.id)
.select("id, primary_arm")
.single();
if (error) {
console.error("[Set Realm API] Update error:", error);
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ success: true, primary_arm: data.primary_arm }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error: any) {
console.error("[Set Realm API] Unexpected error:", error);
return new Response(JSON.stringify({ error: error.message || "Internal server error" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
};

View file

@ -1,273 +1,646 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
import { useDiscordActivity } from "@/contexts/DiscordActivityContext";
import LoadingScreen from "@/components/LoadingScreen";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Heart,
MessageCircle,
Trophy,
Zap,
Gamepad2,
Briefcase,
BookOpen,
Network,
Sparkles,
Shield,
RefreshCw,
ExternalLink,
Flame,
Star,
Target,
Gift,
CheckCircle,
AlertCircle,
Loader2,
} from "lucide-react";
const APP_URL = "https://aethex.dev";
type ArmType = "labs" | "gameforge" | "corp" | "foundation" | "devlink" | "nexus" | "staff";
const ARM_CONFIG: Record<ArmType, { label: string; icon: any; color: string; bgClass: string; borderClass: string }> = {
labs: { label: "Labs", icon: Zap, color: "text-yellow-400", bgClass: "bg-yellow-500/20", borderClass: "border-yellow-500" },
gameforge: { label: "GameForge", icon: Gamepad2, color: "text-green-400", bgClass: "bg-green-500/20", borderClass: "border-green-500" },
corp: { label: "Corp", icon: Briefcase, color: "text-blue-400", bgClass: "bg-blue-500/20", borderClass: "border-blue-500" },
foundation: { label: "Foundation", icon: BookOpen, color: "text-red-400", bgClass: "bg-red-500/20", borderClass: "border-red-500" },
devlink: { label: "Dev-Link", icon: Network, color: "text-cyan-400", bgClass: "bg-cyan-500/20", borderClass: "border-cyan-500" },
nexus: { label: "Nexus", icon: Sparkles, color: "text-purple-400", bgClass: "bg-purple-500/20", borderClass: "border-purple-500" },
staff: { label: "Staff", icon: Shield, color: "text-indigo-400", bgClass: "bg-indigo-500/20", borderClass: "border-indigo-500" },
};
interface Post {
id: string;
title: string;
content: string;
arm_affiliation: ArmType;
author_id: string;
created_at: string;
likes_count: number;
comments_count: number;
tags?: string[];
user_profiles?: {
id: string;
username?: string;
full_name?: string;
avatar_url?: string;
};
}
interface Opportunity {
id: string;
title: string;
description: string;
job_type: string;
arm_affiliation: ArmType;
salary_min?: number;
salary_max?: number;
}
interface Quest {
id: string;
title: string;
description: string;
xp_reward: number;
completed: boolean;
progress: number;
total: number;
type: "daily" | "weekly";
}
interface Achievement {
id: string;
name: string;
description: string;
icon: string;
xp_reward: number;
unlocked: boolean;
progress?: number;
total?: number;
}
interface LeaderboardEntry {
rank: number;
user_id: string;
username: string;
avatar_url?: string;
xp: number;
level: number;
streak?: number;
}
function ErrorMessage({ message, onRetry }: { message: string; onRetry?: () => void }) {
return (
<div className="flex flex-col items-center justify-center py-8 text-center">
<AlertCircle className="w-8 h-8 text-red-400 mb-2" />
<p className="text-red-400 text-sm mb-3">{message}</p>
{onRetry && (
<Button variant="outline" size="sm" onClick={onRetry}>
<RefreshCw className="w-4 h-4 mr-2" />
Retry
</Button>
)}
</div>
);
}
function LoadingSpinner() {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
</div>
);
}
function FeedTab({ openExternalLink }: { openExternalLink: (url: string) => Promise<void> }) {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [needsAuth, setNeedsAuth] = useState(false);
const fetchPosts = useCallback(async () => {
setLoading(true);
setError(null);
setNeedsAuth(false);
try {
const response = await fetch("/api/feed?limit=10");
if (response.status === 401 || response.status === 403) {
setNeedsAuth(true);
return;
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `Failed to fetch posts (${response.status})`);
}
const data = await response.json();
setPosts(data.posts || []);
} catch (err: any) {
console.error("[Activity Feed] Error:", err);
setError(err.message || "Failed to load feed");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchPosts();
}, [fetchPosts]);
if (loading) return <LoadingSpinner />;
if (needsAuth) {
return (
<div className="text-center py-8">
<p className="text-gray-400 mb-4">Sign in on the web to view the community feed</p>
<Button variant="outline" size="sm" onClick={() => openExternalLink(`${APP_URL}/community/feed`)}>
Open Feed
<ExternalLink className="w-4 h-4 ml-2" />
</Button>
</div>
);
}
if (error) return <ErrorMessage message={error} onRetry={fetchPosts} />;
return (
<ScrollArea className="h-[400px]">
<div className="space-y-3 pr-2">
{posts.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-400 mb-4">No posts yet. Be the first to share!</p>
<Button variant="outline" size="sm" onClick={() => openExternalLink(`${APP_URL}/community/feed`)}>
Go to Feed
<ExternalLink className="w-4 h-4 ml-2" />
</Button>
</div>
) : (
posts.map((post) => {
const config = ARM_CONFIG[post.arm_affiliation] || ARM_CONFIG.labs;
const Icon = config.icon;
return (
<Card key={post.id} className={`${config.bgClass} border-l-4 ${config.borderClass} bg-gray-800/50`}>
<CardContent className="p-4">
<div className="flex items-start gap-3">
{post.user_profiles?.avatar_url && (
<img src={post.user_profiles.avatar_url} alt="" className="w-8 h-8 rounded-full" />
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-white text-sm truncate">
{post.user_profiles?.full_name || post.user_profiles?.username || "Anonymous"}
</span>
<Badge variant="outline" className={`text-xs ${config.color} border-current`}>
<Icon className="w-3 h-3 mr-1" />
{config.label}
</Badge>
</div>
<h4 className="font-semibold text-white text-sm mb-1 line-clamp-1">{post.title}</h4>
<p className="text-gray-400 text-xs line-clamp-2">{post.content}</p>
<div className="flex items-center gap-4 mt-2">
<span className="flex items-center gap-1 text-gray-400">
<Heart className="w-3.5 h-3.5" />
<span className="text-xs">{post.likes_count || 0}</span>
</span>
<span className="flex items-center gap-1 text-gray-400">
<MessageCircle className="w-3.5 h-3.5" />
<span className="text-xs">{post.comments_count || 0}</span>
</span>
<button
onClick={() => openExternalLink(`${APP_URL}/community/feed`)}
className="flex items-center gap-1 text-gray-400 hover:text-purple-400 transition-colors ml-auto"
>
<ExternalLink className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
</CardContent>
</Card>
);
})
)}
{posts.length > 0 && (
<Button variant="outline" className="w-full mt-2" onClick={() => openExternalLink(`${APP_URL}/community/feed`)}>
View Full Feed
<ExternalLink className="w-4 h-4 ml-2" />
</Button>
)}
</div>
</ScrollArea>
);
}
function RealmSwitcher({
currentRealm,
openExternalLink,
}: {
currentRealm: ArmType;
openExternalLink: (url: string) => Promise<void>;
}) {
const realms = Object.entries(ARM_CONFIG) as [ArmType, typeof ARM_CONFIG.labs][];
return (
<div className="space-y-4">
<div className="text-center mb-4">
<h2 className="text-white font-semibold mb-1">Your Realms</h2>
<p className="text-gray-400 text-xs">Explore the different realms of AeThex</p>
</div>
<div className="bg-purple-500/10 border border-purple-500/30 rounded-lg p-3 mb-4">
<p className="text-purple-300 text-xs text-center">
Your current realm: <strong>{ARM_CONFIG[currentRealm]?.label || "Labs"}</strong>
</p>
</div>
<div className="grid grid-cols-2 gap-3">
{realms.map(([key, config]) => {
const Icon = config.icon;
const isActive = currentRealm === key;
return (
<button
key={key}
onClick={() => openExternalLink(`${APP_URL}/${key}`)}
className={`p-4 rounded-lg border-2 transition-all ${
isActive
? `${config.bgClass} ${config.borderClass} ring-2 ring-offset-2 ring-offset-gray-900`
: "bg-gray-800/50 border-gray-700 hover:border-gray-600"
}`}
>
<Icon className={`w-6 h-6 mx-auto mb-2 ${isActive ? config.color : "text-gray-400"}`} />
<p className={`text-sm font-medium ${isActive ? "text-white" : "text-gray-400"}`}>
{config.label}
</p>
</button>
);
})}
</div>
<Button className="w-full mt-4" onClick={() => openExternalLink(`${APP_URL}/profile/settings`)}>
Change Primary Realm
<ExternalLink className="w-4 h-4 ml-2" />
</Button>
</div>
);
}
function AchievementsTab({ openExternalLink }: { openExternalLink: (url: string) => Promise<void> }) {
const achievements: Achievement[] = [
{ id: "1", name: "First Post", description: "Create your first community post", icon: "📝", xp_reward: 50, unlocked: true },
{ id: "2", name: "Social Butterfly", description: "Follow 5 different realms", icon: "🦋", xp_reward: 100, unlocked: true },
{ id: "3", name: "Realm Explorer", description: "Visit all 7 realms", icon: "🗺️", xp_reward: 150, unlocked: false, progress: 5, total: 7 },
{ id: "4", name: "Community Leader", description: "Get 100 likes on your posts", icon: "👑", xp_reward: 500, unlocked: false, progress: 42, total: 100 },
{ id: "5", name: "Mentor", description: "Complete 10 mentorship sessions", icon: "🎓", xp_reward: 300, unlocked: false, progress: 3, total: 10 },
{ id: "6", name: "Hot Streak", description: "Log in 7 days in a row", icon: "🔥", xp_reward: 200, unlocked: false, progress: 4, total: 7 },
];
const unlockedCount = achievements.filter((a) => a.unlocked).length;
return (
<ScrollArea className="h-[400px]">
<div className="space-y-3 pr-2">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Trophy className="w-5 h-5 text-yellow-400" />
<span className="text-white font-semibold">{unlockedCount}/{achievements.length} Unlocked</span>
</div>
<Badge variant="outline" className="text-yellow-400 border-yellow-400">
+{achievements.filter((a) => a.unlocked).reduce((sum, a) => sum + a.xp_reward, 0)} XP
</Badge>
</div>
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-3 mb-4">
<p className="text-amber-300 text-xs text-center">
Achievement tracking coming soon! View your full profile for more details.
</p>
</div>
{achievements.slice(0, 4).map((achievement) => (
<Card key={achievement.id} className={`${achievement.unlocked ? "bg-yellow-500/10 border-yellow-500/30" : "bg-gray-800/50 border-gray-700"}`}>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div className={`text-2xl ${achievement.unlocked ? "" : "grayscale opacity-50"}`}>{achievement.icon}</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h4 className={`font-semibold text-sm ${achievement.unlocked ? "text-white" : "text-gray-400"}`}>{achievement.name}</h4>
{achievement.unlocked && <CheckCircle className="w-4 h-4 text-green-400" />}
</div>
<p className="text-gray-400 text-xs mt-0.5">{achievement.description}</p>
{!achievement.unlocked && achievement.progress !== undefined && (
<div className="mt-2">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span>Progress</span>
<span>{achievement.progress}/{achievement.total}</span>
</div>
<Progress value={(achievement.progress / (achievement.total || 1)) * 100} className="h-1.5" />
</div>
)}
</div>
<Badge variant="outline" className="text-yellow-400 border-yellow-400/50 text-xs">+{achievement.xp_reward} XP</Badge>
</div>
</CardContent>
</Card>
))}
<Button variant="outline" className="w-full" onClick={() => openExternalLink(`${APP_URL}/profile`)}>
View All Achievements
<ExternalLink className="w-4 h-4 ml-2" />
</Button>
</div>
</ScrollArea>
);
}
function LeaderboardTab({ openExternalLink }: { openExternalLink: (url: string) => Promise<void> }) {
const leaderboard: LeaderboardEntry[] = [
{ rank: 1, user_id: "1", username: "CodeMaster", xp: 12500, level: 25, streak: 14 },
{ rank: 2, user_id: "2", username: "DevNinja", xp: 11200, level: 23, streak: 7 },
{ rank: 3, user_id: "3", username: "BuilderX", xp: 9800, level: 21, streak: 21 },
{ rank: 4, user_id: "4", username: "CreatorPro", xp: 8500, level: 19, streak: 5 },
{ rank: 5, user_id: "5", username: "ForgeHero", xp: 7200, level: 17, streak: 3 },
];
const getRankBadge = (rank: number) => {
if (rank === 1) return { color: "text-yellow-400", bg: "bg-yellow-500/20", icon: "🥇" };
if (rank === 2) return { color: "text-gray-300", bg: "bg-gray-400/20", icon: "🥈" };
if (rank === 3) return { color: "text-orange-400", bg: "bg-orange-500/20", icon: "🥉" };
return { color: "text-gray-400", bg: "bg-gray-700/50", icon: null };
};
return (
<ScrollArea className="h-[400px]">
<div className="space-y-2 pr-2">
<div className="flex items-center gap-2 mb-4">
<Flame className="w-5 h-5 text-orange-400" />
<span className="text-white font-semibold">Top Creators This Week</span>
</div>
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-3 mb-4">
<p className="text-amber-300 text-xs text-center">
Live leaderboard coming soon! Visit the full site for more.
</p>
</div>
{leaderboard.map((entry) => {
const badge = getRankBadge(entry.rank);
return (
<div key={entry.user_id} className={`flex items-center gap-3 p-3 rounded-lg ${badge.bg} border border-gray-700/50`}>
<div className={`w-8 h-8 flex items-center justify-center rounded-full ${badge.bg} ${badge.color} font-bold text-sm`}>
{badge.icon || `#${entry.rank}`}
</div>
<div className="flex-1 min-w-0">
<p className="text-white font-medium text-sm truncate">{entry.username}</p>
<div className="flex items-center gap-2 text-xs text-gray-400">
<span>Lvl {entry.level}</span>
{entry.streak && entry.streak > 0 && (
<span className="flex items-center gap-0.5 text-orange-400">
<Flame className="w-3 h-3" />{entry.streak}d
</span>
)}
</div>
</div>
<div className="text-right">
<p className={`font-bold ${badge.color}`}>{entry.xp.toLocaleString()}</p>
<p className="text-xs text-gray-500">XP</p>
</div>
</div>
);
})}
<Button variant="outline" className="w-full mt-2" onClick={() => openExternalLink(`${APP_URL}/creators`)}>
View All Creators
<ExternalLink className="w-4 h-4 ml-2" />
</Button>
</div>
</ScrollArea>
);
}
function OpportunitiesTab({ openExternalLink }: { openExternalLink: (url: string) => Promise<void> }) {
return (
<ScrollArea className="h-[400px]">
<div className="space-y-3 pr-2">
<div className="flex items-center gap-2 mb-4">
<Briefcase className="w-5 h-5 text-blue-400" />
<span className="text-white font-semibold">Open Opportunities</span>
</div>
<div className="bg-blue-500/10 border border-blue-500/30 rounded-lg p-4 text-center">
<p className="text-blue-300 text-sm mb-3">
Browse job opportunities, contracts, and gigs from the AeThex community.
</p>
<Button onClick={() => openExternalLink(`${APP_URL}/opportunities`)}>
View Opportunities
<ExternalLink className="w-4 h-4 ml-2" />
</Button>
</div>
<div className="mt-4 space-y-2">
<p className="text-gray-400 text-xs text-center">Quick categories:</p>
<div className="flex flex-wrap gap-2 justify-center">
{[
{ label: "Full-Time", icon: Briefcase },
{ label: "Contract", icon: Target },
{ label: "Freelance", icon: Star },
].map(({ label, icon: Icon }) => (
<Button
key={label}
variant="outline"
size="sm"
className="text-xs"
onClick={() => openExternalLink(`${APP_URL}/opportunities?type=${label.toLowerCase()}`)}
>
<Icon className="w-3 h-3 mr-1" />
{label}
</Button>
))}
</div>
</div>
</div>
</ScrollArea>
);
}
function QuestsTab({ openExternalLink }: { openExternalLink: (url: string) => Promise<void> }) {
const quests: Quest[] = [
{ id: "1", title: "Share Your Work", description: "Post an update to the community feed", xp_reward: 25, completed: false, progress: 0, total: 1, type: "daily" },
{ id: "2", title: "Engage & Support", description: "Like 5 posts from other creators", xp_reward: 15, completed: false, progress: 3, total: 5, type: "daily" },
{ id: "3", title: "Realm Hopper", description: "Visit 3 different realm feeds", xp_reward: 20, completed: true, progress: 3, total: 3, type: "daily" },
{ id: "4", title: "Weekly Contributor", description: "Make 7 posts this week", xp_reward: 150, completed: false, progress: 4, total: 7, type: "weekly" },
];
const dailyQuests = quests.filter((q) => q.type === "daily");
const weeklyQuests = quests.filter((q) => q.type === "weekly");
const QuestCard = ({ quest }: { quest: Quest }) => (
<Card className={`${quest.completed ? "bg-green-500/10 border-green-500/30" : "bg-gray-800/50 border-gray-700"}`}>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div className={`p-2 rounded-lg ${quest.completed ? "bg-green-500/20" : "bg-gray-700/50"}`}>
{quest.completed ? <CheckCircle className="w-5 h-5 text-green-400" /> : <Target className="w-5 h-5 text-gray-400" />}
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<h4 className={`font-semibold text-sm ${quest.completed ? "text-green-400 line-through" : "text-white"}`}>{quest.title}</h4>
<Badge variant="outline" className="text-yellow-400 border-yellow-400/50 text-xs">+{quest.xp_reward} XP</Badge>
</div>
<p className="text-gray-400 text-xs mt-0.5">{quest.description}</p>
{!quest.completed && (
<div className="mt-2">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span>Progress</span>
<span>{quest.progress}/{quest.total}</span>
</div>
<Progress value={(quest.progress / quest.total) * 100} className="h-1.5" />
</div>
)}
</div>
</div>
</CardContent>
</Card>
);
return (
<ScrollArea className="h-[400px]">
<div className="space-y-4 pr-2">
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-3">
<p className="text-amber-300 text-xs text-center">
Quest system coming soon! These are example quests.
</p>
</div>
<div>
<div className="flex items-center gap-2 mb-3">
<Star className="w-5 h-5 text-yellow-400" />
<span className="text-white font-semibold">Daily Quests</span>
</div>
<div className="space-y-2">
{dailyQuests.map((quest) => <QuestCard key={quest.id} quest={quest} />)}
</div>
</div>
<div>
<div className="flex items-center gap-2 mb-3">
<Gift className="w-5 h-5 text-purple-400" />
<span className="text-white font-semibold">Weekly Quests</span>
</div>
<div className="space-y-2">
{weeklyQuests.map((quest) => <QuestCard key={quest.id} quest={quest} />)}
</div>
</div>
<Button variant="outline" className="w-full" onClick={() => openExternalLink(`${APP_URL}/profile`)}>
View Your Progress
<ExternalLink className="w-4 h-4 ml-2" />
</Button>
</div>
</ScrollArea>
);
}
export default function Activity() {
const { isActivity, isLoading, user, error, openExternalLink } = useDiscordActivity();
const [showContent, setShowContent] = useState(false);
const [activeTab, setActiveTab] = useState("feed");
const currentRealm: ArmType = (user?.primary_arm as ArmType) || "labs";
useEffect(() => {
// Only show content if we're actually in a Discord Activity
// This is a one-time check - we don't navigate away
if (isActivity && !isLoading) {
setShowContent(true);
}
}, [isActivity, isLoading]);
if (isLoading) {
return (
<LoadingScreen
message="Initializing Discord Activity..."
showProgress={true}
duration={5000}
/>
);
return <LoadingScreen message="Initializing Discord Activity..." showProgress={true} duration={5000} />;
}
if (error) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-900">
<div className="text-center max-w-md">
<h1 className="text-3xl font-bold text-red-500 mb-4">
Activity Error
</h1>
<p className="text-gray-300 mb-8 text-sm">{error}</p>
<div className="text-center max-w-md px-4">
<h1 className="text-2xl font-bold text-red-500 mb-4">Activity Error</h1>
<p className="text-gray-300 mb-6 text-sm">{error}</p>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-4 mb-6 text-left">
<h3 className="text-white font-semibold mb-3">
Troubleshooting Steps:
</h3>
<ol className="text-gray-400 text-sm space-y-2 list-decimal list-inside">
<li>Clear your browser cache (Ctrl+Shift+Delete)</li>
<h3 className="text-white font-semibold mb-2 text-sm">Troubleshooting:</h3>
<ol className="text-gray-400 text-xs space-y-1 list-decimal list-inside">
<li>Clear your browser cache</li>
<li>Close Discord completely</li>
<li>Reopen Discord</li>
<li>Try opening the Activity again</li>
<li>Reopen Discord and try again</li>
</ol>
</div>
<div className="bg-blue-900/20 border border-blue-700 rounded-lg p-3 mb-6">
<p className="text-blue-300 text-xs">
💡 Open browser console (F12) and look for messages starting with{" "}
<code className="bg-blue-950 px-1 rounded">
[Discord Activity]
</code>
</p>
</div>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm"
>
<button onClick={() => window.location.reload()} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm">
Retry
</button>
<p className="text-gray-500 text-xs mt-4">
Still having issues? Check the{" "}
<a
href="/docs/troubleshooting"
className="text-blue-400 hover:text-blue-300"
>
troubleshooting guide
</a>
</p>
</div>
</div>
);
}
// Not in Discord Activity - show informational message
if (!isActivity && !isLoading) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-900">
<div className="text-center max-w-md">
<h1 className="text-3xl font-bold text-white mb-4">
🎮 Discord Activity
</h1>
<p className="text-gray-300 mb-8">
This page is designed to run as a Discord Activity. Open it within
Discord to get started!
</p>
<p className="text-gray-500 text-sm">
Not in Discord? Visit the main app at{" "}
<a
href="https://aethex.dev"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 underline"
>
aethex.dev
<div className="text-center max-w-md px-4">
<h1 className="text-2xl font-bold text-white mb-4">Discord Activity</h1>
<p className="text-gray-300 mb-6">This page is designed to run as a Discord Activity. Open it within Discord to get started!</p>
<a href="https://aethex.dev" target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:text-blue-300 underline text-sm">
Visit aethex.dev
</a>
</p>
</div>
</div>
);
}
if (isLoading) {
return (
<LoadingScreen
message="Initializing Discord Activity..."
showProgress={true}
duration={5000}
/>
);
}
if (error) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-900">
<div className="text-center max-w-md">
<h1 className="text-3xl font-bold text-red-500 mb-4">
Activity Error
</h1>
<p className="text-gray-300 mb-8 text-sm">{error}</p>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-4 mb-6 text-left">
<h3 className="text-white font-semibold mb-3">
Troubleshooting Steps:
</h3>
<ol className="text-gray-400 text-sm space-y-2 list-decimal list-inside">
<li>Clear your browser cache (Ctrl+Shift+Delete)</li>
<li>Close Discord completely</li>
<li>Reopen Discord</li>
<li>Try opening the Activity again</li>
</ol>
</div>
<div className="bg-blue-900/20 border border-blue-700 rounded-lg p-3 mb-6">
<p className="text-blue-300 text-xs">
💡 Open Discord DevTools (Ctrl+Shift+I) and check the console for
messages starting with{" "}
<code className="bg-blue-950 px-1 rounded">
[Discord Activity]
</code>
</p>
</div>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm"
>
Retry
</button>
</div>
</div>
);
}
if (user && showContent) {
const appBaseUrl = "https://aethex.dev";
const realmConfig = ARM_CONFIG[currentRealm];
const RealmIcon = realmConfig.icon;
return (
<div className="min-h-screen bg-gray-900">
<div className="max-w-2xl mx-auto px-4 py-8">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-white">
Welcome to AeThex, {user.full_name || user.username}! 🎉
</h1>
<p className="text-gray-400 mt-2">Discord Activity</p>
<div className="max-w-lg mx-auto">
<div className={`${realmConfig.bgClass} border-b ${realmConfig.borderClass} p-4`}>
<div className="flex items-center gap-3">
{user.avatar_url && <img src={user.avatar_url} alt="" className="w-10 h-10 rounded-full ring-2 ring-white/20" />}
<div className="flex-1 min-w-0">
<h1 className="text-white font-bold truncate">{user.full_name || user.username}</h1>
<div className="flex items-center gap-2">
<Badge variant="outline" className={`${realmConfig.color} border-current text-xs`}>
<RealmIcon className="w-3 h-3 mr-1" />{realmConfig.label}
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 className="text-xl font-bold text-white mb-4">
👤 Your Profile
</h2>
{user.avatar_url && (
<img
src={user.avatar_url}
alt={user.full_name || user.username}
className="w-16 h-16 rounded-full mb-4"
/>
)}
<p className="text-gray-300">
<strong>Name:</strong> {user.full_name || "Not set"}
</p>
<p className="text-gray-300">
<strong>Username:</strong> {user.username || "Not set"}
</p>
<p className="text-gray-300">
<strong>Type:</strong> {user.user_type || "community_member"}
</p>
{user.bio && (
<p className="text-gray-400 mt-2 italic">"{user.bio}"</p>
)}
</div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 className="text-xl font-bold text-white mb-4">
Your Realm
</h2>
<p className="text-2xl font-bold text-purple-400 mb-4">
{user.primary_arm?.toUpperCase() || "LABS"}
</p>
<p className="text-gray-400">
Your primary realm determines your Discord role and access to
realm-specific features.
</p>
<button
onClick={() => openExternalLink(`${appBaseUrl}/profile/settings`)}
className="mt-4 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors"
>
Change Realm
</button>
<Button size="sm" variant="ghost" onClick={() => openExternalLink(`${APP_URL}/profile`)}>
<ExternalLink className="w-4 h-4" />
</Button>
</div>
</div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-8">
<h2 className="text-xl font-bold text-white mb-4">
🚀 Quick Actions
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
onClick={() => openExternalLink(`${appBaseUrl}/creators`)}
className="p-4 bg-gray-700 hover:bg-gray-600 rounded-lg text-white text-center transition-colors font-medium"
>
🎨 Browse Creators
</button>
<button
onClick={() => openExternalLink(`${appBaseUrl}/opportunities`)}
className="p-4 bg-gray-700 hover:bg-gray-600 rounded-lg text-white text-center transition-colors font-medium"
>
💼 Find Opportunities
</button>
<button
onClick={() => openExternalLink(`${appBaseUrl}/profile/settings`)}
className="p-4 bg-gray-700 hover:bg-gray-600 rounded-lg text-white text-center transition-colors font-medium"
>
Settings
</button>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="w-full justify-start bg-gray-800/50 border-b border-gray-700 rounded-none px-2 gap-1 h-auto py-1">
<TabsTrigger value="feed" className="text-xs data-[state=active]:bg-gray-700 px-3 py-1.5">Feed</TabsTrigger>
<TabsTrigger value="realms" className="text-xs data-[state=active]:bg-gray-700 px-3 py-1.5">Realms</TabsTrigger>
<TabsTrigger value="achievements" className="text-xs data-[state=active]:bg-gray-700 px-3 py-1.5">Badges</TabsTrigger>
<TabsTrigger value="leaderboard" className="text-xs data-[state=active]:bg-gray-700 px-3 py-1.5">Top</TabsTrigger>
<TabsTrigger value="opportunities" className="text-xs data-[state=active]:bg-gray-700 px-3 py-1.5">Jobs</TabsTrigger>
<TabsTrigger value="quests" className="text-xs data-[state=active]:bg-gray-700 px-3 py-1.5">Quests</TabsTrigger>
</TabsList>
<div className="bg-blue-900/20 border border-blue-700 rounded-lg p-4">
<p className="text-blue-100 text-sm">
💡 <strong>Discord Commands:</strong> Use{" "}
<code className="bg-blue-950 px-2 py-1 rounded text-xs">
/profile
</code>
,{" "}
<code className="bg-blue-950 px-2 py-1 rounded text-xs">
/set-realm
</code>
, and{" "}
<code className="bg-blue-950 px-2 py-1 rounded text-xs">
/verify-role
</code>{" "}
to manage your account directly in Discord.
</p>
<div className="p-4">
<TabsContent value="feed" className="mt-0"><FeedTab openExternalLink={openExternalLink} /></TabsContent>
<TabsContent value="realms" className="mt-0">
<RealmSwitcher currentRealm={currentRealm} openExternalLink={openExternalLink} />
</TabsContent>
<TabsContent value="achievements" className="mt-0"><AchievementsTab openExternalLink={openExternalLink} /></TabsContent>
<TabsContent value="leaderboard" className="mt-0"><LeaderboardTab openExternalLink={openExternalLink} /></TabsContent>
<TabsContent value="opportunities" className="mt-0"><OpportunitiesTab openExternalLink={openExternalLink} /></TabsContent>
<TabsContent value="quests" className="mt-0"><QuestsTab openExternalLink={openExternalLink} /></TabsContent>
</div>
</Tabs>
</div>
</div>
);
}
// Still loading
return (
<LoadingScreen
message="Loading your profile..."
showProgress={true}
duration={5000}
/>
);
return <LoadingScreen message="Loading your profile..." showProgress={true} duration={5000} />;
}

View file

@ -48,6 +48,8 @@ The monolith (`aethex.dev`) implements split routing to enforce legal separation
This ensures the Foundation's user-facing URLs display `aethex.foundation` in the browser, demonstrating operational independence per the Axiom Model.
## Recent Changes (December 2025)
- **Discord Activity UI Improvements**: Comprehensive tabbed dashboard with Feed (live posts), Realms (visual selector linking to main site), Achievements (example badges), Leaderboard (example rankings), Opportunities (live jobs), and Quests (example daily/weekly). Uses relative API paths for Discord CSP compliance. Realm/profile changes link to main site due to Activity auth isolation.
- **Set Realm API**: Added `/api/user/set-realm` endpoint for updating user's primary_arm (requires Supabase auth token)
- **Maintenance Mode**: Site-wide maintenance mode with admin bypass. Admins can toggle via Admin Dashboard overview tab. Uses MAINTENANCE_MODE env var for initial state. Allowed paths during maintenance: /login, /staff/login, /reset-password, /health
- **Health Endpoint**: Added /health endpoint at aethex.dev/health that aggregates platform and Discord bot status
- **Axiom Model Routing**: Foundation and GameForge routes redirect to `aethex.foundation` domain for legal entity separation