Yuki / views /PotCalculatorView.tsx
Severian's picture
Upload 43 files
be02369 verified
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;