|
import React, { useState, useMemo } from 'react'; |
|
import { PotRulerIcon } from '../components/icons'; |
|
|
|
type Units = 'cm' | 'in'; |
|
type BonsaiStyle = 'Upright' | 'Cascade' | 'Forest' | 'Literati'; |
|
|
|
const PotCalculatorView: React.FC = () => { |
|
const [units, setUnits] = useState<Units>('cm'); |
|
const [height, setHeight] = useState(30); |
|
const [trunkDiameter, setTrunkDiameter] = useState(3); |
|
const [style, setStyle] = useState<BonsaiStyle>('Upright'); |
|
|
|
const conversionFactor = units === 'in' ? 2.54 : 1; |
|
|
|
const results = useMemo(() => { |
|
let length, width, depth, rationale; |
|
const h = height; |
|
const d = trunkDiameter; |
|
|
|
switch (style) { |
|
case 'Cascade': |
|
length = h * 0.5; |
|
depth = h * 0.6; |
|
width = length; |
|
rationale = "Cascade pots are tall and often square or hexagonal to visually balance the downward-flowing trunk. The depth and stability are crucial."; |
|
break; |
|
case 'Forest': |
|
length = h * 1.5; |
|
depth = d * 0.75; |
|
width = length * 0.6; |
|
rationale = "Forest plantings require wide, very shallow oval or rectangular trays to create a sense of a landscape and accommodate many root systems."; |
|
break; |
|
case 'Literati': |
|
length = d * 3; |
|
depth = d * 1.5; |
|
width = length; |
|
rationale = "Literati pots are typically small, simple, and often round or unusually shaped. They are understated to emphasize the elegant, sparse trunk line."; |
|
break; |
|
case 'Upright': |
|
default: |
|
length = h * 0.66; |
|
depth = d; |
|
width = length * 0.8; |
|
rationale = "For upright styles, the pot length is typically 2/3 of the tree's height. The pot's depth should be equal to the trunk's diameter to provide visual stability."; |
|
break; |
|
} |
|
|
|
const format = (val: number) => { |
|
const converted = val / conversionFactor; |
|
return converted.toFixed(1); |
|
} |
|
|
|
return { |
|
length: format(length), |
|
width: format(width), |
|
depth: format(depth), |
|
rationale, |
|
}; |
|
}, [height, trunkDiameter, style, units, conversionFactor]); |
|
|
|
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"> |
|
<PotRulerIcon className="w-8 h-8 text-amber-800" /> |
|
Pot Ratio Calculator |
|
</h2> |
|
<p className="mt-4 text-lg leading-8 text-stone-600"> |
|
Find the perfect pot size for your bonsai based on traditional aesthetic guidelines. |
|
</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> |
|
<div className="flex justify-between items-center mb-2"> |
|
<label htmlFor="units" className="font-semibold text-stone-800">Units</label> |
|
<div className="flex gap-1 bg-stone-100 p-1 rounded-lg"> |
|
<button onClick={() => setUnits('cm')} className={`px-3 py-1 text-sm font-medium rounded-md ${units === 'cm' ? 'bg-white shadow' : ''}`}>cm</button> |
|
<button onClick={() => setUnits('in')} className={`px-3 py-1 text-sm font-medium rounded-md ${units === 'in' ? 'bg-white shadow' : ''}`}>in</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<label htmlFor="height" className="font-semibold text-stone-800">Tree Height ({units})</label> |
|
<div className="flex items-center gap-4 mt-2"> |
|
<input type="range" id="height" min="10" max="200" value={height} onChange={e => setHeight(Number(e.target.value))} className="w-full h-2 bg-stone-200 rounded-lg appearance-none cursor-pointer accent-amber-700" /> |
|
<input type="number" value={(height / conversionFactor).toFixed(1)} onChange={e => setHeight(Number(e.target.value) * conversionFactor)} className="w-20 p-2 border rounded-md" /> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<label htmlFor="trunkDiameter" className="font-semibold text-stone-800">Trunk Diameter ({units})</label> |
|
<div className="flex items-center gap-4 mt-2"> |
|
<input type="range" id="trunkDiameter" min="1" max="20" step="0.5" value={trunkDiameter} onChange={e => setTrunkDiameter(Number(e.target.value))} className="w-full h-2 bg-stone-200 rounded-lg appearance-none cursor-pointer accent-amber-700" /> |
|
<input type="number" value={(trunkDiameter / conversionFactor).toFixed(1)} onChange={e => setTrunkDiameter(Number(e.target.value) * conversionFactor)} className="w-20 p-2 border rounded-md" /> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<label htmlFor="style" className="font-semibold text-stone-800">Bonsai Style</label> |
|
<select id="style" value={style} onChange={e => setStyle(e.target.value as BonsaiStyle)} className="w-full mt-2 p-2 border rounded-md bg-white"> |
|
<option value="Upright">Formal/Informal Upright</option> |
|
<option value="Cascade">Cascade/Semi-Cascade</option> |
|
<option value="Forest">Forest/Group Planting</option> |
|
<option value="Literati">Literati (Bunjin)</option> |
|
</select> |
|
</div> |
|
</div> |
|
|
|
<div className="bg-white p-6 rounded-xl shadow-lg border-2 border-amber-600 space-y-4"> |
|
<h3 className="text-xl font-bold text-center text-stone-900">Recommended Pot Size</h3> |
|
<div className="flex justify-around text-center"> |
|
<div> |
|
<p className="text-3xl font-bold text-amber-800">{results.length}</p> |
|
<p className="text-sm text-stone-600">Length ({units})</p> |
|
</div> |
|
<div> |
|
<p className="text-3xl font-bold text-amber-800">{results.width}</p> |
|
<p className="text-sm text-stone-600">Width ({units})</p> |
|
</div> |
|
<div> |
|
<p className="text-3xl font-bold text-amber-800">{results.depth}</p> |
|
<p className="text-sm text-stone-600">Depth ({units})</p> |
|
</div> |
|
</div> |
|
<div className="mt-4 pt-4 border-t-2 border-dashed"> |
|
<h4 className="font-semibold text-stone-800">Rationale</h4> |
|
<p className="text-sm text-stone-700 mt-1">{results.rationale}</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default PotCalculatorView; |
|
|