feat: implement multiple saved addresses with modal selector in checkout

- Add AddressController with full CRUD API for saved addresses
- Implement address management UI in My Account > Addresses
- Add modal-based address selector in checkout (Tokopedia-style)
- Hide checkout forms when saved address is selected
- Add search functionality in address modal
- Auto-select default addresses on page load
- Fix variable products to show 'Select Options' instead of 'Add to Cart'
- Add admin toggle for multiple addresses feature
- Clean up debug logs and fix TypeScript errors
This commit is contained in:
Dwindi Ramadhana
2025-12-26 01:16:11 +07:00
parent 9ac09582d2
commit 100f9cce55
27 changed files with 2492 additions and 205 deletions

View File

@@ -17,6 +17,7 @@ interface ProductCardProps {
image?: string;
on_sale?: boolean;
stock_status?: string;
type?: string;
};
onAddToCart?: (product: any) => void;
}
@@ -32,9 +33,18 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
'landscape': 'aspect-[4/3]',
}[layout.aspect_ratio] || 'aspect-square';
const isVariable = product.type === 'variable';
const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// Variable products need to go to product page for attribute selection
if (isVariable) {
window.location.href = `/product/${product.slug}`;
return;
}
onAddToCart?.(product);
};
@@ -122,7 +132,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
{/* Quick Actions */}
<div className="absolute top-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button className="p-2 bg-white rounded-full shadow-md hover:bg-gray-50 flex items-center justify-center">
<button className="font-[inherit] p-2 bg-white rounded-full shadow-md hover:bg-gray-50 flex items-center justify-center">
<Heart className="w-4 h-4 block" />
</button>
</div>
@@ -137,7 +147,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
disabled={product.stock_status === 'outofstock'}
>
{!isTextOnly && addToCart.show_icon && <ShoppingCart className="w-4 h-4 mr-2" />}
{product.stock_status === 'outofstock' ? 'Out of Stock' : 'Add to Cart'}
{product.stock_status === 'outofstock' ? 'Out of Stock' : isVariable ? 'Select Options' : 'Add to Cart'}
</Button>
</div>
)}
@@ -145,7 +155,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
{/* Content */}
<div className={`p-4 flex-1 flex flex-col ${textAlignClass}`}>
<h3 className="font-semibold text-gray-900 mb-2 line-clamp-2 group-hover:text-primary transition-colors">
<h3 className="text-sm font-medium text-gray-900 mb-2 line-clamp-2 leading-snug group-hover:text-primary transition-colors">
{product.name}
</h3>
@@ -153,15 +163,15 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
<div className={`flex items-center gap-2 mb-3 ${(layout.card_text_align || 'left') === 'center' ? 'justify-center' : (layout.card_text_align || 'left') === 'right' ? 'justify-end' : ''}`}>
{product.on_sale && product.regular_price ? (
<>
<span className="text-lg font-bold" style={{ color: 'var(--color-primary)' }}>
<span className="text-base font-bold" style={{ color: 'var(--color-primary)' }}>
{formatPrice(product.sale_price || product.price)}
</span>
<span className="text-sm text-gray-500 line-through">
<span className="text-xs text-gray-500 line-through">
{formatPrice(product.regular_price)}
</span>
</>
) : (
<span className="text-lg font-bold text-gray-900">
<span className="text-base font-bold text-gray-900">
{formatPrice(product.price)}
</span>
)}
@@ -176,7 +186,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
disabled={product.stock_status === 'outofstock'}
>
{!isTextOnly && addToCart.show_icon && <ShoppingCart className="w-4 h-4 mr-2" />}
{product.stock_status === 'outofstock' ? 'Out of Stock' : 'Add to Cart'}
{product.stock_status === 'outofstock' ? 'Out of Stock' : isVariable ? 'Select Options' : 'Add to Cart'}
</Button>
)}
</div>
@@ -224,7 +234,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
disabled={product.stock_status === 'outofstock'}
>
{addToCart.show_icon && <ShoppingCart className="w-4 h-4 mr-2" />}
{product.stock_status === 'outofstock' ? 'Out of Stock' : 'Add to Cart'}
{product.stock_status === 'outofstock' ? 'Out of Stock' : isVariable ? 'Select Options' : 'Add to Cart'}
</Button>
</div>
)}
@@ -232,7 +242,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
{/* Content */}
<div className="text-center">
<h3 className="font-medium text-gray-900 mb-2 group-hover:text-primary transition-colors">
<h3 className="text-sm font-medium text-gray-900 mb-2 leading-snug group-hover:text-primary transition-colors">
{product.name}
</h3>
@@ -240,15 +250,15 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
<div className="flex items-center justify-center gap-2 mb-3">
{product.on_sale && product.regular_price ? (
<>
<span className="font-semibold" style={{ color: 'var(--color-primary)' }}>
<span className="text-base font-bold" style={{ color: 'var(--color-primary)' }}>
{formatPrice(product.sale_price || product.price)}
</span>
<span className="text-sm text-gray-400 line-through">
<span className="text-xs text-gray-400 line-through">
{formatPrice(product.regular_price)}
</span>
</>
) : (
<span className="font-semibold text-gray-900">
<span className="text-base font-bold text-gray-900">
{formatPrice(product.price)}
</span>
)}
@@ -320,7 +330,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
{/* Content */}
<div className="text-center font-serif">
<h3 className="text-lg font-medium text-gray-900 mb-3 tracking-wide group-hover:text-primary transition-colors">
<h3 className="text-sm font-medium text-gray-900 mb-3 tracking-wide leading-snug group-hover:text-primary transition-colors">
{product.name}
</h3>
@@ -328,15 +338,15 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
<div className="flex items-center justify-center gap-3 mb-4">
{product.on_sale && product.regular_price ? (
<>
<span className="text-xl font-medium" style={{ color: 'var(--color-primary)' }}>
<span className="text-lg font-semibold" style={{ color: 'var(--color-primary)' }}>
{formatPrice(product.sale_price || product.price)}
</span>
<span className="text-gray-400 line-through">
<span className="text-sm text-gray-400 line-through">
{formatPrice(product.regular_price)}
</span>
</>
) : (
<span className="text-xl font-medium text-gray-900">
<span className="text-lg font-semibold text-gray-900">
{formatPrice(product.price)}
</span>
)}
@@ -349,7 +359,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
className="w-full font-serif tracking-wider"
disabled={product.stock_status === 'outofstock'}
>
{product.stock_status === 'outofstock' ? 'OUT OF STOCK' : 'ADD TO CART'}
{product.stock_status === 'outofstock' ? 'OUT OF STOCK' : isVariable ? 'SELECT OPTIONS' : 'ADD TO CART'}
</Button>
</div>
</div>
@@ -376,12 +386,12 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
</div>
<div className="p-4 text-center">
<h3 className="font-semibold text-gray-900 mb-2">{product.name}</h3>
<div className="text-xl font-bold mb-3" style={{ color: 'var(--color-primary)' }}>
<h3 className="text-sm font-medium text-gray-900 mb-2 leading-snug">{product.name}</h3>
<div className="text-lg font-bold mb-3" style={{ color: 'var(--color-primary)' }}>
{formatPrice(product.price)}
</div>
<Button onClick={handleAddToCart} className="w-full" size="lg">
Buy Now
{isVariable ? 'Select Options' : 'Buy Now'}
</Button>
</div>
</div>