import { FullPageEmptyState } from '@/components/full-page-empty-state'; import { InsightCard } from '@/components/insights/insight-card'; import { PageContainer } from '@/components/page-container'; import { PageHeader } from '@/components/page-header'; import { Skeleton } from '@/components/skeleton'; import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, } from '@/components/ui/carousel'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { TableButtons } from '@/components/ui/table'; import { useTRPC } from '@/integrations/trpc/react'; import { cn } from '@/utils/cn'; import { PAGE_TITLES, createProjectTitle } from '@/utils/title'; import { useQuery } from '@tanstack/react-query'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { parseAsString, parseAsStringEnum, useQueryState } from 'nuqs'; import { useMemo } from 'react'; export const Route = createFileRoute( '/_app/$organizationId/$projectId/insights', )({ component: Component, head: () => { return { meta: [ { title: createProjectTitle(PAGE_TITLES.INSIGHTS), }, ], }; }, }); type SortOption = | 'impact-desc' | 'impact-asc' | 'severity-desc' | 'severity-asc' | 'recent'; function getModuleDisplayName(moduleKey: string): string { const displayNames: Record = { geo: 'Geographic', devices: 'Devices', referrers: 'Referrers', 'entry-pages': 'Entry Pages', 'page-trends': 'Page Trends', 'exit-pages': 'Exit Pages', 'traffic-anomalies': 'Anomalies', }; return displayNames[moduleKey] || moduleKey.replace('-', ' '); } function Component() { const { projectId } = Route.useParams(); const trpc = useTRPC(); const { data: insights, isLoading } = useQuery( trpc.insight.listAll.queryOptions({ projectId, limit: 500, }), ); const navigate = useNavigate(); const [search, setSearch] = useQueryState( 'search', parseAsString.withDefault(''), ); const [moduleFilter, setModuleFilter] = useQueryState( 'module', parseAsString.withDefault('all'), ); const [windowKindFilter, setWindowKindFilter] = useQueryState( 'window', parseAsStringEnum([ 'all', 'yesterday', 'rolling_7d', 'rolling_30d', ]).withDefault('all'), ); const [severityFilter, setSeverityFilter] = useQueryState( 'severity', parseAsStringEnum(['all', 'severe', 'moderate', 'low', 'none']).withDefault( 'all', ), ); const [directionFilter, setDirectionFilter] = useQueryState( 'direction', parseAsStringEnum(['all', 'up', 'down', 'flat']).withDefault('all'), ); const [sortBy, setSortBy] = useQueryState( 'sort', parseAsStringEnum([ 'impact-desc', 'impact-asc', 'severity-desc', 'severity-asc', 'recent', ]).withDefault('impact-desc'), ); const filteredAndSorted = useMemo(() => { if (!insights) return []; const filtered = insights.filter((insight) => { // Search filter if (search) { const searchLower = search.toLowerCase(); const matchesTitle = insight.title.toLowerCase().includes(searchLower); const matchesSummary = insight.summary ?.toLowerCase() .includes(searchLower); const matchesDimension = insight.dimensionKey .toLowerCase() .includes(searchLower); if (!matchesTitle && !matchesSummary && !matchesDimension) { return false; } } // Module filter if (moduleFilter !== 'all' && insight.moduleKey !== moduleFilter) { return false; } // Window kind filter if ( windowKindFilter !== 'all' && insight.windowKind !== windowKindFilter ) { return false; } // Severity filter if (severityFilter !== 'all') { if (severityFilter === 'none' && insight.severityBand) return false; if ( severityFilter !== 'none' && insight.severityBand !== severityFilter ) return false; } // Direction filter if (directionFilter !== 'all' && insight.direction !== directionFilter) { return false; } return true; }); // Sort (create new array to avoid mutation) const sorted = [...filtered].sort((a, b) => { switch (sortBy) { case 'impact-desc': return (b.impactScore ?? 0) - (a.impactScore ?? 0); case 'impact-asc': return (a.impactScore ?? 0) - (b.impactScore ?? 0); case 'severity-desc': { const severityOrder: Record = { severe: 3, moderate: 2, low: 1, }; const aSev = severityOrder[a.severityBand ?? ''] ?? 0; const bSev = severityOrder[b.severityBand ?? ''] ?? 0; return bSev - aSev; } case 'severity-asc': { const severityOrder: Record = { severe: 3, moderate: 2, low: 1, }; const aSev = severityOrder[a.severityBand ?? ''] ?? 0; const bSev = severityOrder[b.severityBand ?? ''] ?? 0; return aSev - bSev; } case 'recent': return ( new Date(b.firstDetectedAt ?? 0).getTime() - new Date(a.firstDetectedAt ?? 0).getTime() ); default: return 0; } }); return sorted; }, [ insights, search, moduleFilter, windowKindFilter, severityFilter, directionFilter, sortBy, ]); // Group insights by module const groupedByModule = useMemo(() => { const groups = new Map(); for (const insight of filteredAndSorted) { const existing = groups.get(insight.moduleKey) ?? []; existing.push(insight); groups.set(insight.moduleKey, existing); } // Sort modules by impact (referrers first, then by average impact score) return Array.from(groups.entries()).sort( ([keyA, insightsA], [keyB, insightsB]) => { // Referrers always first if (keyA === 'referrers') return -1; if (keyB === 'referrers') return 1; // Calculate average impact for each module const avgImpactA = insightsA.reduce((sum, i) => sum + (i.impactScore ?? 0), 0) / insightsA.length; const avgImpactB = insightsB.reduce((sum, i) => sum + (i.impactScore ?? 0), 0) / insightsB.length; // Sort by average impact (high to low) return avgImpactB - avgImpactA; }, ); }, [filteredAndSorted]); if (isLoading) { return (
{Array.from({ length: 3 }, (_, i) => `section-${i}`).map((key) => (
{Array.from({ length: 4 }, (_, i) => `skeleton-${i}`).map( (cardKey) => ( ), )}
))}
); } return ( void setSearch(e.target.value || null)} className="max-w-xs" /> {filteredAndSorted.length === 0 && !isLoading && ( )} {groupedByModule.length > 0 && (
{groupedByModule.map(([moduleKey, moduleInsights]) => (

{getModuleDisplayName(moduleKey)}

{moduleInsights.length}{' '} {moduleInsights.length === 1 ? 'insight' : 'insights'}
{moduleInsights.map((insight, index) => ( { const filterString = insight.payload?.dimensions .map( (dim) => `${dim.key},is,${encodeURIComponent(dim.value)}`, ) .join(';'); if (filterString) { return () => { navigate({ to: '/$organizationId/$projectId', from: Route.fullPath, search: { f: filterString, }, }); }; } return undefined; })()} /> ))}
))}
)} {filteredAndSorted.length > 0 && (
Showing {filteredAndSorted.length} of {insights?.length ?? 0} insights
)}
); }