From ad43e8d831ffc101688c41beda0baffbabdee733 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 17:38:04 +0000 Subject: [PATCH] feat(CC-086): add Communities page (observations with community subjectType + engagement levels) --- src/app/dashboard/communities/page.tsx | 139 +++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/app/dashboard/communities/page.tsx diff --git a/src/app/dashboard/communities/page.tsx b/src/app/dashboard/communities/page.tsx new file mode 100644 index 0000000..a96572d --- /dev/null +++ b/src/app/dashboard/communities/page.tsx @@ -0,0 +1,139 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Contact { id: string; firstName: string; lastName: string | null; tags: string[]; organization: string | null; createdAt: string; } +interface Observation { id: string; metric: string; value: string; source: string | null; subjectType: string; createdAt: string; } + +const COMMUNITY_TAGS = ['community', 'comunitate', 'group', 'grup', 'association', 'asociatie', 'club', 'network', 'forum', 'slack', 'discord']; +const ENGAGEMENT_LEVELS = [ + { level: 'core', label: 'Core member', icon: '⭐', desc: 'Contributor activ' }, + { level: 'active', label: 'Activ', icon: '🟢', desc: 'Participare regulată' }, + { level: 'observer', label: 'Observer', icon: '👀', desc: 'Urmăresc activitatea' }, +]; + +export default function CommunitiesPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [showAdd, setShowAdd] = useState(false); + const [form, setForm] = useState({ name: '', url: '', type: 'community', level: 'active', since: '' }); + + const { data: observations = [] } = useQuery({ queryKey: ['comm-obs', tenantId], queryFn: () => apiFetch('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + const { data: contacts = [] } = useQuery({ queryKey: ['comm-contacts', tenantId], queryFn: () => apiFetch('/v1/contacts?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 }); + + const communities = useMemo(() => + observations.filter((o) => + o.subjectType === 'community' || + COMMUNITY_TAGS.some((k) => o.metric.toLowerCase().includes(k)) + ).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()), + [observations]); + + const communityContacts = useMemo(() => + contacts.filter((c) => c.tags.some((t) => COMMUNITY_TAGS.includes(t.toLowerCase()))), + [contacts]); + + const addMut = useMutation({ + mutationFn: () => apiFetch('/v1/observations', { tenantId, method: 'POST', body: { + metric: form.type, value: form.name, + source: form.url || undefined, unit: form.level, + subjectType: 'community', + observedAt: form.since ? new Date(form.since).toISOString() : new Date().toISOString(), + confidence: form.level === 'core' ? 1 : form.level === 'active' ? 0.8 : 0.5, + }}), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['comm-obs', tenantId] }); setShowAdd(false); setForm({ name: '', url: '', type: 'community', level: 'active', since: '' }); }, + }); + + return ( +
+
+
+

Comunități & Rețele

+

+ {communities.length} comunități · {communityContacts.length} contacte din comunități +

+
+ +
+ + {showAdd && ( +
+

Comunitate nouă

+
+ setForm((p) => ({ ...p, name: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm((p) => ({ ...p, url: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink font-mono text-xs focus:outline-none focus:ring-2 focus:ring-ring" /> + + +
+ + setForm((p) => ({ ...p, since: e.target.value }))} + className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+
+
+ + +
+
+ )} + + {communities.length === 0 ? ( +
+

🌐

+

Nicio comunitate. Adaugă Slack-uri, Discord-uri, asociații profesionale.

+
+ ) : ( +
+ {communities.map((c) => { + const engLevel = ENGAGEMENT_LEVELS.find((l) => l.level === c.unit) ?? ENGAGEMENT_LEVELS[1]; + return ( +
+
+
+

{c.value}

+

{c.metric}

+
+ {engLevel.icon} +
+
+ + {engLevel.label} + + {c.source && ( + Link → + )} +
+
+ ); + })} +
+ )} +
+ ); +}