feat: Add MicroDAO Dashboard with activity feed and statistics
- Add microdao_activity table for news/updates/events - Add statistics columns to microdaos table - Implement dashboard API endpoints - Create UI components (HeaderCard, ActivitySection, TeamSection) - Add seed data for DAARION DAO - Update backend models and repositories - Add frontend types and API client
This commit is contained in:
61
apps/web/src/app/api/microdao/[slug]/activity/route.ts
Normal file
61
apps/web/src/app/api/microdao/[slug]/activity/route.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const CITY_API_BASE_URL = process.env.INTERNAL_API_URL || process.env.CITY_API_BASE_URL || "http://daarion-city-service:7001";
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
const { slug } = await params;
|
||||
const { searchParams } = new URL(req.url);
|
||||
const limit = searchParams.get("limit") || "20";
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${CITY_API_BASE_URL}/city/microdao/${encodeURIComponent(slug)}/activity?limit=${limit}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json().catch(() => null);
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
} catch (error) {
|
||||
console.error("MicroDAO activity proxy error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch MicroDAO activity" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
const { slug } = await params;
|
||||
const body = await req.json();
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${CITY_API_BASE_URL}/city/microdao/${encodeURIComponent(slug)}/activity`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json().catch(() => null);
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
} catch (error) {
|
||||
console.error("MicroDAO activity create proxy error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create MicroDAO activity" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
31
apps/web/src/app/api/microdao/[slug]/dashboard/route.ts
Normal file
31
apps/web/src/app/api/microdao/[slug]/dashboard/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const CITY_API_BASE_URL = process.env.INTERNAL_API_URL || process.env.CITY_API_BASE_URL || "http://daarion-city-service:7001";
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${CITY_API_BASE_URL}/city/microdao/${encodeURIComponent(slug)}/dashboard`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json().catch(() => null);
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
} catch (error) {
|
||||
console.error("MicroDAO dashboard proxy error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch MicroDAO dashboard" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
107
apps/web/src/components/microdao/MicrodaoActivitySection.tsx
Normal file
107
apps/web/src/components/microdao/MicrodaoActivitySection.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import { MicrodaoActivity, MicrodaoSummary } from "@/lib/types/microdao";
|
||||
|
||||
interface MicrodaoActivitySectionProps {
|
||||
activity: MicrodaoActivity[];
|
||||
microdao: MicrodaoSummary;
|
||||
}
|
||||
|
||||
export function MicrodaoActivitySection({ activity, microdao }: MicrodaoActivitySectionProps) {
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return "щойно";
|
||||
if (diffMins < 60) return `${diffMins} хв тому`;
|
||||
if (diffHours < 24) return `${diffHours} год тому`;
|
||||
if (diffDays < 7) return `${diffDays} дн тому`;
|
||||
|
||||
return date.toLocaleDateString("uk-UA", { day: "numeric", month: "short" });
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const getKindLabel = (kind: string) => {
|
||||
switch (kind) {
|
||||
case "post":
|
||||
return "Новина";
|
||||
case "event":
|
||||
return "Подія";
|
||||
case "update":
|
||||
return "Оновлення";
|
||||
default:
|
||||
return kind;
|
||||
}
|
||||
};
|
||||
|
||||
const getKindColor = (kind: string) => {
|
||||
switch (kind) {
|
||||
case "post":
|
||||
return "bg-blue-500/10 text-blue-300 border-blue-500/30";
|
||||
case "event":
|
||||
return "bg-purple-500/10 text-purple-300 border-purple-500/30";
|
||||
case "update":
|
||||
return "bg-green-500/10 text-green-300 border-green-500/30";
|
||||
default:
|
||||
return "bg-slate-500/10 text-slate-300 border-slate-500/30";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||
<span className="w-1 h-6 bg-cyan-400 rounded-full" />
|
||||
Новини {microdao.name}
|
||||
</h2>
|
||||
|
||||
{activity.length === 0 ? (
|
||||
<div className="text-sm text-slate-500 py-8 text-center">
|
||||
Поки що немає новин
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{activity.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-slate-900/50 border border-slate-700/30 rounded-lg p-4 hover:border-slate-600/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-medium border ${getKindColor(item.kind)}`}>
|
||||
{getKindLabel(item.kind)}
|
||||
</span>
|
||||
{item.title && (
|
||||
<h3 className="text-sm font-semibold text-slate-200">
|
||||
{item.title}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 whitespace-nowrap">
|
||||
{formatDate(item.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-slate-300 leading-relaxed line-clamp-3">
|
||||
{item.body}
|
||||
</p>
|
||||
|
||||
{item.author_name && (
|
||||
<div className="mt-2 text-xs text-slate-500">
|
||||
Автор: {item.author_name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
137
apps/web/src/components/microdao/MicrodaoHeaderCard.tsx
Normal file
137
apps/web/src/components/microdao/MicrodaoHeaderCard.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { MessageCircle, Plus, Users, MessageSquare, Bot } from "lucide-react";
|
||||
import { MicrodaoDashboard } from "@/lib/types/microdao";
|
||||
import { normalizeAssetUrl } from "@/lib/utils/assetUrl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface MicrodaoHeaderCardProps {
|
||||
dashboard: MicrodaoDashboard;
|
||||
}
|
||||
|
||||
export function MicrodaoHeaderCard({ dashboard }: MicrodaoHeaderCardProps) {
|
||||
const { microdao, stats } = dashboard;
|
||||
|
||||
return (
|
||||
<section
|
||||
className="rounded-3xl border border-white/10 bg-gradient-to-br from-sky-950/50 via-slate-900 to-black p-6 md:p-8 space-y-6 relative overflow-hidden shadow-2xl shadow-sky-900/10"
|
||||
style={microdao.banner_url ? {
|
||||
backgroundImage: `linear-gradient(to bottom, rgba(15, 23, 42, 0.85), rgba(15, 23, 42, 0.95)), url(${normalizeAssetUrl(microdao.banner_url)})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
} : undefined}
|
||||
>
|
||||
{/* Background pattern */}
|
||||
{!microdao.banner_url && (
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] opacity-20" />
|
||||
)}
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-6 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-5 max-w-3xl">
|
||||
{/* Badges */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-[11px] uppercase tracking-wider font-semibold border ${
|
||||
microdao.is_platform
|
||||
? "border-amber-500/40 text-amber-300 bg-amber-500/10"
|
||||
: "border-cyan-400/40 text-cyan-300 bg-cyan-500/10"
|
||||
}`}>
|
||||
{microdao.is_platform ? "Platform District" : "MicroDAO"}
|
||||
</span>
|
||||
|
||||
{microdao.district && (
|
||||
<span className="px-2.5 py-0.5 rounded-full text-[11px] uppercase tracking-wider font-medium border border-white/10 text-white/60 bg-white/5">
|
||||
{microdao.district}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-[11px] font-medium border flex items-center gap-1.5 ${
|
||||
microdao.is_active
|
||||
? "border-emerald-500/30 text-emerald-400 bg-emerald-500/5"
|
||||
: "border-slate-600 text-slate-400 bg-slate-800"
|
||||
}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${microdao.is_active ? 'bg-emerald-400' : 'bg-slate-500'}`} />
|
||||
{microdao.is_active ? "Active" : "Inactive"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title & Description with Logo */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Logo */}
|
||||
{normalizeAssetUrl(microdao.logo_url) && (
|
||||
<img
|
||||
src={normalizeAssetUrl(microdao.logo_url)!}
|
||||
alt={microdao.name}
|
||||
className="w-16 h-16 md:w-20 md:h-20 rounded-2xl object-cover bg-slate-700/50 border border-white/10 shadow-lg"
|
||||
/>
|
||||
)}
|
||||
<h1 className="text-3xl md:text-5xl font-bold text-white tracking-tight leading-tight">
|
||||
{microdao.name}
|
||||
</h1>
|
||||
</div>
|
||||
{microdao.description && (
|
||||
<p className="text-base md:text-lg text-slate-300 leading-relaxed max-w-2xl">
|
||||
{microdao.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Key Stats */}
|
||||
<div className="flex flex-wrap gap-3 pt-2">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10">
|
||||
<MessageSquare className="w-4 h-4 text-white/60" />
|
||||
<span className="text-sm text-white/80">{stats.rooms_count} Кімнат</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10">
|
||||
<Users className="w-4 h-4 text-white/60" />
|
||||
<span className="text-sm text-white/80">{stats.citizens_count} Громадян</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10">
|
||||
<Bot className="w-4 h-4 text-white/60" />
|
||||
<span className="text-sm text-white/80">{stats.agents_count} Агентів</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-wrap gap-3 pt-2">
|
||||
<Link href="/city?room=city-lobby">
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 text-cyan-300"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4 mr-2" />
|
||||
Поспілкуватися з DAARWIZZ
|
||||
</Button>
|
||||
</Link>
|
||||
<a href="#microdao-rooms">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-white/5 hover:bg-white/10 border-white/20 text-white/80"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Створити кімнату
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logo (right side) */}
|
||||
<div className="hidden md:flex flex-col gap-3">
|
||||
<div className="w-24 h-24 rounded-2xl bg-slate-800 border border-white/10 flex items-center justify-center overflow-hidden shadow-xl">
|
||||
{normalizeAssetUrl(microdao.logo_url) ? (
|
||||
<img
|
||||
src={normalizeAssetUrl(microdao.logo_url)!}
|
||||
alt={microdao.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 text-slate-600" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
24
apps/web/src/components/microdao/MicrodaoProjectsSection.tsx
Normal file
24
apps/web/src/components/microdao/MicrodaoProjectsSection.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { MicrodaoSummary } from "@/lib/types/microdao";
|
||||
import { FolderKanban } from "lucide-react";
|
||||
|
||||
interface MicrodaoProjectsSectionProps {
|
||||
microdao: MicrodaoSummary;
|
||||
}
|
||||
|
||||
export function MicrodaoProjectsSection({ microdao }: MicrodaoProjectsSectionProps) {
|
||||
return (
|
||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||
<FolderKanban className="w-5 h-5 text-cyan-400" />
|
||||
Проєкти DAO
|
||||
</h2>
|
||||
|
||||
<div className="text-sm text-slate-500 py-8 text-center">
|
||||
Скоро тут будуть проєкти MicroDAO
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
24
apps/web/src/components/microdao/MicrodaoTasksSection.tsx
Normal file
24
apps/web/src/components/microdao/MicrodaoTasksSection.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { MicrodaoSummary } from "@/lib/types/microdao";
|
||||
import { CheckSquare } from "lucide-react";
|
||||
|
||||
interface MicrodaoTasksSectionProps {
|
||||
microdao: MicrodaoSummary;
|
||||
}
|
||||
|
||||
export function MicrodaoTasksSection({ microdao }: MicrodaoTasksSectionProps) {
|
||||
return (
|
||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||
<CheckSquare className="w-5 h-5 text-cyan-400" />
|
||||
Задачі DAO
|
||||
</h2>
|
||||
|
||||
<div className="text-sm text-slate-500 py-8 text-center">
|
||||
Скоро тут будуть задачі MicroDAO
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
78
apps/web/src/components/microdao/MicrodaoTeamSection.tsx
Normal file
78
apps/web/src/components/microdao/MicrodaoTeamSection.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { PublicCitizenSummary } from "@/lib/types/microdao";
|
||||
import { normalizeAssetUrl } from "@/lib/utils/assetUrl";
|
||||
import { Users, ArrowRight } from "lucide-react";
|
||||
|
||||
interface MicrodaoTeamSectionProps {
|
||||
citizens: PublicCitizenSummary[];
|
||||
microdaoSlug?: string;
|
||||
}
|
||||
|
||||
export function MicrodaoTeamSection({ citizens, microdaoSlug }: MicrodaoTeamSectionProps) {
|
||||
return (
|
||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-cyan-400" />
|
||||
Команда
|
||||
<span className="text-sm font-normal text-slate-500">({citizens.length})</span>
|
||||
</h2>
|
||||
{microdaoSlug && (
|
||||
<Link
|
||||
href={`/citizens${microdaoSlug ? `?microdao=${microdaoSlug}` : ''}`}
|
||||
className="text-xs text-cyan-400 hover:text-cyan-300 flex items-center gap-1"
|
||||
>
|
||||
Всі громадяни
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{citizens.length === 0 ? (
|
||||
<div className="text-sm text-slate-500 py-4 text-center">
|
||||
Немає громадян у цьому MicroDAO
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{citizens.slice(0, 6).map((citizen) => (
|
||||
<Link
|
||||
key={citizen.slug || citizen.display_name}
|
||||
href={citizen.slug ? `/citizens/${citizen.slug}` : '#'}
|
||||
className="bg-slate-900/50 border border-slate-700/30 rounded-lg p-3 hover:border-cyan-500/40 transition-colors flex items-center gap-3"
|
||||
>
|
||||
{citizen.avatar_url ? (
|
||||
<img
|
||||
src={normalizeAssetUrl(citizen.avatar_url)!}
|
||||
alt={citizen.display_name}
|
||||
className="w-10 h-10 rounded-lg object-cover bg-slate-700/50"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-lg bg-slate-700/50 flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-slate-500" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-200 truncate">
|
||||
{citizen.display_name}
|
||||
</p>
|
||||
{citizen.public_title && (
|
||||
<p className="text-xs text-slate-500 truncate">
|
||||
{citizen.public_title}
|
||||
</p>
|
||||
)}
|
||||
{citizen.district && (
|
||||
<p className="text-[10px] text-slate-600 mt-1">
|
||||
{citizen.district}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import type { MicrodaoSummary, MicrodaoDetail, MicrodaoRoomsList, CityRoomSummary } from '@/lib/types/microdao';
|
||||
import type { MicrodaoSummary, MicrodaoDetail, MicrodaoRoomsList, CityRoomSummary, MicrodaoDashboard } from '@/lib/types/microdao';
|
||||
import { fetchMicrodaoDashboard } from '@/lib/api/microdao';
|
||||
|
||||
interface UseMicrodaoListOptions {
|
||||
district?: string;
|
||||
@@ -302,3 +303,60 @@ export function useMicrodaoAgents(
|
||||
mutate: fetchData,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MicroDAO Dashboard Hook (TASK_PHASE_MICRODAO_DASHBOARD_v1)
|
||||
// =============================================================================
|
||||
|
||||
interface UseMicrodaoDashboardOptions {
|
||||
refreshInterval?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface UseMicrodaoDashboardResult {
|
||||
dashboard: MicrodaoDashboard | null;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
mutate: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useMicrodaoDashboard(
|
||||
slug: string | undefined,
|
||||
options: UseMicrodaoDashboardOptions = {}
|
||||
): UseMicrodaoDashboardResult {
|
||||
const { refreshInterval = 60000, enabled = true } = options;
|
||||
|
||||
const [dashboard, setDashboard] = useState<MicrodaoDashboard | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!enabled || !slug) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const data = await fetchMicrodaoDashboard(slug);
|
||||
setDashboard(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to fetch dashboard'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [slug, enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
const interval = setInterval(fetchData, refreshInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [fetchData, refreshInterval]);
|
||||
|
||||
const mutate = useCallback(async () => {
|
||||
await fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { dashboard, isLoading, error, mutate };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
* MicroDAO API Client (Task 029)
|
||||
*/
|
||||
|
||||
import { MicrodaoOption, CityRoomSummary } from "@/lib/types/microdao";
|
||||
import {
|
||||
MicrodaoOption,
|
||||
CityRoomSummary,
|
||||
MicrodaoDashboard,
|
||||
MicrodaoActivity,
|
||||
CreateMicrodaoActivity
|
||||
} from "@/lib/types/microdao";
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
|
||||
@@ -177,3 +177,47 @@ export interface AgentMicrodaoMembership {
|
||||
is_core: boolean;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MicroDAO Activity & Dashboard (TASK_PHASE_MICRODAO_DASHBOARD_v1)
|
||||
// =============================================================================
|
||||
|
||||
export type MicrodaoActivityKind = "post" | "event" | "update";
|
||||
|
||||
export interface MicrodaoActivity {
|
||||
id: string;
|
||||
microdao_slug: string;
|
||||
kind: MicrodaoActivityKind;
|
||||
title?: string | null;
|
||||
body: string;
|
||||
author_agent_id?: string | null;
|
||||
author_name?: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreateMicrodaoActivity {
|
||||
kind: MicrodaoActivityKind;
|
||||
title?: string | null;
|
||||
body: string;
|
||||
author_agent_id?: string | null;
|
||||
author_name?: string | null;
|
||||
}
|
||||
|
||||
export interface MicrodaoStats {
|
||||
rooms_count: number;
|
||||
citizens_count: number;
|
||||
agents_count: number;
|
||||
last_update_at?: string | null;
|
||||
}
|
||||
|
||||
export interface MicrodaoDashboard {
|
||||
microdao: MicrodaoSummary;
|
||||
stats: MicrodaoStats;
|
||||
recent_activity: MicrodaoActivity[];
|
||||
rooms: CityRoomSummary[];
|
||||
citizens: PublicCitizenSummary[];
|
||||
}
|
||||
|
||||
// Re-export PublicCitizenSummary from citizens types
|
||||
import type { PublicCitizenSummary } from './citizens';
|
||||
export type { PublicCitizenSummary };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user