Files
microdao-daarion/src/features/microdao/MicrodaoListPage.tsx
Apple a6e531a098 fix: NODE1_REPAIR - healthchecks, dependencies, SSR env, telegram gateway
TASK_PHASE_NODE1_REPAIR:
- Fix daarion-web SSR: use CITY_API_BASE_URL instead of 127.0.0.1
- Fix auth API routes: use AUTH_API_URL env var
- Add wget to Dockerfiles for healthchecks (stt, ocr, web-search, swapper, vector-db, rag)
- Update healthchecks to use wget instead of curl
- Fix vector-db-service: update torch==2.4.0, sentence-transformers==2.6.1
- Fix rag-service: correct haystack imports for v2.x
- Fix telegram-gateway: remove msg.ack() for non-JetStream NATS
- Add /health endpoint to nginx mvp-routes.conf
- Add room_role, is_public, sort_order columns to city_rooms migration
- Add TASK_PHASE_NODE1_REPAIR.md and DEPLOY_NODE1_REPAIR.md docs

Previous tasks included:
- TASK 039-044: Orchestrator rooms, Matrix chat cleanup, CrewAI integration
2025-11-29 05:17:08 -08:00

257 lines
9.5 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.
/**
* MicrodaoListPage Component
* Phase 7: List of user's microDAOs
*/
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMicrodaos } from './hooks/useMicrodaos';
import { createMicrodao, type MicrodaoCreate } from '@/api/microdao';
import { MicrodaoBrandBadge } from './components/MicrodaoBrandBadge';
export function MicrodaoListPage() {
const navigate = useNavigate();
const { microdaos, loading, error, refetch } = useMicrodaos();
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [creating, setCreating] = useState(false);
const [createError, setCreateError] = useState<string | null>(null);
const [newName, setNewName] = useState('');
const [newSlug, setNewSlug] = useState('');
const [newDescription, setNewDescription] = useState('');
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
try {
setCreating(true);
setCreateError(null);
const data: MicrodaoCreate = {
name: newName,
slug: newSlug,
description: newDescription || undefined,
};
const microdao = await createMicrodao(data);
// Success
setCreateDialogOpen(false);
setNewName('');
setNewSlug('');
setNewDescription('');
refetch();
// Navigate to new microDAO
navigate(`/microdao/${microdao.slug}`);
} catch (err) {
console.error('Failed to create microDAO:', err);
setCreateError((err as Error).message);
} finally {
setCreating(false);
}
};
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">
🏛 Мої microDAO
</h1>
<p className="text-gray-600 mt-1">
Керуйте вашими спільнотами та організаціями
</p>
</div>
<button
onClick={() => setCreateDialogOpen(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Створити microDAO
</button>
</div>
</div>
</div>
{/* Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Loading */}
{loading && (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" />
<div className="text-gray-600">Завантаження...</div>
</div>
)}
{/* Error */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
<div className="text-red-600 mb-2"> Помилка завантаження</div>
<div className="text-sm text-red-500">{error.message}</div>
</div>
)}
{/* Empty */}
{!loading && !error && microdaos.length === 0 && (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-12 text-center">
<div className="text-6xl mb-4">🏛</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">
Немає microDAO
</h3>
<p className="text-gray-600 mb-4">
Створіть ваше перше microDAO
</p>
<button
onClick={() => setCreateDialogOpen(true)}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Створити microDAO
</button>
</div>
)}
{/* List */}
{!loading && microdaos.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{microdaos.map((dao) => (
<div
key={dao.id}
onClick={() => navigate(`/microdao/${dao.slug}`)}
className="bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-lg hover:border-blue-500 transition-all cursor-pointer group relative"
>
{/* Banner Background */}
{dao.banner_url && (
<div
className="absolute inset-0 h-32 bg-cover bg-center opacity-10 group-hover:opacity-20 transition-opacity"
style={{ backgroundImage: `url(${dao.banner_url})` }}
/>
)}
<div className="p-6 relative z-10">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<MicrodaoBrandBadge name={dao.name} logoUrl={dao.logo_url} size="md" />
<div>
<h3 className="text-xl font-semibold text-gray-900">
{dao.name}
</h3>
<p className="text-xs text-gray-500">@{dao.slug}</p>
</div>
</div>
</div>
{dao.description && (
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
{dao.description}
</p>
)}
<div className="flex items-center gap-4 text-sm text-gray-500 mt-auto">
<div className="flex items-center gap-1">
<span>👥</span> {dao.member_count || 0}
</div>
<div className="flex items-center gap-1">
<span>🤖</span> {dao.agent_count || 0}
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Create Dialog */}
{createDialogOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">
Створити microDAO
</h2>
<button
onClick={() => setCreateDialogOpen(false)}
className="text-gray-400 hover:text-gray-600 text-2xl"
>
×
</button>
</div>
<form onSubmit={handleCreate} className="p-6 space-y-4">
{createError && (
<div className="bg-red-50 border border-red-200 rounded p-3 text-red-700 text-sm">
{createError}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Назва *
</label>
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
required
placeholder="DAARION Core"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Slug (URL) *
</label>
<input
type="text"
value={newSlug}
onChange={(e) => setNewSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
required
placeholder="daarion-core"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Опис
</label>
<textarea
value={newDescription}
onChange={(e) => setNewDescription(e.target.value)}
rows={3}
placeholder="Короткий опис microDAO..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex justify-end gap-3 pt-4">
<button
type="button"
onClick={() => setCreateDialogOpen(false)}
disabled={creating}
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50"
>
Скасувати
</button>
<button
type="submit"
disabled={creating || !newName || !newSlug}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-300"
>
{creating ? 'Створення...' : 'Створити'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}