feat: add Console UI for MicroDAO management

- Create ConsolePage with navigation
- Add WalletInfo component (balance display and access checks)
- Add CreateMicroDaoForm (with balance validation)
- Add MicroDaoList component (display teams/MicroDAO)
- Add InviteMemberForm (with balance checks for admin/member)
- Add wallet API client
- Update teams API with inviteMember function
- Add /console route to App.tsx
This commit is contained in:
Apple
2025-11-15 09:00:59 -08:00
parent 582ab75b03
commit a54a7b078c
9 changed files with 834 additions and 4 deletions

137
CONSOLE_UI_SUMMARY.md Normal file
View File

@@ -0,0 +1,137 @@
# Console UI - Підсумок реалізації
## ✅ Що створено
### 1. Сторінка Console (`/console`)
- **Файл:** `src/pages/ConsolePage.tsx`
- **Маршрут:** `/console`
- **Функціонал:**
- Навігація між списком MicroDAO та створенням
- Відображення Wallet інформації
- Управління MicroDAO
### 2. Компоненти Console
#### WalletInfo
- **Файл:** `src/components/console/WalletInfo.tsx`
- **Функціонал:**
- Відображення балансів DAARION та DAAR
- Перевірка можливості створення MicroDAO (≥ 1.00 DAARION)
- Перевірка ролі Admin (≥ 1.00 DAARION)
- Перевірка можливості використання сервісу (≥ 0.01 DAARION)
#### CreateMicroDaoForm
- **Файл:** `src/components/console/CreateMicroDaoForm.tsx`
- **Функціонал:**
- Форма створення MicroDAO
- Автоматична перевірка балансу перед створенням
- Генерація slug з назви
- Вибір типу (community, guild, lab, personal)
- Вибір режиму (public, confidential)
#### MicroDaoList
- **Файл:** `src/components/console/MicroDaoList.tsx`
- **Функціонал:**
- Відображення списку MicroDAO
- Відображення типу та режиму
- Позначка для DAARION.city (type='city')
- Можливість вибору MicroDAO для запрошення
#### InviteMemberForm
- **Файл:** `src/components/console/InviteMemberForm.tsx`
- **Функціонал:**
- Форма запрошення користувача
- Перевірка балансу Admin (≥ 1.00 DAARION)
- Вибір ролі (admin/member)
- Відображення вимог до балансу запрошеного користувача
### 3. API функції
#### Wallet API
- **Файл:** `src/api/wallet.ts`
- **Функції:**
- `getBalances()` - отримання балансів користувача
#### Teams API (оновлено)
- **Файл:** `src/api/teams.ts`
- **Додано:**
- `inviteMember()` - запрошення користувача в MicroDAO
- Оновлено URL endpoints на `/api/v1/teams`
---
## 🎨 UI/UX Особливості
### Дизайн
- Використовує Tailwind CSS
- Адаптивний layout (grid на великих екранах)
- Кольорові індикатори статусу балансу
- Інформативні повідомлення про помилки
### Валідація
- Перевірка балансу перед створенням MicroDAO
- Перевірка балансу перед запрошенням
- Валідація форми (обов'язкові поля, email формат)
- Автоматична генерація slug
### Користувацький досвід
- Чіткі індикатори можливостей (✓/✗)
- Пояснення вимог до балансу
- Можливість оновлення балансу
- Навігація між різними режимами
---
## 📋 Правила доступу (відображені в UI)
### Створення MicroDAO
- **Потрібно:** ≥ 1.00 DAARION на балансі
- **Відображення:** Зелений індикатор в WalletInfo та CreateMicroDaoForm
### Роль Admin
- **Потрібно:** ≥ 1.00 DAARION на балансі
- **Відображення:** Зелений індикатор в WalletInfo
### Запрошення користувача
- **Admin потрібно:** ≥ 1.00 DAARION на балансі
- **Запрошений Admin:** ≥ 1.00 DAARION на балансі
- **Запрошений Member:** ≥ 0.01 DAARION на балансі
- **Відображення:** Індикатори в InviteMemberForm
### Використання сервісу
- **Потрібно:** ≥ 0.01 DAARION на балансі
- **Відображення:** Зелений індикатор в WalletInfo
---
## 🔗 Інтеграція
### Маршрути
- `/console` - головна сторінка Console
- Додано в `src/App.tsx`
### API Endpoints
- `GET /api/v1/wallet/balances` - отримання балансів
- `GET /api/v1/teams` - список MicroDAO
- `POST /api/v1/teams` - створення MicroDAO
- `POST /api/v1/teams/:teamId/members` - запрошення користувача
---
## 🚀 Наступні кроки
### Backend
- [ ] Реалізувати реальну інтеграцію з БД для teams
- [ ] Реалізувати отримання user_id з email при запрошенні
- [ ] Додати створення team_member record при запрошенні
### Frontend
- [ ] Додати оновлення списку MicroDAO після створення
- [ ] Додати детальну сторінку MicroDAO
- [ ] Додати управління налаштуваннями MicroDAO
- [ ] Додати відображення членів MicroDAO
---
**Останнє оновлення:** 2024-11-14

View File

@@ -1,11 +1,13 @@
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { OnboardingPage } from './pages/OnboardingPage';
import { ConsolePage } from './pages/ConsolePage';
function App() {
return (
<Routes>
<Route path="/onboarding" element={<OnboardingPage />} />
<Route path="/console" element={<ConsolePage />} />
<Route path="/" element={<div>Home - Coming soon</div>} />
</Routes>
);

View File

@@ -2,18 +2,35 @@ import { apiGet, apiPost, apiPatch } from './client';
import type { Team, CreateTeamRequest, UpdateTeamRequest } from '../types/api';
export async function createTeam(data: CreateTeamRequest): Promise<Team> {
return apiPost<Team>('/teams', data);
return apiPost<Team>('/api/v1/teams', data);
}
export async function getTeams(): Promise<{ teams: Team[] }> {
return apiGet<{ teams: Team[] }>('/teams');
return apiGet<{ teams: Team[] }>('/api/v1/teams');
}
export async function getTeam(teamId: string): Promise<Team> {
return apiGet<Team>(`/teams/${teamId}`);
return apiGet<Team>(`/api/v1/teams/${teamId}`);
}
export async function updateTeam(teamId: string, data: UpdateTeamRequest): Promise<Team> {
return apiPatch<Team>(`/teams/${teamId}`, data);
return apiPatch<Team>(`/api/v1/teams/${teamId}`, data);
}
export interface InviteMemberRequest {
email: string;
role: 'admin' | 'member';
}
export interface InviteMemberResponse {
team_id: string;
user_id: string;
email: string;
role: string;
status: string;
}
export async function inviteMember(teamId: string, data: InviteMemberRequest): Promise<InviteMemberResponse> {
return apiPost<InviteMemberResponse>(`/api/v1/teams/${teamId}/members`, data);
}

11
src/api/wallet.ts Normal file
View File

@@ -0,0 +1,11 @@
import { apiGet } from './client';
import type { Balance } from '../domain/wallet/types';
export interface WalletBalancesResponse {
balances: Balance[];
}
export async function getBalances(): Promise<WalletBalancesResponse> {
return apiGet<WalletBalancesResponse>('/api/v1/wallet/balances');
}

View File

@@ -0,0 +1,204 @@
import React, { useState, useEffect } from 'react';
import { createTeam } from '../../api/teams';
import { getBalances } from '../../api/wallet';
import type { Team, CreateTeamRequest } from '../../types/api';
import type { Balance } from '../../domain/wallet/types';
interface CreateMicroDaoFormProps {
onSuccess?: (team: Team) => void;
onCancel?: () => void;
}
export function CreateMicroDaoForm({ onSuccess, onCancel }: CreateMicroDaoFormProps) {
const [name, setName] = useState('');
const [slug, setSlug] = useState('');
const [description, setDescription] = useState('');
const [mode, setMode] = useState<'public' | 'confidential'>('public');
const [type, setType] = useState<'community' | 'guild' | 'lab' | 'personal'>('community');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [canCreate, setCanCreate] = useState(false);
const [daarionBalance, setDaarionBalance] = useState<number>(0);
useEffect(() => {
checkBalance();
}, []);
const checkBalance = async () => {
try {
const data = await getBalances();
const daarion = data.balances?.find(b => b.symbol === 'DAARION');
const balance = daarion ? parseFloat(daarion.amount) : 0;
setDaarionBalance(balance);
setCanCreate(balance >= 1.0);
} catch (err: any) {
setError('Помилка перевірки балансу: ' + err.message);
}
};
const generateSlug = (name: string) => {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newName = e.target.value;
setName(newName);
if (!slug || slug === generateSlug(name)) {
setSlug(generateSlug(newName));
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!canCreate) {
setError('Недостатньо DAARION на балансі. Потрібно мінімум 1.00 DAARION');
return;
}
setLoading(true);
setError(null);
try {
const request: CreateTeamRequest = {
name,
slug: slug || generateSlug(name),
description: description || undefined,
mode,
type,
};
const team = await createTeam(request);
onSuccess?.(team);
} catch (err: any) {
setError(err.message || 'Помилка створення MicroDAO');
} finally {
setLoading(false);
}
};
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Створити MicroDAO</h2>
{/* Balance Check */}
<div className={`mb-4 p-3 rounded ${canCreate ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}>
<div className="flex items-center gap-2">
<span className={canCreate ? 'text-green-600' : 'text-red-600'}>
{canCreate ? '✓' : '✗'}
</span>
<span className="text-sm">
Баланс DAARION: <strong>{daarionBalance.toFixed(2)}</strong>
{!canCreate && ' (потрібно ≥ 1.00)'}
</span>
</div>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Назва MicroDAO *
</label>
<input
type="text"
value={name}
onChange={handleNameChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Моя спільнота"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Slug (URL) *
</label>
<input
type="text"
value={slug}
onChange={(e) => setSlug(e.target.value)}
required
pattern="[a-z0-9-]+"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="my-community"
/>
<p className="mt-1 text-xs text-gray-500">
Тільки маленькі літери, цифри та дефіси
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Опис
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Опис вашої спільноти..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Тип
</label>
<select
value={type}
onChange={(e) => setType(e.target.value as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="community">Спільнота</option>
<option value="guild">Гільдія</option>
<option value="lab">Лабораторія</option>
<option value="personal">Особисте</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Режим
</label>
<select
value={mode}
onChange={(e) => setMode(e.target.value as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="public">Публічний</option>
<option value="confidential">Конфіденційний</option>
</select>
</div>
<div className="flex gap-3 pt-4">
<button
type="submit"
disabled={loading || !canCreate}
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Створення...' : 'Створити MicroDAO'}
</button>
{onCancel && (
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
Скасувати
</button>
)}
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,148 @@
import React, { useState, useEffect } from 'react';
import { getBalances } from '../../api/wallet';
import { inviteMember } from '../../api/teams';
import type { Team } from '../../types/api';
interface InviteMemberFormProps {
team: Team;
onSuccess?: () => void;
onCancel?: () => void;
}
export function InviteMemberForm({ team, onSuccess, onCancel }: InviteMemberFormProps) {
const [email, setEmail] = useState('');
const [role, setRole] = useState<'admin' | 'member'>('member');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [canInvite, setCanInvite] = useState(false);
const [daarionBalance, setDaarionBalance] = useState<number>(0);
useEffect(() => {
checkBalance();
}, []);
const checkBalance = async () => {
try {
const data = await getBalances();
const daarion = data.balances?.find(b => b.symbol === 'DAARION');
const balance = daarion ? parseFloat(daarion.amount) : 0;
setDaarionBalance(balance);
setCanInvite(balance >= 1.0); // Admin needs 1 DAARION to invite
} catch (err: any) {
setError('Помилка перевірки балансу: ' + err.message);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!canInvite) {
setError('Недостатньо DAARION на балансі. Потрібно мінімум 1.00 DAARION для запрошення');
return;
}
setLoading(true);
setError(null);
try {
await inviteMember(team.id, { email, role });
setEmail('');
onSuccess?.();
} catch (err: any) {
setError(err.message || 'Помилка запрошення користувача');
} finally {
setLoading(false);
}
};
const requiredBalance = role === 'admin' ? 1.0 : 0.01;
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">
Запросити користувача в {team.name}
</h2>
{/* Balance Check */}
<div className={`mb-4 p-3 rounded ${canInvite ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}>
<div className="flex items-center gap-2 mb-2">
<span className={canInvite ? 'text-green-600' : 'text-red-600'}>
{canInvite ? '✓' : '✗'}
</span>
<span className="text-sm">
Ваш баланс DAARION: <strong>{daarionBalance.toFixed(2)}</strong>
{!canInvite && ' (потрібно ≥ 1.00 для запрошення)'}
</span>
</div>
<div className="text-xs text-gray-600 mt-1">
Запрошений користувач має мати:
<strong className="ml-1">
{role === 'admin' ? '≥ 1.00 DAARION' : '≥ 0.01 DAARION'}
</strong>
</div>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email користувача *
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="user@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Роль *
</label>
<select
value={role}
onChange={(e) => setRole(e.target.value as 'admin' | 'member')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="member">Member (потрібно 0.01 DAARION)</option>
<option value="admin">Admin (потрібно 1.00 DAARION)</option>
</select>
<p className="mt-1 text-xs text-gray-500">
{role === 'admin'
? 'Admin може запрошувати інших користувачів та керувати MicroDAO'
: 'Member може використовувати функції MicroDAO'}
</p>
</div>
<div className="flex gap-3 pt-4">
<button
type="submit"
disabled={loading || !canInvite}
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Запрошення...' : 'Запросити'}
</button>
{onCancel && (
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
Скасувати
</button>
)}
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,114 @@
import React, { useEffect, useState } from 'react';
import { getTeams } from '../../api/teams';
import type { Team } from '../../types/api';
interface MicroDaoListProps {
onSelectTeam?: (team: Team) => void;
}
export function MicroDaoList({ onSelectTeam }: MicroDaoListProps) {
const [teams, setTeams] = useState<Team[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadTeams();
}, []);
const loadTeams = async () => {
try {
setLoading(true);
const data = await getTeams();
setTeams(data.teams || []);
setError(null);
} catch (err: any) {
setError(err.message || 'Помилка завантаження списку MicroDAO');
} finally {
setLoading(false);
}
};
const getTypeLabel = (type?: string) => {
const labels: Record<string, string> = {
city: 'Місто',
platform: 'Платформа',
community: 'Спільнота',
guild: 'Гільдія',
lab: 'Лабораторія',
personal: 'Особисте',
};
return labels[type || 'community'] || 'Спільнота';
};
const getModeLabel = (mode: string) => {
return mode === 'public' ? 'Публічний' : 'Конфіденційний';
};
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Мої MicroDAO</h2>
<button
onClick={loadTeams}
className="text-sm text-blue-600 hover:text-blue-800"
>
Оновити
</button>
</div>
{loading && <div className="text-gray-500">Завантаження...</div>}
{error && <div className="text-red-500 mb-4">{error}</div>}
{!loading && !error && (
<>
{teams.length === 0 ? (
<div className="text-gray-500 text-center py-8">
<p>У вас ще немає MicroDAO</p>
<p className="text-sm mt-2">Створіть перше MicroDAO, щоб почати</p>
</div>
) : (
<div className="space-y-3">
{teams.map((team) => (
<div
key={team.id}
onClick={() => onSelectTeam?.(team)}
className={`p-4 border rounded-lg cursor-pointer hover:bg-gray-50 transition-colors ${
onSelectTeam ? 'cursor-pointer' : ''
}`}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<h3 className="font-semibold text-lg">{team.name}</h3>
{team.description && (
<p className="text-sm text-gray-600 mt-1">{team.description}</p>
)}
<div className="flex gap-3 mt-2 text-xs text-gray-500">
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded">
{getTypeLabel(team.type)}
</span>
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded">
{getModeLabel(team.mode)}
</span>
{team.slug && (
<span className="px-2 py-1 bg-green-100 text-green-700 rounded">
{team.slug}
</span>
)}
</div>
</div>
{team.type === 'city' && (
<span className="ml-4 px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-xs font-medium">
DAARION.city
</span>
)}
</div>
</div>
))}
</div>
)}
</>
)}
</div>
);
}

View File

@@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react';
import { getBalances } from '../../api/wallet';
import type { Balance } from '../../domain/wallet/types';
export function WalletInfo() {
const [balances, setBalances] = useState<Balance[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadBalances();
}, []);
const loadBalances = async () => {
try {
setLoading(true);
const data = await getBalances();
setBalances(data.balances || []);
setError(null);
} catch (err: any) {
setError(err.message || 'Помилка завантаження балансу');
} finally {
setLoading(false);
}
};
const daarionBalance = balances.find(b => b.symbol === 'DAARION');
const daarBalance = balances.find(b => b.symbol === 'DAAR');
const canCreateMicroDao = daarionBalance && parseFloat(daarionBalance.amount) >= 1.0;
const canUseMicroDao = daarionBalance && parseFloat(daarionBalance.amount) >= 0.01;
const isAdmin = canCreateMicroDao;
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Wallet</h2>
{loading && <div className="text-gray-500">Завантаження...</div>}
{error && <div className="text-red-500 mb-4">{error}</div>}
{!loading && !error && (
<div className="space-y-4">
{/* DAARION Balance */}
<div className="border-b pb-4">
<div className="flex justify-between items-center mb-2">
<span className="text-gray-600">DAARION</span>
<span className="text-2xl font-bold">
{daarionBalance ? parseFloat(daarionBalance.amount).toFixed(2) : '0.00'}
</span>
</div>
<div className="text-sm text-gray-500 space-y-1">
<div className={`flex items-center gap-2 ${canCreateMicroDao ? 'text-green-600' : 'text-gray-400'}`}>
{canCreateMicroDao ? '✓' : '○'} Можна створити MicroDAO (потрібно 1.00)
</div>
<div className={`flex items-center gap-2 ${isAdmin ? 'text-green-600' : 'text-gray-400'}`}>
{isAdmin ? '✓' : '○'} Роль Admin (потрібно 1.00)
</div>
<div className={`flex items-center gap-2 ${canUseMicroDao ? 'text-green-600' : 'text-gray-400'}`}>
{canUseMicroDao ? '✓' : '○'} Можна використовувати сервіс (потрібно 0.01)
</div>
</div>
</div>
{/* DAAR Balance */}
{daarBalance && (
<div>
<div className="flex justify-between items-center">
<span className="text-gray-600">DAAR</span>
<span className="text-xl font-semibold">
{parseFloat(daarBalance.amount).toFixed(2)}
</span>
</div>
</div>
)}
{!daarionBalance && !daarBalance && (
<div className="text-gray-500 text-sm">Немає токенів на балансі</div>
)}
</div>
)}
<button
onClick={loadBalances}
className="mt-4 text-sm text-blue-600 hover:text-blue-800"
>
Оновити баланс
</button>
</div>
);
}

106
src/pages/ConsolePage.tsx Normal file
View File

@@ -0,0 +1,106 @@
import React, { useState } from 'react';
import { WalletInfo } from '../components/console/WalletInfo';
import { CreateMicroDaoForm } from '../components/console/CreateMicroDaoForm';
import { MicroDaoList } from '../components/console/MicroDaoList';
import { InviteMemberForm } from '../components/console/InviteMemberForm';
import type { Team } from '../types/api';
type View = 'list' | 'create' | 'invite';
export function ConsolePage() {
const [currentView, setCurrentView] = useState<View>('list');
const [selectedTeam, setSelectedTeam] = useState<Team | null>(null);
const handleCreateSuccess = (team: Team) => {
setSelectedTeam(null);
setCurrentView('list');
// TODO: Refresh teams list
};
const handleInviteSuccess = () => {
setSelectedTeam(null);
setCurrentView('list');
};
const handleSelectTeam = (team: Team) => {
setSelectedTeam(team);
setCurrentView('invite');
};
return (
<div className="min-h-screen bg-slate-50">
<div className="container mx-auto px-4 py-8">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900">Console</h1>
<p className="text-gray-600 mt-2">Управління MicroDAO та DAARION.city</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Wallet Info */}
<div className="lg:col-span-1">
<WalletInfo />
</div>
{/* Right Column - Main Content */}
<div className="lg:col-span-2 space-y-6">
{/* Navigation */}
<div className="bg-white rounded-lg shadow p-4">
<div className="flex gap-3">
<button
onClick={() => {
setCurrentView('list');
setSelectedTeam(null);
}}
className={`px-4 py-2 rounded-md transition-colors ${
currentView === 'list'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Список MicroDAO
</button>
<button
onClick={() => {
setCurrentView('create');
setSelectedTeam(null);
}}
className={`px-4 py-2 rounded-md transition-colors ${
currentView === 'create'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Створити MicroDAO
</button>
</div>
</div>
{/* Content */}
{currentView === 'list' && (
<MicroDaoList onSelectTeam={handleSelectTeam} />
)}
{currentView === 'create' && (
<CreateMicroDaoForm
onSuccess={handleCreateSuccess}
onCancel={() => setCurrentView('list')}
/>
)}
{currentView === 'invite' && selectedTeam && (
<InviteMemberForm
team={selectedTeam}
onSuccess={handleInviteSuccess}
onCancel={() => {
setSelectedTeam(null);
setCurrentView('list');
}}
/>
)}
</div>
</div>
</div>
</div>
);
}