From 4f4025fe83656b86f532cbac848d846c887f55b3 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Mon, 8 Dec 2025 22:13:52 +0000 Subject: [PATCH] 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 --- .replit | 4 + api/user/set-realm.ts | 82 ++++ client/pages/Activity.tsx | 815 +++++++++++++++++++++++++++----------- replit.md | 2 + 4 files changed, 682 insertions(+), 221 deletions(-) create mode 100644 api/user/set-realm.ts diff --git a/.replit b/.replit index d96c59f0..11bf1300 100644 --- a/.replit +++ b/.replit @@ -55,6 +55,10 @@ externalPort = 3000 localPort = 40437 externalPort = 3001 +[[ports]] +localPort = 43107 +externalPort = 4200 + [deployment] deploymentTarget = "autoscale" run = ["node", "dist/server/production.mjs"] diff --git a/api/user/set-realm.ts b/api/user/set-realm.ts new file mode 100644 index 00000000..947e9d62 --- /dev/null +++ b/api/user/set-realm.ts @@ -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" }, + }); + } +}; diff --git a/client/pages/Activity.tsx b/client/pages/Activity.tsx index 7a43fa4e..61d26a6d 100644 --- a/client/pages/Activity.tsx +++ b/client/pages/Activity.tsx @@ -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 = { + 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 ( +
+ +

{message}

+ {onRetry && ( + + )} +
+ ); +} + +function LoadingSpinner() { + return ( +
+ +
+ ); +} + +function FeedTab({ openExternalLink }: { openExternalLink: (url: string) => Promise }) { + const [posts, setPosts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ; + if (needsAuth) { + return ( +
+

Sign in on the web to view the community feed

+ +
+ ); + } + if (error) return ; + + return ( + +
+ {posts.length === 0 ? ( +
+

No posts yet. Be the first to share!

+ +
+ ) : ( + posts.map((post) => { + const config = ARM_CONFIG[post.arm_affiliation] || ARM_CONFIG.labs; + const Icon = config.icon; + return ( + + +
+ {post.user_profiles?.avatar_url && ( + + )} +
+
+ + {post.user_profiles?.full_name || post.user_profiles?.username || "Anonymous"} + + + + {config.label} + +
+

{post.title}

+

{post.content}

+
+ + + {post.likes_count || 0} + + + + {post.comments_count || 0} + + +
+
+
+
+
+ ); + }) + )} + {posts.length > 0 && ( + + )} +
+
+ ); +} + +function RealmSwitcher({ + currentRealm, + openExternalLink, +}: { + currentRealm: ArmType; + openExternalLink: (url: string) => Promise; +}) { + const realms = Object.entries(ARM_CONFIG) as [ArmType, typeof ARM_CONFIG.labs][]; + + return ( +
+
+

Your Realms

+

Explore the different realms of AeThex

+
+ +
+

+ Your current realm: {ARM_CONFIG[currentRealm]?.label || "Labs"} +

+
+ +
+ {realms.map(([key, config]) => { + const Icon = config.icon; + const isActive = currentRealm === key; + return ( + + ); + })} +
+ + +
+ ); +} + +function AchievementsTab({ openExternalLink }: { openExternalLink: (url: string) => Promise }) { + 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 ( + +
+
+
+ + {unlockedCount}/{achievements.length} Unlocked +
+ + +{achievements.filter((a) => a.unlocked).reduce((sum, a) => sum + a.xp_reward, 0)} XP + +
+
+

+ Achievement tracking coming soon! View your full profile for more details. +

+
+ {achievements.slice(0, 4).map((achievement) => ( + + +
+
{achievement.icon}
+
+
+

{achievement.name}

+ {achievement.unlocked && } +
+

{achievement.description}

+ {!achievement.unlocked && achievement.progress !== undefined && ( +
+
+ Progress + {achievement.progress}/{achievement.total} +
+ +
+ )} +
+ +{achievement.xp_reward} XP +
+
+
+ ))} + +
+
+ ); +} + +function LeaderboardTab({ openExternalLink }: { openExternalLink: (url: string) => Promise }) { + 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 ( + +
+
+ + Top Creators This Week +
+
+

+ Live leaderboard coming soon! Visit the full site for more. +

+
+ {leaderboard.map((entry) => { + const badge = getRankBadge(entry.rank); + return ( +
+
+ {badge.icon || `#${entry.rank}`} +
+
+

{entry.username}

+
+ Lvl {entry.level} + {entry.streak && entry.streak > 0 && ( + + {entry.streak}d + + )} +
+
+
+

{entry.xp.toLocaleString()}

+

XP

+
+
+ ); + })} + +
+
+ ); +} + +function OpportunitiesTab({ openExternalLink }: { openExternalLink: (url: string) => Promise }) { + return ( + +
+
+ + Open Opportunities +
+
+

+ Browse job opportunities, contracts, and gigs from the AeThex community. +

+ +
+ +
+

Quick categories:

+
+ {[ + { label: "Full-Time", icon: Briefcase }, + { label: "Contract", icon: Target }, + { label: "Freelance", icon: Star }, + ].map(({ label, icon: Icon }) => ( + + ))} +
+
+
+
+ ); +} + +function QuestsTab({ openExternalLink }: { openExternalLink: (url: string) => Promise }) { + 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 }) => ( + + +
+
+ {quest.completed ? : } +
+
+
+

{quest.title}

+ +{quest.xp_reward} XP +
+

{quest.description}

+ {!quest.completed && ( +
+
+ Progress + {quest.progress}/{quest.total} +
+ +
+ )} +
+
+
+
+ ); + + return ( + +
+
+

+ Quest system coming soon! These are example quests. +

+
+
+
+ + Daily Quests +
+
+ {dailyQuests.map((quest) => )} +
+
+
+
+ + Weekly Quests +
+
+ {weeklyQuests.map((quest) => )} +
+
+ +
+
+ ); +} 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 ( - - ); + return ; } if (error) { return (
-
-

- ❌ Activity Error -

-

{error}

- +
+

Activity Error

+

{error}

-

- Troubleshooting Steps: -

-
    -
  1. Clear your browser cache (Ctrl+Shift+Delete)
  2. +

    Troubleshooting:

    +
      +
    1. Clear your browser cache
    2. Close Discord completely
    3. -
    4. Reopen Discord
    5. -
    6. Try opening the Activity again
    7. +
    8. Reopen Discord and try again
- -
-

- 💡 Open browser console (F12) and look for messages starting with{" "} - - [Discord Activity] - -

-
- - - -

- Still having issues? Check the{" "} - - troubleshooting guide - -

); } - // Not in Discord Activity - show informational message if (!isActivity && !isLoading) { return (
-
-

- 🎮 Discord Activity -

-

- This page is designed to run as a Discord Activity. Open it within - Discord to get started! -

-

- Not in Discord? Visit the main app at{" "} - - aethex.dev - -

-
-
- ); - } - - if (isLoading) { - return ( - - ); - } - - if (error) { - return ( -
-
-

- ❌ Activity Error -

-

{error}

- -
-

- Troubleshooting Steps: -

-
    -
  1. Clear your browser cache (Ctrl+Shift+Delete)
  2. -
  3. Close Discord completely
  4. -
  5. Reopen Discord
  6. -
  7. Try opening the Activity again
  8. -
-
- -
-

- 💡 Open Discord DevTools (Ctrl+Shift+I) and check the console for - messages starting with{" "} - - [Discord Activity] - -

-
- - +
+

Discord Activity

+

This page is designed to run as a Discord Activity. Open it within Discord to get started!

+ + Visit aethex.dev +
); } if (user && showContent) { - const appBaseUrl = "https://aethex.dev"; + const realmConfig = ARM_CONFIG[currentRealm]; + const RealmIcon = realmConfig.icon; return (
-
-
-

- Welcome to AeThex, {user.full_name || user.username}! 🎉 -

-

Discord Activity

-
- -
-
-

- 👤 Your Profile -

- {user.avatar_url && ( - {user.full_name - )} -

- Name: {user.full_name || "Not set"} -

-

- Username: {user.username || "Not set"} -

-

- Type: {user.user_type || "community_member"} -

- {user.bio && ( -

"{user.bio}"

- )} -
- -
-

- ⚔️ Your Realm -

-

- {user.primary_arm?.toUpperCase() || "LABS"} -

-

- Your primary realm determines your Discord role and access to - realm-specific features. -

- +
+
+
+ {user.avatar_url && } +
+

{user.full_name || user.username}

+
+ + {realmConfig.label} + +
+
+
-
-

- 🚀 Quick Actions -

-
- - - -
-
+ + + Feed + Realms + Badges + Top + Jobs + Quests + -
-

- 💡 Discord Commands: Use{" "} - - /profile - - ,{" "} - - /set-realm - - , and{" "} - - /verify-role - {" "} - to manage your account directly in Discord. -

-
+
+ + + + + + + + +
+
); } - // Still loading - return ( - - ); + return ; } diff --git a/replit.md b/replit.md index 9887ee11..2f7685be 100644 --- a/replit.md +++ b/replit.md @@ -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