Initial commit - WhyRating Engine (Google Reviews Scraper)

This commit is contained in:
Alejandro Gutiérrez
2026-02-02 18:19:00 +00:00
parent 0543a08242
commit 2206ddeff2
136 changed files with 51138 additions and 855 deletions

View File

@@ -42,7 +42,7 @@ export function BarChartWidget({
error,
onRefresh,
}: BarChartWidgetProps) {
const chartConfig = config.config as ChartWidgetConfig;
const chartConfig = config.config as unknown as ChartWidgetConfig;
const chartData = data?.data || [];
return (
@@ -58,25 +58,25 @@ export function BarChartWidget({
margin={{ top: 5, right: 20, left: 0, bottom: 5 }}
>
{chartConfig.show_grid !== false && (
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<CartesianGrid strokeDasharray="3 3" stroke="#d1d5db" />
)}
<XAxis
dataKey={chartConfig.x_axis?.key || 'x'}
tick={{ fontSize: 12 }}
tick={{ fontSize: 12, fill: '#374151' }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
axisLine={{ stroke: '#d1d5db' }}
/>
<YAxis
tick={{ fontSize: 12 }}
tick={{ fontSize: 12, fill: '#374151' }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
axisLine={{ stroke: '#d1d5db' }}
label={
chartConfig.y_axis?.label
? {
value: chartConfig.y_axis.label,
angle: -90,
position: 'insideLeft',
style: { fontSize: 12 },
style: { fontSize: 12, fill: '#374151' },
}
: undefined
}

View File

@@ -26,7 +26,7 @@ export function DataTableWidget({
onPageChange,
currentPage = 1,
}: DataTableWidgetProps) {
const tableConfig = config.config as TableWidgetConfig;
const tableConfig = config.config as unknown as TableWidgetConfig;
const rows = data?.data || [];
const total = data?.total || 0;
const pageSize = tableConfig.page_size || 10;
@@ -42,13 +42,13 @@ export function DataTableWidget({
<div className="flex flex-col h-full">
{/* Table */}
<div className="flex-1 overflow-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800 sticky top-0">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50 sticky top-0">
<tr>
{tableConfig.columns.map((col) => (
<th
key={col.key}
className={`px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider ${
className={`px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider ${
col.align === 'right'
? 'text-right'
: col.align === 'center'
@@ -62,16 +62,16 @@ export function DataTableWidget({
))}
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tbody className="bg-white divide-y divide-gray-200">
{rows.map((row, rowIndex) => (
<tr
key={row[tableConfig.row_key] as string || rowIndex}
className="hover:bg-gray-50 dark:hover:bg-gray-800"
className="hover:bg-gray-50"
>
{tableConfig.columns.map((col) => (
<td
key={col.key}
className={`px-4 py-3 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap ${
className={`px-4 py-3 text-sm text-gray-900 whitespace-nowrap ${
col.align === 'right'
? 'text-right'
: col.align === 'center'
@@ -90,8 +90,8 @@ export function DataTableWidget({
{/* Pagination */}
{tableConfig.show_pagination !== false && totalPages > 1 && onPageChange && (
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<div className="text-sm text-gray-500">
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200 bg-gray-50">
<div className="text-sm text-gray-600">
Showing {(currentPage - 1) * pageSize + 1} to{' '}
{Math.min(currentPage * pageSize, total)} of {total}
</div>
@@ -99,17 +99,17 @@ export function DataTableWidget({
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage <= 1}
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="p-1 rounded text-gray-600 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeft className="w-5 h-5" />
</button>
<span className="text-sm text-gray-700 dark:text-gray-300">
<span className="text-sm text-gray-700">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="p-1 rounded text-gray-600 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronRight className="w-5 h-5" />
</button>

View File

@@ -21,7 +21,7 @@ export function HeatmapWidget({
error,
onRefresh,
}: HeatmapWidgetProps) {
const heatmapConfig = config.config as HeatmapConfig;
const heatmapConfig = config.config as unknown as HeatmapConfig;
const rawData = data?.data || [];
// Extract unique x and y values
@@ -77,7 +77,7 @@ export function HeatmapWidget({
<tbody>
{yValues.map((y) => (
<tr key={y}>
<td className="px-2 py-2 text-xs font-medium text-gray-700 dark:text-gray-300">
<td className="px-2 py-2 text-xs font-medium text-gray-700">
{y}
</td>
{xValues.map((x) => {

View File

@@ -42,7 +42,7 @@ export function LineChartWidget({
error,
onRefresh,
}: LineChartWidgetProps) {
const chartConfig = config.config as ChartWidgetConfig;
const chartConfig = config.config as unknown as ChartWidgetConfig;
const chartData = data?.data || [];
return (
@@ -58,25 +58,25 @@ export function LineChartWidget({
margin={{ top: 5, right: 20, left: 0, bottom: 5 }}
>
{chartConfig.show_grid !== false && (
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<CartesianGrid strokeDasharray="3 3" stroke="#d1d5db" />
)}
<XAxis
dataKey={chartConfig.x_axis?.key || 'x'}
tick={{ fontSize: 12 }}
tick={{ fontSize: 12, fill: '#374151' }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
axisLine={{ stroke: '#d1d5db' }}
/>
<YAxis
tick={{ fontSize: 12 }}
tick={{ fontSize: 12, fill: '#374151' }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
axisLine={{ stroke: '#d1d5db' }}
label={
chartConfig.y_axis?.label
? {
value: chartConfig.y_axis.label,
angle: -90,
position: 'insideLeft',
style: { fontSize: 12 },
style: { fontSize: 12, fill: '#374151' },
}
: undefined
}

View File

@@ -41,7 +41,7 @@ export function PieChartWidget({
error,
onRefresh,
}: PieChartWidgetProps) {
const chartConfig = config.config as PieChartConfig;
const chartConfig = config.config as unknown as PieChartConfig;
const chartData = data?.data || [];
const colors = chartConfig.colors || DEFAULT_COLORS;
const innerRadius = chartConfig.inner_radius || 0; // 0 = pie, > 0 = donut
@@ -71,7 +71,7 @@ export function PieChartWidget({
nameKey="name"
label={
chartConfig.show_labels !== false
? ({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`
? ({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`
: undefined
}
labelLine={chartConfig.show_labels !== false}
@@ -89,7 +89,7 @@ export function PieChartWidget({
border: '1px solid #e5e7eb',
borderRadius: '0.375rem',
}}
formatter={(value: number) => [value.toLocaleString(), 'Count']}
formatter={(value) => [(value ?? 0).toLocaleString(), 'Count']}
/>
{chartConfig.show_legend !== false && (
<Legend

View File

@@ -29,14 +29,14 @@ const ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
activity: Activity,
};
// Color mapping
// Color mapping (light mode optimized for bg-gray-50 background)
const COLORS: Record<string, string> = {
blue: 'text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-900/30',
green: 'text-green-600 bg-green-100 dark:text-green-400 dark:bg-green-900/30',
red: 'text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-900/30',
yellow: 'text-yellow-600 bg-yellow-100 dark:text-yellow-400 dark:bg-yellow-900/30',
purple: 'text-purple-600 bg-purple-100 dark:text-purple-400 dark:bg-purple-900/30',
gray: 'text-gray-600 bg-gray-100 dark:text-gray-400 dark:bg-gray-700',
blue: 'text-blue-700 bg-blue-100',
green: 'text-green-700 bg-green-100',
red: 'text-red-700 bg-red-100',
yellow: 'text-yellow-700 bg-yellow-100',
purple: 'text-purple-700 bg-purple-100',
gray: 'text-gray-700 bg-gray-100',
};
/**
@@ -70,7 +70,7 @@ function formatValue(value: number | string, format?: string): string {
* Stat card widget for displaying KPIs.
*/
export function StatCard({ config, data, loading, error, onRefresh }: StatCardProps) {
const widgetConfig = config.config as StatCardConfig;
const widgetConfig = config.config as unknown as StatCardConfig;
const Icon = widgetConfig.icon ? ICONS[widgetConfig.icon] : Activity;
const colorClass = widgetConfig.color ? COLORS[widgetConfig.color] : COLORS.gray;
@@ -82,7 +82,7 @@ export function StatCard({ config, data, loading, error, onRefresh }: StatCardPr
<WidgetWrapper config={config} loading={loading} error={error} onRefresh={onRefresh}>
<div className="flex items-center justify-between h-full">
<div className="flex-1">
<p className="text-3xl font-bold text-gray-900 dark:text-gray-100">
<p className="text-3xl font-bold text-gray-900">
{formatValue(value, widgetConfig.format)}
</p>
{trend !== undefined && (

View File

@@ -23,17 +23,17 @@ export function WidgetWrapper({
children,
}: WidgetWrapperProps) {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm h-full flex flex-col">
<div className="bg-white rounded-lg border-2 border-gray-200 shadow-sm h-full flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="font-medium text-gray-900 dark:text-gray-100 text-sm">
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200">
<h3 className="font-semibold text-gray-900 text-sm">
{config.title}
</h3>
{onRefresh && (
<button
onClick={onRefresh}
disabled={loading}
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-50"
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50"
title="Refresh"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
@@ -47,14 +47,14 @@ export function WidgetWrapper({
<div className="flex items-center justify-center h-full">
<div className="text-center">
<AlertCircle className="w-8 h-8 text-red-500 mx-auto mb-2" />
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
<p className="text-sm text-red-600">{error}</p>
</div>
</div>
) : loading ? (
<div className="flex items-center justify-center h-full">
<div className="animate-pulse flex flex-col items-center">
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded mb-2" />
<div className="h-3 w-16 bg-gray-200 dark:bg-gray-700 rounded" />
<div className="h-4 w-24 bg-gray-200 rounded mb-2" />
<div className="h-3 w-16 bg-gray-200 rounded" />
</div>
</div>
) : (