Add reference size input for custom garment dimensions

- Add Reference Size (cm) section with Width and Height inputs
- Rulers now use reference size instead of product print area dimensions
- Measurements scale to garment size specified by user
- Add to bag button disabled until reference sizes are provided
- Rulers span full canvas and show custom scale (e.g., 0-40cm x 0-60cm)
- Reference size required for personalization checkout

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-18 12:19:35 +01:00
co-authored by Claude Haiku 4.5
parent c20c696ccb
commit 2595adc305
+67 -26
View File
@@ -168,6 +168,8 @@ function PrintSurface({
onNodeReady, onNodeReady,
scale, scale,
product, product,
referenceWidthCm,
referenceHeightCm,
}: { }: {
photo: React.ReactNode; photo: React.ReactNode;
printArea: PrintArea; printArea: PrintArea;
@@ -183,6 +185,8 @@ function PrintSurface({
onNodeReady: () => void; onNodeReady: () => void;
scale: number; scale: number;
product: ProductDTO; product: ProductDTO;
referenceWidthCm: number | null;
referenceHeightCm: number | null;
}) { }) {
const stageW = printArea.wPct * DISPLAY; const stageW = printArea.wPct * DISPLAY;
const stageH = printArea.hPct * DISPLAY; const stageH = printArea.hPct * DISPLAY;
@@ -204,13 +208,13 @@ function PrintSurface({
</div> </div>
{/* Horizontal Ruler */} {/* Horizontal Ruler */}
{product.printAreaWidthMm > 0 && stageW > 0 && ( {referenceWidthCm && referenceWidthCm > 0 && (
<div <div
className="absolute pointer-events-none bg-paper border-b border-line text-xs font-mono" className="absolute pointer-events-none bg-paper border-b border-line text-xs font-mono"
style={{ style={{
left: stageLeft, left: 0,
top: stageTop - 20, top: -20,
width: stageW, width: DISPLAY,
height: 18, height: 18,
display: 'flex', display: 'flex',
alignItems: 'flex-end', alignItems: 'flex-end',
@@ -219,12 +223,11 @@ function PrintSurface({
> >
{(() => { {(() => {
const marks = []; const marks = [];
const cmPerStageW = product.printAreaWidthMm / 10; // total cm width of stage const pxPerCm = DISPLAY / referenceWidthCm;
const pxPerCm = stageW / cmPerStageW;
for (let cm = 0; cm <= Math.ceil(cmPerStageW); cm += 2) { for (let cm = 0; cm <= Math.ceil(referenceWidthCm); cm += 2) {
const px = cm * pxPerCm; const px = cm * pxPerCm;
if (px > stageW) break; if (px > DISPLAY) break;
marks.push( marks.push(
<div <div
key={`h-${cm}`} key={`h-${cm}`}
@@ -247,14 +250,14 @@ function PrintSurface({
)} )}
{/* Vertical Ruler */} {/* Vertical Ruler */}
{product.printAreaHeightMm > 0 && stageH > 0 && ( {referenceHeightCm && referenceHeightCm > 0 && (
<div <div
className="absolute pointer-events-none bg-paper border-r border-line text-xs font-mono" className="absolute pointer-events-none bg-paper border-r border-line text-xs font-mono"
style={{ style={{
left: stageLeft - 28, left: -28,
top: stageTop, top: 0,
width: 26, width: 26,
height: stageH, height: DISPLAY,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-end', alignItems: 'flex-end',
@@ -263,12 +266,11 @@ function PrintSurface({
> >
{(() => { {(() => {
const marks = []; const marks = [];
const cmPerStageH = product.printAreaHeightMm / 10; // total cm height of stage const pxPerCm = DISPLAY / referenceHeightCm;
const pxPerCm = stageH / cmPerStageH;
for (let cm = 0; cm <= Math.ceil(cmPerStageH); cm += 2) { for (let cm = 0; cm <= Math.ceil(referenceHeightCm); cm += 2) {
const px = cm * pxPerCm; const px = cm * pxPerCm;
if (px > stageH) break; if (px > DISPLAY) break;
marks.push( marks.push(
<div <div
key={`v-${cm}`} key={`v-${cm}`}
@@ -404,6 +406,8 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
const [justAdded, setJustAdded] = useState(false); const [justAdded, setJustAdded] = useState(false);
const [showLoginPrompt, setShowLoginPrompt] = useState(false); const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const [nodeReadyTick, setNodeReadyTick] = useState(0); const [nodeReadyTick, setNodeReadyTick] = useState(0);
const [referenceWidthCm, setReferenceWidthCm] = useState<number | null>(null);
const [referenceHeightCm, setReferenceHeightCm] = useState<number | null>(null);
const stageFrontRef = useRef<Konva.Stage | null>(null); const stageFrontRef = useRef<Konva.Stage | null>(null);
const stageBackRef = useRef<Konva.Stage | null>(null); const stageBackRef = useRef<Konva.Stage | null>(null);
@@ -439,14 +443,12 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
const activeStageW = activePrintArea.wPct * DISPLAY; const activeStageW = activePrintArea.wPct * DISPLAY;
const activeStageH = activePrintArea.hPct * DISPLAY; const activeStageH = activePrintArea.hPct * DISPLAY;
// Calculate cm per pixel for display // Calculate cm per pixel for display using reference size
const printAreaWidthPx = activePrintArea.wPct * DISPLAY; // Reference size maps to full canvas width/height (560px)
// printAreaWidthMm is in mm, divide by 10 to get cm const cmPerPx = referenceWidthCm && referenceWidthCm > 0 ? referenceWidthCm / DISPLAY : 0;
// printAreaWidthPx is in pixels const cmPerPxHeight = referenceHeightCm && referenceHeightCm > 0 ? referenceHeightCm / DISPLAY : 0;
// cmPerPx = cm per pixel
const mmPerPx = printAreaWidthPx > 0 ? product.printAreaWidthMm / printAreaWidthPx : 0;
const cmPerPx = mmPerPx > 0 ? mmPerPx / 10 : 0;
const pxToCm = (px: number) => { const pxToCm = (px: number) => {
if (!cmPerPx || cmPerPx <= 0) return '—';
const cm = px * cmPerPx; const cm = px * cmPerPx;
return isNaN(cm) ? '?' : cm.toFixed(2); return isNaN(cm) ? '?' : cm.toFixed(2);
}; };
@@ -748,6 +750,8 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
onNodeReady={() => setNodeReadyTick((t) => t + 1)} onNodeReady={() => setNodeReadyTick((t) => t + 1)}
scale={canvasScale} scale={canvasScale}
product={product} product={product}
referenceWidthCm={referenceWidthCm}
referenceHeightCm={referenceHeightCm}
/> />
{hasBack && ( {hasBack && (
@@ -766,6 +770,8 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
onNodeReady={() => setNodeReadyTick((t) => t + 1)} onNodeReady={() => setNodeReadyTick((t) => t + 1)}
scale={canvasScale} scale={canvasScale}
product={product} product={product}
referenceWidthCm={referenceWidthCm}
referenceHeightCm={referenceHeightCm}
/> />
)} )}
</> </>
@@ -855,6 +861,38 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
</div> </div>
)} )}
{canPersonalize && (
<div>
<p className="tag-label mb-3">Reference Size (cm)</p>
<div className="flex gap-3">
<div className="flex-1">
<label className="text-xs text-muted">Width</label>
<input
type="number"
min="1"
step="0.1"
value={referenceWidthCm ?? ''}
onChange={(e) => setReferenceWidthCm(e.target.value ? Number(e.target.value) : null)}
placeholder="e.g. 40"
className="w-full border border-line bg-paper px-3 py-2 text-sm placeholder:text-muted"
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted">Height</label>
<input
type="number"
min="1"
step="0.1"
value={referenceHeightCm ?? ''}
onChange={(e) => setReferenceHeightCm(e.target.value ? Number(e.target.value) : null)}
placeholder="e.g. 60"
className="w-full border border-line bg-paper px-3 py-2 text-sm placeholder:text-muted"
/>
</div>
</div>
</div>
)}
{canPersonalize && ( {canPersonalize && (
<div> <div>
<p className="tag-label mb-3"> <p className="tag-label mb-3">
@@ -1002,9 +1040,9 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
</div> </div>
<button <button
onClick={handleAddToBag} onClick={handleAddToBag}
disabled={elementsFront.length === 0 && elementsBack.length === 0} disabled={!canPersonalize || (elementsFront.length === 0 && elementsBack.length === 0) || !referenceWidthCm || !referenceHeightCm}
className={`flex-1 px-6 py-3 text-sm font-medium transition-colors ${ className={`flex-1 px-6 py-3 text-sm font-medium transition-colors ${
elementsFront.length === 0 && elementsBack.length === 0 !canPersonalize || (elementsFront.length === 0 && elementsBack.length === 0) || !referenceWidthCm || !referenceHeightCm
? 'bg-muted text-muted/50 cursor-not-allowed' ? 'bg-muted text-muted/50 cursor-not-allowed'
: 'bg-clay text-paper hover:bg-clay-dark' : 'bg-clay text-paper hover:bg-clay-dark'
}`} }`}
@@ -1012,7 +1050,10 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
Add to bag <Price cents={((elementsFront.length > 0 || elementsBack.length > 0) ? effectivePriceForPersonalised : product.effectivePrice) * quantity} /> Add to bag <Price cents={((elementsFront.length > 0 || elementsBack.length > 0) ? effectivePriceForPersonalised : product.effectivePrice) * quantity} />
</button> </button>
</div> </div>
{elementsFront.length === 0 && elementsBack.length === 0 && ( {canPersonalize && (!referenceWidthCm || !referenceHeightCm) && (
<p className="text-xs text-splash-pink">Please enter the reference size (width and height) before adding to bag</p>
)}
{canPersonalize && (elementsFront.length === 0 && elementsBack.length === 0) && (referenceWidthCm && referenceHeightCm) && (
<p className="text-xs text-splash-pink">Add text or images to personalize your design before adding to bag</p> <p className="text-xs text-splash-pink">Add text or images to personalize your design before adding to bag</p>
)} )}
{justAdded && <p className="text-sm text-pine">Added to your bag.</p>} {justAdded && <p className="text-sm text-pine">Added to your bag.</p>}