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:
co-authored by
Claude Haiku 4.5
parent
c20c696ccb
commit
2595adc305
@@ -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>}
|
||||||
|
|||||||
Reference in New Issue
Block a user