Add new page designs

This commit is contained in:
Urtzi Alfaro
2025-08-30 19:11:15 +02:00
parent 221781731c
commit 62b1ab9cb1
12 changed files with 2129 additions and 1240 deletions

View File

@@ -0,0 +1,215 @@
import { forwardRef } from 'react';
import { clsx } from 'clsx';
import { LucideIcon } from 'lucide-react';
import { Card } from '../Card';
export type StatsCardVariant = 'default' | 'success' | 'info' | 'warning' | 'error' | 'purple';
export type StatsCardSize = 'sm' | 'md' | 'lg';
export interface StatsCardProps {
title: string;
value: string | number;
icon?: LucideIcon;
variant?: StatsCardVariant;
size?: StatsCardSize;
trend?: {
value: number;
label?: string;
direction: 'up' | 'down' | 'neutral';
};
subtitle?: string;
loading?: boolean;
className?: string;
formatValue?: (value: string | number) => string;
onClick?: () => void;
}
const StatsCard = forwardRef<HTMLDivElement, StatsCardProps>(({
title,
value,
icon: Icon,
variant = 'default',
size = 'md',
trend,
subtitle,
loading = false,
className,
formatValue,
onClick,
...props
}, ref) => {
const formattedValue = formatValue ? formatValue(value) : value;
const variantStyles = {
default: {
iconColor: 'var(--text-tertiary)',
valueColor: 'var(--text-primary)',
iconBg: 'var(--bg-tertiary)',
},
success: {
iconColor: 'var(--color-success)',
valueColor: 'var(--color-success)',
iconBg: 'var(--color-success-50)',
},
info: {
iconColor: 'var(--color-info)',
valueColor: 'var(--color-info)',
iconBg: 'var(--color-info-50)',
},
warning: {
iconColor: 'var(--color-warning)',
valueColor: 'var(--color-warning)',
iconBg: 'var(--color-warning-50)',
},
error: {
iconColor: 'var(--color-error)',
valueColor: 'var(--color-error)',
iconBg: 'var(--color-error-50)',
},
purple: {
iconColor: '#a78bfa',
valueColor: '#a78bfa',
iconBg: 'rgba(167, 139, 250, 0.1)',
},
};
const sizeStyles = {
sm: {
padding: 'p-4',
iconSize: 20,
valueSize: 'text-2xl',
titleSize: 'text-sm',
iconPadding: 'p-2',
},
md: {
padding: 'p-6',
iconSize: 24,
valueSize: 'text-3xl',
titleSize: 'text-sm',
iconPadding: 'p-2.5',
},
lg: {
padding: 'p-8',
iconSize: 28,
valueSize: 'text-4xl',
titleSize: 'text-base',
iconPadding: 'p-3',
},
};
const currentVariant = variantStyles[variant];
const currentSize = sizeStyles[size];
const isInteractive = !!onClick;
const cardClasses = clsx(
{
'animate-pulse': loading,
},
className
);
const iconContainerClasses = clsx(
'rounded-xl flex items-center justify-center',
currentSize.iconPadding
);
const iconContainerStyle = {
backgroundColor: currentVariant.iconBg,
};
if (loading) {
return (
<Card
padding={size}
interactive={isInteractive}
className={cardClasses}
ref={ref}
{...props}
>
<div className="flex items-start justify-between">
<div className="flex-1 space-y-4">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20"></div>
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-16"></div>
{subtitle && <div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-24"></div>}
</div>
<div
className="w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-xl"
></div>
</div>
</Card>
);
}
return (
<Card
padding={size}
interactive={isInteractive}
onClick={onClick}
className={cardClasses}
ref={ref}
{...props}
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<h3
className={clsx('font-medium mb-2 leading-tight', currentSize.titleSize)}
style={{ color: 'var(--text-secondary)' }}
>
{title}
</h3>
<div className="flex items-baseline gap-2 mb-1">
<span
className={clsx('font-bold leading-none', currentSize.valueSize)}
style={{ color: currentVariant.valueColor }}
>
{formattedValue}
</span>
{trend && (
<span
className={clsx(
'text-xs font-medium px-1.5 py-0.5 rounded-full',
{
'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400': trend.direction === 'up',
'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400': trend.direction === 'down',
'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300': trend.direction === 'neutral',
}
)}
>
{trend.direction === 'up' && '+'}
{trend.value}
{trend.label && ` ${trend.label}`}
</span>
)}
</div>
{subtitle && (
<p
className="text-xs leading-relaxed"
style={{ color: 'var(--text-tertiary)' }}
>
{subtitle}
</p>
)}
</div>
{Icon && (
<div
className={iconContainerClasses}
style={iconContainerStyle}
>
<Icon
size={currentSize.iconSize}
style={{ color: currentVariant.iconColor }}
/>
</div>
)}
</div>
</Card>
);
});
StatsCard.displayName = 'StatsCard';
export default StatsCard;

View File

@@ -0,0 +1,201 @@
import { FC } from 'react';
import { StatsGrid, StatsCard } from './index';
import { pagePresets, businessMetrics, formatters } from './StatsPresets';
import {
Calendar,
CheckCircle,
Clock,
AlertTriangle,
Zap,
Shield
} from 'lucide-react';
// Example: Production Management Stats (matching your screenshot)
export const ProductionStatsExample: FC = () => {
// Sample data
const productionData = {
dailyTarget: 150,
completed: 85,
inProgress: 12,
pending: 53,
efficiency: 78,
quality: 94,
};
return (
<StatsGrid
stats={pagePresets.production(productionData)}
columns={6}
title="Gestión de Producción"
description="Planifica y controla la producción diaria de tu panadería"
/>
);
};
// Example: Custom Stats Configuration
export const CustomStatsExample = () => {
return (
<StatsGrid
stats={[
{
title: 'Meta Diaria',
value: 150,
icon: Calendar,
variant: 'default',
},
{
title: 'Completado',
value: 85,
icon: CheckCircle,
variant: 'success',
},
{
title: 'En Proceso',
value: 12,
icon: Clock,
variant: 'info',
},
{
title: 'Pendiente',
value: 53,
icon: AlertTriangle,
variant: 'warning',
},
{
title: 'Eficiencia',
value: 78,
icon: Zap,
variant: 'purple',
formatValue: formatters.percentage,
},
{
title: 'Calidad',
value: 94,
icon: Shield,
variant: 'success',
formatValue: formatters.percentage,
},
]}
columns={6}
/>
);
};
// Example: Sales Dashboard
export const SalesStatsExample = () => {
const salesData = {
revenue: 15420,
revenueGrowth: 12.5,
orders: 142,
ordersGrowth: -3.2,
customers: 89,
};
return (
<StatsGrid
stats={pagePresets.sales(salesData)}
columns={3}
title="Ventas del Día"
description="Resumen de rendimiento de ventas"
/>
);
};
// Example: Individual StatsCard Usage
export const IndividualStatsExample = () => {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatsCard
title="Pedidos Hoy"
value={45}
icon={CheckCircle}
variant="success"
trend={{
value: 15,
direction: 'up',
label: '%',
}}
/>
<StatsCard
title="Ingresos"
value={2840}
icon={Calendar}
variant="info"
formatValue={formatters.currency}
subtitle="Últimas 24 horas"
/>
<StatsCard
title="Productos Vendidos"
value={1250}
icon={Zap}
variant="default"
formatValue={formatters.number}
size="lg"
/>
<StatsCard
title="Satisfacción"
value={96}
icon={Shield}
variant="success"
formatValue={formatters.percentage}
onClick={() => console.log('Navigate to satisfaction details')}
/>
</div>
);
};
// Example: Loading State
export const LoadingStatsExample = () => {
return (
<StatsGrid
stats={[
{ title: '', value: '' },
{ title: '', value: '' },
{ title: '', value: '' },
{ title: '', value: '' },
]}
loading
columns={4}
/>
);
};
// Example: Using Business Metrics Directly
export const BusinessMetricsExample = () => {
return (
<div className="space-y-8">
<StatsGrid
stats={[
businessMetrics.production.dailyTarget(150),
businessMetrics.production.completed(85),
businessMetrics.production.efficiency(78),
]}
columns={3}
title="Producción"
/>
<StatsGrid
stats={[
businessMetrics.sales.revenue(15420, { value: 12.5, direction: 'up' }),
businessMetrics.sales.orders(142, { value: 3.2, direction: 'down' }),
businessMetrics.sales.customers(89),
]}
columns={3}
title="Ventas"
/>
<StatsGrid
stats={[
businessMetrics.inventory.totalItems(450),
businessMetrics.inventory.lowStock(12),
businessMetrics.inventory.outOfStock(3),
]}
columns={3}
title="Inventario"
/>
</div>
);
};

View File

@@ -0,0 +1,94 @@
import React from 'react';
import { clsx } from 'clsx';
import StatsCard, { StatsCardProps } from './StatsCard';
export interface StatsGridProps {
stats: StatsCardProps[];
columns?: 1 | 2 | 3 | 4 | 5 | 6;
gap?: 'sm' | 'md' | 'lg';
className?: string;
loading?: boolean;
title?: string;
description?: string;
}
const StatsGrid: React.FC<StatsGridProps> = ({
stats,
columns = 3,
gap = 'md',
className,
loading = false,
title,
description,
}) => {
const gridClasses = {
1: 'grid-cols-1',
2: 'grid-cols-1 sm:grid-cols-2',
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
5: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5',
6: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6',
};
const gapClasses = {
sm: 'gap-3',
md: 'gap-4',
lg: 'gap-6',
};
const containerClasses = clsx(
'grid',
gridClasses[columns],
gapClasses[gap],
className
);
return (
<div className="space-y-6">
{/* Header */}
{(title || description) && (
<div className="space-y-2">
{title && (
<h2
className="text-2xl font-bold leading-tight"
style={{ color: 'var(--text-primary)' }}
>
{title}
</h2>
)}
{description && (
<p
className="text-base leading-relaxed"
style={{ color: 'var(--text-secondary)' }}
>
{description}
</p>
)}
</div>
)}
{/* Stats Grid */}
<div className={containerClasses}>
{loading
? Array.from({ length: stats.length || 6 }).map((_, index) => (
<StatsCard
key={index}
title=""
value=""
loading
/>
))
: stats.map((stat, index) => (
<StatsCard
key={stat.title || index}
{...stat}
loading={loading}
/>
))
}
</div>
</div>
);
};
export default StatsGrid;

View File

@@ -0,0 +1,237 @@
import {
Calendar,
CheckCircle,
Clock,
AlertTriangle,
Zap,
Shield,
TrendingUp,
Package,
Users,
DollarSign,
BarChart3,
Target,
Activity,
Award
} from 'lucide-react';
import { StatsCardProps, StatsCardVariant } from './StatsCard';
// Common formatting functions
export const formatters = {
percentage: (value: string | number): string => `${value}%`,
currency: (value: string | number): string => `${parseFloat(String(value)).toFixed(2)}`,
number: (value: string | number): string => parseFloat(String(value)).toLocaleString('es-ES'),
compact: (value: string | number): string => {
const num = parseFloat(String(value));
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
return num.toString();
},
};
// Icon mappings for common stat types
export const statIcons = {
target: Calendar,
completed: CheckCircle,
inProgress: Clock,
pending: AlertTriangle,
efficiency: Zap,
quality: Shield,
growth: TrendingUp,
inventory: Package,
users: Users,
revenue: DollarSign,
analytics: BarChart3,
goals: Target,
activity: Activity,
achievement: Award,
};
// Variant mappings for common stat types
export const statVariants: Record<string, StatsCardVariant> = {
target: 'default',
completed: 'success',
inProgress: 'info',
pending: 'warning',
efficiency: 'purple',
quality: 'success',
error: 'error',
revenue: 'success',
growth: 'success',
decline: 'error',
};
// Predefined stat configurations for common business metrics
export const businessMetrics = {
production: {
dailyTarget: (value: number): StatsCardProps => ({
title: 'Meta Diaria',
value,
icon: statIcons.target,
variant: statVariants.target,
formatValue: formatters.number,
}),
completed: (value: number): StatsCardProps => ({
title: 'Completado',
value,
icon: statIcons.completed,
variant: statVariants.completed,
formatValue: formatters.number,
}),
inProgress: (value: number): StatsCardProps => ({
title: 'En Proceso',
value,
icon: statIcons.inProgress,
variant: statVariants.inProgress,
formatValue: formatters.number,
}),
pending: (value: number): StatsCardProps => ({
title: 'Pendiente',
value,
icon: statIcons.pending,
variant: statVariants.pending,
formatValue: formatters.number,
}),
efficiency: (value: number): StatsCardProps => ({
title: 'Eficiencia',
value,
icon: statIcons.efficiency,
variant: statVariants.efficiency,
formatValue: formatters.percentage,
}),
quality: (value: number): StatsCardProps => ({
title: 'Calidad',
value,
icon: statIcons.quality,
variant: statVariants.quality,
formatValue: formatters.percentage,
}),
},
sales: {
revenue: (value: number, trend?: { value: number; direction: 'up' | 'down' | 'neutral'; label?: string }): StatsCardProps => ({
title: 'Ingresos',
value,
icon: statIcons.revenue,
variant: statVariants.revenue,
formatValue: formatters.currency,
trend,
}),
orders: (value: number, trend?: { value: number; direction: 'up' | 'down' | 'neutral'; label?: string }): StatsCardProps => ({
title: 'Pedidos',
value,
icon: statIcons.analytics,
variant: statVariants.target,
formatValue: formatters.number,
trend,
}),
customers: (value: number): StatsCardProps => ({
title: 'Clientes',
value,
icon: statIcons.users,
variant: statVariants.target,
formatValue: formatters.number,
}),
},
inventory: {
totalItems: (value: number): StatsCardProps => ({
title: 'Total Items',
value,
icon: statIcons.inventory,
variant: statVariants.target,
formatValue: formatters.number,
}),
lowStock: (value: number): StatsCardProps => ({
title: 'Stock Bajo',
value,
icon: statIcons.pending,
variant: value > 0 ? statVariants.pending : statVariants.completed,
formatValue: formatters.number,
}),
outOfStock: (value: number): StatsCardProps => ({
title: 'Sin Stock',
value,
icon: statIcons.pending,
variant: value > 0 ? statVariants.error : statVariants.completed,
formatValue: formatters.number,
}),
},
performance: {
growth: (value: number): StatsCardProps => ({
title: 'Crecimiento',
value,
icon: statIcons.growth,
variant: value >= 0 ? statVariants.growth : statVariants.decline,
formatValue: formatters.percentage,
trend: {
value: Math.abs(value),
direction: value >= 0 ? 'up' : 'down',
label: 'vs mes anterior',
},
}),
satisfaction: (value: number): StatsCardProps => ({
title: 'Satisfacción',
value,
icon: statIcons.achievement,
variant: value >= 80 ? statVariants.quality : value >= 60 ? statVariants.pending : statVariants.error,
formatValue: formatters.percentage,
}),
},
};
// Quick preset configurations for common page layouts
export const pagePresets = {
production: (data: {
dailyTarget: number;
completed: number;
inProgress: number;
pending: number;
efficiency: number;
quality: number;
}): StatsCardProps[] => [
businessMetrics.production.dailyTarget(data.dailyTarget),
businessMetrics.production.completed(data.completed),
businessMetrics.production.inProgress(data.inProgress),
businessMetrics.production.pending(data.pending),
businessMetrics.production.efficiency(data.efficiency),
businessMetrics.production.quality(data.quality),
],
sales: (data: {
revenue: number;
revenueGrowth?: number;
orders: number;
ordersGrowth?: number;
customers: number;
}): StatsCardProps[] => [
businessMetrics.sales.revenue(
data.revenue,
data.revenueGrowth ? {
value: data.revenueGrowth,
direction: data.revenueGrowth >= 0 ? 'up' : 'down',
label: '%',
} : undefined
),
businessMetrics.sales.orders(
data.orders,
data.ordersGrowth ? {
value: data.ordersGrowth,
direction: data.ordersGrowth >= 0 ? 'up' : 'down',
label: '%',
} : undefined
),
businessMetrics.sales.customers(data.customers),
],
inventory: (data: {
totalItems: number;
lowStock: number;
outOfStock: number;
}): StatsCardProps[] => [
businessMetrics.inventory.totalItems(data.totalItems),
businessMetrics.inventory.lowStock(data.lowStock),
businessMetrics.inventory.outOfStock(data.outOfStock),
],
};

View File

@@ -0,0 +1,4 @@
export { default as StatsCard } from './StatsCard';
export { default as StatsGrid } from './StatsGrid';
export type { StatsCardProps, StatsCardVariant, StatsCardSize } from './StatsCard';
export type { StatsGridProps } from './StatsGrid';