Add a ne model and card design across pages

This commit is contained in:
Urtzi Alfaro
2025-08-31 10:46:13 +02:00
parent ab21149acf
commit a8b73e22ea
14 changed files with 1865 additions and 820 deletions

View File

@@ -1,12 +1,13 @@
import React, { useState } from 'react';
import { Plus, Download, Star, Clock, Users, DollarSign, Package, Eye, Edit, ChefHat, Timer } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
const RecipesPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedRecipe, setSelectedRecipe] = useState<typeof mockRecipes[0] | null>(null);
const mockRecipes = [
@@ -100,38 +101,34 @@ const RecipesPage: React.FC = () => {
},
];
const getCategoryBadge = (category: string) => {
const getRecipeStatusConfig = (category: string, difficulty: string, rating: number) => {
const categoryConfig = {
bread: { color: 'default', text: 'Pan' },
pastry: { color: 'warning', text: 'Bollería' },
cake: { color: 'secondary', text: 'Tarta' },
cookie: { color: 'info', text: 'Galleta' },
other: { color: 'outline', text: 'Otro' },
bread: { text: 'Pan', icon: ChefHat },
pastry: { text: 'Bollería', icon: ChefHat },
cake: { text: 'Tarta', icon: ChefHat },
cookie: { text: 'Galleta', icon: ChefHat },
other: { text: 'Otro', icon: ChefHat },
};
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
return (
<Badge
variant={config.color as any}
text={config.text}
/>
);
};
const getDifficultyBadge = (difficulty: string) => {
const difficultyConfig = {
easy: { color: 'success', text: 'Fácil' },
medium: { color: 'warning', text: 'Medio' },
hard: { color: 'error', text: 'Difícil' },
easy: { icon: '', label: 'Fácil' },
medium: { icon: '●●', label: 'Medio' },
hard: { icon: '●●●', label: 'Difícil' },
};
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig];
return (
<Badge
variant={config?.color as any}
text={config?.text || difficulty}
/>
);
const categoryInfo = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
const difficultyInfo = difficultyConfig[difficulty as keyof typeof difficultyConfig];
const isPopular = rating >= 4.7;
return {
color: getStatusColor(category),
text: categoryInfo.text,
icon: categoryInfo.icon,
difficultyIcon: difficultyInfo?.icon || '●',
difficultyLabel: difficultyInfo?.label || difficulty,
isCritical: false,
isHighlight: isPopular
};
};
const formatTime = (minutes: number) => {
@@ -245,130 +242,65 @@ const RecipesPage: React.FC = () => {
{/* Recipes Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredRecipes.map((recipe) => (
<Card key={recipe.id} className="p-4">
<div className="space-y-4">
{/* Header with Image */}
<div className="flex items-start gap-3">
<div className="flex-shrink-0">
<img
src={recipe.image}
alt={recipe.name}
className="w-16 h-16 rounded-lg object-cover bg-[var(--bg-secondary)]"
/>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-[var(--text-primary)] truncate">
{recipe.name}
</div>
<div className="flex items-center gap-1 mt-1">
<Star className="w-3 h-3 text-yellow-400 fill-current" />
<span className="text-xs text-[var(--text-secondary)]">{recipe.rating}</span>
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1 line-clamp-2">
{recipe.description}
</p>
</div>
</div>
{/* Badges */}
<div className="flex gap-2 flex-wrap">
{getCategoryBadge(recipe.category)}
{getDifficultyBadge(recipe.difficulty)}
</div>
{/* Time and Yield */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-[var(--text-primary)]">
{formatTime(recipe.prepTime + recipe.bakingTime)}
</span>
</div>
<div className="flex items-center gap-1">
<Users className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-[var(--text-primary)]">
{recipe.yield} porciones
</span>
</div>
</div>
{/* Financial Info */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Costo:</span>
<span className="font-medium text-[var(--text-primary)]">
{formatters.currency(recipe.cost)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Precio:</span>
<span className="font-medium text-[var(--color-success)]">
{formatters.currency(recipe.price)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Margen:</span>
<span className="font-bold text-[var(--color-success)]">
{formatters.currency(recipe.profit)}
</span>
</div>
</div>
{/* Profit Margin Bar */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--text-secondary)]">Margen de beneficio</span>
<span className="text-[var(--text-primary)]">
{Math.round((recipe.profit / recipe.price) * 100)}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
(recipe.profit / recipe.price) > 0.5
? 'bg-[var(--color-success)]'
: (recipe.profit / recipe.price) > 0.3
? 'bg-[var(--color-warning)]'
: 'bg-[var(--color-error)]'
}`}
style={{ width: `${Math.min((recipe.profit / recipe.price) * 100, 100)}%` }}
/>
</div>
</div>
{/* Ingredients Count */}
<div className="text-xs text-[var(--text-secondary)]">
{recipe.ingredients.length} ingredientes principales
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
{filteredRecipes.map((recipe) => {
const statusConfig = getRecipeStatusConfig(recipe.category, recipe.difficulty, recipe.rating);
const profitMargin = Math.round((recipe.profit / recipe.price) * 100);
const totalTime = formatTime(recipe.prepTime + recipe.bakingTime);
return (
<StatusCard
key={recipe.id}
id={recipe.id}
statusIndicator={statusConfig}
title={recipe.name}
subtitle={`${statusConfig.text}${statusConfig.difficultyLabel}${statusConfig.isHighlight ? ' ★' + recipe.rating : ''}`}
primaryValue={formatters.currency(recipe.profit)}
primaryValueLabel="margen"
secondaryInfo={{
label: 'Precio de venta',
value: `${formatters.currency(recipe.price)} (costo: ${formatters.currency(recipe.cost)})`
}}
progress={{
label: 'Margen de beneficio',
percentage: profitMargin,
color: profitMargin > 50 ? '#10b981' : profitMargin > 30 ? '#f59e0b' : '#ef4444'
}}
metadata={[
`Tiempo: ${totalTime}`,
`Porciones: ${recipe.yield}`,
`${recipe.ingredients.length} ingredientes principales`
]}
actions={[
{
label: 'Ver',
icon: Eye,
variant: 'outline',
onClick: () => {
setSelectedRecipe(recipe);
setModalMode('view');
setShowForm(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="primary"
size="sm"
className="flex-1"
onClick={() => console.log('Produce recipe', recipe.id)}
>
<ChefHat className="w-4 h-4 mr-1" />
Producir
</Button>
</div>
</div>
</Card>
))}
}
},
{
label: 'Editar',
icon: Edit,
variant: 'outline',
onClick: () => {
setSelectedRecipe(recipe);
setModalMode('edit');
setShowForm(true);
}
},
{
label: 'Producir',
icon: ChefHat,
variant: 'primary',
onClick: () => console.log('Produce recipe', recipe.id)
}
]}
/>
);
})}
</div>
{/* Empty State */}
@@ -388,57 +320,133 @@ const RecipesPage: React.FC = () => {
</div>
)}
{/* Recipe Form Modal - Placeholder */}
{showForm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-full max-w-2xl max-h-[90vh] overflow-auto m-4 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
{selectedRecipe ? 'Ver Receta' : 'Nueva Receta'}
</h2>
<Button
variant="outline"
size="sm"
onClick={() => {
setShowForm(false);
setSelectedRecipe(null);
}}
>
Cerrar
</Button>
</div>
{selectedRecipe && (
<div className="space-y-4">
<img
src={selectedRecipe.image}
alt={selectedRecipe.name}
className="w-full h-48 object-cover rounded-lg"
/>
<h3 className="text-lg font-medium">{selectedRecipe.name}</h3>
<p className="text-[var(--text-secondary)]">{selectedRecipe.description}</p>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Tiempo total:</span> {formatTime(selectedRecipe.prepTime + selectedRecipe.bakingTime)}
</div>
<div>
<span className="font-medium">Rendimiento:</span> {selectedRecipe.yield} porciones
</div>
</div>
<div>
<h4 className="font-medium mb-2">Ingredientes:</h4>
<ul className="space-y-1 text-sm">
{selectedRecipe.ingredients.map((ing, i) => (
<li key={i} className="flex justify-between">
<span>{ing.name}</span>
<span>{ing.quantity} {ing.unit}</span>
</li>
))}
</ul>
</div>
</div>
)}
</Card>
</div>
{/* Recipe Details Modal */}
{showForm && selectedRecipe && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedRecipe(null);
setModalMode('view');
}}
mode={modalMode}
onModeChange={setModalMode}
title={selectedRecipe.name}
subtitle={selectedRecipe.description}
statusIndicator={getRecipeStatusConfig(selectedRecipe.category, selectedRecipe.difficulty, selectedRecipe.rating)}
image={selectedRecipe.image}
size="xl"
sections={[
{
title: 'Información Básica',
icon: ChefHat,
fields: [
{
label: 'Categoría',
value: selectedRecipe.category,
type: 'status'
},
{
label: 'Dificultad',
value: selectedRecipe.difficulty
},
{
label: 'Valoración',
value: `${selectedRecipe.rating}`,
highlight: selectedRecipe.rating >= 4.7
},
{
label: 'Rendimiento',
value: `${selectedRecipe.yield} porciones`
}
]
},
{
title: 'Tiempos',
icon: Clock,
fields: [
{
label: 'Tiempo de preparación',
value: formatTime(selectedRecipe.prepTime)
},
{
label: 'Tiempo de horneado',
value: formatTime(selectedRecipe.bakingTime)
},
{
label: 'Tiempo total',
value: formatTime(selectedRecipe.prepTime + selectedRecipe.bakingTime),
highlight: true
}
]
},
{
title: 'Análisis Financiero',
icon: DollarSign,
fields: [
{
label: 'Costo de producción',
value: selectedRecipe.cost,
type: 'currency'
},
{
label: 'Precio de venta',
value: selectedRecipe.price,
type: 'currency'
},
{
label: 'Margen de beneficio',
value: selectedRecipe.profit,
type: 'currency',
highlight: true
},
{
label: 'Porcentaje de margen',
value: Math.round((selectedRecipe.profit / selectedRecipe.price) * 100),
type: 'percentage',
highlight: true
}
]
},
{
title: 'Ingredientes',
icon: Package,
fields: [
{
label: 'Lista de ingredientes',
value: selectedRecipe.ingredients.map(ing => `${ing.name}: ${ing.quantity} ${ing.unit}`),
type: 'list',
span: 2
}
]
},
{
title: 'Etiquetas',
fields: [
{
label: 'Tags',
value: selectedRecipe.tags.join(', '),
span: 2
}
]
}
]}
actions={[
{
label: 'Producir',
icon: ChefHat,
variant: 'primary',
onClick: () => {
console.log('Producing recipe:', selectedRecipe.id);
setShowForm(false);
setSelectedRecipe(null);
}
}
]}
onEdit={() => {
console.log('Editing recipe:', selectedRecipe.id);
}}
/>
)}
</div>
);