From c9d76816276591eec1002ed3f97f17cd10a536d9 Mon Sep 17 00:00:00 2001 From: Apple Date: Sun, 30 Nov 2025 11:40:30 -0800 Subject: [PATCH] feat: District Portals Frontend Pages: - /districts - list of all districts with cards - /districts/[slug] - district detail page - /soul, /greenfood, /energy-union - shortcut redirects UI Features: - District-specific colors and icons - Lead agent + core team display - District rooms list with Matrix status - District nodes list - Chat widget for lobby room - Stats (agents, rooms, nodes count) --- apps/web/src/app/districts/[slug]/page.tsx | 374 +++++++++++++++++++++ apps/web/src/app/districts/page.tsx | 196 +++++++++++ apps/web/src/app/energy-union/page.tsx | 6 + apps/web/src/app/greenfood/page.tsx | 6 + apps/web/src/app/soul/page.tsx | 6 + 5 files changed, 588 insertions(+) create mode 100644 apps/web/src/app/districts/[slug]/page.tsx create mode 100644 apps/web/src/app/districts/page.tsx create mode 100644 apps/web/src/app/energy-union/page.tsx create mode 100644 apps/web/src/app/greenfood/page.tsx create mode 100644 apps/web/src/app/soul/page.tsx diff --git a/apps/web/src/app/districts/[slug]/page.tsx b/apps/web/src/app/districts/[slug]/page.tsx new file mode 100644 index 00000000..8baacf42 --- /dev/null +++ b/apps/web/src/app/districts/[slug]/page.tsx @@ -0,0 +1,374 @@ +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { + ArrowLeft, Building2, Bot, Users, MessageSquare, + Home, Zap, Leaf, Heart, Server, Activity +} from 'lucide-react' +import { CityChatWidget } from '@/components/city/CityChatWidget' + +export const dynamic = 'force-dynamic' + +interface DistrictAgent { + id: string + name: string + kind: string | null + status: string | null + avatar_url: string | null + role: string | null + is_core?: boolean +} + +interface DistrictRoom { + id: string + slug: string + name: string + description: string | null + matrix_room_id: string | null + room_role: string | null + is_public: boolean +} + +interface DistrictNode { + id: string + name: string + kind: string | null + status: string | null + location: string | null +} + +interface DistrictDetail { + district: { + id: string + slug: string + name: string + description: string | null + dao_type: string + } + lead_agent: DistrictAgent | null + core_team: DistrictAgent[] + agents: DistrictAgent[] + rooms: DistrictRoom[] + nodes: DistrictNode[] + stats: { + agents_count: number + rooms_count: number + nodes_count: number + } +} + +interface PageProps { + params: Promise<{ slug: string }> +} + +const districtIcons: Record = { + 'soul': Heart, + 'greenfood': Leaf, + 'energy-union': Zap, +} + +const districtColors: Record = { + 'soul': { bg: 'from-purple-500/20 to-pink-600/20', text: 'text-purple-400', border: 'border-purple-500/30' }, + 'greenfood': { bg: 'from-emerald-500/20 to-green-600/20', text: 'text-emerald-400', border: 'border-emerald-500/30' }, + 'energy-union': { bg: 'from-amber-500/20 to-orange-600/20', text: 'text-amber-400', border: 'border-amber-500/30' }, +} + +async function getDistrict(slug: string): Promise { + try { + const apiUrl = process.env.INTERNAL_API_URL || 'http://daarion-city-service:7001' + const res = await fetch(`${apiUrl}/api/v1/districts/${slug}`, { + cache: 'no-store' + }) + if (!res.ok) return null + return res.json() + } catch (error) { + console.error('Failed to fetch district:', error) + return null + } +} + +export default async function DistrictPage({ params }: PageProps) { + const { slug } = await params + const data = await getDistrict(slug) + + if (!data) { + notFound() + } + + const { district, lead_agent, core_team, agents, rooms, nodes, stats } = data + const Icon = districtIcons[slug] || Building2 + const colors = districtColors[slug] || { bg: 'from-cyan-500/20 to-blue-600/20', text: 'text-cyan-400', border: 'border-cyan-500/30' } + + // Find lobby room for chat + const lobbyRoom = rooms.find(r => r.slug.includes('lobby')) + + return ( +
+ {/* Header */} +
+
+ {/* Breadcrumb */} + + + {/* Title */} +
+
+ +
+
+
+

{district.name}

+ + District + +
+

+ {district.description || 'District платформа DAARION.city'} +

+
+
+ + {/* Stats */} +
+
+ + {stats.agents_count} + агентів +
+
+ + {stats.rooms_count} + кімнат +
+
+ + {stats.nodes_count} + нод +
+
+
+
+ + {/* Main Content */} +
+
+ {/* Left Column - Chat */} +
+ {/* Chat Widget */} + {lobbyRoom && ( +
+
+

+ + {lobbyRoom.name} +

+

+ Головна кімната District-а +

+
+ +
+ )} + + {/* Rooms */} + {rooms.length > 0 && ( +
+

+ + Кімнати District-а +

+
+ {rooms.map((room) => ( + +
+
+

+ {room.name} +

+ {room.room_role && ( +

+ {room.room_role} +

+ )} +
+ {room.matrix_room_id && ( + + )} +
+ + ))} +
+
+ )} + + {/* Nodes */} + {nodes.length > 0 && ( +
+

+ + Ноди District-а +

+
+ {nodes.map((node) => ( + +
+ +
+

+ {node.name} +

+

+ {node.kind} • {node.location || 'Unknown location'} +

+
+
+ + {node.status || 'unknown'} + + + ))} +
+
+ )} +
+ + {/* Right Column - Agents */} +
+ {/* Lead Agent */} + {lead_agent && ( +
+

+ + Lead Agent +

+ +
+
+ {lead_agent.avatar_url ? ( + // eslint-disable-next-line @next/next/no-img-element + {lead_agent.name} + ) : ( + + )} +
+ +
+
+

+ {lead_agent.name} +

+

+ {lead_agent.kind || 'Agent'} • District Lead +

+
+ +
+ )} + + {/* Core Team */} + {core_team.length > 0 && ( +
+

+ + Core Team +

+
+ {core_team.map((agent) => ( + +
+
+ +
+ +
+
+

+ {agent.name} +

+

+ {agent.kind || 'agent'} +

+
+ + ))} +
+
+ )} + + {/* All Agents */} + {agents.length > 0 && ( +
+

+ + Всі агенти ({agents.length}) +

+
+ {agents.map((agent) => ( + +
+ + + {agent.name} + +
+ + {agent.role || agent.kind} + + + ))} +
+
+ )} +
+
+
+
+ ) +} + diff --git a/apps/web/src/app/districts/page.tsx b/apps/web/src/app/districts/page.tsx new file mode 100644 index 00000000..2898e93b --- /dev/null +++ b/apps/web/src/app/districts/page.tsx @@ -0,0 +1,196 @@ +'use client' + +import { useState, useEffect } from 'react' +import Link from 'next/link' +import { Building2, Users, Bot, ArrowRight, Loader2, MapPin, Zap, Leaf, Heart } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface DistrictSummary { + id: string + slug: string + name: string + description: string | null + dao_type: string + lead_agent: { + id: string + name: string + avatar_url: string | null + } | null + rooms_count: number + rooms: { id: string; slug: string; name: string }[] +} + +const districtIcons: Record = { + 'soul': Heart, + 'greenfood': Leaf, + 'energy-union': Zap, +} + +const districtColors: Record = { + 'soul': 'from-purple-500/20 to-pink-600/20 border-purple-500/30', + 'greenfood': 'from-emerald-500/20 to-green-600/20 border-emerald-500/30', + 'energy-union': 'from-amber-500/20 to-orange-600/20 border-amber-500/30', +} + +export default function DistrictsPage() { + const [districts, setDistricts] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + async function fetchDistricts() { + try { + const res = await fetch('/api/v1/districts') + if (!res.ok) throw new Error('Failed to fetch districts') + const data = await res.json() + setDistricts(data) + } catch (err) { + console.error('Failed to fetch districts:', err) + setError(err instanceof Error ? err.message : 'Unknown error') + } finally { + setLoading(false) + } + } + fetchDistricts() + }, []) + + if (loading) { + return ( +
+ +
+ ) + } + + if (error) { + return ( +
+
+

{error}

+ +
+
+ ) + } + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

District Portals

+

Платформи та спільноти DAARION.city

+
+
+
+ + {/* Districts Grid */} + {districts.length === 0 ? ( +
+ +

+ District-и не знайдено +

+

+ Поки що немає активних District-ів +

+
+ ) : ( +
+ {districts.map((district) => ( + + ))} +
+ )} + + {/* Info Section */} +
+

Що таке District?

+

+ District — це платформа або тематична спільнота всередині DAARION.city. + Кожен District має свого Lead Agent-а, команду агентів, власні кімнати + та може містити MicroDAO. Districts є точками входу для користувачів + у відповідні екосистеми: wellness (SOUL), food systems (GREENFOOD), + energy/compute (ENERGY UNION). +

+
+
+
+ ) +} + +function DistrictCard({ district }: { district: DistrictSummary }) { + const Icon = districtIcons[district.slug] || Building2 + const colorClass = districtColors[district.slug] || 'from-cyan-500/20 to-blue-600/20 border-cyan-500/30' + + return ( + + {/* Header */} +
+
+
+ +
+
+

+ {district.name} +

+ + District + +
+
+
+ + {/* Description */} +

+ {district.description || 'Без опису'} +

+ + {/* Lead Agent */} + {district.lead_agent && ( +
+
+ +
+
+

{district.lead_agent.name}

+

Lead Agent

+
+
+ )} + + {/* Stats */} +
+
+ + + {district.rooms_count} кімнат + +
+ +
+ + ) +} + diff --git a/apps/web/src/app/energy-union/page.tsx b/apps/web/src/app/energy-union/page.tsx new file mode 100644 index 00000000..bdb4451d --- /dev/null +++ b/apps/web/src/app/energy-union/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from 'next/navigation' + +export default function EnergyUnionPage() { + redirect('/districts/energy-union') +} + diff --git a/apps/web/src/app/greenfood/page.tsx b/apps/web/src/app/greenfood/page.tsx new file mode 100644 index 00000000..93a9e207 --- /dev/null +++ b/apps/web/src/app/greenfood/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from 'next/navigation' + +export default function GreenfoodPage() { + redirect('/districts/greenfood') +} + diff --git a/apps/web/src/app/soul/page.tsx b/apps/web/src/app/soul/page.tsx new file mode 100644 index 00000000..27b21dd3 --- /dev/null +++ b/apps/web/src/app/soul/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from 'next/navigation' + +export default function SoulPage() { + redirect('/districts/soul') +} +