diff --git a/CONSOLE_UI_SUMMARY.md b/CONSOLE_UI_SUMMARY.md new file mode 100644 index 00000000..143efffa --- /dev/null +++ b/CONSOLE_UI_SUMMARY.md @@ -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 + diff --git a/src/App.tsx b/src/App.tsx index 5e3d1d1a..9505e38d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( } /> + } /> Home - Coming soon} /> ); diff --git a/src/api/teams.ts b/src/api/teams.ts index 6d10b1f7..6a1bde07 100644 --- a/src/api/teams.ts +++ b/src/api/teams.ts @@ -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 { - return apiPost('/teams', data); + return apiPost('/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 { - return apiGet(`/teams/${teamId}`); + return apiGet(`/api/v1/teams/${teamId}`); } export async function updateTeam(teamId: string, data: UpdateTeamRequest): Promise { - return apiPatch(`/teams/${teamId}`, data); + return apiPatch(`/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 { + return apiPost(`/api/v1/teams/${teamId}/members`, data); } diff --git a/src/api/wallet.ts b/src/api/wallet.ts new file mode 100644 index 00000000..c5582125 --- /dev/null +++ b/src/api/wallet.ts @@ -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 { + return apiGet('/api/v1/wallet/balances'); +} + diff --git a/src/components/console/CreateMicroDaoForm.tsx b/src/components/console/CreateMicroDaoForm.tsx new file mode 100644 index 00000000..871ac179 --- /dev/null +++ b/src/components/console/CreateMicroDaoForm.tsx @@ -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(null); + const [canCreate, setCanCreate] = useState(false); + const [daarionBalance, setDaarionBalance] = useState(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) => { + 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 ( + + Створити MicroDAO + + {/* Balance Check */} + + + + {canCreate ? '✓' : '✗'} + + + Баланс DAARION: {daarionBalance.toFixed(2)} + {!canCreate && ' (потрібно ≥ 1.00)'} + + + + + {error && ( + + {error} + + )} + + + + + Назва MicroDAO * + + + + + + + Slug (URL) * + + 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" + /> + + Тільки маленькі літери, цифри та дефіси + + + + + + Опис + + 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="Опис вашої спільноти..." + /> + + + + + Тип + + 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" + > + Спільнота + Гільдія + Лабораторія + Особисте + + + + + + Режим + + 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" + > + Публічний + Конфіденційний + + + + + + {loading ? 'Створення...' : 'Створити MicroDAO'} + + {onCancel && ( + + Скасувати + + )} + + + + ); +} + diff --git a/src/components/console/InviteMemberForm.tsx b/src/components/console/InviteMemberForm.tsx new file mode 100644 index 00000000..b93e2b26 --- /dev/null +++ b/src/components/console/InviteMemberForm.tsx @@ -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(null); + const [canInvite, setCanInvite] = useState(false); + const [daarionBalance, setDaarionBalance] = useState(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 ( + + + Запросити користувача в {team.name} + + + {/* Balance Check */} + + + + {canInvite ? '✓' : '✗'} + + + Ваш баланс DAARION: {daarionBalance.toFixed(2)} + {!canInvite && ' (потрібно ≥ 1.00 для запрошення)'} + + + + Запрошений користувач має мати: + + {role === 'admin' ? '≥ 1.00 DAARION' : '≥ 0.01 DAARION'} + + + + + {error && ( + + {error} + + )} + + + + + Email користувача * + + 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" + /> + + + + + Роль * + + 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" + > + Member (потрібно ≥ 0.01 DAARION) + Admin (потрібно ≥ 1.00 DAARION) + + + {role === 'admin' + ? 'Admin може запрошувати інших користувачів та керувати MicroDAO' + : 'Member може використовувати функції MicroDAO'} + + + + + + {loading ? 'Запрошення...' : 'Запросити'} + + {onCancel && ( + + Скасувати + + )} + + + + ); +} + diff --git a/src/components/console/MicroDaoList.tsx b/src/components/console/MicroDaoList.tsx new file mode 100644 index 00000000..082c1701 --- /dev/null +++ b/src/components/console/MicroDaoList.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 = { + city: 'Місто', + platform: 'Платформа', + community: 'Спільнота', + guild: 'Гільдія', + lab: 'Лабораторія', + personal: 'Особисте', + }; + return labels[type || 'community'] || 'Спільнота'; + }; + + const getModeLabel = (mode: string) => { + return mode === 'public' ? 'Публічний' : 'Конфіденційний'; + }; + + return ( + + + Мої MicroDAO + + Оновити + + + + {loading && Завантаження...} + {error && {error}} + + {!loading && !error && ( + <> + {teams.length === 0 ? ( + + У вас ще немає MicroDAO + Створіть перше MicroDAO, щоб почати + + ) : ( + + {teams.map((team) => ( + onSelectTeam?.(team)} + className={`p-4 border rounded-lg cursor-pointer hover:bg-gray-50 transition-colors ${ + onSelectTeam ? 'cursor-pointer' : '' + }`} + > + + + {team.name} + {team.description && ( + {team.description} + )} + + + {getTypeLabel(team.type)} + + + {getModeLabel(team.mode)} + + {team.slug && ( + + {team.slug} + + )} + + + {team.type === 'city' && ( + + DAARION.city + + )} + + + ))} + + )} + > + )} + + ); +} + diff --git a/src/components/console/WalletInfo.tsx b/src/components/console/WalletInfo.tsx new file mode 100644 index 00000000..cfdbb21c --- /dev/null +++ b/src/components/console/WalletInfo.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( + + Wallet + + {loading && Завантаження...} + {error && {error}} + + {!loading && !error && ( + + {/* DAARION Balance */} + + + DAARION + + {daarionBalance ? parseFloat(daarionBalance.amount).toFixed(2) : '0.00'} + + + + + {canCreateMicroDao ? '✓' : '○'} Можна створити MicroDAO (потрібно ≥ 1.00) + + + {isAdmin ? '✓' : '○'} Роль Admin (потрібно ≥ 1.00) + + + {canUseMicroDao ? '✓' : '○'} Можна використовувати сервіс (потрібно ≥ 0.01) + + + + + {/* DAAR Balance */} + {daarBalance && ( + + + DAAR + + {parseFloat(daarBalance.amount).toFixed(2)} + + + + )} + + {!daarionBalance && !daarBalance && ( + Немає токенів на балансі + )} + + )} + + + Оновити баланс + + + ); +} + diff --git a/src/pages/ConsolePage.tsx b/src/pages/ConsolePage.tsx new file mode 100644 index 00000000..4a4ed217 --- /dev/null +++ b/src/pages/ConsolePage.tsx @@ -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('list'); + const [selectedTeam, setSelectedTeam] = useState(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 ( + + + + Console + Управління MicroDAO та DAARION.city + + + + {/* Left Column - Wallet Info */} + + + + + {/* Right Column - Main Content */} + + {/* Navigation */} + + + { + 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 + + { + 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 + + + + + {/* Content */} + {currentView === 'list' && ( + + )} + + {currentView === 'create' && ( + setCurrentView('list')} + /> + )} + + {currentView === 'invite' && selectedTeam && ( + { + setSelectedTeam(null); + setCurrentView('list'); + }} + /> + )} + + + + + ); +} +
+ Тільки маленькі літери, цифри та дефіси +
+ {role === 'admin' + ? 'Admin може запрошувати інших користувачів та керувати MicroDAO' + : 'Member може використовувати функції MicroDAO'} +
У вас ще немає MicroDAO
Створіть перше MicroDAO, щоб почати
{team.description}
Управління MicroDAO та DAARION.city