'use client'; import { useState, useMemo } from 'react'; import { useReactTable, getCoreRowModel, getFilteredRowModel, getSortedRowModel, getPaginationRowModel, ColumnDef, flexRender, SortingState, ColumnFiltersState, } from '@tanstack/react-table'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, LineChart, Line } from 'recharts'; import { Star, TrendingUp, Image, FileText, MessageSquare, Calendar, ArrowUpDown, ArrowUp, ArrowDown, Search, Download, Filter, AlertTriangle, ThumbsUp, ThumbsDown } from 'lucide-react'; import { Review, calculateReviewStats, getSentimentLabel, getSentimentColor, DateRange, filterReviewsByDateRange, calculateTimelineData } from '@/lib/analytics'; interface ReviewAnalyticsProps { reviews: Review[]; businessName?: string; } export default function ReviewAnalytics({ reviews, businessName }: ReviewAnalyticsProps) { const [sorting, setSorting] = useState([{ id: 'date', desc: true }]); // Default: newest first const [columnFilters, setColumnFiltersState] = useState([]); const [globalFilter, setGlobalFilter] = useState(''); const [selectedRatings, setSelectedRatings] = useState([1, 2, 3, 4, 5]); const [selectedSentiments, setSelectedSentiments] = useState<('positive' | 'neutral' | 'negative')[]>(['positive', 'neutral', 'negative']); const [dateRange, setDateRange] = useState('all'); // Filter reviews by date range const dateFilteredReviews = useMemo(() => { return filterReviewsByDateRange(reviews, dateRange); }, [reviews, dateRange]); // Calculate statistics on date-filtered reviews const stats = useMemo(() => calculateReviewStats(dateFilteredReviews), [dateFilteredReviews]); // Calculate timeline data for chart const timelineData = useMemo(() => calculateTimelineData(dateFilteredReviews), [dateFilteredReviews]); // Filter reviews by selected ratings and sentiments (for table) const filteredReviews = useMemo(() => { return dateFilteredReviews.filter(r => { const matchesRating = selectedRatings.includes(r.rating); const sentiment = getSentimentLabel(r.rating); const matchesSentiment = selectedSentiments.includes(sentiment); const matchesSearch = !globalFilter || r.author.toLowerCase().includes(globalFilter.toLowerCase()) || r.text?.toLowerCase().includes(globalFilter.toLowerCase()) || r.date_text.toLowerCase().includes(globalFilter.toLowerCase()); return matchesRating && matchesSentiment && matchesSearch; }); }, [dateFilteredReviews, selectedRatings, selectedSentiments, globalFilter]); const toggleRating = (rating: number) => { setSelectedRatings(prev => prev.includes(rating) ? prev.filter(r => r !== rating) : [...prev, rating] ); }; const toggleSentiment = (sentiment: 'positive' | 'neutral' | 'negative') => { setSelectedSentiments(prev => prev.includes(sentiment) ? prev.filter(s => s !== sentiment) : [...prev, sentiment] ); }; const clearAllFilters = () => { setDateRange('all'); setSelectedRatings([1, 2, 3, 4, 5]); setSelectedSentiments(['positive', 'neutral', 'negative']); setGlobalFilter(''); }; const hasActiveFilters = dateRange !== 'all' || selectedRatings.length < 5 || selectedSentiments.length < 3 || globalFilter !== ''; const exportFilteredData = () => { const dataStr = JSON.stringify(filteredReviews, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = `reviews-filtered-${dateRange}-${new Date().toISOString().split('T')[0]}.json`; link.click(); }; // Chart colors const COLORS = { positive: '#16a34a', neutral: '#ca8a04', negative: '#dc2626', }; // Table columns const columns = useMemo[]>( () => [ { accessorKey: 'author', header: ({ column }) => { return ( ); }, cell: ({ row }) => (
{row.original.avatar_url && ( {row.original.author} )} {row.original.author}
), }, { accessorKey: 'rating', header: ({ column }) => { return ( ); }, cell: ({ row }) => (
{[...Array(5)].map((_, i) => ( ))} {row.original.rating}
), filterFn: (row, id, value) => { return value.includes(row.getValue(id)); }, }, { accessorKey: 'centerDate', id: 'date', header: ({ column }) => { return ( ); }, sortingFn: (rowA, rowB) => { const dateA = rowA.original.centerDate?.getTime() || 0; const dateB = rowB.original.centerDate?.getTime() || 0; return dateA - dateB; }, cell: ({ row }) => { const formatDate = (date: Date) => { return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); }; const getUncertaintyDays = (minDate: Date, maxDate: Date) => { const diffMs = Math.abs(maxDate.getTime() - minDate.getTime()); return Math.round(diffMs / (1000 * 60 * 60 * 24)); }; return (
{row.original.date_text}
{row.original.minDate && row.original.maxDate && row.original.centerDate && (
Range: {formatDate(row.original.maxDate)} - {formatDate(row.original.minDate)}
Center: {formatDate(row.original.centerDate)}
±{getUncertaintyDays(row.original.minDate, row.original.maxDate)} days uncertainty
)}
); }, }, { accessorKey: 'text', header: 'Review', cell: ({ row }) => { const [expanded, setExpanded] = useState(false); const text = row.original.text || 'No review text'; const sentiment = getSentimentLabel(row.original.rating); return (
{sentiment.toUpperCase()}

{text}

{text.length > 100 && ( )}
); }, }, ], [] ); const table = useReactTable({ data: filteredReviews, columns, state: { sorting, }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 10, }, }, }); return (
{/* Header */}

{businessName ? `${businessName} - Analytics` : 'Review Analytics'}

Comprehensive insights from {reviews.length} total reviews

{/* Enhanced Filters */}
{/* Time Period Filter */}
Time Period: {(['week', 'month', 'year', 'all'] as DateRange[]).map((range) => ( ))}
{/* Sentiment Filter */}
Sentiment: {(['positive', 'neutral', 'negative'] as const).map((sentiment) => ( ))}
{/* Filter Summary */}
Showing {filteredReviews.length} of {reviews.length} reviews {hasActiveFilters && (filtered)} {hasActiveFilters && ( )}
{/* KPI Cards */}
{/* Average Rating */}
Avg Rating
{stats.averageRating.toFixed(1)}★
{stats.totalReviews} total reviews
{/* Positive Reviews */}
{ setSelectedSentiments(['positive']); setDateRange('all'); }}>
Positive
{stats.sentimentBreakdown.positive}
{stats.sentimentScore.toFixed(0)}% positive (4-5★)
{/* Neutral Reviews */}
{ setSelectedSentiments(['neutral']); setDateRange('all'); }}>
Neutral
{stats.sentimentBreakdown.neutral}
{((stats.sentimentBreakdown.neutral / stats.totalReviews) * 100).toFixed(0)}% neutral (3★)
{/* Negative Reviews - Alert */}
{ setSelectedSentiments(['negative']); setDateRange('all'); }}>
Negative
{stats.negativeReviews}
{((stats.negativeReviews / stats.totalReviews) * 100).toFixed(0)}% negative (1-2★)
{/* Recent Activity */}
setDateRange('month')}>
Recent
{stats.recentReviews}
last 30 days
{/* Review Length */}
Avg Length
{stats.avgReviewLength}
words per review
{/* Photos */}
With Photos
{stats.photoCount}
{((stats.photoCount / stats.totalReviews) * 100).toFixed(0)}% have avatars
{/* Total Reviews */}
Total
{stats.totalReviews}
all time
{/* Rating Timeline with Rolling Average */} {timelineData.length > 0 && (

Rating Trend Over Time

)} {/* Charts Grid */}
{/* Rating Distribution - Interactive */}

Rating Distribution (click to filter)

{ if (data && data.activePayload && data.activePayload[0]) { const rating = data.activePayload[0].payload.rating; setSelectedRatings([rating]); setSelectedSentiments(['positive', 'neutral', 'negative']); } }} style={{ cursor: 'pointer' }} > { if (active && payload && payload.length) { return (

{payload[0].payload.rating}★

{payload[0].value} reviews ({payload[0].payload.percentage.toFixed(1)}%)

Click to filter

); } return null; }} />
{/* Sentiment Breakdown - Interactive */}

Sentiment Breakdown (click to filter)

`${name} ${(percent * 100).toFixed(0)}%`} outerRadius={80} fill="#8884d8" dataKey="value" style={{ fontWeight: 700, fontSize: '13px', cursor: 'pointer' }} onClick={(data) => { if (data && data.sentiment) { setSelectedSentiments([data.sentiment as 'positive' | 'neutral' | 'negative']); setSelectedRatings([1, 2, 3, 4, 5]); } }} > { if (active && payload && payload.length) { return (

{payload[0].name}

{payload[0].value} reviews

Click to filter

); } return null; }} />
{/* Top Keywords */}

Top Keywords

{/* Reviews Table */}

Review Details

{/* Search */}
setGlobalFilter(e.target.value)} placeholder="Search by author, review text, or date..." className="w-full pl-10 pr-4 py-3 border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-medium" />
{/* Table */}
{table.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map(header => ( ))} ))} {table.getRowModel().rows.map(row => ( {row.getVisibleCells().map(cell => ( ))} ))}
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{/* Pagination */}
Showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} to{' '} {Math.min((table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, filteredReviews.length)} of{' '} {filteredReviews.length} reviews
); }