diff --git a/client/components/admin/AdminTierBadgeManager.tsx b/client/components/admin/AdminTierBadgeManager.tsx new file mode 100644 index 00000000..f937882e --- /dev/null +++ b/client/components/admin/AdminTierBadgeManager.tsx @@ -0,0 +1,453 @@ +import { useCallback, useEffect, useState } from "react"; +import { + aethexUserService, + aethexBadgeService, + aethexTierService, + type AethexUserProfile, + type AethexBadge, + type AethexUserBadge, +} from "@/lib/aethex-database-adapter"; +import { aethexToast } from "@/lib/aethex-toast"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Crown, + Star, + Sparkles, + Search, + Loader2, + Plus, + X, + Shield, +} from "lucide-react"; + +interface AdminTierBadgeManagerProps { + profiles?: AethexUserProfile[]; + onProfilesChange?: () => void; +} + +export default function AdminTierBadgeManager({ + profiles: externalProfiles, + onProfilesChange, +}: AdminTierBadgeManagerProps) { + const [profiles, setProfiles] = useState([]); + const [allBadges, setAllBadges] = useState([]); + const [loading, setLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedUserId, setSelectedUserId] = useState(null); + const [selectedUserBadges, setSelectedUserBadges] = useState([]); + const [selectedUserTier, setSelectedUserTier] = useState<"free" | "pro" | "council">("free"); + const [loadingUserData, setLoadingUserData] = useState(false); + const [updatingTier, setUpdatingTier] = useState(false); + const [updatingBadge, setUpdatingBadge] = useState(null); + + const loadData = useCallback(async () => { + setLoading(true); + try { + const [profileList, badgeList] = await Promise.all([ + externalProfiles ? Promise.resolve(externalProfiles) : aethexUserService.listProfiles(200), + aethexBadgeService.getAllBadges(), + ]); + setProfiles(profileList); + setAllBadges(badgeList); + } catch (err) { + console.warn("Failed to load data:", err); + aethexToast.error({ description: "Failed to load user data" }); + } finally { + setLoading(false); + } + }, [externalProfiles]); + + useEffect(() => { + loadData(); + }, [loadData]); + + const loadSelectedUserData = useCallback(async (userId: string) => { + setLoadingUserData(true); + try { + const [tier, badges] = await Promise.all([ + aethexTierService.getUserTier(userId), + aethexBadgeService.getUserBadges(userId), + ]); + setSelectedUserTier(tier); + setSelectedUserBadges(badges); + } catch (err) { + console.warn("Failed to load user tier/badges:", err); + } finally { + setLoadingUserData(false); + } + }, []); + + useEffect(() => { + if (selectedUserId) { + loadSelectedUserData(selectedUserId); + } + }, [selectedUserId, loadSelectedUserData]); + + const handleTierChange = async (newTier: "free" | "pro" | "council") => { + if (!selectedUserId) return; + setUpdatingTier(true); + try { + const success = await aethexTierService.setUserTier(selectedUserId, newTier); + if (success) { + setSelectedUserTier(newTier); + aethexToast.success({ description: `Tier updated to ${newTier.charAt(0).toUpperCase() + newTier.slice(1)}` }); + onProfilesChange?.(); + } else { + aethexToast.error({ description: "Failed to update tier" }); + } + } catch (err) { + console.warn("Failed to update tier:", err); + aethexToast.error({ description: "Failed to update tier" }); + } finally { + setUpdatingTier(false); + } + }; + + const handleAwardBadge = async (badgeSlug: string) => { + if (!selectedUserId) return; + setUpdatingBadge(badgeSlug); + try { + const success = await aethexBadgeService.awardBadge(selectedUserId, badgeSlug); + if (success) { + await loadSelectedUserData(selectedUserId); + aethexToast.success({ description: "Badge awarded successfully" }); + } else { + aethexToast.error({ description: "Failed to award badge" }); + } + } catch (err) { + console.warn("Failed to award badge:", err); + aethexToast.error({ description: "Failed to award badge" }); + } finally { + setUpdatingBadge(null); + } + }; + + const handleRevokeBadge = async (badgeSlug: string) => { + if (!selectedUserId) return; + setUpdatingBadge(badgeSlug); + try { + const success = await aethexBadgeService.revokeBadge(selectedUserId, badgeSlug); + if (success) { + await loadSelectedUserData(selectedUserId); + aethexToast.success({ description: "Badge revoked" }); + } else { + aethexToast.error({ description: "Failed to revoke badge" }); + } + } catch (err) { + console.warn("Failed to revoke badge:", err); + aethexToast.error({ description: "Failed to revoke badge" }); + } finally { + setUpdatingBadge(null); + } + }; + + const filteredProfiles = profiles.filter((p) => { + const query = searchQuery.toLowerCase(); + return ( + (p.username?.toLowerCase() || "").includes(query) || + (p.full_name?.toLowerCase() || "").includes(query) || + (p.email?.toLowerCase() || "").includes(query) + ); + }); + + const selectedProfile = profiles.find((p) => p.id === selectedUserId); + const userBadgeSlugs = selectedUserBadges.map((ub) => ub.badge?.slug).filter(Boolean); + const availableBadges = allBadges.filter((b) => !userBadgeSlugs.includes(b.slug)); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ + + + + Select User + + + Search and select a user to manage their tier and badges. + + + +
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+
+ {filteredProfiles.slice(0, 50).map((profile) => ( + + ))} + {filteredProfiles.length === 0 && ( +

+ No users found +

+ )} +
+
+
+ +
+ {selectedProfile ? ( + <> + + +
+ + + + {(selectedProfile.username || selectedProfile.full_name || "U") + .slice(0, 2) + .toUpperCase()} + + +
+ + {selectedProfile.username || selectedProfile.full_name || "Unknown User"} + + {selectedProfile.email || selectedProfile.id} +
+
+
+ +
+

+ Subscription Tier +

+ {loadingUserData ? ( +
+ + Loading... +
+ ) : ( +
+
+ {selectedUserTier === "council" ? ( + + ) : selectedUserTier === "pro" ? ( + + ) : ( + + )} + {selectedUserTier} +
+ + {updatingTier && } +
+ )} +
+
+
+ + + + + + User Badges + + + Manage badges for this user. Some badges unlock AI personas. + + + + {loadingUserData ? ( +
+ + Loading badges... +
+ ) : ( + <> +
+

+ Earned Badges ({selectedUserBadges.length}) +

+ {selectedUserBadges.length > 0 ? ( +
+ {selectedUserBadges.map((ub) => ( +
+
+ {ub.badge?.icon || "🏆"} +
+
+

+ {ub.badge?.name || "Badge"} +

+

+ {new Date(ub.earned_at).toLocaleDateString()} +

+
+ +
+ ))} +
+ ) : ( +

+ No badges earned yet. +

+ )} +
+ +
+

+ Award New Badge +

+ {availableBadges.length > 0 ? ( +
+ {availableBadges.map((badge) => ( +
+
+ {badge.icon || "🏅"} +
+
+

+ {badge.name} +

+ {badge.unlocks_persona && ( + + Unlocks AI + + )} +
+ +
+ ))} +
+ ) : ( +

+ {allBadges.length === 0 + ? "No badges defined in the system yet." + : "User has all available badges."} +

+ )} +
+ + )} +
+
+ + ) : ( + + +
+ +

+ Select a user from the list to manage their tier and badges. +

+
+
+
+ )} +
+
+ ); +} diff --git a/client/pages/Admin.tsx b/client/pages/Admin.tsx index 6b37c89d..b26dd1a4 100644 --- a/client/pages/Admin.tsx +++ b/client/pages/Admin.tsx @@ -46,6 +46,7 @@ import AdminEthosVerification from "@/pages/admin/AdminEthosVerification"; import AdminGameForgeStudio from "@/components/admin/AdminGameForgeStudio"; import AdminFoundationManager from "@/components/admin/AdminFoundationManager"; import AdminNexusManager from "@/components/admin/AdminNexusManager"; +import AdminTierBadgeManager from "@/components/admin/AdminTierBadgeManager"; import { changelogEntries } from "@/pages/Changelog"; import { blogSeedPosts } from "@/data/blogSeed"; import { @@ -391,6 +392,7 @@ export default function Admin() { Ethos Discord Operations + Tiers & Badges @@ -787,6 +789,13 @@ export default function Admin() { + + + + diff --git a/client/pages/Profile.tsx b/client/pages/Profile.tsx index 6075d590..be3d53e6 100644 --- a/client/pages/Profile.tsx +++ b/client/pages/Profile.tsx @@ -5,7 +5,10 @@ import LoadingScreen from "@/components/LoadingScreen"; import { useAuth } from "@/contexts/AuthContext"; import { aethexAchievementService, + aethexBadgeService, + aethexTierService, type AethexAchievement, + type AethexUserBadge, } from "@/lib/aethex-database-adapter"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; @@ -31,6 +34,9 @@ import { Code2, Globe, Award, + Star, + Crown, + Sparkles, } from "lucide-react"; interface ProfileStat { @@ -65,6 +71,9 @@ const Profile = () => { const { user, profile, loading: authLoading } = useAuth(); const [achievements, setAchievements] = useState([]); const [loadingAchievements, setLoadingAchievements] = useState(false); + const [userTier, setUserTier] = useState<"free" | "pro" | "council">("free"); + const [userBadges, setUserBadges] = useState([]); + const [loadingTierBadges, setLoadingTierBadges] = useState(false); const username = profile?.username || user?.email?.split("@")[0] || "creator"; const passportHref = `/passport/${encodeURIComponent(username)}`; @@ -96,6 +105,27 @@ const Profile = () => { loadAchievements().catch(() => undefined); }, [user?.id]); + useEffect(() => { + const loadTierAndBadges = async () => { + if (!user?.id) return; + setLoadingTierBadges(true); + try { + const [tier, badges] = await Promise.all([ + aethexTierService.getUserTier(user.id), + aethexBadgeService.getUserBadges(user.id), + ]); + setUserTier(tier); + setUserBadges(badges); + } catch (error) { + console.warn("Failed to load tier/badges for profile", error); + } finally { + setLoadingTierBadges(false); + } + }; + + loadTierAndBadges().catch(() => undefined); + }, [user?.id]); + const stats = useMemo(() => { const level = Math.max(1, Number(profile?.level ?? 1)); const totalXp = Math.max(0, Number(profile?.total_xp ?? 0)); @@ -289,6 +319,124 @@ const Profile = () => { + {/* MEMBERSHIP TIER & BADGES */} + + + + + Membership & Badges + + + Your subscription tier and earned badges unlock AI personas and features. + + + + {/* Membership Tier */} +
+

+ Current Tier +

+ {loadingTierBadges ? ( +
+
+ Loading membership info... +
+ ) : ( +
+
+ {userTier === "council" ? ( + + ) : userTier === "pro" ? ( + + ) : ( + + )} +
+

+ {userTier === "council" + ? "Council Member" + : userTier === "pro" + ? "Pro Member" + : "Free Tier"} +

+

+ {userTier === "council" + ? "Full access to all AI personas" + : userTier === "pro" + ? "Access to Pro-tier AI personas" + : "Basic AI persona access"} +

+
+
+ {userTier === "free" && ( + + )} +
+ )} +
+ + {/* Earned Badges */} +
+

+ Earned Badges +

+ {loadingTierBadges ? ( +
+
+ Loading badges... +
+ ) : userBadges.length > 0 ? ( +
+ {userBadges.map((ub) => ( +
+
+ {ub.badge?.icon || "🏆"} +
+
+

+ {ub.badge?.name || "Badge"} +

+

+ {ub.badge?.description || `Earned ${new Date(ub.earned_at).toLocaleDateString()}`} +

+
+ {ub.badge?.unlocks_persona && ( + + Unlocks AI + + )} +
+ ))} +
+ ) : ( +

+ Complete activities and challenges to earn badges that unlock exclusive features and AI personas. +

+ )} +
+ + +