feat: Multiple fixes and features
1. Add allow_custom_avatar toggle to Customer Settings 2. Implement coupon apply/remove in Cart and Checkout pages 3. Update Cart interface with coupons array and discount_total 4. Implement Downloads page to fetch from /account/downloads API
This commit is contained in:
@@ -5,11 +5,12 @@ import { useCheckoutSettings } from '@/hooks/useAppearanceSettings';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Container from '@/components/Layout/Container';
|
||||
import { formatPrice } from '@/lib/currency';
|
||||
import { ArrowLeft, ShoppingBag, MapPin, Check, Edit2 } from 'lucide-react';
|
||||
import { ArrowLeft, ShoppingBag, MapPin, Check, Edit2, Loader2, X, Tag } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { api } from '@/lib/api/client';
|
||||
import { AddressSelector } from '@/components/AddressSelector';
|
||||
import { applyCoupon, removeCoupon, fetchCart } from '@/lib/cart/api';
|
||||
|
||||
interface SavedAddress {
|
||||
id: number;
|
||||
@@ -34,6 +35,10 @@ export default function Checkout() {
|
||||
const { cart } = useCartStore();
|
||||
const { layout, elements } = useCheckoutSettings();
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [couponCode, setCouponCode] = useState('');
|
||||
const [isApplyingCoupon, setIsApplyingCoupon] = useState(false);
|
||||
const [appliedCoupons, setAppliedCoupons] = useState<{ code: string; discount: number }[]>([]);
|
||||
const [discountTotal, setDiscountTotal] = useState(0);
|
||||
const user = (window as any).woonoowCustomer?.user;
|
||||
|
||||
// Check if cart contains only virtual/downloadable products
|
||||
@@ -189,6 +194,57 @@ export default function Checkout() {
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleApplyCoupon = async () => {
|
||||
if (!couponCode.trim()) return;
|
||||
|
||||
setIsApplyingCoupon(true);
|
||||
try {
|
||||
const updatedCart = await applyCoupon(couponCode.trim());
|
||||
if (updatedCart.coupons) {
|
||||
setAppliedCoupons(updatedCart.coupons);
|
||||
setDiscountTotal(updatedCart.discount_total || 0);
|
||||
}
|
||||
setCouponCode('');
|
||||
toast.success('Coupon applied successfully');
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to apply coupon');
|
||||
} finally {
|
||||
setIsApplyingCoupon(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCoupon = async (code: string) => {
|
||||
setIsApplyingCoupon(true);
|
||||
try {
|
||||
const updatedCart = await removeCoupon(code);
|
||||
if (updatedCart.coupons) {
|
||||
setAppliedCoupons(updatedCart.coupons);
|
||||
setDiscountTotal(updatedCart.discount_total || 0);
|
||||
}
|
||||
toast.success('Coupon removed');
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to remove coupon');
|
||||
} finally {
|
||||
setIsApplyingCoupon(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load cart data including coupons on mount
|
||||
useEffect(() => {
|
||||
const loadCartData = async () => {
|
||||
try {
|
||||
const cartData = await fetchCart();
|
||||
if (cartData.coupons) {
|
||||
setAppliedCoupons(cartData.coupons);
|
||||
setDiscountTotal(cartData.discount_total || 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load cart data:', error);
|
||||
}
|
||||
};
|
||||
loadCartData();
|
||||
}, []);
|
||||
|
||||
const handlePlaceOrder = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsProcessing(true);
|
||||
@@ -652,10 +708,45 @@ export default function Checkout() {
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter coupon code"
|
||||
value={couponCode}
|
||||
onChange={(e) => setCouponCode(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleApplyCoupon())}
|
||||
className="flex-1 px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
disabled={isApplyingCoupon}
|
||||
/>
|
||||
<Button type="button" variant="outline" size="sm">Apply</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleApplyCoupon}
|
||||
disabled={isApplyingCoupon || !couponCode.trim()}
|
||||
>
|
||||
{isApplyingCoupon ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Apply'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Applied Coupons */}
|
||||
{appliedCoupons.length > 0 && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{appliedCoupons.map((coupon) => (
|
||||
<div key={coupon.code} className="flex items-center justify-between p-2 bg-green-50 border border-green-200 rounded-md">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-green-600" />
|
||||
<span className="text-sm font-medium text-green-800">{coupon.code}</span>
|
||||
<span className="text-sm text-green-600">-{formatPrice(coupon.discount)}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveCoupon(coupon.code)}
|
||||
className="text-green-600 hover:text-green-800 p-1"
|
||||
disabled={isApplyingCoupon}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -702,6 +793,13 @@ export default function Checkout() {
|
||||
<span>Subtotal</span>
|
||||
<span>{formatPrice(subtotal)}</span>
|
||||
</div>
|
||||
{/* Show discount if coupons applied */}
|
||||
{discountTotal > 0 && (
|
||||
<div className="flex justify-between text-sm text-green-600">
|
||||
<span>Discount</span>
|
||||
<span>-{formatPrice(discountTotal)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Shipping</span>
|
||||
<span>{shipping === 0 ? 'Free' : formatPrice(shipping)}</span>
|
||||
@@ -714,7 +812,7 @@ export default function Checkout() {
|
||||
)}
|
||||
<div className="border-t pt-2 flex justify-between font-bold text-lg">
|
||||
<span>Total</span>
|
||||
<span>{formatPrice(total)}</span>
|
||||
<span>{formatPrice(total - discountTotal)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user