|
|
|
import React, { useState, useMemo } from 'react'; |
|
import { ShovelIcon, PlusCircleIcon, Trash2Icon } from '../components/icons'; |
|
|
|
type PotShape = 'rectangular' | 'round'; |
|
type Units = 'cm' | 'in'; |
|
|
|
const SoilVolumeCalculatorView: React.FC = () => { |
|
const [units, setUnits] = useState<Units>('in'); |
|
const [shape, setShape] = useState<PotShape>('rectangular'); |
|
const [dimensions, setDimensions] = useState({ length: 12, width: 8, depth: 4, diameter: 10 }); |
|
const [recipe, setRecipe] = useState([ |
|
{ id: 1, name: 'Akadama', percentage: 40 }, |
|
{ id: 2, name: 'Pumice', percentage: 30 }, |
|
{ id: 3, name: 'Lava Rock', percentage: 30 }, |
|
]); |
|
|
|
const totalPercentage = useMemo(() => recipe.reduce((sum, item) => sum + item.percentage, 0), [recipe]); |
|
|
|
const volumeCm3 = useMemo(() => { |
|
const d = dimensions; |
|
const toCm = units === 'in' ? 2.54 : 1; |
|
if (shape === 'rectangular') { |
|
return (d.length * toCm) * (d.width * toCm) * (d.depth * toCm); |
|
} else { |
|
const radius = (d.diameter * toCm) / 2; |
|
return Math.PI * (radius * radius) * (d.depth * toCm); |
|
} |
|
}, [dimensions, shape, units]); |
|
|
|
const handleRecipeChange = (id: number, field: 'name' | 'percentage', value: string | number) => { |
|
setRecipe(prev => prev.map(item => item.id === id ? { ...item, [field]: value } : item)); |
|
}; |
|
|
|
const addComponent = () => { |
|
setRecipe(prev => [...prev, { id: Date.now(), name: 'New Component', percentage: 0 }]); |
|
}; |
|
|
|
const removeComponent = (id: number) => { |
|
setRecipe(prev => prev.filter(item => item.id !== id)); |
|
}; |
|
|
|
const renderVolume = (volumeLiters: number) => ( |
|
<div className="text-sm text-stone-600"> |
|
<span>{volumeLiters.toFixed(2)} L</span> / |
|
<span>{(volumeLiters * 1.05669).toFixed(2)} qts</span> / |
|
<span>{(volumeLiters * 0.264172).toFixed(2)} gal</span> |
|
</div> |
|
); |
|
|
|
return ( |
|
<div className="space-y-8 max-w-4xl mx-auto"> |
|
<header className="text-center"> |
|
<h2 className="text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl flex items-center justify-center gap-3"> |
|
<ShovelIcon className="w-8 h-8 text-orange-900" /> |
|
Soil Mix Calculator |
|
</h2> |
|
<p className="mt-4 text-lg leading-8 text-stone-600"> |
|
Mix the perfect amount of soil for your pot. No more waste or running out halfway through repotting. |
|
</p> |
|
</header> |
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-start"> |
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-stone-200 space-y-6"> |
|
<div> |
|
<h3 className="font-semibold text-stone-800 mb-2">1. Pot Dimensions</h3> |
|
<div className="flex gap-4 mb-4"> |
|
<div className="flex-1"> |
|
<label className="text-sm font-medium">Shape</label> |
|
<select value={shape} onChange={e => setShape(e.target.value as PotShape)} className="w-full mt-1 p-2 border rounded-md bg-white"> |
|
<option value="rectangular">Rectangular</option> |
|
<option value="round">Round</option> |
|
</select> |
|
</div> |
|
<div className="flex-1"> |
|
<label className="text-sm font-medium">Units</label> |
|
<select value={units} onChange={e => setUnits(e.target.value as Units)} className="w-full mt-1 p-2 border rounded-md bg-white"> |
|
<option value="cm">Centimeters</option> |
|
<option value="in">Inches</option> |
|
</select> |
|
</div> |
|
</div> |
|
<div className="grid grid-cols-2 gap-4"> |
|
{shape === 'rectangular' ? ( |
|
<> |
|
<div><label>Length</label><input type="number" value={dimensions.length} onChange={e => setDimensions(d => ({...d, length: Number(e.target.value)}))} className="w-full mt-1 p-2 border rounded-md" /></div> |
|
<div><label>Width</label><input type="number" value={dimensions.width} onChange={e => setDimensions(d => ({...d, width: Number(e.target.value)}))} className="w-full mt-1 p-2 border rounded-md" /></div> |
|
</> |
|
) : ( |
|
<div><label>Diameter</label><input type="number" value={dimensions.diameter} onChange={e => setDimensions(d => ({...d, diameter: Number(e.target.value)}))} className="w-full mt-1 p-2 border rounded-md" /></div> |
|
)} |
|
<div><label>Depth</label><input type="number" value={dimensions.depth} onChange={e => setDimensions(d => ({...d, depth: Number(e.target.value)}))} className="w-full mt-1 p-2 border rounded-md" /></div> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<h3 className="font-semibold text-stone-800 mb-2">2. Soil Recipe</h3> |
|
<div className="space-y-2"> |
|
{recipe.map(item => ( |
|
<div key={item.id} className="flex items-center gap-2"> |
|
<input type="text" value={item.name} onChange={e => handleRecipeChange(item.id, 'name', e.target.value)} className="w-full p-2 border rounded-md" /> |
|
<input type="number" value={item.percentage} onChange={e => handleRecipeChange(item.id, 'percentage', Number(e.target.value))} className="w-24 p-2 border rounded-md" /> |
|
<span className="font-bold text-stone-500">%</span> |
|
<button onClick={() => removeComponent(item.id)} className="p-2 text-red-500 hover:bg-red-100 rounded-md"><Trash2Icon className="w-5 h-5"/></button> |
|
</div> |
|
))} |
|
</div> |
|
<button onClick={addComponent} className="mt-2 flex items-center gap-2 text-sm font-semibold text-green-700 hover:text-green-600"> |
|
<PlusCircleIcon className="w-5 h-5"/> Add Component |
|
</button> |
|
<div className="mt-4 w-full bg-stone-200 rounded-full h-2.5"> |
|
<div className={`h-2.5 rounded-full ${totalPercentage > 100 ? 'bg-red-500' : 'bg-green-600'}`} style={{ width: `${Math.min(100, totalPercentage)}%` }}></div> |
|
</div> |
|
<p className={`text-center text-sm mt-1 font-bold ${totalPercentage !== 100 ? 'text-red-600 animate-pulse' : 'text-green-700'}`}> |
|
Total: {totalPercentage}% |
|
</p> |
|
</div> |
|
</div> |
|
|
|
<div className="bg-white p-6 rounded-xl shadow-lg border-2 border-orange-800 space-y-4"> |
|
<h3 className="text-xl font-bold text-center text-stone-900">Component Volumes Needed</h3> |
|
<div className="text-center"> |
|
<p className="text-stone-600">Total Pot Volume:</p> |
|
<p className="font-bold text-lg text-orange-900">{renderVolume(volumeCm3 / 1000)}</p> |
|
</div> |
|
<div className="divide-y divide-stone-200 border-t pt-4"> |
|
{recipe.map(item => { |
|
const componentVolumeL = (volumeCm3 / 1000) * (item.percentage / 100); |
|
return ( |
|
<div key={item.id} className="py-3"> |
|
<div className="flex justify-between items-center font-bold"> |
|
<span className="text-stone-800">{item.name}</span> |
|
<span className="text-orange-900">{item.percentage}%</span> |
|
</div> |
|
<div className="text-right">{renderVolume(componentVolumeL)}</div> |
|
</div> |
|
) |
|
})} |
|
</div> |
|
{totalPercentage !== 100 && ( |
|
<p className="text-center text-red-600 text-sm font-semibold p-2 bg-red-50 rounded-md"> |
|
Your recipe percentages must add up to 100% for an accurate calculation. |
|
</p> |
|
)} |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default SoilVolumeCalculatorView; |
|
|