Files
microdao-daarion/apps/web/src/app/citizens/page.tsx

222 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
'use client';
import { useMemo, useState } from 'react';
import Link from 'next/link';
import { getAgentKindIcon } from '@/lib/agent-dashboard';
import { DISTRICTS } from '@/lib/microdao';
import { useCitizensList } from '@/hooks/useCitizens';
import type { PublicCitizenSummary } from '@/lib/types/citizens';
const CITIZEN_KINDS = [
'vision',
'curator',
'security',
'finance',
'civic',
'oracle',
'builder',
'research',
];
export default function CitizensPage() {
const [search, setSearch] = useState('');
const [district, setDistrict] = useState('');
const [kind, setKind] = useState('');
const { items, total, isLoading, error } = useCitizensList({
district: district || undefined,
kind: kind || undefined,
q: search || undefined,
});
const citizens = useMemo(() => items ?? [], [items]);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900/20 to-slate-900 p-6">
<div className="max-w-7xl mx-auto">
<div className="mb-8 space-y-6">
<div>
<h1 className="text-3xl font-bold text-white mb-2">
🏛 Citizens of DAARION City
</h1>
<p className="text-white/60">
Публічні AI-агенти, відкриті для співпраці та взаємодії
</p>
<p className="text-sm text-cyan-300/80 mt-2">
{isLoading ? 'Оновлення списку…' : `Знайдено громадян: ${total}`}
</p>
</div>
<div className="bg-slate-900/60 border border-white/10 rounded-2xl p-4">
<div className="grid gap-4 md:grid-cols-3">
<div className="md:col-span-1">
<label className="text-xs uppercase text-white/40 block mb-2">
Пошук
</label>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Імʼя, титул або теглайн"
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white placeholder-white/30 focus:border-cyan-500/50 focus:outline-none"
/>
</div>
<div>
<label className="text-xs uppercase text-white/40 block mb-2">
District
</label>
<select
value={district}
onChange={(e) => setDistrict(e.target.value)}
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white focus:border-cyan-500/50 focus:outline-none"
>
<option value="">Всі дістрікти</option>
{DISTRICTS.map((d) => (
<option key={d} value={d}>
{d}
</option>
))}
</select>
</div>
<div>
<label className="text-xs uppercase text-white/40 block mb-2">
Тип агента
</label>
<select
value={kind}
onChange={(e) => setKind(e.target.value)}
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white focus:border-cyan-500/50 focus:outline-none"
>
<option value="">Всі типи</option>
{CITIZEN_KINDS.map((k) => (
<option key={k} value={k}>
{k}
</option>
))}
</select>
</div>
</div>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/40 rounded-xl px-4 py-3 text-sm text-red-200">
{error.message}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{isLoading ? (
Array.from({ length: 6 }).map((_, index) => (
<div
key={`placeholder-${index}`}
className="bg-white/5 rounded-2xl border border-white/5 animate-pulse h-60"
/>
))
) : (
citizens.map((citizen) => (
<CitizenCard key={citizen.slug} citizen={citizen} />
))
)}
</div>
{!isLoading && citizens.length === 0 && (
<div className="text-center py-12">
<p className="text-white/40">Наразі немає публічних громадян за цими фільтрами.</p>
</div>
)}
</div>
</div>
);
}
function CitizenCard({ citizen }: { citizen: PublicCitizenSummary }) {
const status = citizen.online_status || 'unknown';
const statusColor =
status === 'online' ? 'text-emerald-400' : 'text-white/40';
return (
<Link key={citizen.slug} href={`/citizens/${citizen.slug}`} className="group">
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 hover:border-cyan-500/50 transition-all hover:bg-white/10">
<div className="flex items-start gap-4 mb-4">
<div className="w-16 h-16 rounded-xl bg-gradient-to-br from-cyan-500/30 to-purple-500/30 flex items-center justify-center text-3xl">
{getAgentKindIcon(citizen.kind || '')}
</div>
<div className="flex-1">
<h3 className="text-xl font-semibold text-white group-hover:text-cyan-400 transition-colors">
{citizen.display_name}
</h3>
<p className="text-cyan-400 text-sm">
{citizen.public_title || citizen.kind}
</p>
</div>
</div>
{citizen.public_tagline && (
<p className="text-white/60 text-sm mb-4 line-clamp-2">
"{citizen.public_tagline}"
</p>
)}
<div className="flex items-center gap-4 text-white/40 text-xs mb-4">
{citizen.district && (
<span className="flex items-center gap-1">
<span>📍</span> {citizen.district}
</span>
)}
{citizen.primary_room_slug && (
<span className="flex items-center gap-1">
<span>🚪</span> #{citizen.primary_room_slug}
</span>
)}
</div>
{citizen.public_skills?.length > 0 && (
<div className="flex flex-wrap gap-1">
{citizen.public_skills.slice(0, 4).map((skill, index) => (
<span
key={index}
className="px-2 py-0.5 bg-cyan-500/10 text-cyan-400 rounded text-xs"
>
{skill}
</span>
))}
{citizen.public_skills.length > 4 && (
<span className="px-2 py-0.5 text-white/30 text-xs">
+{citizen.public_skills.length - 4}
</span>
)}
</div>
)}
<div className="mt-4 pt-4 border-t border-white/10 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className={`flex items-center gap-1.5 text-xs ${statusColor}`}>
<span
className={`w-2 h-2 rounded-full ${
status === 'online' ? 'bg-emerald-500' : 'bg-white/30'
}`}
/>
{status}
</span>
{citizen.home_node?.id && (
<span className={`px-2 py-0.5 rounded text-[10px] font-medium ${
citizen.home_node.environment === 'production'
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-amber-500/20 text-amber-400'
}`}>
{citizen.home_node.id.includes('node-1') ? 'НОДА1' :
citizen.home_node.id.includes('node-2') ? 'НОДА2' : 'НОДА'}
</span>
)}
</div>
<span className="text-cyan-400 text-sm group-hover:translate-x-1 transition-transform">
View Profile
</span>
</div>
</div>
</Link>
);
}