fix: Guest wishlist now fetches and displays full product details
Problem: Guest wishlist showed only product IDs without any useful information
- No product name, image, or price
- Product links used ID instead of slug (broken routing)
- Completely useless user experience
Solution: Fetch full product details from API for guest wishlist
- Added useEffect to fetch products by IDs: /shop/products?include=123,456,789
- Display actual product data: name, image, price, stock status
- Use product slug for proper navigation: /product/{slug}
- Same rich experience as logged-in users
Implementation:
1. Added ProductData interface for type safety
2. Added guestProducts state and loadingGuest state
3. Fetch products when guest has wishlist items (productIds.size > 0)
4. Display full product cards with images, names, prices
5. Navigate using slug instead of ID
6. Remove from both localStorage and display list
Result:
✅ Guests see full product information (name, image, price)
✅ Product links work correctly (/product/product-slug)
✅ Can remove items from wishlist page
✅ Professional user experience matching logged-in users
✅ No more useless 'Product #123' placeholders
Files Modified:
- customer-spa/src/pages/Wishlist.tsx (fetch and display logic)
- customer-spa/dist/app.js (rebuilt)
This commit is contained in:
@@ -1,10 +1,23 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Trash2, ShoppingCart, Heart } from 'lucide-react';
|
import { Trash2, ShoppingCart, Heart } from 'lucide-react';
|
||||||
import { useWishlist } from '@/hooks/useWishlist';
|
import { useWishlist } from '@/hooks/useWishlist';
|
||||||
import { useCartStore } from '@/lib/cart/store';
|
import { useCartStore } from '@/lib/cart/store';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { apiClient } from '@/lib/api/client';
|
||||||
|
|
||||||
|
interface ProductData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
price: string;
|
||||||
|
regular_price?: string;
|
||||||
|
sale_price?: string;
|
||||||
|
image?: string;
|
||||||
|
on_sale?: boolean;
|
||||||
|
stock_status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public Wishlist Page - Accessible to both guests and logged-in users
|
* Public Wishlist Page - Accessible to both guests and logged-in users
|
||||||
@@ -15,18 +28,39 @@ export default function Wishlist() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { items, isLoading, isLoggedIn, removeFromWishlist, productIds } = useWishlist();
|
const { items, isLoading, isLoggedIn, removeFromWishlist, productIds } = useWishlist();
|
||||||
const { addItem } = useCartStore();
|
const { addItem } = useCartStore();
|
||||||
|
const [guestProducts, setGuestProducts] = useState<ProductData[]>([]);
|
||||||
|
const [loadingGuest, setLoadingGuest] = useState(false);
|
||||||
|
|
||||||
|
// Fetch product details for guest wishlist
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchGuestProducts = async () => {
|
||||||
|
if (!isLoggedIn && productIds.size > 0) {
|
||||||
|
setLoadingGuest(true);
|
||||||
|
try {
|
||||||
|
const ids = Array.from(productIds).join(',');
|
||||||
|
const response = await apiClient.get<any>(`/shop/products?include=${ids}`);
|
||||||
|
setGuestProducts(response.products || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch guest wishlist products:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingGuest(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchGuestProducts();
|
||||||
|
}, [isLoggedIn, productIds]);
|
||||||
|
|
||||||
const handleRemove = async (productId: number) => {
|
const handleRemove = async (productId: number) => {
|
||||||
await removeFromWishlist(productId);
|
await removeFromWishlist(productId);
|
||||||
|
// Remove from guest products list
|
||||||
|
setGuestProducts(prev => prev.filter(p => p.id !== productId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToCart = (productId: number, productName: string) => {
|
const handleAddToCart = (productId: number, productName: string) => {
|
||||||
// For guests with localStorage wishlist, we only have IDs
|
|
||||||
// Navigate to product page for now
|
|
||||||
navigate(`/product/${productId}`);
|
navigate(`/product/${productId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || loadingGuest) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -36,10 +70,9 @@ export default function Wishlist() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guest mode: only have product IDs from localStorage
|
// Guest mode: have product details fetched from API
|
||||||
const guestWishlistIds = !isLoggedIn ? Array.from(productIds) : [];
|
const hasGuestItems = !isLoggedIn && guestProducts.length > 0;
|
||||||
const hasGuestItems = guestWishlistIds.length > 0;
|
const hasLoggedInItems = isLoggedIn && items.length > 0;
|
||||||
const hasLoggedInItems = items.length > 0;
|
|
||||||
|
|
||||||
if (!hasGuestItems && !hasLoggedInItems) {
|
if (!hasGuestItems && !hasLoggedInItems) {
|
||||||
return (
|
return (
|
||||||
@@ -68,46 +101,57 @@ export default function Wishlist() {
|
|||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h1 className="text-3xl font-bold">My Wishlist</h1>
|
<h1 className="text-3xl font-bold">My Wishlist</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
{isLoggedIn ? `${items.length} items` : `${guestWishlistIds.length} items`}
|
{isLoggedIn ? `${items.length} items` : `${guestProducts.length} items`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Guest Mode: Show IDs only with limited functionality */}
|
{/* Guest Mode: Show full product details */}
|
||||||
{!isLoggedIn && hasGuestItems && (
|
{!isLoggedIn && hasGuestItems && (
|
||||||
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
<p className="text-sm text-blue-800">
|
<p className="text-sm text-blue-800">
|
||||||
<strong>Guest Wishlist:</strong> You have {guestWishlistIds.length} items saved locally.
|
<strong>Guest Wishlist:</strong> You have {guestProducts.length} items saved locally.
|
||||||
<a href="/wp-login.php" className="underline ml-1">Login</a> to see full details and sync your wishlist.
|
<a href="/wp-login.php" className="underline ml-1">Login</a> to sync your wishlist to your account.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Guest Wishlist Items (localStorage only - show IDs) */}
|
{/* Guest Wishlist Items (with full product details) */}
|
||||||
{!isLoggedIn && hasGuestItems && (
|
{!isLoggedIn && hasGuestItems && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{guestWishlistIds.map((productId: number) => (
|
{guestProducts.map((product) => (
|
||||||
<div key={`guest-${productId}`} className="bg-white border rounded-lg p-4 flex items-center justify-between">
|
<div key={`guest-${product.id}`} className="bg-white border rounded-lg p-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
{product.image ? (
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-20 h-20 object-cover rounded"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div className="w-20 h-20 bg-gray-200 rounded flex items-center justify-center">
|
<div className="w-20 h-20 bg-gray-200 rounded flex items-center justify-center">
|
||||||
<Heart className="w-8 h-8 text-gray-400" />
|
<Heart className="w-8 h-8 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold">Product #{productId}</h3>
|
<h3 className="font-semibold">{product.name}</h3>
|
||||||
<p className="text-sm text-gray-600">Login to see details</p>
|
<p className="text-sm text-gray-600">{product.price}</p>
|
||||||
|
{product.stock_status === 'outofstock' && (
|
||||||
|
<p className="text-sm text-red-600">Out of stock</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => navigate(`/product/${productId.toString()}`)}
|
onClick={() => navigate(`/product/${product.slug}`)}
|
||||||
>
|
>
|
||||||
View Product
|
View
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleRemove(productId)}
|
onClick={() => handleRemove(product.id)}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user