diff --git a/client/pages/Directory.tsx b/client/pages/Directory.tsx index 8bd5de19..ea5616d6 100644 --- a/client/pages/Directory.tsx +++ b/client/pages/Directory.tsx @@ -5,8 +5,10 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; import { supabase } from "@/lib/supabase"; -import { devconnect, hasDevConnect } from "@/lib/supabase-devconnect"; +import { devconnect } from "@/lib/supabase-devconnect"; import { useEffect, useMemo, useState } from "react"; function initials(name?: string | null) { @@ -18,10 +20,14 @@ export default function Directory() { const [query, setQuery] = useState(""); const [hideAeThex, setHideAeThex] = useState(true); const source = devconnect ? "DevConnect" : "AeThex"; - type BasicDev = { id: string; name: string; avatar_url?: string | null; location?: string | null; user_type?: string | null; experience_level?: string | null }; + type BasicDev = { id: string; name: string; avatar_url?: string | null; location?: string | null; user_type?: string | null; experience_level?: string | null; tags?: string[] | null; verified?: boolean; updated_at?: string | null; total_xp?: number | null }; const [devs, setDevs] = useState([]); - type Studio = { id: string; name: string; description?: string | null; type?: string | null; is_recruiting?: boolean | null; recruiting_roles?: string[] | null; tags?: string[] | null; slug?: string | null; visibility?: string | null; members_count?: number }; + type StudioMember = { id: string; name: string; avatar_url?: string | null }; + type Studio = { id: string; name: string; description?: string | null; type?: string | null; is_recruiting?: boolean | null; recruiting_roles?: string[] | null; tags?: string[] | null; slug?: string | null; visibility?: string | null; members_count?: number; members?: StudioMember[] }; const [studios, setStudios] = useState([]); + const [skillFilter, setSkillFilter] = useState("all"); + const [regionFilter, setRegionFilter] = useState("all"); + const [sortMode, setSortMode] = useState("relevance"); useEffect(() => { const client = devconnect || supabase; @@ -33,6 +39,10 @@ export default function Directory() { location: u.location || u.city || u.country || null, user_type: u.user_type || u.role || null, experience_level: u.experience_level || u.seniority || null, + tags: u.tags || u.skills || null, + verified: Boolean(u.is_verified || (u.subscription && String(u.subscription).toLowerCase().includes("pro")) || (u.badges && String(u.badges).toLowerCase().includes("verified"))), + updated_at: u.updated_at || null, + total_xp: u.total_xp || u.xp || null, }); client @@ -87,11 +97,43 @@ export default function Directory() { .then(({ data: d2 }) => setStudios((d2 || []).map(mapStudio))); } }); + // Fetch member avatars for studios (DevConnect only) + if (client === devconnect) { + const ids = studios.map((s) => s.id).slice(0, 30); + if (ids.length) { + devconnect + ?.from("collective_members" as any) + .select("collective_id, profile_id") + .in("collective_id", ids) + .limit(200) + .then(async ({ data }) => { + const byCollective: Record = {}; + (data || []).forEach((row: any) => { + const cid = String(row.collective_id); + if (!byCollective[cid]) byCollective[cid] = []; + if (byCollective[cid].length < 5) byCollective[cid].push(String(row.profile_id)); + }); + const profileIds = Array.from(new Set(Object.values(byCollective).flat())); + if (profileIds.length) { + const { data: profs } = await devconnect + ?.from("profiles" as any) + .select("id, display_name, avatar_url") + .in("id", profileIds); + const map: Record = {}; + (profs || []).forEach((p: any) => { + map[String(p.id)] = { id: String(p.id), name: p.display_name || "Member", avatar_url: p.avatar_url || null }; + }); + setStudios((prev) => prev.map((s) => ({ ...s, members: (byCollective[s.id] || []).map((pid) => map[pid]).filter(Boolean) }))); + } + }) + .catch(() => {}); + } + } }, []); const filteredDevs = useMemo(() => { const q = query.trim().toLowerCase(); - return devs.filter((u) => { + let list = devs.filter((u) => { if (hideAeThex && u.user_type === "staff") return false; if (!q) return true; return ( @@ -99,7 +141,20 @@ export default function Directory() { (u.location || "").toLowerCase().includes(q) ); }); - }, [devs, query, hideAeThex]); + + if (skillFilter !== "all") { + list = list.filter((u) => (u.tags || []).map(String).map((s) => s.toLowerCase()).includes(skillFilter.toLowerCase()) || (u.user_type || "").toLowerCase() === skillFilter.toLowerCase()); + } + if (regionFilter !== "all") { + list = list.filter((u) => (u.location || "").toLowerCase().includes(regionFilter.toLowerCase())); + } + if (sortMode === "active") { + list = [...list].sort((a, b) => (b.total_xp || 0) - (a.total_xp || 0)); + } else if (sortMode === "recent") { + list = [...list].sort((a, b) => new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime()); + } + return list; + }, [devs, query, hideAeThex, skillFilter, regionFilter, sortMode]); const filteredStudios = useMemo(() => { const q = query.trim().toLowerCase(); @@ -134,8 +189,45 @@ export default function Directory() { -
- setQuery(e.target.value)} /> +
+ setQuery(e.target.value)} className="md:col-span-2" /> + + +
@@ -147,6 +239,9 @@ export default function Directory() { +
+ Showing {filteredDevs.length} creators from {source} +
{filteredDevs.map((u) => ( @@ -156,11 +251,19 @@ export default function Directory() { {initials(u.name)}
-
{u.name || "Developer"}
+
+ {u.name || "Developer"} + {u.verified && ( + Verified + )} +
{u.location || "Global"}
- {u.user_type} + {u.user_type && {u.user_type}} {u.experience_level && {u.experience_level}} + {(u.tags || []).slice(0, 3).map((t) => ( + {String(t)} + ))}
@@ -170,6 +273,9 @@ export default function Directory() { +
+ Showing {filteredStudios.length} studios from {source} +
{filteredStudios.map((t) => ( @@ -186,12 +292,29 @@ export default function Directory() {

{t.description || ""}

-
- {typeof t.members_count === "number" && ( - {t.members_count} members - )} - {(t.recruiting_roles && t.recruiting_roles.length > 0) && ( - Roles: {t.recruiting_roles!.join(", ")} + {t.members && t.members.length > 0 && ( +
+ {t.members.slice(0,5).map((m) => ( + + + {initials(m.name)} + + ))} +
+ )} +
+
+ {typeof t.members_count === "number" && ( + {t.members_count} members + )} + {(t.recruiting_roles && t.recruiting_roles.length > 0) && ( + Roles: {t.recruiting_roles!.join(", ")} + )} +
+ {t.slug && ( + )}
{(t.tags && t.tags.length > 0) && (