diff --git a/src/components/DesignCanvas.tsx b/src/components/DesignCanvas.tsx index 51947dc..c5c8f3e 100644 --- a/src/components/DesignCanvas.tsx +++ b/src/components/DesignCanvas.tsx @@ -394,6 +394,8 @@ function PrintSurface({ export default function DesignCanvas({ product }: { product: ProductDTO }) { const [color, setColor] = useState(product.colors[0]); const [size, setSize] = useState(product.sizes[0] ?? null); + const [designWidthCm, setDesignWidthCm] = useState(null); + const [designHeightCm, setDesignHeightCm] = useState(null); const [view, setView] = useState<'front' | 'back'>('front'); const [elementsFront, setElementsFront] = useState([]); const [elementsBack, setElementsBack] = useState([]); @@ -402,6 +404,11 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) { const [justAdded, setJustAdded] = useState(false); const [showLoginPrompt, setShowLoginPrompt] = useState(false); const [nodeReadyTick, setNodeReadyTick] = useState(0); + const [approvalStatus, setApprovalStatus] = useState<'PENDING' | 'APPROVED' | 'REJECTED' | null>(null); + const [submittingForApproval, setSubmittingForApproval] = useState(false); + const [designApprovalId, setDesignApprovalId] = useState(null); + const [submissionMessage, setSubmissionMessage] = useState(null); + const [imageDimensions, setImageDimensions] = useState>({}); const stageFrontRef = useRef(null); const stageBackRef = useRef(null); @@ -419,6 +426,53 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) { // touching that coordinate system. Konva reads the container's actual // rendered (post-transform) bounding box for pointer math, so clicks/drags // still line up correctly at any scale. + useEffect(() => { + if (typeof window === 'undefined') return; + + // Load approval status + const approvalSaved = localStorage.getItem(`designApproval_${product.id}`); + if (approvalSaved) { + try { + const data = JSON.parse(approvalSaved); + setDesignApprovalId(data.id); + setApprovalStatus(data.status); + console.log('βœ“ Approval status restored'); + } catch (e) { + console.error('Failed to load approval status:', e); + } + } + + // Load design draft (saved when user tried to checkout without login) + const designDraft = localStorage.getItem(`designDraft_${product.id}`); + if (designDraft) { + try { + const data = JSON.parse(designDraft); + console.log('πŸ“‹ Found design draft, restoring...', data); + + if (data.elementsFront && data.elementsFront.length > 0) { + setElementsFront(data.elementsFront); + console.log('βœ“ Restored', data.elementsFront.length, 'front elements'); + } + if (data.elementsBack && data.elementsBack.length > 0) { + setElementsBack(data.elementsBack); + console.log('βœ“ Restored', data.elementsBack.length, 'back elements'); + } + if (data.color) setColor(data.color); + if (data.size) setSize(data.size); + if (data.designWidthCm !== null && data.designWidthCm !== undefined) setDesignWidthCm(data.designWidthCm); + if (data.designHeightCm !== null && data.designHeightCm !== undefined) setDesignHeightCm(data.designHeightCm); + if (data.imageDimensions) setImageDimensions(data.imageDimensions); + if (data.quantity) setQuantity(data.quantity); + + console.log('βœ… Design fully restored from draft'); + // Keep the draft in localStorage until user completes checkout + // Only remove after successful add to bag + } catch (e) { + console.error('Failed to load design draft:', e); + } + } + }, [product.id]); + useLayoutEffect(() => { const el = canvasContainerRef.current; if (!el) return; @@ -565,7 +619,23 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) { const handleAddToBag = async () => { const { isAuthenticated } = await checkCustomerAuth(); if (!isAuthenticated) { + // Save design draft before redirecting to login + if (typeof window !== 'undefined' && (elementsFront.length > 0 || elementsBack.length > 0)) { + localStorage.setItem(`designDraft_${product.id}`, JSON.stringify({ + elementsFront, + elementsBack, + color, + size, + designWidthCm, + designHeightCm, + imageDimensions, + quantity, + timestamp: new Date().toISOString(), + })); + console.log('Design saved to draft'); + } setShowLoginPrompt(true); + return; } if (!canPersonalize) { @@ -585,7 +655,15 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) { designPreviewUrlBack: null, placementPreviewUrl: flatPhoto || null, placementPreviewUrlBack: product.imageUrlBack ?? null, + designWidthCm: null, + designHeightCm: null, }); + + // Clear design draft if any + if (typeof window !== 'undefined') { + localStorage.removeItem(`designDraft_${product.id}`); + } + setJustAdded(true); setTimeout(() => setJustAdded(false), 2200); return; @@ -646,7 +724,7 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) { backGuides.forEach((g) => g.show()); } - addItem({ + const cartItem = { cartItemId: uuid(), productId: product.id, slug: product.slug, @@ -661,12 +739,151 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) { designPreviewUrlBack: backDataUrl, placementPreviewUrl: frontPlacementUrl ?? frontDataUrl, placementPreviewUrlBack: backPlacementUrl ?? backDataUrl, - }); + designWidthCm, + designHeightCm, + }; + console.log('Adding to cart with dimensions:', { designWidthCm, designHeightCm, cartItem }); + addItem(cartItem); + + // Clear design draft now that it's been added to cart + if (typeof window !== 'undefined') { + localStorage.removeItem(`designDraft_${product.id}`); + console.log('βœ“ Design draft cleared after adding to cart'); + } + setJustAdded(true); setTimeout(() => setJustAdded(false), 2200); }); }; + const handleSubmitForApproval = async () => { + // First, save design to localStorage (in case user needs to login) + if (typeof window !== 'undefined' && (elementsFront.length > 0 || elementsBack.length > 0)) { + localStorage.setItem(`designDraft_${product.id}`, JSON.stringify({ + elementsFront, + elementsBack, + color, + size, + designWidthCm, + designHeightCm, + imageDimensions, + quantity, + timestamp: new Date().toISOString(), + })); + } + + if (elementsFront.length === 0 && elementsBack.length === 0) { + return; + } + + setSubmittingForApproval(true); + setSelectedId(null); + requestAnimationFrame(async () => { + const frontStage = stageFrontRef.current; + if (!frontStage) { + setSubmittingForApproval(false); + return; + } + + const frontGuides = frontStage.find('.printGuide'); + frontGuides.forEach((g) => g.hide()); + const frontDataUrl = frontStage.toDataURL({ pixelRatio: 2 }); + + const frontBox = { + left: frontPrintArea.xPct * DISPLAY, + top: frontPrintArea.yPct * DISPLAY, + w: frontPrintArea.wPct * DISPLAY, + h: frontPrintArea.hPct * DISPLAY, + }; + const frontPhotoInfo = getPhotoInfo('front'); + const frontPlacementUrl = frontPhotoInfo + ? await composePlacementImage( + frontPhotoInfo.url, + frontPhotoInfo.tint ? { tint: true, color } : null, + frontStage, + frontBox, + ).catch(() => null) + : null; + frontGuides.forEach((g) => g.show()); + + let backDataUrl: string | null = null; + let backPlacementUrl: string | null = null; + if (hasBack && stageBackRef.current) { + const backStage = stageBackRef.current; + const backGuides = backStage.find('.printGuide'); + backGuides.forEach((g) => g.hide()); + backDataUrl = backStage.toDataURL({ pixelRatio: 2 }); + + const backBox = { + left: backPrintArea.xPct * DISPLAY, + top: backPrintArea.yPct * DISPLAY, + w: backPrintArea.wPct * DISPLAY, + h: backPrintArea.hPct * DISPLAY, + }; + const backPhotoInfo = getPhotoInfo('back'); + backPlacementUrl = backPhotoInfo + ? await composePlacementImage( + backPhotoInfo.url, + backPhotoInfo.tint ? { tint: true, color } : null, + backStage, + backBox, + ).catch(() => null) + : null; + backGuides.forEach((g) => g.show()); + } + + try { + const res = await fetch('/api/designs/submit', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + productId: product.id, + designJson: JSON.stringify({ front: elementsFront, back: elementsBack }), + designPreviewUrl: frontDataUrl, + designPreviewUrlBack: backDataUrl, + placementPreviewUrl: frontPlacementUrl ?? frontDataUrl, + placementPreviewUrlBack: backPlacementUrl ?? backDataUrl, + designWidthCm, + designHeightCm, + designImageDimensions: Object.entries(imageDimensions).map(([imageId, dims]) => ({ + imageId, + widthCm: dims.widthCm, + heightCm: dims.heightCm, + })), + color, + size, + }), + }); + + if (res.ok) { + const design = await res.json(); + setDesignApprovalId(design.id); + setApprovalStatus('PENDING'); + setSubmissionMessage(`βœ“ Design submitted for approval! You'll receive an email when it's ready to add to your bag.`); + // Save to localStorage so status persists on page reload + if (typeof window !== 'undefined') { + localStorage.setItem(`designApproval_${product.id}`, JSON.stringify({ + id: design.id, + status: 'PENDING', + submittedAt: new Date().toISOString(), + })); + } + console.log('Design submitted for approval:', design.id); + setTimeout(() => setSubmissionMessage(null), 5000); + } else if (res.status === 401) { + setShowLoginPrompt(true); + console.log('User not authenticated, showing login prompt'); + } else { + const err = await res.json().catch(() => ({ error: 'Unknown error' })); + console.error('Failed to submit design for approval:', err.error); + setSubmissionMessage(`βœ— Failed to submit design: ${err.error || 'Unknown error'}`); + } + } finally { + setSubmittingForApproval(false); + } + }); + }; + function renderPhoto(v: 'front' | 'back') { const colorPhoto = v === 'front' ? product.colorPhotos[color] : product.colorPhotosBack[color]; if (colorPhoto) { @@ -850,11 +1067,145 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) { )} + {canPersonalize && (

Your design {hasBack && β€” editing the {view}}

+ + {/* Image Dimension Inputs */} + {(() => { + const images = view === 'front' ? elementsFront.filter(el => el.type === 'image') : elementsBack.filter(el => el.type === 'image'); + return images.length > 0 ? ( +
+ {images.map((img, idx) => ( +
+

Image {idx + 1} Dimensions

+
+ + +
+
+ ))} +
+ ) : null; + })()} + + {/* Font Properties Inputs */} + {(() => { + const texts = view === 'front' ? elementsFront.filter(el => el.type === 'text') : elementsBack.filter(el => el.type === 'text'); + return texts.length > 0 ? ( +
+ {texts.map((text, idx) => ( +
+

Font {idx + 1}

+
+ + +
+ + +
+ +
+
+ ))} +
+ ) : null; + })()} +
- {selected && ( -
-

Position & Size (cm)

-
-
- X: - {pxToCm(selected.x)} cm -
-
- Y: - {pxToCm(selected.y)} cm -
- {selected.type === 'image' && ( - <> -
- Width: - {pxToCm(selected.width)} cm -
-
- Height: - {pxToCm(selected.height)} cm -
- - )} -
-
- )} - {selected?.type === 'text' && (
)} - {selected?.type === 'image' && ( -
-
- Size: - {Math.round(selected.width)} Γ— {Math.round(selected.height)} px -
- -
- )}
)}
+ {submissionMessage && ( +

+ {submissionMessage} +

+ )} + {approvalStatus === 'PENDING' && !submissionMessage && ( +

+ ⏳ Your design is pending admin approval. You'll receive an email when it's ready to add to your bag. +

+ )} + {approvalStatus === 'APPROVED' && ( +

+ βœ“ Your design has been approved! You can now add it to your bag. +

+ )} + {approvalStatus === 'REJECTED' && ( +

+ Your design needs changes. Check your messages for feedback. +

+ )} +
- + {approvalStatus === 'APPROVED' ? ( + + ) : ( + + )}
- {elementsFront.length === 0 && elementsBack.length === 0 && ( -

Add text or images to personalize your design before adding to bag

+ {elementsFront.length === 0 && elementsBack.length === 0 && !approvalStatus && ( +

Add text or images to personalize your design before submitting

)} {justAdded &&

Added to your bag.

}
diff --git a/src/components/LoginPromptModal.tsx b/src/components/LoginPromptModal.tsx index 2ebdb61..dc6fd9a 100644 --- a/src/components/LoginPromptModal.tsx +++ b/src/components/LoginPromptModal.tsx @@ -1,6 +1,7 @@ 'use client'; import Link from 'next/link'; +import { useEffect, useState } from 'react'; interface LoginPromptModalProps { isOpen: boolean; @@ -8,8 +9,18 @@ interface LoginPromptModalProps { } export default function LoginPromptModal({ isOpen, onClose }: LoginPromptModalProps) { + const [currentUrl, setCurrentUrl] = useState(''); + + useEffect(() => { + if (typeof window !== 'undefined') { + setCurrentUrl(window.location.pathname + window.location.search); + } + }, [isOpen]); + if (!isOpen) return null; + const nextParam = currentUrl ? `?next=${encodeURIComponent(currentUrl)}` : ''; + return (
@@ -20,14 +31,14 @@ export default function LoginPromptModal({ isOpen, onClose }: LoginPromptModalPr
Create account