Prettier format pending files

This commit is contained in:
Builder.io 2025-10-19 03:47:28 +00:00
parent a75782c700
commit eb8d014737
3 changed files with 220 additions and 52 deletions

View file

@ -7,6 +7,10 @@ export const hasDevConnect = Boolean(devUrl && devAnon);
export const devconnect: SupabaseClient | null = hasDevConnect export const devconnect: SupabaseClient | null = hasDevConnect
? createClient(devUrl!, devAnon!, { ? createClient(devUrl!, devAnon!, {
auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false }, auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
},
}) })
: null; : null;

View file

@ -896,8 +896,7 @@ export default function Community() {
{ {
id: "directory", id: "directory",
title: "Browse Creators & Studios", title: "Browse Creators & Studios",
description: description: "Discover nonAeThex developers and independent studios.",
"Discover nonAeThex developers and independent studios.",
href: "/directory", href: "/directory",
label: "Open directory", label: "Open directory",
}, },

View file

@ -1,28 +1,66 @@
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { supabase } from "@/lib/supabase"; import { supabase } from "@/lib/supabase";
import { devconnect } from "@/lib/supabase-devconnect"; import { devconnect } from "@/lib/supabase-devconnect";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
function initials(name?: string | null) { function initials(name?: string | null) {
const parts = (name || "").trim().split(/\s+/); const parts = (name || "").trim().split(/\s+/);
return ((parts[0] || "").charAt(0) + (parts[1] || "").charAt(0)).toUpperCase() || "A"; return (
((parts[0] || "").charAt(0) + (parts[1] || "").charAt(0)).toUpperCase() ||
"A"
);
} }
export default function Directory() { export default function Directory() {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [hideAeThex, setHideAeThex] = useState(true); const [hideAeThex, setHideAeThex] = useState(true);
const source = devconnect ? "DevConnect" : "AeThex"; 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; tags?: string[] | null; verified?: boolean; updated_at?: string | null; total_xp?: number | 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<BasicDev[]>([]); const [devs, setDevs] = useState<BasicDev[]>([]);
type StudioMember = { id: string; name: string; avatar_url?: string | null }; 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[] }; 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<Studio[]>([]); const [studios, setStudios] = useState<Studio[]>([]);
const [skillFilter, setSkillFilter] = useState<string>("all"); const [skillFilter, setSkillFilter] = useState<string>("all");
const [regionFilter, setRegionFilter] = useState<string>("all"); const [regionFilter, setRegionFilter] = useState<string>("all");
@ -39,7 +77,12 @@ export default function Directory() {
user_type: u.user_type || u.role || null, user_type: u.user_type || u.role || null,
experience_level: u.experience_level || u.seniority || null, experience_level: u.experience_level || u.seniority || null,
tags: u.tags || u.skills || 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"))), 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, updated_at: u.updated_at || null,
total_xp: u.total_xp || u.xp || null, total_xp: u.total_xp || u.xp || null,
}); });
@ -64,7 +107,7 @@ export default function Directory() {
id: String(r.id), id: String(r.id),
name: r.name, name: r.name,
description: r.description || null, description: r.description || null,
type: r.type || (r.visibility || null), type: r.type || r.visibility || null,
is_recruiting: r.is_recruiting ?? null, is_recruiting: r.is_recruiting ?? null,
recruiting_roles: r.recruiting_roles ?? null, recruiting_roles: r.recruiting_roles ?? null,
tags: r.tags ?? null, tags: r.tags ?? null,
@ -83,7 +126,7 @@ export default function Directory() {
.select( .select(
client === devconnect client === devconnect
? "id,name,description,type,is_recruiting,recruiting_roles,tags,slug,created_at, collective_members:collective_members(count)" ? "id,name,description,type,is_recruiting,recruiting_roles,tags,slug,created_at, collective_members:collective_members(count)"
: "id,name,description,visibility,created_at, team_memberships:team_memberships(count)" : "id,name,description,visibility,created_at, team_memberships:team_memberships(count)",
) )
.limit(200) .limit(200)
.then(({ data, error }) => { .then(({ data, error }) => {
@ -91,7 +134,9 @@ export default function Directory() {
else if (client !== supabase) { else if (client !== supabase) {
supabase supabase
.from<any>("teams" as any) .from<any>("teams" as any)
.select("id,name,description,visibility,created_at, team_memberships:team_memberships(count)") .select(
"id,name,description,visibility,created_at, team_memberships:team_memberships(count)",
)
.limit(200) .limit(200)
.then(({ data: d2 }) => setStudios((d2 || []).map(mapStudio))); .then(({ data: d2 }) => setStudios((d2 || []).map(mapStudio)));
} }
@ -110,9 +155,12 @@ export default function Directory() {
(data || []).forEach((row: any) => { (data || []).forEach((row: any) => {
const cid = String(row.collective_id); const cid = String(row.collective_id);
if (!byCollective[cid]) byCollective[cid] = []; if (!byCollective[cid]) byCollective[cid] = [];
if (byCollective[cid].length < 5) byCollective[cid].push(String(row.profile_id)); if (byCollective[cid].length < 5)
byCollective[cid].push(String(row.profile_id));
}); });
const profileIds = Array.from(new Set(Object.values(byCollective).flat())); const profileIds = Array.from(
new Set(Object.values(byCollective).flat()),
);
if (profileIds.length) { if (profileIds.length) {
const { data: profs } = await devconnect const { data: profs } = await devconnect
?.from<any>("profiles" as any) ?.from<any>("profiles" as any)
@ -120,9 +168,20 @@ export default function Directory() {
.in("id", profileIds); .in("id", profileIds);
const map: Record<string, StudioMember> = {}; const map: Record<string, StudioMember> = {};
(profs || []).forEach((p: any) => { (profs || []).forEach((p: any) => {
map[String(p.id)] = { id: String(p.id), name: p.display_name || "Member", avatar_url: p.avatar_url || null }; 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) }))); setStudios((prev) =>
prev.map((s) => ({
...s,
members: (byCollective[s.id] || [])
.map((pid) => map[pid])
.filter(Boolean),
})),
);
} }
}) })
.catch(() => {}); .catch(() => {});
@ -142,15 +201,28 @@ export default function Directory() {
}); });
if (skillFilter !== "all") { if (skillFilter !== "all") {
list = list.filter((u) => (u.tags || []).map(String).map((s) => s.toLowerCase()).includes(skillFilter.toLowerCase()) || (u.user_type || "").toLowerCase() === skillFilter.toLowerCase()); list = list.filter(
(u) =>
(u.tags || [])
.map(String)
.map((s) => s.toLowerCase())
.includes(skillFilter.toLowerCase()) ||
(u.user_type || "").toLowerCase() === skillFilter.toLowerCase(),
);
} }
if (regionFilter !== "all") { if (regionFilter !== "all") {
list = list.filter((u) => (u.location || "").toLowerCase().includes(regionFilter.toLowerCase())); list = list.filter((u) =>
(u.location || "").toLowerCase().includes(regionFilter.toLowerCase()),
);
} }
if (sortMode === "active") { if (sortMode === "active") {
list = [...list].sort((a, b) => (b.total_xp || 0) - (a.total_xp || 0)); list = [...list].sort((a, b) => (b.total_xp || 0) - (a.total_xp || 0));
} else if (sortMode === "recent") { } else if (sortMode === "recent") {
list = [...list].sort((a, b) => new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime()); list = [...list].sort(
(a, b) =>
new Date(b.updated_at || 0).getTime() -
new Date(a.updated_at || 0).getTime(),
);
} }
return list; return list;
}, [devs, query, hideAeThex, skillFilter, regionFilter, sortMode]); }, [devs, query, hideAeThex, skillFilter, regionFilter, sortMode]);
@ -173,34 +245,63 @@ export default function Directory() {
<section className="container mx-auto max-w-6xl px-4"> <section className="container mx-auto max-w-6xl px-4">
<div className="flex items-center justify-between gap-3 flex-wrap"> <div className="flex items-center justify-between gap-3 flex-wrap">
<div> <div>
<Badge variant="outline" className="border-aethex-400/50 text-aethex-300">Directory</Badge> <Badge
<h1 className="mt-2 text-4xl font-extrabold text-gradient">Creators & Studios</h1> variant="outline"
<p className="text-muted-foreground max-w-2xl mt-1">Browse nonAeThex creators and studios. Optin visibility; public info only.</p> className="border-aethex-400/50 text-aethex-300"
>
Directory
</Badge>
<h1 className="mt-2 text-4xl font-extrabold text-gradient">
Creators & Studios
</h1>
<p className="text-muted-foreground max-w-2xl mt-1">
Browse nonAeThex creators and studios. Optin visibility;
public info only.
</p>
<div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground"> <div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
<span>Source:</span> <span>Source:</span>
<Badge variant="outline" className="uppercase tracking-wide">{source}</Badge> <Badge variant="outline" className="uppercase tracking-wide">
{source}
</Badge>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label className="flex items-center gap-2 text-sm text-muted-foreground"> <label className="flex items-center gap-2 text-sm text-muted-foreground">
<input type="checkbox" checked={hideAeThex} onChange={(e) => setHideAeThex(e.target.checked)} /> <input
type="checkbox"
checked={hideAeThex}
onChange={(e) => setHideAeThex(e.target.checked)}
/>
Hide AeThexaffiliated (staff) Hide AeThexaffiliated (staff)
</label> </label>
</div> </div>
</div> </div>
<div className="mt-4 grid gap-3 md:grid-cols-4"> <div className="mt-4 grid gap-3 md:grid-cols-4">
<Input placeholder="Search name, handle, or location" value={query} onChange={(e) => setQuery(e.target.value)} className="md:col-span-2" /> <Input
placeholder="Search name, handle, or location"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="md:col-span-2"
/>
<Select value={skillFilter} onValueChange={setSkillFilter}> <Select value={skillFilter} onValueChange={setSkillFilter}>
<SelectTrigger aria-label="Skill"> <SelectTrigger aria-label="Skill">
<SelectValue placeholder="Skill / Role" /> <SelectValue placeholder="Skill / Role" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All skills</SelectItem> <SelectItem value="all">All skills</SelectItem>
{[...new Set(devs.flatMap((d) => (d.tags || []).map(String)).concat(devs.map((d) => d.user_type || []).flat()))] {[
...new Set(
devs
.flatMap((d) => (d.tags || []).map(String))
.concat(devs.map((d) => d.user_type || []).flat()),
),
]
.filter(Boolean) .filter(Boolean)
.slice(0, 30) .slice(0, 30)
.map((s) => ( .map((s) => (
<SelectItem key={String(s)} value={String(s)}>{String(s)}</SelectItem> <SelectItem key={String(s)} value={String(s)}>
{String(s)}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@ -210,10 +311,18 @@ export default function Directory() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All regions</SelectItem> <SelectItem value="all">All regions</SelectItem>
{[...new Set(devs.map((d) => (d.location || "").split(",").pop()?.trim()).filter(Boolean))] {[
...new Set(
devs
.map((d) => (d.location || "").split(",").pop()?.trim())
.filter(Boolean),
),
]
.slice(0, 30) .slice(0, 30)
.map((r) => ( .map((r) => (
<SelectItem key={String(r)} value={String(r)}>{String(r)}</SelectItem> <SelectItem key={String(r)} value={String(r)}>
{String(r)}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@ -239,29 +348,52 @@ export default function Directory() {
<TabsContent value="devs"> <TabsContent value="devs">
<div className="mb-4 rounded-lg border border-border/40 bg-background/60 p-3 text-xs text-muted-foreground"> <div className="mb-4 rounded-lg border border-border/40 bg-background/60 p-3 text-xs text-muted-foreground">
Showing {filteredDevs.length} creators from <span className="font-medium">{source}</span> Showing {filteredDevs.length} creators from{" "}
<span className="font-medium">{source}</span>
</div> </div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{filteredDevs.map((u) => ( {filteredDevs.map((u) => (
<Card key={u.id} className="border-border/40 bg-card/60 backdrop-blur transition hover:border-aethex-400/50"> <Card
key={u.id}
className="border-border/40 bg-card/60 backdrop-blur transition hover:border-aethex-400/50"
>
<CardContent className="p-4 flex items-center gap-4"> <CardContent className="p-4 flex items-center gap-4">
<Avatar className="h-12 w-12"> <Avatar className="h-12 w-12">
<AvatarImage src={u.avatar_url || undefined} alt={u.name || "Developer"} /> <AvatarImage
src={u.avatar_url || undefined}
alt={u.name || "Developer"}
/>
<AvatarFallback>{initials(u.name)}</AvatarFallback> <AvatarFallback>{initials(u.name)}</AvatarFallback>
</Avatar> </Avatar>
<div className="min-w-0"> <div className="min-w-0">
<div className="font-medium truncate flex items-center gap-2"> <div className="font-medium truncate flex items-center gap-2">
{u.name || "Developer"} {u.name || "Developer"}
{u.verified && ( {u.verified && (
<Badge className="bg-emerald-500/10 text-emerald-200 border-emerald-400/40">Verified</Badge> <Badge className="bg-emerald-500/10 text-emerald-200 border-emerald-400/40">
Verified
</Badge>
)} )}
</div> </div>
<div className="text-xs text-muted-foreground truncate">{u.location || "Global"}</div> <div className="text-xs text-muted-foreground truncate">
{u.location || "Global"}
</div>
<div className="mt-1 flex flex-wrap gap-2"> <div className="mt-1 flex flex-wrap gap-2">
{u.user_type && <Badge variant="outline">{u.user_type}</Badge>} {u.user_type && (
{u.experience_level && <Badge variant="outline">{u.experience_level}</Badge>} <Badge variant="outline">{u.user_type}</Badge>
)}
{u.experience_level && (
<Badge variant="outline">
{u.experience_level}
</Badge>
)}
{(u.tags || []).slice(0, 3).map((t) => ( {(u.tags || []).slice(0, 3).map((t) => (
<Badge key={String(t)} variant="outline" className="text-xs">{String(t)}</Badge> <Badge
key={String(t)}
variant="outline"
className="text-xs"
>
{String(t)}
</Badge>
))} ))}
</div> </div>
</div> </div>
@ -273,30 +405,48 @@ export default function Directory() {
<TabsContent value="studios"> <TabsContent value="studios">
<div className="mb-4 rounded-lg border border-border/40 bg-background/60 p-3 text-xs text-muted-foreground"> <div className="mb-4 rounded-lg border border-border/40 bg-background/60 p-3 text-xs text-muted-foreground">
Showing {filteredStudios.length} studios from <span className="font-medium">{source}</span> Showing {filteredStudios.length} studios from{" "}
<span className="font-medium">{source}</span>
</div> </div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{filteredStudios.map((t) => ( {filteredStudios.map((t) => (
<Card key={t.id} className="border-border/40 bg-card/60 backdrop-blur transition hover:border-aethex-400/50"> <Card
key={t.id}
className="border-border/40 bg-card/60 backdrop-blur transition hover:border-aethex-400/50"
>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<CardTitle className="text-lg">{t.name}</CardTitle> <CardTitle className="text-lg">{t.name}</CardTitle>
{t.is_recruiting && ( {t.is_recruiting && (
<Badge className="bg-emerald-500/10 text-emerald-200 border-emerald-400/40">Recruiting</Badge> <Badge className="bg-emerald-500/10 text-emerald-200 border-emerald-400/40">
Recruiting
</Badge>
)} )}
</div> </div>
{(t.type || t.visibility) && ( {(t.type || t.visibility) && (
<CardDescription className="capitalize">{t.type || t.visibility}</CardDescription> <CardDescription className="capitalize">
{t.type || t.visibility}
</CardDescription>
)} )}
</CardHeader> </CardHeader>
<CardContent className="pt-0 space-y-3"> <CardContent className="pt-0 space-y-3">
<p className="text-sm text-muted-foreground line-clamp-3">{t.description || ""}</p> <p className="text-sm text-muted-foreground line-clamp-3">
{t.description || ""}
</p>
{t.members && t.members.length > 0 && ( {t.members && t.members.length > 0 && (
<div className="flex -space-x-2"> <div className="flex -space-x-2">
{t.members.slice(0,5).map((m) => ( {t.members.slice(0, 5).map((m) => (
<Avatar key={m.id} className="h-7 w-7 ring-2 ring-background"> <Avatar
<AvatarImage src={m.avatar_url || undefined} alt={m.name} /> key={m.id}
<AvatarFallback>{initials(m.name)}</AvatarFallback> className="h-7 w-7 ring-2 ring-background"
>
<AvatarImage
src={m.avatar_url || undefined}
alt={m.name}
/>
<AvatarFallback>
{initials(m.name)}
</AvatarFallback>
</Avatar> </Avatar>
))} ))}
</div> </div>
@ -306,20 +456,35 @@ export default function Directory() {
{typeof t.members_count === "number" && ( {typeof t.members_count === "number" && (
<span>{t.members_count} members</span> <span>{t.members_count} members</span>
)} )}
{(t.recruiting_roles && t.recruiting_roles.length > 0) && ( {t.recruiting_roles &&
<span>Roles: {t.recruiting_roles!.join(", ")}</span> t.recruiting_roles.length > 0 && (
)} <span>
Roles: {t.recruiting_roles!.join(", ")}
</span>
)}
</div> </div>
{t.slug && ( {t.slug && (
<Button asChild size="sm" variant="outline"> <Button asChild size="sm" variant="outline">
<a href={`https://devconnect.sbs/collectives/${t.slug}`} target="_blank" rel="noreferrer noopener">Apply</a> <a
href={`https://devconnect.sbs/collectives/${t.slug}`}
target="_blank"
rel="noreferrer noopener"
>
Apply
</a>
</Button> </Button>
)} )}
</div> </div>
{(t.tags && t.tags.length > 0) && ( {t.tags && t.tags.length > 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{t.tags!.map((tag) => ( {t.tags!.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">{tag}</Badge> <Badge
key={tag}
variant="outline"
className="text-xs"
>
{tag}
</Badge>
))} ))}
</div> </div>
)} )}