From 0b7bddb81c266fab8ea3f02ed7df8f8ff8acd44d Mon Sep 17 00:00:00 2001 From: Hammer Date: Thu, 29 Jan 2026 12:35:33 +0000 Subject: [PATCH] feat: Network Matching page - AI-powered client connection suggestions - New /network page with match cards, score badges, category filters - Network stats dashboard (clients, matches, avg score, top connectors) - Category-based filtering (industry, interests, location, business, social) - Adjustable minimum score threshold - AI introduction generation button per match - Added Network to sidebar navigation - Types: NetworkMatch, NetworkStats --- src/App.tsx | 2 + src/components/Layout.tsx | 1 + src/lib/api.ts | 25 +++ src/pages/NetworkPage.tsx | 313 ++++++++++++++++++++++++++++++++++++++ src/types/index.ts | 19 +++ 5 files changed, 360 insertions(+) create mode 100644 src/pages/NetworkPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 9793769..811d1e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import EventsPage from '@/pages/EventsPage'; import EmailsPage from '@/pages/EmailsPage'; import SettingsPage from '@/pages/SettingsPage'; import AdminPage from '@/pages/AdminPage'; +import NetworkPage from '@/pages/NetworkPage'; import InvitePage from '@/pages/InvitePage'; import ForgotPasswordPage from '@/pages/ForgotPasswordPage'; import ResetPasswordPage from '@/pages/ResetPasswordPage'; @@ -48,6 +49,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 18b68bd..33f4e2e 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -12,6 +12,7 @@ const baseNavItems = [ { path: '/clients', label: 'Clients', icon: Users }, { path: '/events', label: 'Events', icon: Calendar }, { path: '/emails', label: 'Emails', icon: Mail }, + { path: '/network', label: 'Network', icon: Network }, { path: '/settings', label: 'Settings', icon: Settings }, ]; diff --git a/src/lib/api.ts b/src/lib/api.ts index 8339ca3..b1f5e91 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -239,6 +239,31 @@ class ApiClient { await this.fetch(`/emails/${id}`, { method: 'DELETE' }); } + // Network Matching + async getNetworkMatches(params?: { minScore?: number; limit?: number }): Promise<{ matches: NetworkMatch[]; total: number; clientCount: number }> { + const searchParams = new URLSearchParams(); + if (params?.minScore) searchParams.set('minScore', String(params.minScore)); + if (params?.limit) searchParams.set('limit', String(params.limit)); + const query = searchParams.toString(); + return this.fetch(`/network/matches${query ? `?${query}` : ''}`); + } + + async getClientMatches(clientId: string, minScore?: number): Promise<{ matches: NetworkMatch[]; client: { id: string; name: string } }> { + const query = minScore ? `?minScore=${minScore}` : ''; + return this.fetch(`/network/matches/${clientId}${query}`); + } + + async generateIntro(clientAId: string, clientBId: string, reasons: string[], provider?: string): Promise<{ introSuggestion: string }> { + return this.fetch('/network/intro', { + method: 'POST', + body: JSON.stringify({ clientAId, clientBId, reasons, provider }), + }); + } + + async getNetworkStats(): Promise { + return this.fetch('/network/stats'); + } + // Admin async getUsers(): Promise { return this.fetch('/admin/users'); diff --git a/src/pages/NetworkPage.tsx b/src/pages/NetworkPage.tsx new file mode 100644 index 0000000..9dc2959 --- /dev/null +++ b/src/pages/NetworkPage.tsx @@ -0,0 +1,313 @@ +import { useEffect, useState, useCallback } from 'react'; +import { Link } from 'react-router-dom'; +import { api } from '@/lib/api'; +import type { NetworkMatch, NetworkStats } from '@/types'; +import { + Network, Users, Sparkles, ArrowRight, TrendingUp, + MapPin, Briefcase, Heart, Building2, Star, Loader2, Filter, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { PageLoader } from '@/components/LoadingSpinner'; + +const categoryConfig: Record = { + industry: { label: 'Industry', icon: Building2, color: 'text-blue-600', bg: 'bg-blue-50' }, + interests: { label: 'Interests', icon: Heart, color: 'text-pink-600', bg: 'bg-pink-50' }, + location: { label: 'Location', icon: MapPin, color: 'text-emerald-600', bg: 'bg-emerald-50' }, + business: { label: 'Business', icon: Briefcase, color: 'text-purple-600', bg: 'bg-purple-50' }, + social: { label: 'Social', icon: Users, color: 'text-amber-600', bg: 'bg-amber-50' }, +}; + +function ScoreBadge({ score }: { score: number }) { + const color = score >= 60 ? 'bg-emerald-100 text-emerald-700' : + score >= 40 ? 'bg-blue-100 text-blue-700' : + score >= 25 ? 'bg-amber-100 text-amber-700' : + 'bg-slate-100 text-slate-600'; + return ( + + {score}% + + ); +} + +function MatchCard({ match, onGenerateIntro, generatingIntro }: { + match: NetworkMatch; + onGenerateIntro: (match: NetworkMatch) => void; + generatingIntro: boolean; +}) { + const config = categoryConfig[match.category] || categoryConfig.social; + const Icon = config.icon; + + return ( +
+ {/* Header */} +
+
+ + {config.label} +
+ +
+ + {/* People */} +
+ +
+ {match.clientA.name.split(' ').map(n => n[0]).join('')} +
+ {match.clientA.name} + +
+ +
+ +
+ {match.clientB.name.split(' ').map(n => n[0]).join('')} +
+ {match.clientB.name} + +
+ + {/* Reasons */} +
+ {match.reasons.map((reason, i) => ( +
+ + {reason} +
+ ))} +
+ + {/* Intro suggestion */} +
+

{match.introSuggestion}

+
+ + {/* Actions */} +
+ +
+
+ ); +} + +export default function NetworkPage() { + const [matches, setMatches] = useState([]); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [generatingId, setGeneratingId] = useState(null); + const [categoryFilter, setCategoryFilter] = useState(null); + const [minScore, setMinScore] = useState(20); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const [matchData, statsData] = await Promise.all([ + api.getNetworkMatches({ minScore, limit: 100 }), + api.getNetworkStats(), + ]); + setMatches(matchData.matches); + setStats(statsData); + } catch (e) { + console.error('Failed to load network data:', e); + } finally { + setLoading(false); + } + }, [minScore]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const handleGenerateIntro = async (match: NetworkMatch) => { + const id = `${match.clientA.id}-${match.clientB.id}`; + setGeneratingId(id); + try { + const result = await api.generateIntro(match.clientA.id, match.clientB.id, match.reasons); + setMatches(prev => prev.map(m => + m.clientA.id === match.clientA.id && m.clientB.id === match.clientB.id + ? { ...m, introSuggestion: result.introSuggestion } + : m + )); + } catch (e) { + console.error('Failed to generate intro:', e); + } finally { + setGeneratingId(null); + } + }; + + const filtered = categoryFilter + ? matches.filter(m => m.category === categoryFilter) + : matches; + + if (loading) return ; + + const categories = ['industry', 'interests', 'location', 'business', 'social']; + + return ( +
+ {/* Header */} +
+
+

Network Matching

+

+ AI-powered suggestions for connecting your clients +

+
+
+ + {/* Stats */} + {stats && ( +
+
+
+ + Clients +
+

{stats.totalClients}

+
+
+
+ + Matches Found +
+

{stats.totalMatches}

+
+
+
+ + Avg Score +
+

{stats.avgScore}%

+
+
+
+ + Top Connector +
+

+ {stats.topConnectors[0]?.name || '—'} +

+
+
+ )} + + {/* Top Connectors */} + {stats && stats.topConnectors.length > 0 && ( +
+

Most Connected Clients

+
+ {stats.topConnectors.map((c) => ( + +
+ {c.name.split(' ').map(n => n[0]).join('')} +
+ {c.name} + {c.matchCount} matches + + ))} +
+
+ )} + + {/* Filters */} +
+
+ + Filter: +
+ + {categories.map(cat => { + const config = categoryConfig[cat]!; + const count = matches.filter(m => m.category === cat).length; + if (count === 0) return null; + return ( + + ); + })} +
+ + +
+
+ + {/* Match Cards */} + {filtered.length === 0 ? ( +
+ +

No matches found

+

+ {matches.length === 0 + ? 'Add more clients with detailed profiles (interests, industry, location) to find connections.' + : 'Try adjusting the filter or minimum score.'} +

+ {matches.length === 0 && ( + + Add Clients + + )} +
+ ) : ( +
+ {filtered.map((match) => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/types/index.ts b/src/types/index.ts index 1c93740..ca25a6f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -112,6 +112,25 @@ export interface EmailGenerate { provider?: 'anthropic' | 'openai'; } +export interface NetworkMatch { + clientA: { id: string; name: string }; + clientB: { id: string; name: string }; + score: number; + reasons: string[]; + introSuggestion: string; + category: 'industry' | 'interests' | 'location' | 'business' | 'social'; +} + +export interface NetworkStats { + totalClients: number; + totalMatches: number; + avgScore: number; + industries: [string, number][]; + locations: [string, number][]; + categories: Record; + topConnectors: { id: string; name: string; matchCount: number }[]; +} + export interface Invite { id: string; email: string;