Initial commit - WhyRating Engine (Google Reviews Scraper)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user