Yuki / views /SoilVolumeCalculatorView.tsx
Severian's picture
Upload 43 files
be02369 verified
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 { // round
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> /&nbsp;
<span>{(volumeLiters * 1.05669).toFixed(2)} qts</span> /&nbsp;
<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;