aethex-forge/client/pages/Directory.tsx
Builder.io d3a945ae18 Create Directory page for browsing external devs and studios
cgen-f5d3f65fd5c7450880f58e1115dce97b
2025-10-19 03:18:59 +00:00

132 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Layout from "@/components/Layout";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
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 { supabase } from "@/lib/supabase";
import { useEffect, useMemo, useState } from "react";
function initials(name?: string | null) {
const parts = (name || "").trim().split(/\s+/);
return ((parts[0] || "").charAt(0) + (parts[1] || "").charAt(0)).toUpperCase() || "A";
}
export default function Directory() {
const [query, setQuery] = useState("");
const [hideAeThex, setHideAeThex] = useState(true);
const [devs, setDevs] = useState<any[]>([]);
const [studios, setStudios] = useState<any[]>([]);
useEffect(() => {
supabase
.from<any>("user_profiles" as any)
.select("id,full_name,username,avatar_url,location,user_type,experience_level,website_url,github_url,linkedin_url")
.limit(200)
.then(({ data }) => setDevs(data || []));
supabase
.from<any>("teams" as any)
.select("id,name,description,visibility,created_at")
.limit(200)
.then(({ data }) => setStudios(data || []));
}, []);
const filteredDevs = useMemo(() => {
const q = query.trim().toLowerCase();
return devs.filter((u) => {
if (hideAeThex && u.user_type === "staff") return false;
if (!q) return true;
return (
(u.full_name || "").toLowerCase().includes(q) ||
(u.username || "").toLowerCase().includes(q) ||
(u.location || "").toLowerCase().includes(q)
);
});
}, [devs, query, hideAeThex]);
const filteredStudios = useMemo(() => {
const q = query.trim().toLowerCase();
return studios.filter((t) => {
if (!q) return true;
return (
(t.name || "").toLowerCase().includes(q) ||
(t.description || "").toLowerCase().includes(q)
);
});
}, [studios, query]);
return (
<Layout>
<div className="min-h-screen bg-aethex-gradient py-12">
<section className="container mx-auto max-w-6xl px-4">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div>
<Badge variant="outline" 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>
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 text-sm text-muted-foreground">
<input type="checkbox" checked={hideAeThex} onChange={(e) => setHideAeThex(e.target.checked)} />
Hide AeThexaffiliated (staff)
</label>
</div>
</div>
<div className="mt-4 flex items-center gap-3">
<Input placeholder="Search name, handle, or location" value={query} onChange={(e) => setQuery(e.target.value)} />
</div>
</section>
<section className="container mx-auto max-w-7xl px-4 mt-6">
<Tabs defaultValue="devs">
<TabsList>
<TabsTrigger value="devs">Developers</TabsTrigger>
<TabsTrigger value="studios">Studios</TabsTrigger>
</TabsList>
<TabsContent value="devs">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{filteredDevs.map((u) => (
<Card key={u.id} className="border-border/40 bg-card/60 backdrop-blur">
<CardContent className="p-4 flex items-center gap-4">
<Avatar className="h-12 w-12">
<AvatarImage src={u.avatar_url || undefined} alt={u.full_name || u.username || "Developer"} />
<AvatarFallback>{initials(u.full_name || u.username)}</AvatarFallback>
</Avatar>
<div className="min-w-0">
<div className="font-medium truncate">{u.full_name || u.username || "Developer"}</div>
<div className="text-xs text-muted-foreground truncate">{u.location || "Global"}</div>
<div className="mt-1 flex flex-wrap gap-2">
<Badge variant="outline">{u.user_type}</Badge>
{u.experience_level && <Badge variant="outline">{u.experience_level}</Badge>}
</div>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="studios">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{filteredStudios.map((t) => (
<Card key={t.id} className="border-border/40 bg-card/60 backdrop-blur">
<CardHeader className="pb-2">
<CardTitle className="text-lg">{t.name}</CardTitle>
{t.visibility && <CardDescription className="capitalize">{t.visibility}</CardDescription>}
</CardHeader>
<CardContent className="pt-0">
<p className="text-sm text-muted-foreground line-clamp-3">{t.description || ""}</p>
</CardContent>
</Card>
))}
</div>
</TabsContent>
</Tabs>
</section>
</div>
</Layout>
);
}