Skip to content

Commit 495c0e4

Browse files
authored
Merge pull request #9 from Gonna-AI/claude/fix-customer-graph-XrzqK
fix: Add real trend indicators to customer graph and retheme About/Co…
2 parents 902dbe4 + 10b23e1 commit 495c0e4

File tree

3 files changed

+232
-72
lines changed

3 files changed

+232
-72
lines changed

src/components/DashboardViews/CustomerGraphView.tsx

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
ShieldAlert,
2121
ShieldCheck,
2222
Sparkles,
23+
TrendingDown,
24+
TrendingUp,
2325
UserRound,
2426
XCircle,
2527
ZoomIn,
@@ -105,6 +107,35 @@ const GRAPH_HEIGHT = 660;
105107
const RANGE_OPTIONS = ['7d', '30d', '90d', 'all'] as const;
106108
const TYPE_OPTIONS = ['all', 'voice', 'text'] as const;
107109

110+
const STATS_SNAPSHOT_KEY = 'clerktree_customer_graph_stats_snapshot_v1';
111+
112+
interface StatsSnapshot {
113+
totalCustomers: number;
114+
totalEdges: number;
115+
totalClusters: number;
116+
highRiskClusters: number;
117+
opportunityClusters: number;
118+
capturedAt: string;
119+
}
120+
121+
function loadStatsSnapshot(): StatsSnapshot | null {
122+
try {
123+
const raw = window.localStorage.getItem(STATS_SNAPSHOT_KEY);
124+
if (!raw) return null;
125+
return JSON.parse(raw) as StatsSnapshot;
126+
} catch {
127+
return null;
128+
}
129+
}
130+
131+
function saveStatsSnapshot(stats: StatsSnapshot): void {
132+
try {
133+
window.localStorage.setItem(STATS_SNAPSHOT_KEY, JSON.stringify(stats));
134+
} catch {
135+
// Ignore storage failures.
136+
}
137+
}
138+
108139
function formatPercent(value: number): string {
109140
return `${Math.round(value * 100)}%`;
110141
}
@@ -182,6 +213,7 @@ export default function CustomerGraphView({ isDark = true }: CustomerGraphViewPr
182213
const [selectedClusterId, setSelectedClusterId] = useState<string | null>(null);
183214
const [hoveredProfileId, setHoveredProfileId] = useState<string | null>(null);
184215

216+
const [previousStats, setPreviousStats] = useState<StatsSnapshot | null>(null);
185217
const [renderedNodes, setRenderedNodes] = useState<VisualNode[]>([]);
186218
const [renderedLinks, setRenderedLinks] = useState<VisualLink[]>([]);
187219

@@ -284,6 +316,24 @@ export default function CustomerGraphView({ isDark = true }: CustomerGraphViewPr
284316
setGraphModelFromView(model);
285317
}, [model, setGraphModelFromView]);
286318

319+
useEffect(() => {
320+
const prev = loadStatsSnapshot();
321+
if (prev) setPreviousStats(prev);
322+
}, []);
323+
324+
useEffect(() => {
325+
if (!model) return;
326+
const snapshot: StatsSnapshot = {
327+
totalCustomers: model.stats.totalCustomers,
328+
totalEdges: model.stats.totalEdges,
329+
totalClusters: model.stats.totalClusters,
330+
highRiskClusters: model.stats.highRiskClusters,
331+
opportunityClusters: model.stats.opportunityClusters,
332+
capturedAt: model.generatedAt,
333+
};
334+
saveStatsSnapshot(snapshot);
335+
}, [model]);
336+
287337
useEffect(() => {
288338
if (selectedPlaybookId || playbooks.length === 0) return;
289339
setSelectedPlaybookId(playbooks[0].id);
@@ -935,34 +985,39 @@ export default function CustomerGraphView({ isDark = true }: CustomerGraphViewPr
935985
<MetricCard
936986
title={t('customerGraph.kpi.customers')}
937987
value={model?.stats.totalCustomers ?? 0}
988+
previousValue={previousStats?.totalCustomers}
938989
icon={<UserRound className="w-4 h-4" />}
939990
color="cyan"
940991
isDark={isDark}
941992
/>
942993
<MetricCard
943994
title={t('customerGraph.kpi.clusters')}
944995
value={model?.stats.totalClusters ?? 0}
996+
previousValue={previousStats?.totalClusters}
945997
icon={<Network className="w-4 h-4" />}
946998
color="blue"
947999
isDark={isDark}
9481000
/>
9491001
<MetricCard
9501002
title={t('customerGraph.kpi.edges')}
9511003
value={model?.stats.totalEdges ?? 0}
1004+
previousValue={previousStats?.totalEdges}
9521005
icon={<Sparkles className="w-4 h-4" />}
9531006
color="purple"
9541007
isDark={isDark}
9551008
/>
9561009
<MetricCard
9571010
title={t('customerGraph.kpi.risk')}
9581011
value={model?.stats.highRiskClusters ?? 0}
1012+
previousValue={previousStats?.highRiskClusters}
9591013
icon={<ShieldAlert className="w-4 h-4" />}
9601014
color="orange"
9611015
isDark={isDark}
9621016
/>
9631017
<MetricCard
9641018
title={t('customerGraph.kpi.opportunity')}
9651019
value={model?.stats.opportunityClusters ?? 0}
1020+
previousValue={previousStats?.opportunityClusters}
9661021
icon={<Brain className="w-4 h-4" />}
9671022
color="emerald"
9681023
isDark={isDark}
@@ -1650,6 +1705,11 @@ export default function CustomerGraphView({ isDark = true }: CustomerGraphViewPr
16501705
<p className={cn('text-sm font-semibold', isDark ? 'text-white' : 'text-black')}>
16511706
{getLabel(selectedProfile)}
16521707
</p>
1708+
{selectedProfile.contact.email && (
1709+
<p className={cn('text-[11px] mt-1', isDark ? 'text-white/50' : 'text-black/50')}>
1710+
{selectedProfile.contact.email}
1711+
</p>
1712+
)}
16531713
<div className={cn('grid grid-cols-2 gap-2 mt-2 text-[11px]', isDark ? 'text-white/60' : 'text-black/60')}>
16541714
<div>
16551715
<p>{t('customerGraph.details.interactions')}</p>
@@ -1659,6 +1719,41 @@ export default function CustomerGraphView({ isDark = true }: CustomerGraphViewPr
16591719
<p>{t('customerGraph.details.lastSeen')}</p>
16601720
<p className={cn('font-semibold text-sm mt-0.5', isDark ? 'text-white' : 'text-black')}>{formatDate(selectedProfile.lastSeen)}</p>
16611721
</div>
1722+
<div>
1723+
<p>Risk</p>
1724+
<div className="flex items-center gap-1.5 mt-0.5">
1725+
<div className={cn('h-1.5 rounded-full flex-1 overflow-hidden', isDark ? 'bg-white/10' : 'bg-black/10')}>
1726+
<div
1727+
className={cn(
1728+
'h-full rounded-full',
1729+
selectedProfile.riskScore >= 0.65
1730+
? 'bg-rose-400'
1731+
: selectedProfile.riskScore >= 0.4
1732+
? 'bg-amber-400'
1733+
: 'bg-emerald-400',
1734+
)}
1735+
style={{ width: `${Math.max(4, Math.round(selectedProfile.riskScore * 100))}%` }}
1736+
/>
1737+
</div>
1738+
<span className={cn('font-semibold text-xs', isDark ? 'text-white' : 'text-black')}>
1739+
{Math.round(selectedProfile.riskScore * 100)}%
1740+
</span>
1741+
</div>
1742+
</div>
1743+
<div>
1744+
<p>Opportunity</p>
1745+
<div className="flex items-center gap-1.5 mt-0.5">
1746+
<div className={cn('h-1.5 rounded-full flex-1 overflow-hidden', isDark ? 'bg-white/10' : 'bg-black/10')}>
1747+
<div
1748+
className="h-full rounded-full bg-cyan-400"
1749+
style={{ width: `${Math.max(4, Math.round(selectedProfile.opportunityScore * 100))}%` }}
1750+
/>
1751+
</div>
1752+
<span className={cn('font-semibold text-xs', isDark ? 'text-white' : 'text-black')}>
1753+
{Math.round(selectedProfile.opportunityScore * 100)}%
1754+
</span>
1755+
</div>
1756+
</div>
16621757
</div>
16631758
</div>
16641759

@@ -2345,16 +2440,28 @@ export default function CustomerGraphView({ isDark = true }: CustomerGraphViewPr
23452440
function MetricCard({
23462441
title,
23472442
value,
2443+
previousValue,
23482444
icon,
23492445
color,
23502446
isDark,
23512447
}: {
23522448
title: string;
23532449
value: number;
2450+
previousValue?: number;
23542451
icon: ReactNode;
23552452
color: 'cyan' | 'blue' | 'purple' | 'orange' | 'emerald';
23562453
isDark: boolean;
23572454
}) {
2455+
const delta = previousValue !== undefined && previousValue !== 0
2456+
? value - previousValue
2457+
: null;
2458+
const deltaPercent = delta !== null && previousValue
2459+
? Math.round((delta / previousValue) * 100)
2460+
: null;
2461+
const isUp = delta !== null && delta > 0;
2462+
const isDown = delta !== null && delta < 0;
2463+
const isNeutral = delta === null || delta === 0;
2464+
23582465
return (
23592466
<div className={cn(
23602467
'relative overflow-hidden rounded-xl border p-4',
@@ -2380,9 +2487,34 @@ function MetricCard({
23802487
</span>
23812488
</div>
23822489

2383-
<p className={cn('text-3xl font-semibold mt-2', isDark ? 'text-white' : 'text-black')}>
2384-
{value}
2385-
</p>
2490+
<div className="flex items-end gap-2 mt-2">
2491+
<p className={cn('text-3xl font-semibold', isDark ? 'text-white' : 'text-black')}>
2492+
{value}
2493+
</p>
2494+
{!isNeutral && deltaPercent !== null && (
2495+
<span className={cn(
2496+
'inline-flex items-center gap-0.5 text-[11px] font-semibold px-1.5 py-0.5 rounded-md mb-1',
2497+
isUp
2498+
? (color === 'orange'
2499+
? (isDark ? 'bg-rose-500/15 text-rose-300' : 'bg-rose-50 text-rose-600')
2500+
: (isDark ? 'bg-emerald-500/15 text-emerald-300' : 'bg-emerald-50 text-emerald-600'))
2501+
: (color === 'orange'
2502+
? (isDark ? 'bg-emerald-500/15 text-emerald-300' : 'bg-emerald-50 text-emerald-600')
2503+
: (isDark ? 'bg-rose-500/15 text-rose-300' : 'bg-rose-50 text-rose-600')),
2504+
)}>
2505+
{isUp ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
2506+
{isUp ? '+' : ''}{deltaPercent}%
2507+
</span>
2508+
)}
2509+
{isNeutral && previousValue !== undefined && (
2510+
<span className={cn(
2511+
'inline-flex items-center gap-0.5 text-[11px] font-medium px-1.5 py-0.5 rounded-md mb-1',
2512+
isDark ? 'bg-white/5 text-white/45' : 'bg-black/5 text-black/45',
2513+
)}>
2514+
-
2515+
</span>
2516+
)}
2517+
</div>
23862518
</div>
23872519
</div>
23882520
);

src/pages/About.tsx

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,39 @@ export default function About() {
1818
description="Learn about ClerkTree's mission to transform legal and claims operations with intelligent automation. Meet our team and see our vision."
1919
canonical="https://clerktree.com/about"
2020
/>
21-
{/* Blue theme background accents */}
21+
{/* Orange/warm theme background accents matching main landing page */}
2222
<div className="fixed inset-0 bg-[rgb(10,10,10)] -z-10">
23+
{/* Core warm light source top-right */}
2324
<div
24-
className="absolute top-0 right-0 -translate-y-1/4 translate-x-1/4 w-96 md:w-[800px] h-96 md:h-[800px] opacity-40"
25+
className="absolute top-[-10%] right-[-10%] w-[80%] h-[100%] pointer-events-none"
2526
style={{
26-
background: 'radial-gradient(circle, rgba(59,130,246,0.6) 0%, rgba(59,130,246,0.25) 40%, transparent 100%)',
27-
filter: 'blur(80px)',
27+
background: 'radial-gradient(circle at 70% 30%, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0.06) 30%, transparent 60%)',
28+
filter: 'blur(40px)',
29+
}}
30+
/>
31+
{/* Diagonal ray wash */}
32+
<div
33+
className="absolute top-0 right-0 w-full h-full pointer-events-none"
34+
style={{
35+
background: 'linear-gradient(215deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.015) 40%, transparent 65%)',
2836
}}
2937
/>
38+
{/* Subtle orange ambient glow */}
3039
<div
31-
className="absolute bottom-0 left-0 translate-y-1/4 -translate-x-1/4 w-72 md:w-[600px] h-72 md:h-[600px] opacity-30"
40+
className="absolute bottom-0 left-0 translate-y-1/4 -translate-x-1/4 w-72 md:w-[600px] h-72 md:h-[600px] opacity-20"
3241
style={{
33-
background: 'radial-gradient(circle, rgba(29,78,216,0.5) 0%, rgba(29,78,216,0.2) 40%, transparent 100%)',
42+
background: 'radial-gradient(circle, rgba(255,138,91,0.4) 0%, rgba(255,138,91,0.15) 40%, transparent 100%)',
3443
filter: 'blur(80px)',
3544
}}
3645
/>
46+
{/* Grainy noise overlay */}
47+
<div
48+
className="absolute inset-0 opacity-[0.035] pointer-events-none"
49+
style={{
50+
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
51+
backgroundSize: '128px 128px',
52+
}}
53+
/>
3754
</div>
3855

3956
<SharedHeader />
@@ -43,12 +60,11 @@ export default function About() {
4360

4461
{/* Header */}
4562
<div className="text-center mb-20">
63+
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-[#FF8A5B] sm:text-sm mb-6">
64+
{t('about.future')}
65+
</p>
4666
<h1 className="text-5xl md:text-7xl font-bold tracking-tight">
4767
<span className="bg-gradient-to-r from-white via-white/95 to-white/90 text-transparent bg-clip-text">
48-
{t('about.future')}
49-
</span>
50-
<br />
51-
<span className="bg-gradient-to-r from-blue-400 via-cyan-400 to-blue-600 text-transparent bg-clip-text">
5268
{t('about.opsIntel')}
5369
</span>
5470
</h1>
@@ -59,10 +75,10 @@ export default function About() {
5975
<div className="w-full max-w-5xl">
6076
<h2 className="text-4xl md:text-5xl font-bold text-white tracking-tight mb-12 text-center">{t('about.whyTitle')}</h2>
6177
<div>
62-
<p className="text-lg md:text-xl text-white leading-relaxed">
78+
<p className="text-lg md:text-xl text-white/80 leading-relaxed">
6379
{t('about.whyDesc1')}
6480
</p>
65-
<p className="text-lg md:text-xl text-white leading-relaxed mt-6">
81+
<p className="text-lg md:text-xl text-white/80 leading-relaxed mt-6">
6682
{t('about.whyDesc2')}
6783
</p>
6884
</div>
@@ -73,7 +89,7 @@ export default function About() {
7389
<div className="mb-20 flex justify-center">
7490
<div className="w-full max-w-5xl">
7591
<h3 className="text-4xl md:text-5xl font-bold text-white tracking-tight mb-12 text-center">{t('about.missionTitle')}</h3>
76-
<p className="text-lg md:text-xl text-white leading-relaxed">
92+
<p className="text-lg md:text-xl text-white/80 leading-relaxed">
7793
{t('about.missionDesc')}
7894
</p>
7995
</div>
@@ -82,7 +98,7 @@ export default function About() {
8298
{/* Team Section */}
8399
<div className="mb-32 relative">
84100
{/* Background elements for modern feel */}
85-
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[120%] h-full bg-blue-500/5 blur-[120px] -z-10 rounded-full" />
101+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[120%] h-full bg-[#FF8A5B]/5 blur-[120px] -z-10 rounded-full" />
86102

87103
<div className="text-center mb-16 relative z-10">
88104
<h2 className="text-4xl md:text-5xl font-bold text-white tracking-tight mb-4">
@@ -147,9 +163,9 @@ export default function About() {
147163
].map((member, index) => (
148164
<div
149165
key={index}
150-
className="group relative w-full sm:w-[calc(50%-2rem)] lg:w-[calc(33.33%-2rem)] xl:w-[calc(30%-2rem)] min-w-[320px] max-w-md grow rounded-3xl bg-white/5 border border-white/10 hover:border-blue-500/30 transition-all duration-500 hover:-translate-y-2 hover:shadow-[0_0_30px_-5px_rgba(59,130,246,0.15)] overflow-hidden flex flex-col"
166+
className="group relative w-full sm:w-[calc(50%-2rem)] lg:w-[calc(33.33%-2rem)] xl:w-[calc(30%-2rem)] min-w-[320px] max-w-md grow rounded-3xl bg-white/5 border border-white/10 hover:border-[#FF8A5B]/30 transition-all duration-500 hover:-translate-y-2 hover:shadow-[0_0_30px_-5px_rgba(255,138,91,0.15)] overflow-hidden flex flex-col"
151167
>
152-
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-blue-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
168+
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-[#FF8A5B]/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
153169

154170
{/* LinkedIn Icon - Top Right */}
155171
{member.linkedin && (
@@ -172,7 +188,7 @@ export default function About() {
172188
)}
173189

174190
<div className="p-6 flex flex-col items-center h-full relative z-10">
175-
<div className="w-28 h-28 mb-6 rounded-full p-[2px] bg-gradient-to-br from-white/10 to-white/5 group-hover:from-blue-500/50 group-hover:to-cyan-500/50 transition-all duration-500">
191+
<div className="w-28 h-28 mb-6 rounded-full p-[2px] bg-gradient-to-br from-white/10 to-white/5 group-hover:from-[#FF8A5B]/50 group-hover:to-orange-400/50 transition-all duration-500">
176192
<div className="w-full h-full rounded-full overflow-hidden bg-black/50">
177193
<img
178194
src={member.image}
@@ -188,13 +204,13 @@ export default function About() {
188204
</div>
189205
</div>
190206

191-
<h3 className="text-lg font-bold text-white mb-3 text-center tracking-tight group-hover:text-blue-200 transition-colors">
207+
<h3 className="text-lg font-bold text-white mb-3 text-center tracking-tight group-hover:text-[#FF8A5B] transition-colors">
192208
{member.name}
193209
</h3>
194210

195211
<div className="flex-grow flex items-start justify-center">
196212
{member.description ? (
197-
<p className="text-sm text-white text-center leading-relaxed font-medium transition-colors">
213+
<p className="text-sm text-white/80 text-center leading-relaxed font-medium transition-colors">
198214
{member.description}
199215
</p>
200216
) : (
@@ -211,8 +227,8 @@ export default function About() {
211227

212228

213229
{/* CTA */}
214-
<div className="text-center py-16 rounded-2xl border border-blue-500/20 bg-gradient-to-br from-blue-500/5 to-cyan-500/5 backdrop-blur-sm">
215-
<h2 className="text-3xl md:text-4xl font-bold mb-4 bg-gradient-to-r from-blue-400 to-cyan-400 text-transparent bg-clip-text">
230+
<div className="text-center py-16 rounded-2xl border border-[#FF8A5B]/20 bg-gradient-to-br from-[#FF8A5B]/5 to-orange-500/5 backdrop-blur-sm">
231+
<h2 className="text-3xl md:text-4xl font-bold mb-4 bg-gradient-to-r from-[#FF8A5B] to-orange-400 text-transparent bg-clip-text">
216232
{t('about.joinTitle')}
217233
</h2>
218234
<p className="text-white/60 mb-8 max-w-2xl mx-auto">
@@ -221,7 +237,7 @@ export default function About() {
221237
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
222238
<button
223239
onClick={() => navigate('/contact')}
224-
className="px-8 py-4 rounded-xl bg-gradient-to-r from-blue-500/20 to-cyan-500/20 border-2 border-blue-500/30 text-white font-semibold hover:from-blue-500/30 hover:to-cyan-500/30 hover:border-blue-500/50 transition-all duration-300"
240+
className="px-8 py-4 rounded-xl bg-[#E5E5E5] hover:bg-white text-black font-semibold transition-all duration-300"
225241
>
226242
{t('about.getInTouch')}
227243
</button>
@@ -247,4 +263,3 @@ export default function About() {
247263
</div>
248264
);
249265
}
250-

0 commit comments

Comments
 (0)