Points now subtract from costs

This commit is contained in:
augmentedpotato
2026-04-15 02:44:14 -06:00
parent 47f60dc309
commit dfc3d627c8
6 changed files with 192 additions and 7 deletions

View File

@@ -87,6 +87,7 @@ export default function CartPage() {
removeItem,
clearCart,
applyCoupon,
applyPoints,
removeCoupon,
checkout,
cancelCheckout,
@@ -98,6 +99,10 @@ export default function CartPage() {
const [couponSuccess, setCouponSuccess] = useState(null);
const [couponLoading, setCouponLoading] = useState(false);
const [pointsLoading, setPointsLoading] = useState(false);
const [pointsError, setPointsError] = useState(null);
const [optimisticPointsApplied, setOptimisticPointsApplied] = useState(null);
const [checkoutLoading, setCheckoutLoading] = useState(false);
const [checkoutError, setCheckoutError] = useState(null);
const [clientSecret, setClientSecret] = useState(null);
@@ -118,6 +123,8 @@ export default function CartPage() {
cart.items.forEach((i) => (map[i.cartItemId] = i.quantity));
setLocalQuantities(map);
}
// Sync optimistic state back to server truth whenever cart updates
setOptimisticPointsApplied(null);
}, [cart]);
// If the cart arrives already locked (e.g. user closed the page mid-checkout)
@@ -184,6 +191,20 @@ export default function CartPage() {
}
}
async function handleTogglePoints(checked) {
setOptimisticPointsApplied(checked);
setPointsLoading(true);
setPointsError(null);
try {
await applyPoints(checked);
} catch (err) {
setOptimisticPointsApplied(null);
setPointsError(err.message || "Failed to apply loyalty points.");
} finally {
setPointsLoading(false);
}
}
async function handleRemoveCoupon() {
setCouponLoading(true);
setCouponError(null);
@@ -213,6 +234,7 @@ export default function CartPage() {
}
else if (result?.status === "succeeded") {
refreshUser().catch(() => {});
setConfirmed(true);
}
}
@@ -341,7 +363,7 @@ export default function CartPage() {
{parseFloat(cart.discountAmount ?? 0) > 0 && (
<div className="cart-summary-row cart-summary-discount">
<span>
Discount
Coupon discount
{cart.couponCode && ` (${cart.couponCode}`}
{(() => {
const t = cart.couponDiscountType?.toUpperCase();
@@ -356,15 +378,21 @@ export default function CartPage() {
<span>${parseFloat(cart.discountAmount).toFixed(2)}</span>
</div>
)}
{parseFloat(cart.pointsDiscountAmount ?? 0) > 0 && (
<div className="cart-summary-row cart-summary-discount">
<span>Loyalty discount ({Math.round(parseFloat(cart.pointsDiscountAmount) * 20)} pts)</span>
<span>${parseFloat(cart.pointsDiscountAmount).toFixed(2)}</span>
</div>
)}
<div className="cart-summary-row cart-summary-total">
<span>Total</span>
<div className="cart-total-prices">
{parseFloat(cart.discountAmount ?? 0) > 0 && (
{(parseFloat(cart.discountAmount ?? 0) > 0 || parseFloat(cart.pointsDiscountAmount ?? 0) > 0) && (
<span className="cart-total-original">
${parseFloat(cart.subtotalAmount ?? 0).toFixed(2)}
</span>
)}
<span className={parseFloat(cart.discountAmount ?? 0) > 0 ? "cart-total-discounted" : ""}>
<span className={(parseFloat(cart.discountAmount ?? 0) > 0 || parseFloat(cart.pointsDiscountAmount ?? 0) > 0) ? "cart-total-discounted" : ""}>
${parseFloat(cart.totalAmount ?? 0).toFixed(2)}
</span>
</div>
@@ -382,6 +410,36 @@ export default function CartPage() {
</div>
)}
{user?.role === "CUSTOMER" && (
<div className="cart-points-section">
<div className="cart-points-balance-row">
<span>Your points balance:</span>
<strong>{cart.availableLoyaltyPoints ?? 0} pts</strong>
</div>
{(cart.availableLoyaltyPoints ?? 0) < 20 ? (
<p className="cart-points-msg">You need at least 20 points to redeem $1.</p>
) : (
<label className="cart-points-label">
<input
type="checkbox"
className="cart-points-checkbox"
checked={optimisticPointsApplied !== null ? optimisticPointsApplied : !!cart.pointsApplied}
disabled={pointsLoading}
onChange={(e) => handleTogglePoints(e.target.checked)}
/>
Use loyalty points for this purchase
</label>
)}
{pointsError && <p className="cart-points-msg" style={{ color: "#dc2626" }}>{pointsError}</p>}
{(optimisticPointsApplied ?? !!cart.pointsApplied) && parseFloat(cart.pointsDiscountAmount ?? 0) > 0 && (
<div className="cart-points-applied-detail">
<span>Applying {Math.round(parseFloat(cart.pointsDiscountAmount) * 20)} pts:</span>
<span>${parseFloat(cart.pointsDiscountAmount).toFixed(2)} off</span>
</div>
)}
</div>
)}
<div className="cart-coupon-section">
{cart.couponCode && (
<div className="cart-coupon-applied">

View File

@@ -2417,6 +2417,71 @@ body {
text-align: center;
}
.cart-points-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid #e5e7eb;
}
.cart-points-balance-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
color: #374151;
}
.cart-points-balance-row strong {
color: #7c3aed;
font-weight: 700;
}
.cart-points-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: #374151;
cursor: pointer;
user-select: none;
}
.cart-points-label:has(.cart-points-checkbox:disabled) {
cursor: not-allowed;
opacity: 0.6;
}
.cart-points-checkbox {
width: 1rem;
height: 1rem;
accent-color: #7c3aed;
cursor: pointer;
flex-shrink: 0;
}
.cart-points-msg {
font-size: 0.82rem;
color: #6b7280;
margin: 0;
font-style: italic;
}
.cart-points-applied-detail {
display: flex;
justify-content: space-between;
align-items: center;
background: #f5f3ff;
border: 1px solid #ddd6fe;
border-radius: 6px;
padding: 0.4rem 0.7rem;
font-size: 0.85rem;
color: #5b21b6;
font-weight: 500;
}
.cart-coupon-section {
display: flex;
flex-direction: column;