feat: MicroDAO Dashboard Frontend - list + detail pages

This commit is contained in:
Apple
2025-11-28 01:39:27 -08:00
parent 467c7fc83d
commit 88375d441f
7 changed files with 609 additions and 1 deletions

View File

@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from "next/server";
const CITY_API_BASE_URL = process.env.CITY_API_BASE_URL || "http://localhost:7001";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params;
try {
const res = await fetch(
`${CITY_API_BASE_URL}/city/microdao/${encodeURIComponent(slug)}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
cache: "no-store",
}
);
const data = await res.json().catch(() => null);
return NextResponse.json(data, { status: res.status });
} catch (error) {
console.error("MicroDAO detail proxy error:", error);
return NextResponse.json(
{ error: "Failed to fetch MicroDAO detail" },
{ status: 502 }
);
}
}

View File

@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from "next/server";
const CITY_API_BASE_URL = process.env.CITY_API_BASE_URL || "http://localhost:7001";
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const district = searchParams.get("district");
const q = searchParams.get("q");
const url = new URL("/city/microdao", CITY_API_BASE_URL);
if (district) url.searchParams.set("district", district);
if (q) url.searchParams.set("q", q);
try {
const res = await fetch(url.toString(), {
method: "GET",
headers: { "Content-Type": "application/json" },
cache: "no-store",
});
const data = await res.json().catch(() => null);
return NextResponse.json(data, { status: res.status });
} catch (error) {
console.error("MicroDAO list proxy error:", error);
return NextResponse.json(
{ error: "Failed to fetch MicroDAO list" },
{ status: 502 }
);
}
}

View File

@@ -0,0 +1,279 @@
"use client";
import { useParams } from "next/navigation";
import Link from "next/link";
import { useMicrodaoDetail } from "@/hooks/useMicrodao";
import { DISTRICT_COLORS } from "@/lib/microdao";
export default function MicrodaoDetailPage() {
const params = useParams();
const slug = params?.slug as string;
const { microdao, isLoading, error } = useMicrodaoDetail(slug);
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 flex items-center justify-center">
<div className="animate-pulse text-slate-400">Завантаження...</div>
</div>
);
}
if (error || !microdao) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 flex items-center justify-center">
<div className="text-center space-y-4">
<div className="text-red-400">MicroDAO не знайдено</div>
<Link href="/microdao" className="text-sm text-cyan-400 hover:underline">
Повернутися до списку
</Link>
</div>
</div>
);
}
const orchestrator = microdao.agents.find(
(a) => a.agent_id === microdao.orchestrator_agent_id
);
const telegramChannels = microdao.channels.filter((c) => c.kind === "telegram");
const matrixChannels = microdao.channels.filter((c) => c.kind === "matrix");
const cityRooms = microdao.channels.filter((c) => c.kind === "city_room");
const crewChannels = microdao.channels.filter((c) => c.kind === "crew");
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
<div className="max-w-4xl mx-auto px-4 py-8 space-y-8">
{/* Back link */}
<Link
href="/microdao"
className="inline-flex items-center gap-2 text-sm text-slate-400 hover:text-cyan-400 transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Всі MicroDAO
</Link>
{/* Header */}
<header className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-2">
<div className="flex items-center gap-3">
{microdao.logo_url && (
<img
src={microdao.logo_url}
alt={microdao.name}
className="w-12 h-12 rounded-lg object-cover"
/>
)}
<div>
<h1 className="text-2xl font-bold text-slate-100">{microdao.name}</h1>
{microdao.description && (
<p className="text-sm text-slate-400 mt-1">{microdao.description}</p>
)}
</div>
</div>
<div className="flex flex-wrap gap-2">
{microdao.district && (
<span
className={`text-xs px-3 py-1 rounded-full border font-medium ${
DISTRICT_COLORS[microdao.district] ||
"bg-slate-500/10 text-slate-400 border-slate-500/30"
}`}
>
{microdao.district}
</span>
)}
<span
className={`text-xs px-3 py-1 rounded-full border font-medium ${
microdao.is_active
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/30"
: "bg-amber-500/10 text-amber-400 border-amber-500/30"
}`}
>
{microdao.is_active ? "Active" : "Inactive"}
</span>
{microdao.is_public && (
<span className="text-xs px-3 py-1 rounded-full border font-medium bg-blue-500/10 text-blue-400 border-blue-500/30">
Public
</span>
)}
</div>
</div>
{orchestrator && (
<div className="text-right">
<div className="text-xs text-slate-500 mb-1">Оркестратор</div>
<Link
href={`/agents/${orchestrator.agent_id}`}
className="text-sm font-medium text-cyan-400 hover:text-cyan-300 transition-colors"
>
{orchestrator.display_name}
</Link>
</div>
)}
</div>
</header>
{/* Agents */}
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<svg className="w-5 h-5 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
Агентська команда
</h2>
{microdao.agents.length === 0 ? (
<div className="text-sm text-slate-500">Агенти ще не привʼязані.</div>
) : (
<div className="grid gap-3 sm:grid-cols-2">
{microdao.agents.map((a) => (
<div
key={a.agent_id}
className="bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 flex items-center justify-between"
>
<div className="space-y-0.5">
<Link
href={`/agents/${a.agent_id}`}
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors"
>
{a.display_name}
</Link>
{a.role && (
<div className="text-xs text-slate-500 capitalize">{a.role}</div>
)}
</div>
{a.is_core && (
<span className="text-[10px] px-2 py-1 rounded-full bg-violet-500/10 text-violet-400 border border-violet-500/30 uppercase tracking-wide">
Core
</span>
)}
</div>
))}
</div>
)}
</section>
{/* Channels */}
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-6">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<svg className="w-5 h-5 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
Канали та кімнати
</h2>
{telegramChannels.length === 0 &&
matrixChannels.length === 0 &&
cityRooms.length === 0 &&
crewChannels.length === 0 ? (
<div className="text-sm text-slate-500">Канали ще не налаштовані.</div>
) : (
<div className="space-y-4">
{/* Telegram */}
{telegramChannels.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-slate-500 uppercase tracking-wide">Telegram</div>
<div className="flex flex-wrap gap-2">
{telegramChannels.map((c) => (
<a
key={c.ref_id}
href={`https://t.me/${c.ref_id.replace("@", "")}`}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-500/10 border border-blue-500/30 rounded-full text-sm text-blue-400 hover:bg-blue-500/20 transition-colors"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
{c.display_name || c.ref_id}
</a>
))}
</div>
</div>
)}
{/* Matrix */}
{matrixChannels.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-slate-500 uppercase tracking-wide">Matrix</div>
<div className="flex flex-wrap gap-2">
{matrixChannels.map((c) => (
<span
key={c.ref_id}
className="inline-flex items-center gap-2 px-4 py-2 bg-green-500/10 border border-green-500/30 rounded-full text-sm text-green-400"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.33-.439.576-.12.245-.18.567-.18.958v5.043H4.833V7.81zm13.086 15.64V.55h1.648V0H24v24h-2.28v-.55z"/>
</svg>
{c.display_name || c.ref_id}
</span>
))}
</div>
</div>
)}
{/* City Rooms */}
{cityRooms.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-slate-500 uppercase tracking-wide">Міські кімнати</div>
<div className="flex flex-wrap gap-2">
{cityRooms.map((c) => (
<Link
key={c.ref_id}
href={`/city/${c.ref_id}`}
className="inline-flex items-center gap-2 px-4 py-2 bg-violet-500/10 border border-violet-500/30 rounded-full text-sm text-violet-400 hover:bg-violet-500/20 transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
{c.display_name || c.ref_id}
</Link>
))}
</div>
</div>
)}
{/* CrewAI */}
{crewChannels.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-slate-500 uppercase tracking-wide">CrewAI сценарії</div>
<div className="flex flex-wrap gap-2">
{crewChannels.map((c) => (
<span
key={c.ref_id}
className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500/10 border border-orange-500/30 rounded-full text-sm text-orange-400"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
{c.display_name || c.ref_id}
</span>
))}
</div>
</div>
)}
</div>
)}
</section>
{/* Future: Stats & Tokens */}
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<svg className="w-5 h-5 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Статистика та токени
</h2>
<div className="text-sm text-slate-500">
Цей блок буде наповнено метриками MicroDAO (участь, транзакції, голосування),
коли буде готова токеноміка та governance-шар.
</div>
</section>
</div>
</div>
);
}

View File

@@ -0,0 +1,134 @@
"use client";
import { useState } from "react";
import { useMicrodaoList } from "@/hooks/useMicrodao";
import { DISTRICTS, DISTRICT_COLORS } from "@/lib/microdao";
import Link from "next/link";
export default function MicrodaoListPage() {
const [district, setDistrict] = useState<string | undefined>();
const [q, setQ] = useState("");
const { items, isLoading, error } = useMicrodaoList({ district, q: q || undefined });
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
<div className="max-w-6xl mx-auto px-4 py-8 space-y-8">
{/* Header */}
<header className="space-y-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-cyan-400 to-violet-400 bg-clip-text text-transparent">
MicroDAO
</h1>
<p className="text-sm text-slate-400 mt-1">
Кластери агентів і організацій у DAARION.city
</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<input
type="text"
placeholder="Пошук за назвою..."
value={q}
onChange={(e) => setQ(e.target.value)}
className="bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-2 text-sm text-slate-200 placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50"
/>
<select
value={district ?? ""}
onChange={(e) => setDistrict(e.target.value || undefined)}
className="bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-2 text-sm text-slate-200 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50"
>
<option value="">Всі дистрикти</option>
{DISTRICTS.map((d) => (
<option key={d} value={d}>
{d}
</option>
))}
</select>
</div>
</div>
</header>
{/* Content */}
{isLoading ? (
<div className="flex items-center justify-center py-20">
<div className="animate-pulse text-slate-400">Завантаження...</div>
</div>
) : error ? (
<div className="flex items-center justify-center py-20">
<div className="text-red-400">Помилка завантаження даних</div>
</div>
) : items.length === 0 ? (
<div className="flex items-center justify-center py-20">
<div className="text-slate-500">
{q || district ? "Нічого не знайдено" : "Поки що немає MicroDAO"}
</div>
</div>
) : (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{items.map((m) => (
<Link
key={m.id}
href={`/microdao/${m.slug}`}
className="group relative bg-slate-800/30 border border-slate-700/50 rounded-xl p-5 hover:border-cyan-500/30 hover:bg-slate-800/50 transition-all duration-300"
>
{/* Glow effect on hover */}
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-cyan-500/5 to-violet-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative space-y-3">
{/* Title + District */}
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<h2 className="font-semibold text-slate-100 group-hover:text-cyan-400 transition-colors">
{m.name}
</h2>
{m.description && (
<p className="text-xs text-slate-400 line-clamp-2">
{m.description}
</p>
)}
</div>
{m.district && (
<span
className={`shrink-0 text-[10px] px-2 py-1 rounded-full border font-medium ${
DISTRICT_COLORS[m.district] || "bg-slate-500/10 text-slate-400 border-slate-500/30"
}`}
>
{m.district}
</span>
)}
</div>
{/* Stats */}
<div className="flex items-center gap-4 text-xs text-slate-500">
<div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<span>{m.agents_count} агентів</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<span>{m.channels_count} каналів</span>
</div>
</div>
{/* Status indicator */}
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${m.is_active ? "bg-emerald-500" : "bg-amber-500"}`} />
<span className="text-[10px] text-slate-500 uppercase tracking-wide">
{m.is_active ? "Active" : "Inactive"}
</span>
</div>
</div>
</Link>
))}
</div>
)}
</div>
</div>
);
}

View File

@@ -3,16 +3,19 @@
import { useState } from 'react'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import { Menu, X, Home, Building2, User, Sparkles, Bot, Wallet, LogOut, Loader2 } from 'lucide-react'
import { Menu, X, Home, Building2, User, Sparkles, Bot, Wallet, LogOut, Loader2, Server, Users, Network } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useAuth } from '@/context/AuthContext'
const navItems = [
{ href: '/', label: 'Головна', icon: Home },
{ href: '/city', label: 'Місто', icon: Building2 },
{ href: '/citizens', label: 'Громадяни', icon: Users },
{ href: '/agents', label: 'Агенти', icon: Bot },
{ href: '/microdao', label: 'MicroDAO', icon: Network },
{ href: '/governance', label: 'DAO', icon: Wallet },
{ href: '/secondme', label: 'Second Me', icon: User },
{ href: '/nodes', label: 'Ноди', icon: Server },
]
export function Navigation() {

View File

@@ -0,0 +1,57 @@
import useSWR from "swr";
import type { MicrodaoSummary, MicrodaoDetail } from "@/lib/microdao";
const fetcher = (url: string) =>
fetch(url).then(async (res) => {
if (!res.ok) {
const body = await res.json().catch(() => null);
const error = new Error(body?.error || "Failed to fetch");
(error as any).status = res.status;
throw error;
}
return res.json();
});
export function useMicrodaoList(params?: { district?: string; q?: string }) {
const search = new URLSearchParams();
if (params?.district) search.set("district", params.district);
if (params?.q) search.set("q", params.q);
const key = `/api/microdao${search.toString() ? `?${search.toString()}` : ""}`;
const { data, error, isLoading, mutate } = useSWR<MicrodaoSummary[]>(
key,
fetcher,
{
refreshInterval: 60_000, // Refresh every minute
revalidateOnFocus: true,
}
);
return {
items: data ?? [],
total: data?.length ?? 0,
isLoading,
error,
mutate,
};
}
export function useMicrodaoDetail(slug: string | undefined) {
const { data, error, isLoading, mutate } = useSWR<MicrodaoDetail>(
slug ? `/api/microdao/${encodeURIComponent(slug)}` : null,
fetcher,
{
refreshInterval: 30_000,
revalidateOnFocus: true,
}
);
return {
microdao: data,
isLoading,
error,
mutate,
};
}

View File

@@ -0,0 +1,73 @@
// =============================================================================
// MicroDAO Types
// =============================================================================
export interface MicrodaoSummary {
id: string;
slug: string;
name: string;
description?: string | null;
district?: string | null;
orchestrator_agent_id: string;
is_active: boolean;
logo_url?: string | null;
agents_count: number;
rooms_count: number;
channels_count: number;
}
export interface MicrodaoChannel {
kind: "matrix" | "telegram" | "city_room" | "crew" | string;
ref_id: string;
display_name?: string | null;
is_primary: boolean;
}
export interface MicrodaoAgent {
agent_id: string;
display_name: string;
role?: string | null;
is_core: boolean;
}
export interface MicrodaoDetail {
id: string;
slug: string;
name: string;
description?: string | null;
district?: string | null;
orchestrator_agent_id: string;
orchestrator_display_name?: string | null;
is_active: boolean;
is_public: boolean;
logo_url?: string | null;
agents: MicrodaoAgent[];
channels: MicrodaoChannel[];
}
// =============================================================================
// District colors for UI
// =============================================================================
export const DISTRICT_COLORS: Record<string, string> = {
Core: "bg-violet-500/10 text-violet-400 border-violet-500/30",
Energy: "bg-amber-500/10 text-amber-400 border-amber-500/30",
Green: "bg-emerald-500/10 text-emerald-400 border-emerald-500/30",
Clan: "bg-blue-500/10 text-blue-400 border-blue-500/30",
Soul: "bg-purple-500/10 text-purple-400 border-purple-500/30",
Council: "bg-rose-500/10 text-rose-400 border-rose-500/30",
Labs: "bg-cyan-500/10 text-cyan-400 border-cyan-500/30",
Creators: "bg-orange-500/10 text-orange-400 border-orange-500/30",
};
export const DISTRICTS = [
"Core",
"Energy",
"Green",
"Clan",
"Soul",
"Council",
"Labs",
"Creators",
];