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:
137
CONSOLE_UI_SUMMARY.md
Normal file
137
CONSOLE_UI_SUMMARY.md
Normal 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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
11
src/api/wallet.ts
Normal 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');
|
||||
}
|
||||
|
||||
204
src/components/console/CreateMicroDaoForm.tsx
Normal file
204
src/components/console/CreateMicroDaoForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
148
src/components/console/InviteMemberForm.tsx
Normal file
148
src/components/console/InviteMemberForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
114
src/components/console/MicroDaoList.tsx
Normal file
114
src/components/console/MicroDaoList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
91
src/components/console/WalletInfo.tsx
Normal file
91
src/components/console/WalletInfo.tsx
Normal 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
106
src/pages/ConsolePage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user