Skip to content

Commit e2fc0f5

Browse files
committed
fix(insights): full-page layout, single-row expand, sort, reduce button noise
- Remove card-in-card: use h-full overflow-y-auto with border-b rows directly, matching monitors/links page layout - Severity left border accent (red/amber/blue) for instant visual scanning - Only one row expanded at a time (accordion) for cleaner reading - Full row is clickable to toggle expand/collapse, including expanded area - Dismiss button appears on row hover (group-hover:opacity pattern) - Reduced expanded actions from 7 to: Investigate CTA + View analytics link + kebab menu (copy/link) + compact feedback thumbs pushed right - Added sort dropdown: Priority (default), Newest, Biggest change - Filter bar is a border-b strip between header and list, not a card header
1 parent 14b2c7c commit e2fc0f5

2 files changed

Lines changed: 371 additions & 383 deletions

File tree

apps/dashboard/app/(main)/insights/_components/insight-card.tsx

Lines changed: 103 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CaretDownIcon,
77
ChartLineUpIcon,
88
CopyIcon,
9+
DotsThreeIcon,
910
GaugeIcon,
1011
LightningIcon,
1112
LinkIcon,
@@ -19,7 +20,7 @@ import {
1920
XIcon,
2021
} from "@phosphor-icons/react";
2122
import Link from "next/link";
22-
import { type ReactNode, useMemo, useState } from "react";
23+
import { type ReactNode, useMemo } from "react";
2324
import { toast } from "sonner";
2425
import type {
2526
Insight,
@@ -30,10 +31,15 @@ import type {
3031
import {
3132
buildInsightCopyText,
3233
buildInsightShareUrl,
33-
formatComparisonWindow,
3434
formatInsightFreshness,
3535
} from "@/app/(main)/insights/lib/insight-meta";
3636
import type { InsightFeedbackVote } from "@/app/(main)/insights/lib/insight-feedback-vote";
37+
import {
38+
DropdownMenu,
39+
DropdownMenuContent,
40+
DropdownMenuItem,
41+
DropdownMenuTrigger,
42+
} from "@/components/ui/dropdown-menu";
3743
import { Skeleton } from "@/components/ui/skeleton";
3844
import { cn } from "@/lib/utils";
3945

@@ -108,22 +114,10 @@ const TYPE_STYLES: Record<
108114
},
109115
};
110116

111-
const SEVERITY_STYLES: Record<
112-
InsightSeverity,
113-
{ label: string; color: string }
114-
> = {
115-
critical: {
116-
label: "Critical",
117-
color: "text-red-600 dark:text-red-400",
118-
},
119-
warning: {
120-
label: "Warning",
121-
color: "text-amber-600 dark:text-amber-400",
122-
},
123-
info: {
124-
label: "Info",
125-
color: "text-blue-600 dark:text-blue-400",
126-
},
117+
const SEVERITY_BORDER: Record<InsightSeverity, string> = {
118+
critical: "border-l-red-500",
119+
warning: "border-l-amber-500",
120+
info: "border-l-blue-400",
127121
};
128122

129123
const SENTIMENT_STYLES: Record<
@@ -159,33 +153,32 @@ function buildDiagnosticPrompt(insight: Insight): string {
159153

160154
export interface InsightCardProps {
161155
insight: Insight;
156+
expanded: boolean;
157+
onToggleAction: () => void;
162158
onDismissAction?: () => void;
163159
feedbackVote?: InsightFeedbackVote | null;
164160
onFeedbackAction?: (vote: InsightFeedbackVote | null) => void;
165161
}
166162

167163
export function InsightCard({
168164
insight,
165+
expanded,
166+
onToggleAction,
169167
onDismissAction,
170168
feedbackVote,
171169
onFeedbackAction,
172170
}: InsightCardProps) {
173-
const [expanded, setExpanded] = useState(false);
174171
const typeStyle = TYPE_STYLES[insight.type];
175-
const severityStyle = SEVERITY_STYLES[insight.severity];
176172
const sentimentStyle = SENTIMENT_STYLES[insight.sentiment];
173+
const freshnessLine = formatInsightFreshness(insight);
177174

178175
const agentHref = useMemo(() => {
179176
const chatId = crypto.randomUUID();
180177
const prompt = encodeURIComponent(buildDiagnosticPrompt(insight));
181178
return `/websites/${insight.websiteId}/agent/${chatId}?prompt=${prompt}`;
182179
}, [insight]);
183180

184-
const comparisonLine = formatComparisonWindow(insight);
185-
const freshnessLine = formatInsightFreshness(insight);
186-
187-
const copySummaryAction = async (e: React.MouseEvent) => {
188-
e.stopPropagation();
181+
const copySummaryAction = async () => {
189182
try {
190183
await navigator.clipboard.writeText(buildInsightCopyText(insight));
191184
toast.success("Copied insight to clipboard");
@@ -194,8 +187,7 @@ export function InsightCard({
194187
}
195188
};
196189

197-
const copyLinkAction = async (e: React.MouseEvent) => {
198-
e.stopPropagation();
190+
const copyLinkAction = async () => {
199191
const url = buildInsightShareUrl(insight.id);
200192
if (!url) {
201193
return;
@@ -211,15 +203,23 @@ export function InsightCard({
211203
return (
212204
<div
213205
className={cn(
214-
"scroll-mt-24 transition-colors",
206+
"group scroll-mt-24 border-b border-l-2 transition-colors",
207+
SEVERITY_BORDER[insight.severity],
215208
expanded ? "bg-accent/20" : "hover:bg-accent/40"
216209
)}
217210
id={`insight-${insight.id}`}
218211
>
219-
<button
220-
className="flex w-full items-start gap-3 px-4 py-3 text-left sm:px-6"
221-
onClick={() => setExpanded((v) => !v)}
222-
type="button"
212+
<div
213+
className="flex cursor-pointer items-start gap-3 px-4 py-3 sm:px-6"
214+
onClick={onToggleAction}
215+
onKeyDown={(e) => {
216+
if (e.key === "Enter" || e.key === " ") {
217+
e.preventDefault();
218+
onToggleAction();
219+
}
220+
}}
221+
role="button"
222+
tabIndex={0}
223223
>
224224
<div
225225
className={cn(
@@ -236,9 +236,19 @@ export function InsightCard({
236236
{insight.title}
237237
</p>
238238
<div className="flex shrink-0 items-center gap-1.5">
239-
<span className={cn("text-[11px]", severityStyle.color)}>
240-
{severityStyle.label}
241-
</span>
239+
{onDismissAction && (
240+
<button
241+
aria-label="Dismiss insight"
242+
className="flex size-6 items-center justify-center rounded text-muted-foreground opacity-0 transition-all hover:bg-accent hover:text-foreground group-hover:opacity-100"
243+
onClick={(e) => {
244+
e.stopPropagation();
245+
onDismissAction();
246+
}}
247+
type="button"
248+
>
249+
<XIcon className="size-3" weight="bold" />
250+
</button>
251+
)}
242252
<span className="font-mono text-[11px] text-muted-foreground tabular-nums">
243253
{insight.priority}/10
244254
</span>
@@ -283,20 +293,25 @@ export function InsightCard({
283293
</p>
284294
)}
285295
</div>
286-
</button>
296+
</div>
287297

288298
{expanded && (
289-
<div className="space-y-2.5 px-4 pb-3 pl-14 sm:pl-15">
290-
<p className="text-pretty text-muted-foreground text-xs leading-relaxed">
299+
<div
300+
className="space-y-2.5 px-4 pb-4 pl-14 sm:pl-15"
301+
onClick={onToggleAction}
302+
onKeyDown={(e) => {
303+
if (e.key === "Enter" || e.key === " ") {
304+
e.preventDefault();
305+
onToggleAction();
306+
}
307+
}}
308+
role="button"
309+
tabIndex={-1}
310+
>
311+
<p className="text-pretty text-muted-foreground text-sm leading-relaxed">
291312
{insight.description}
292313
</p>
293314

294-
{comparisonLine && (
295-
<p className="text-balance text-muted-foreground/70 text-[11px]">
296-
{comparisonLine}
297-
</p>
298-
)}
299-
300315
<div className="flex items-start gap-2 rounded bg-accent/60 px-2.5 py-2">
301316
<SparkleIcon
302317
className="mt-px size-3 shrink-0 text-primary"
@@ -307,67 +322,62 @@ export function InsightCard({
307322
</p>
308323
</div>
309324

310-
<div className="flex flex-wrap items-center gap-2">
325+
<div
326+
className="flex items-center gap-2 pt-0.5"
327+
onClick={(e) => e.stopPropagation()}
328+
onKeyDown={(e) => e.stopPropagation()}
329+
role="group"
330+
>
311331
<Link
312332
className="inline-flex items-center gap-1.5 rounded bg-primary px-3 py-1.5 font-medium text-primary-foreground text-xs transition-opacity hover:opacity-90"
313333
href={agentHref}
314-
onClick={(e) => e.stopPropagation()}
315334
>
316-
Investigate with Databunny
335+
Investigate
317336
<ArrowRightIcon className="size-3" weight="fill" />
318337
</Link>
319338
<Link
320-
className="inline-flex items-center gap-1.5 rounded border px-3 py-1.5 font-medium text-foreground text-xs transition-colors hover:bg-accent"
339+
className="text-muted-foreground text-xs transition-colors hover:text-foreground"
321340
href={insight.link}
322-
onClick={(e) => e.stopPropagation()}
323341
>
324342
View analytics
325343
</Link>
326-
<button
327-
className="inline-flex items-center gap-1 rounded border px-2 py-1.5 text-muted-foreground text-xs transition-colors hover:bg-accent hover:text-foreground"
328-
onClick={copySummaryAction}
329-
type="button"
330-
>
331-
<CopyIcon aria-hidden className="size-3.5" weight="duotone" />
332-
</button>
333-
<button
334-
className="inline-flex items-center gap-1 rounded border px-2 py-1.5 text-muted-foreground text-xs transition-colors hover:bg-accent hover:text-foreground"
335-
onClick={copyLinkAction}
336-
type="button"
337-
>
338-
<LinkIcon aria-hidden className="size-3.5" weight="duotone" />
339-
</button>
340344

341-
{onDismissAction && (
342-
<button
343-
aria-label="Dismiss insight"
344-
className="inline-flex items-center gap-1 rounded border px-2 py-1.5 text-muted-foreground text-xs transition-colors hover:bg-accent hover:text-foreground"
345-
onClick={(e) => {
346-
e.stopPropagation();
347-
onDismissAction();
348-
}}
349-
type="button"
350-
>
351-
<XIcon aria-hidden className="size-3.5" weight="bold" />
352-
</button>
353-
)}
345+
<DropdownMenu>
346+
<DropdownMenuTrigger asChild>
347+
<button
348+
aria-label="More actions"
349+
className="flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
350+
type="button"
351+
>
352+
<DotsThreeIcon className="size-4" weight="bold" />
353+
</button>
354+
</DropdownMenuTrigger>
355+
<DropdownMenuContent align="start" className="w-40">
356+
<DropdownMenuItem onClick={copySummaryAction}>
357+
<CopyIcon className="size-4" weight="duotone" />
358+
Copy insight
359+
</DropdownMenuItem>
360+
<DropdownMenuItem onClick={copyLinkAction}>
361+
<LinkIcon className="size-4" weight="duotone" />
362+
Copy link
363+
</DropdownMenuItem>
364+
</DropdownMenuContent>
365+
</DropdownMenu>
354366

355367
{onFeedbackAction && (
356-
<>
357-
<span className="mx-0.5 h-4 w-px bg-border" />
368+
<div className="ml-auto flex items-center gap-1">
358369
<button
359370
aria-label="Mark as helpful"
360371
aria-pressed={feedbackVote === "up"}
361372
className={cn(
362-
"inline-flex size-7 items-center justify-center rounded border transition-colors",
373+
"flex size-6 items-center justify-center rounded transition-colors",
363374
feedbackVote === "up"
364-
? "border-primary bg-primary/10 text-primary"
365-
: "text-muted-foreground hover:bg-accent hover:text-foreground"
375+
? "bg-primary/10 text-primary"
376+
: "text-muted-foreground/50 hover:text-foreground"
366377
)}
367-
onClick={(e) => {
368-
e.stopPropagation();
369-
onFeedbackAction(feedbackVote === "up" ? null : "up");
370-
}}
378+
onClick={() =>
379+
onFeedbackAction(feedbackVote === "up" ? null : "up")
380+
}
371381
type="button"
372382
>
373383
<ThumbsUpIcon className="size-3.5" weight="duotone" />
@@ -376,20 +386,19 @@ export function InsightCard({
376386
aria-label="Mark as not helpful"
377387
aria-pressed={feedbackVote === "down"}
378388
className={cn(
379-
"inline-flex size-7 items-center justify-center rounded border transition-colors",
389+
"flex size-6 items-center justify-center rounded transition-colors",
380390
feedbackVote === "down"
381-
? "border-destructive bg-destructive/10 text-destructive"
382-
: "text-muted-foreground hover:bg-accent hover:text-foreground"
391+
? "bg-destructive/10 text-destructive"
392+
: "text-muted-foreground/50 hover:text-foreground"
383393
)}
384-
onClick={(e) => {
385-
e.stopPropagation();
386-
onFeedbackAction(feedbackVote === "down" ? null : "down");
387-
}}
394+
onClick={() =>
395+
onFeedbackAction(feedbackVote === "down" ? null : "down")
396+
}
388397
type="button"
389398
>
390399
<ThumbsDownIcon className="size-3.5" weight="duotone" />
391400
</button>
392-
</>
401+
</div>
393402
)}
394403
</div>
395404
</div>
@@ -400,7 +409,7 @@ export function InsightCard({
400409

401410
export function InsightCardSkeleton() {
402411
return (
403-
<div className="flex items-start gap-3 px-4 py-3 sm:px-6">
412+
<div className="flex items-start gap-3 border-b px-4 py-3 sm:px-6">
404413
<Skeleton className="mt-0.5 size-7 shrink-0 rounded" />
405414
<div className="min-w-0 flex-1 space-y-2">
406415
<div className="flex items-start justify-between gap-2">

0 commit comments

Comments
 (0)