'use client'; import { useRef, useState } from 'react'; type Box = { xPct: number; yPct: number; wPct: number; hPct: number }; // 0-100 const DEFAULT_BOX: Box = { xPct: 30, yPct: 25, wPct: 40, hPct: 35 }; const MIN_SIZE = 6; // percent export default function PhotoPrintAreaField({ variant = 'front', existingImageUrl, }: { variant?: 'front' | 'back'; existingImageUrl?: string; }) { const [previewUrl, setPreviewUrl] = useState(null); const [box, setBox] = useState(DEFAULT_BOX); const [showPrintAreaEditor, setShowPrintAreaEditor] = useState(false); const [isRemoved, setIsRemoved] = useState(false); const containerRef = useRef(null); const fileInputRef = useRef(null); const dragState = useRef<{ mode: 'move' | 'resize'; startX: number; startY: number; startBox: Box; } | null>(null); const fieldName = variant === 'front' ? 'image' : 'imageBack'; const suffix = variant === 'front' ? '' : 'Back'; const removeFieldName = variant === 'front' ? 'removeImage' : 'removeImageBack'; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) { setPreviewUrl(null); setShowPrintAreaEditor(false); setIsRemoved(false); return; } setPreviewUrl(URL.createObjectURL(file)); setBox(DEFAULT_BOX); setShowPrintAreaEditor(true); setIsRemoved(false); }; const handleClearFile = () => { setPreviewUrl(null); setShowPrintAreaEditor(false); setIsRemoved(true); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const handleBackToFile = () => { setShowPrintAreaEditor(false); }; const currentImageUrl = previewUrl || (isRemoved ? null : existingImageUrl); const onBoxMouseDown = (e: React.MouseEvent) => { e.preventDefault(); dragState.current = { mode: 'move', startX: e.clientX, startY: e.clientY, startBox: box }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }; const onResizeMouseDown = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); dragState.current = { mode: 'resize', startX: e.clientX, startY: e.clientY, startBox: box }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }; const onMouseMove = (e: MouseEvent) => { const state = dragState.current; const rect = containerRef.current?.getBoundingClientRect(); if (!state || !rect) return; const dxPct = ((e.clientX - state.startX) / rect.width) * 100; const dyPct = ((e.clientY - state.startY) / rect.height) * 100; if (state.mode === 'move') { const xPct = clamp(state.startBox.xPct + dxPct, 0, 100 - state.startBox.wPct); const yPct = clamp(state.startBox.yPct + dyPct, 0, 100 - state.startBox.hPct); setBox({ ...state.startBox, xPct, yPct }); } else { const wPct = clamp(state.startBox.wPct + dxPct, MIN_SIZE, 100 - state.startBox.xPct); const hPct = clamp(state.startBox.hPct + dyPct, MIN_SIZE, 100 - state.startBox.yPct); setBox({ ...state.startBox, wPct, hPct }); } }; const onMouseUp = () => { dragState.current = null; window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; return (
{currentImageUrl && !showPrintAreaEditor && (
{/* eslint-disable-next-line @next/next/no-img-element */} Product preview

Current photo. Upload a new one below to replace it.

)} {!currentImageUrl || !showPrintAreaEditor ? ( <> {variant === 'front' ? (

Uploading a real photo replaces the illustration everywhere this product is shown. One tradeoff: the color swatches below can no longer recolor a photo the way they recolor the illustration — they'll still be saved with each order, but the picture itself stays fixed. Leave this blank to keep using the illustration (which does recolor live).

) : (

If set, customers get a Front/Back toggle and can add a separate design to the back — drag a print-area box below just like the front.

)} {currentImageUrl && !showPrintAreaEditor && (
)} ) : null} {currentImageUrl && showPrintAreaEditor && (

Drag the box to mark the print area

Cropped to a square here — that's the same crop the design tool uses, so the box lines up correctly.

{/* eslint-disable-next-line @next/next/no-img-element */} Product preview

Drag the pink handle to resize.

)}
); } function clamp(n: number, min: number, max: number) { return Math.min(Math.max(n, min), Math.max(min, max)); }