Fix design persistence and enhance font/image property controls
- Fix: Redirect users back to product page after login by passing 'next' parameter in LoginPromptModal. Previously users were redirected to /account and lost design context. Now they return to where they started (e.g., /made-to-order/ classic-t-shirt) where design drafts are restored from localStorage. - Fix: Clear design draft from localStorage only AFTER successful cart addition, not immediately. This ensures draft persists if user needs to login mid-design. - Feature: Add Font Properties boxes (Font 1, Font 2, etc.) similar to Image Dimensions. Each text element now displays with editable controls for: • Text content • Font family (dropdown with all fonts) • Font size (slider 10-80pt with display) • Text color (color picker) • Rotation (slider 0-360 degrees) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
0e14fc7981
commit
859956b9d8
+407
-63
@@ -394,6 +394,8 @@ function PrintSurface({
|
||||
export default function DesignCanvas({ product }: { product: ProductDTO }) {
|
||||
const [color, setColor] = useState(product.colors[0]);
|
||||
const [size, setSize] = useState<string | null>(product.sizes[0] ?? null);
|
||||
const [designWidthCm, setDesignWidthCm] = useState<number | null>(null);
|
||||
const [designHeightCm, setDesignHeightCm] = useState<number | null>(null);
|
||||
const [view, setView] = useState<'front' | 'back'>('front');
|
||||
const [elementsFront, setElementsFront] = useState<DesignElement[]>([]);
|
||||
const [elementsBack, setElementsBack] = useState<DesignElement[]>([]);
|
||||
@@ -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<string | null>(null);
|
||||
const [submissionMessage, setSubmissionMessage] = useState<string | null>(null);
|
||||
const [imageDimensions, setImageDimensions] = useState<Record<string, { widthCm: number | null; heightCm: number | null }>>({});
|
||||
|
||||
const stageFrontRef = useRef<Konva.Stage | null>(null);
|
||||
const stageBackRef = useRef<Konva.Stage | null>(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 }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{canPersonalize && (
|
||||
<div>
|
||||
<p className="tag-label mb-3">
|
||||
Your design {hasBack && <span className="normal-case text-muted">— editing the {view}</span>}
|
||||
</p>
|
||||
|
||||
{/* Image Dimension Inputs */}
|
||||
{(() => {
|
||||
const images = view === 'front' ? elementsFront.filter(el => el.type === 'image') : elementsBack.filter(el => el.type === 'image');
|
||||
return images.length > 0 ? (
|
||||
<div className="mb-4 space-y-3">
|
||||
{images.map((img, idx) => (
|
||||
<div key={img.id} className="border border-line bg-surface p-3">
|
||||
<p className="tag-label mb-2">Image {idx + 1} Dimensions</p>
|
||||
<div className="flex gap-3">
|
||||
<label className="flex flex-col gap-1 flex-1">
|
||||
<span className="text-xs text-muted">Width (cm)</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
value={imageDimensions[img.id]?.widthCm ?? ''}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value ? Number(e.target.value) : null;
|
||||
setImageDimensions((prev) => ({
|
||||
...prev,
|
||||
[img.id]: { ...prev[img.id], widthCm: val },
|
||||
}));
|
||||
}}
|
||||
placeholder="e.g. 10"
|
||||
className="border border-line bg-paper px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 flex-1">
|
||||
<span className="text-xs text-muted">Height (cm)</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
value={imageDimensions[img.id]?.heightCm ?? ''}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value ? Number(e.target.value) : null;
|
||||
setImageDimensions((prev) => ({
|
||||
...prev,
|
||||
[img.id]: { ...prev[img.id], heightCm: val },
|
||||
}));
|
||||
}}
|
||||
placeholder="e.g. 10"
|
||||
className="border border-line bg-paper px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
|
||||
{/* Font Properties Inputs */}
|
||||
{(() => {
|
||||
const texts = view === 'front' ? elementsFront.filter(el => el.type === 'text') : elementsBack.filter(el => el.type === 'text');
|
||||
return texts.length > 0 ? (
|
||||
<div className="mb-4 space-y-3">
|
||||
{texts.map((text, idx) => (
|
||||
<div key={text.id} className="border border-line bg-surface p-3">
|
||||
<p className="tag-label mb-3">Font {idx + 1}</p>
|
||||
<div className="space-y-3">
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted">Text</span>
|
||||
<input
|
||||
type="text"
|
||||
value={text.text}
|
||||
onChange={(e) => updateElement(text.id, { text: e.target.value })}
|
||||
className="border border-line bg-paper px-3 py-2 text-sm"
|
||||
placeholder="Text content"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted">Font Family</span>
|
||||
<select
|
||||
value={text.fontFamily}
|
||||
onChange={(e) => updateElement(text.id, { fontFamily: e.target.value })}
|
||||
className="border border-line bg-paper px-3 py-2 text-sm"
|
||||
style={{ fontFamily: text.fontFamily }}
|
||||
>
|
||||
{FONTS.map((f) => (
|
||||
<option key={f} value={f} style={{ fontFamily: f }}>
|
||||
{f}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
<label className="flex flex-col gap-1 flex-1">
|
||||
<span className="text-xs text-muted">Font Size</span>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="range"
|
||||
min={10}
|
||||
max={80}
|
||||
value={text.fontSize}
|
||||
onChange={(e) => updateElement(text.id, { fontSize: Number(e.target.value) })}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-right font-mono text-xs">{text.fontSize}pt</span>
|
||||
</div>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted">Color</span>
|
||||
<input
|
||||
type="color"
|
||||
value={text.fill}
|
||||
onChange={(e) => updateElement(text.id, { fill: e.target.value })}
|
||||
className="h-10 w-16 border border-line bg-paper p-1 cursor-pointer"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted">Rotation (degrees)</span>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={360}
|
||||
value={text.rotation || 0}
|
||||
onChange={(e) => updateElement(text.id, { rotation: Number(e.target.value) })}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-right font-mono text-xs">{text.rotation || 0}°</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button onClick={addText} className="border border-ink px-4 py-2 text-sm hover:border-clay hover:bg-clay hover:text-paper">
|
||||
+ Add text
|
||||
@@ -876,34 +1227,6 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div className="mt-4 border border-line bg-surface p-4">
|
||||
<p className="mb-3 font-medium text-sm">Position & Size (cm)</p>
|
||||
<div className="space-y-2 text-xs font-mono">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted">X:</span>
|
||||
<span>{pxToCm(selected.x)} cm</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted">Y:</span>
|
||||
<span>{pxToCm(selected.y)} cm</span>
|
||||
</div>
|
||||
{selected.type === 'image' && (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted">Width:</span>
|
||||
<span>{pxToCm(selected.width)} cm</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted">Height:</span>
|
||||
<span>{pxToCm(selected.height)} cm</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selected?.type === 'text' && (
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3 border border-line bg-surface p-4">
|
||||
<input
|
||||
@@ -931,7 +1254,7 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
|
||||
className="h-8 w-8 border border-line bg-paper p-0"
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm text-muted">
|
||||
Size
|
||||
Font Size
|
||||
<input
|
||||
type="range"
|
||||
min={10}
|
||||
@@ -939,6 +1262,7 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
|
||||
value={selected.fontSize}
|
||||
onChange={(e) => updateElement(selected.id, { fontSize: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="w-12 text-right font-mono text-xs">{selected.fontSize}pt</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm text-muted">
|
||||
Angle
|
||||
@@ -954,29 +1278,35 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selected?.type === 'image' && (
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3 border border-line bg-surface p-4">
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="font-medium">Size:</span>
|
||||
<span className="font-mono text-xs">{Math.round(selected.width)} × {Math.round(selected.height)} px</span>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm text-muted">
|
||||
Angle
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={360}
|
||||
value={selected.rotation || 0}
|
||||
onChange={(e) => updateElement(selected.id, { rotation: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="w-10 text-right font-mono text-xs">{selected.rotation || 0}°</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{submissionMessage && (
|
||||
<p className={`text-sm px-4 py-3 rounded border ${
|
||||
submissionMessage.startsWith('✓')
|
||||
? 'text-green-700 bg-green-50 border-green-200'
|
||||
: 'text-splash-pink bg-red-50 border-splash-pink/30'
|
||||
}`}>
|
||||
{submissionMessage}
|
||||
</p>
|
||||
)}
|
||||
{approvalStatus === 'PENDING' && !submissionMessage && (
|
||||
<p className="text-sm text-orange-600 bg-orange-50 border border-orange-200 px-4 py-3 rounded">
|
||||
⏳ Your design is pending admin approval. You'll receive an email when it's ready to add to your bag.
|
||||
</p>
|
||||
)}
|
||||
{approvalStatus === 'APPROVED' && (
|
||||
<p className="text-sm text-pine bg-green-50 border border-green-200 px-4 py-3 rounded">
|
||||
✓ Your design has been approved! You can now add it to your bag.
|
||||
</p>
|
||||
)}
|
||||
{approvalStatus === 'REJECTED' && (
|
||||
<p className="text-sm text-splash-pink bg-red-50 border border-splash-pink/30 px-4 py-3 rounded">
|
||||
Your design needs changes. Check your messages for feedback.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center border border-ink">
|
||||
<button
|
||||
@@ -995,20 +1325,34 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddToBag}
|
||||
disabled={elementsFront.length === 0 && elementsBack.length === 0}
|
||||
className={`flex-1 px-6 py-3 text-sm font-medium transition-colors ${
|
||||
elementsFront.length === 0 && elementsBack.length === 0
|
||||
? 'bg-muted text-muted/50 cursor-not-allowed'
|
||||
: 'bg-clay text-paper hover:bg-clay-dark'
|
||||
}`}
|
||||
>
|
||||
Add to bag — <Price cents={((elementsFront.length > 0 || elementsBack.length > 0) ? effectivePriceForPersonalised : product.effectivePrice) * quantity} />
|
||||
</button>
|
||||
{approvalStatus === 'APPROVED' ? (
|
||||
<button
|
||||
onClick={handleAddToBag}
|
||||
disabled={elementsFront.length === 0 && elementsBack.length === 0}
|
||||
className={`flex-1 px-6 py-3 text-sm font-medium transition-colors ${
|
||||
elementsFront.length === 0 && elementsBack.length === 0
|
||||
? 'bg-muted text-muted/50 cursor-not-allowed'
|
||||
: 'bg-clay text-paper hover:bg-clay-dark'
|
||||
}`}
|
||||
>
|
||||
Add to bag — <Price cents={((elementsFront.length > 0 || elementsBack.length > 0) ? effectivePriceForPersonalised : product.effectivePrice) * quantity} />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSubmitForApproval}
|
||||
disabled={elementsFront.length === 0 && elementsBack.length === 0 || submittingForApproval || approvalStatus === 'PENDING'}
|
||||
className={`flex-1 px-6 py-3 text-sm font-medium transition-colors ${
|
||||
elementsFront.length === 0 && elementsBack.length === 0 || submittingForApproval || approvalStatus === 'PENDING'
|
||||
? 'bg-muted text-muted/50 cursor-not-allowed'
|
||||
: 'bg-clay text-paper hover:bg-clay-dark'
|
||||
}`}
|
||||
>
|
||||
{submittingForApproval ? 'Submitting…' : 'Submit for Approval'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{elementsFront.length === 0 && elementsBack.length === 0 && (
|
||||
<p className="text-xs text-splash-pink">Add text or images to personalize your design before adding to bag</p>
|
||||
{elementsFront.length === 0 && elementsBack.length === 0 && !approvalStatus && (
|
||||
<p className="text-xs text-splash-pink">Add text or images to personalize your design before submitting</p>
|
||||
)}
|
||||
{justAdded && <p className="text-sm text-pine">Added to your bag.</p>}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="mx-4 w-full max-w-sm rounded-lg border border-line bg-paper p-6 shadow-lg">
|
||||
@@ -20,14 +31,14 @@ export default function LoginPromptModal({ isOpen, onClose }: LoginPromptModalPr
|
||||
|
||||
<div className="mt-6 space-y-3">
|
||||
<Link
|
||||
href="/account/register"
|
||||
href={`/account/register${nextParam}`}
|
||||
onClick={onClose}
|
||||
className="block w-full rounded bg-clay px-4 py-3 text-center text-sm font-medium text-paper transition-colors hover:bg-clay-dark"
|
||||
>
|
||||
Create account
|
||||
</Link>
|
||||
<Link
|
||||
href="/account/login"
|
||||
href={`/account/login${nextParam}`}
|
||||
onClick={onClose}
|
||||
className="block w-full rounded border border-ink px-4 py-3 text-center text-sm font-medium text-ink transition-colors hover:border-clay hover:bg-clay hover:text-paper"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user