feat: implement wishlist feature with admin toggle

- Add WishlistController with full CRUD API
- Create wishlist page in My Account
- Add heart icon to all product card layouts (always visible)
- Implement useWishlist hook for state management
- Add wishlist toggle in admin Settings > Customer
- Fix wishlist menu visibility based on admin settings
- Fix double navigation in wishlist page
- Fix variable product navigation to use React Router
- Add TypeScript type casting fix for addresses
This commit is contained in:
Dwindi Ramadhana
2025-12-26 01:44:15 +07:00
parent 100f9cce55
commit 0b08ddefa1
9 changed files with 608 additions and 10 deletions

View File

@@ -13,6 +13,7 @@ import { formatMoney, getStoreCurrency } from '@/lib/currency';
interface CustomerSettings { interface CustomerSettings {
auto_register_members: boolean; auto_register_members: boolean;
multiple_addresses_enabled: boolean; multiple_addresses_enabled: boolean;
wishlist_enabled: boolean;
vip_min_spent: number; vip_min_spent: number;
vip_min_orders: number; vip_min_orders: number;
vip_timeframe: 'all' | '30' | '90' | '365'; vip_timeframe: 'all' | '30' | '90' | '365';
@@ -24,6 +25,7 @@ export default function CustomersSettings() {
const [settings, setSettings] = useState<CustomerSettings>({ const [settings, setSettings] = useState<CustomerSettings>({
auto_register_members: false, auto_register_members: false,
multiple_addresses_enabled: true, multiple_addresses_enabled: true,
wishlist_enabled: true,
vip_min_spent: 1000, vip_min_spent: 1000,
vip_min_orders: 10, vip_min_orders: 10,
vip_timeframe: 'all', vip_timeframe: 'all',
@@ -137,6 +139,14 @@ export default function CustomersSettings() {
checked={settings.multiple_addresses_enabled} checked={settings.multiple_addresses_enabled}
onCheckedChange={(checked) => setSettings({ ...settings, multiple_addresses_enabled: checked })} onCheckedChange={(checked) => setSettings({ ...settings, multiple_addresses_enabled: checked })}
/> />
<ToggleField
id="wishlist_enabled"
label={__('Enable wishlist')}
description={__('Allow customers to save products to their wishlist for later purchase. Customers can add products to wishlist from product cards and manage them in their account.')}
checked={settings.wishlist_enabled}
onCheckedChange={(checked) => setSettings({ ...settings, wishlist_enabled: checked })}
/>
</div> </div>
</SettingsCard> </SettingsCard>

View File

@@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { ShoppingCart, Heart } from 'lucide-react'; import { ShoppingCart, Heart } from 'lucide-react';
import { formatPrice, formatDiscount } from '@/lib/currency'; import { formatPrice, formatDiscount } from '@/lib/currency';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { useLayout } from '@/contexts/ThemeContext'; import { useLayout } from '@/contexts/ThemeContext';
import { useShopSettings } from '@/hooks/useAppearanceSettings'; import { useShopSettings } from '@/hooks/useAppearanceSettings';
import { useWishlist } from '@/hooks/useWishlist';
interface ProductCardProps { interface ProductCardProps {
product: { product: {
@@ -23,8 +24,18 @@ interface ProductCardProps {
} }
export function ProductCard({ product, onAddToCart }: ProductCardProps) { export function ProductCard({ product, onAddToCart }: ProductCardProps) {
const navigate = useNavigate();
const { isClassic, isModern, isBoutique, isLaunch } = useLayout(); const { isClassic, isModern, isBoutique, isLaunch } = useLayout();
const { layout, elements, addToCart, saleBadge, isLoading } = useShopSettings(); const { layout, elements, addToCart, saleBadge, isLoading } = useShopSettings();
const { isEnabled: wishlistEnabled, isInWishlist, toggleWishlist } = useWishlist();
const inWishlist = wishlistEnabled && isInWishlist(product.id);
const handleWishlistClick = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
await toggleWishlist(product.id);
};
// Aspect ratio classes // Aspect ratio classes
const aspectRatioClass = { const aspectRatioClass = {
@@ -41,7 +52,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
// Variable products need to go to product page for attribute selection // Variable products need to go to product page for attribute selection
if (isVariable) { if (isVariable) {
window.location.href = `/product/${product.slug}`; navigate(`/product/${product.slug}`);
return; return;
} }
@@ -130,12 +141,22 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
</div> </div>
)} )}
{/* Quick Actions */} {/* Wishlist Button */}
<div className="absolute top-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity"> {wishlistEnabled && (
<button className="font-[inherit] p-2 bg-white rounded-full shadow-md hover:bg-gray-50 flex items-center justify-center"> <div className="absolute top-2 left-2 z-10">
<Heart className="w-4 h-4 block" /> <button
onClick={handleWishlistClick}
className={`font-[inherit] p-2 rounded-full shadow-md hover:bg-gray-50 flex items-center justify-center transition-all ${
inWishlist ? 'bg-red-50' : 'bg-white'
}`}
title={inWishlist ? 'Remove from wishlist' : 'Add to wishlist'}
>
<Heart className={`w-4 h-4 block transition-all ${
inWishlist ? 'fill-red-500 text-red-500' : ''
}`} />
</button> </button>
</div> </div>
)}
{/* Hover/Overlay Button */} {/* Hover/Overlay Button */}
{showButtonOnHover && ( {showButtonOnHover && (
@@ -224,6 +245,23 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
</div> </div>
)} )}
{/* Wishlist Button */}
{wishlistEnabled && (
<div className="absolute top-4 right-4 z-10">
<button
onClick={handleWishlistClick}
className={`font-[inherit] p-2 rounded-full shadow-md hover:bg-gray-50 flex items-center justify-center transition-all ${
inWishlist ? 'bg-red-50' : 'bg-white'
}`}
title={inWishlist ? 'Remove from wishlist' : 'Add to wishlist'}
>
<Heart className={`w-4 h-4 block transition-all ${
inWishlist ? 'fill-red-500 text-red-500' : ''
}`} />
</button>
</div>
)}
{/* Hover Overlay - Only show if position is hover/overlay */} {/* Hover Overlay - Only show if position is hover/overlay */}
{showButtonOnHover && ( {showButtonOnHover && (
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-10 transition-all duration-300 flex items-center justify-center"> <div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-10 transition-all duration-300 flex items-center justify-center">
@@ -326,6 +364,23 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
{discount} {discount}
</div> </div>
)} )}
{/* Wishlist Button */}
{wishlistEnabled && (
<div className="absolute top-6 left-6 z-10">
<button
onClick={handleWishlistClick}
className={`font-[inherit] p-2 rounded-full shadow-md hover:bg-gray-50 flex items-center justify-center transition-all ${
inWishlist ? 'bg-red-50' : 'bg-white'
}`}
title={inWishlist ? 'Remove from wishlist' : 'Add to wishlist'}
>
<Heart className={`w-4 h-4 block transition-all ${
inWishlist ? 'fill-red-500 text-red-500' : ''
}`} />
</button>
</div>
)}
</div> </div>
{/* Content */} {/* Content */}
@@ -383,6 +438,23 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
No Image No Image
</div> </div>
)} )}
{/* Wishlist Button */}
{wishlistEnabled && (
<div className="absolute top-3 right-3 z-10">
<button
onClick={handleWishlistClick}
className={`font-[inherit] p-2 rounded-full shadow-md hover:bg-gray-50 flex items-center justify-center transition-all ${
inWishlist ? 'bg-red-50' : 'bg-white'
}`}
title={inWishlist ? 'Remove from wishlist' : 'Add to wishlist'}
>
<Heart className={`w-4 h-4 block transition-all ${
inWishlist ? 'fill-red-500 text-red-500' : ''
}`} />
</button>
</div>
)}
</div> </div>
<div className="p-4 text-center"> <div className="p-4 text-center">

View File

@@ -0,0 +1,111 @@
import { useState, useEffect, useCallback } from 'react';
import { api } from '@/lib/api/client';
import { toast } from 'sonner';
interface WishlistItem {
product_id: number;
name: string;
slug: string;
price: string;
regular_price?: string;
sale_price?: string;
image?: string;
on_sale?: boolean;
stock_status?: string;
type?: string;
added_at: string;
}
export function useWishlist() {
const [items, setItems] = useState<WishlistItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [productIds, setProductIds] = useState<Set<number>>(new Set());
// Check if wishlist is enabled
const isEnabled = (window as any).woonoowCustomer?.settings?.wishlist_enabled !== false;
const isLoggedIn = (window as any).woonoowCustomer?.user?.isLoggedIn;
// Load wishlist on mount
useEffect(() => {
if (isEnabled && isLoggedIn) {
loadWishlist();
}
}, [isEnabled, isLoggedIn]);
const loadWishlist = useCallback(async () => {
if (!isLoggedIn) return;
try {
setIsLoading(true);
const data = await api.get<WishlistItem[]>('/account/wishlist');
setItems(data);
setProductIds(new Set(data.map(item => item.product_id)));
} catch (error) {
console.error('Failed to load wishlist:', error);
} finally {
setIsLoading(false);
}
}, [isLoggedIn]);
const addToWishlist = useCallback(async (productId: number) => {
if (!isLoggedIn) {
toast.error('Please login to add items to wishlist');
return false;
}
try {
await api.post('/account/wishlist', { product_id: productId });
await loadWishlist(); // Reload to get full product details
toast.success('Added to wishlist');
return true;
} catch (error: any) {
const message = error?.message || 'Failed to add to wishlist';
toast.error(message);
return false;
}
}, [isLoggedIn, loadWishlist]);
const removeFromWishlist = useCallback(async (productId: number) => {
if (!isLoggedIn) return false;
try {
await api.delete(`/account/wishlist/${productId}`);
setItems(items.filter(item => item.product_id !== productId));
setProductIds(prev => {
const newSet = new Set(prev);
newSet.delete(productId);
return newSet;
});
toast.success('Removed from wishlist');
return true;
} catch (error) {
toast.error('Failed to remove from wishlist');
return false;
}
}, [isLoggedIn, items]);
const toggleWishlist = useCallback(async (productId: number) => {
if (productIds.has(productId)) {
return await removeFromWishlist(productId);
} else {
return await addToWishlist(productId);
}
}, [productIds, addToWishlist, removeFromWishlist]);
const isInWishlist = useCallback((productId: number) => {
return productIds.has(productId);
}, [productIds]);
return {
items,
isLoading,
isEnabled,
isLoggedIn,
count: items.length,
addToWishlist,
removeFromWishlist,
toggleWishlist,
isInWishlist,
refresh: loadWishlist,
};
}

View File

@@ -0,0 +1,213 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Heart, ShoppingCart, Trash2, X } from 'lucide-react';
import { api } from '@/lib/api/client';
import { useCartStore } from '@/lib/cart/store';
import { Button } from '@/components/ui/button';
import { formatPrice } from '@/lib/currency';
import { toast } from 'sonner';
interface WishlistItem {
product_id: number;
name: string;
slug: string;
price: string;
regular_price?: string;
sale_price?: string;
image?: string;
on_sale?: boolean;
stock_status?: string;
type?: string;
added_at: string;
}
export default function Wishlist() {
const navigate = useNavigate();
const { addItem } = useCartStore();
const [items, setItems] = useState<WishlistItem[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadWishlist();
}, []);
const loadWishlist = async () => {
try {
const data = await api.get<WishlistItem[]>('/account/wishlist');
setItems(data);
} catch (error) {
console.error('Failed to load wishlist:', error);
toast.error('Failed to load wishlist');
} finally {
setLoading(false);
}
};
const handleRemove = async (productId: number) => {
try {
await api.delete(`/account/wishlist/${productId}`);
setItems(items.filter(item => item.product_id !== productId));
toast.success('Removed from wishlist');
} catch (error) {
console.error('Failed to remove from wishlist:', error);
toast.error('Failed to remove from wishlist');
}
};
const handleAddToCart = async (item: WishlistItem) => {
if (item.type === 'variable') {
navigate(`/product/${item.slug}`);
return;
}
try {
const response = await api.post('/cart/add', {
product_id: item.product_id,
quantity: 1,
});
toast.success('Added to cart');
} catch (error) {
console.error('Failed to add to cart:', error);
toast.error('Failed to add to cart');
}
};
const handleClearAll = async () => {
if (!confirm('Are you sure you want to clear your entire wishlist?')) {
return;
}
try {
await api.post('/account/wishlist/clear', {});
setItems([]);
toast.success('Wishlist cleared');
} catch (error) {
console.error('Failed to clear wishlist:', error);
toast.error('Failed to clear wishlist');
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold">My Wishlist</h1>
<p className="text-gray-600 text-sm mt-1">
{items.length} {items.length === 1 ? 'item' : 'items'}
</p>
</div>
{items.length > 0 && (
<Button
variant="outline"
size="sm"
onClick={handleClearAll}
className="text-red-600 hover:text-red-700 hover:border-red-600"
>
<Trash2 className="w-4 h-4 mr-2" />
Clear All
</Button>
)}
</div>
{/* Empty State */}
{items.length === 0 ? (
<div className="text-center py-16 bg-white border rounded-lg">
<Heart className="w-16 h-16 mx-auto text-gray-300 mb-4" />
<h2 className="text-xl font-semibold mb-2">Your wishlist is empty</h2>
<p className="text-gray-600 mb-6">
Save your favorite products to buy them later
</p>
<Button onClick={() => navigate('/shop')}>
Browse Products
</Button>
</div>
) : (
/* Wishlist Grid */
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map((item) => (
<div
key={item.product_id}
className="bg-white border rounded-lg overflow-hidden hover:shadow-lg transition-shadow group"
>
{/* Image */}
<div className="relative aspect-square bg-gray-100">
<button
onClick={() => handleRemove(item.product_id)}
className="absolute top-3 right-3 z-10 p-2 bg-white rounded-full shadow-md hover:bg-red-50 transition-colors"
title="Remove from wishlist"
>
<X className="w-4 h-4 text-red-600" />
</button>
<img
src={item.image || '/placeholder.png'}
alt={item.name}
className="w-full h-full object-cover cursor-pointer"
onClick={() => navigate(`/product/${item.slug}`)}
/>
{item.on_sale && (
<div className="absolute top-3 left-3 bg-red-600 text-white text-xs font-bold px-2 py-1 rounded">
SALE
</div>
)}
</div>
{/* Content */}
<div className="p-4">
<h3
className="font-medium text-gray-900 mb-2 line-clamp-2 cursor-pointer hover:text-primary transition-colors"
onClick={() => navigate(`/product/${item.slug}`)}
>
{item.name}
</h3>
{/* Price */}
<div className="flex items-center gap-2 mb-4">
{item.on_sale && item.regular_price ? (
<>
<span className="text-lg font-bold text-primary">
{formatPrice(item.sale_price || item.price)}
</span>
<span className="text-sm text-gray-500 line-through">
{formatPrice(item.regular_price)}
</span>
</>
) : (
<span className="text-lg font-bold text-gray-900">
{formatPrice(item.price)}
</span>
)}
</div>
{/* Actions */}
<Button
onClick={() => handleAddToCart(item)}
disabled={item.stock_status === 'outofstock'}
className="w-full"
size="sm"
>
<ShoppingCart className="w-4 h-4 mr-2" />
{item.stock_status === 'outofstock'
? 'Out of Stock'
: item.type === 'variable'
? 'Select Options'
: 'Add to Cart'}
</Button>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -1,6 +1,6 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { LayoutDashboard, ShoppingBag, Download, MapPin, User, LogOut } from 'lucide-react'; import { LayoutDashboard, ShoppingBag, Download, MapPin, Heart, User, LogOut } from 'lucide-react';
interface AccountLayoutProps { interface AccountLayoutProps {
children: ReactNode; children: ReactNode;
@@ -9,15 +9,22 @@ interface AccountLayoutProps {
export function AccountLayout({ children }: AccountLayoutProps) { export function AccountLayout({ children }: AccountLayoutProps) {
const location = useLocation(); const location = useLocation();
const user = (window as any).woonoowCustomer?.user; const user = (window as any).woonoowCustomer?.user;
const wishlistEnabled = (window as any).woonoowCustomer?.settings?.wishlist_enabled !== false;
const menuItems = [ const allMenuItems = [
{ id: 'dashboard', label: 'Dashboard', path: '/my-account', icon: LayoutDashboard }, { id: 'dashboard', label: 'Dashboard', path: '/my-account', icon: LayoutDashboard },
{ id: 'orders', label: 'Orders', path: '/my-account/orders', icon: ShoppingBag }, { id: 'orders', label: 'Orders', path: '/my-account/orders', icon: ShoppingBag },
{ id: 'downloads', label: 'Downloads', path: '/my-account/downloads', icon: Download }, { id: 'downloads', label: 'Downloads', path: '/my-account/downloads', icon: Download },
{ id: 'addresses', label: 'Addresses', path: '/my-account/addresses', icon: MapPin }, { id: 'addresses', label: 'Addresses', path: '/my-account/addresses', icon: MapPin },
{ id: 'wishlist', label: 'Wishlist', path: '/my-account/wishlist', icon: Heart },
{ id: 'account-details', label: 'Account Details', path: '/my-account/account-details', icon: User }, { id: 'account-details', label: 'Account Details', path: '/my-account/account-details', icon: User },
]; ];
// Filter out wishlist if disabled
const menuItems = allMenuItems.filter(item =>
item.id !== 'wishlist' || wishlistEnabled
);
const handleLogout = () => { const handleLogout = () => {
window.location.href = '/wp-login.php?action=logout'; window.location.href = '/wp-login.php?action=logout';
}; };

View File

@@ -7,6 +7,7 @@ import Orders from './Orders';
import OrderDetails from './OrderDetails'; import OrderDetails from './OrderDetails';
import Downloads from './Downloads'; import Downloads from './Downloads';
import Addresses from './Addresses'; import Addresses from './Addresses';
import Wishlist from './Wishlist';
import AccountDetails from './AccountDetails'; import AccountDetails from './AccountDetails';
export default function Account() { export default function Account() {
@@ -27,6 +28,7 @@ export default function Account() {
<Route path="orders/:orderId" element={<OrderDetails />} /> <Route path="orders/:orderId" element={<OrderDetails />} />
<Route path="downloads" element={<Downloads />} /> <Route path="downloads" element={<Downloads />} />
<Route path="addresses" element={<Addresses />} /> <Route path="addresses" element={<Addresses />} />
<Route path="wishlist" element={<Wishlist />} />
<Route path="account-details" element={<AccountDetails />} /> <Route path="account-details" element={<AccountDetails />} />
<Route path="*" element={<Navigate to="/my-account" replace />} /> <Route path="*" element={<Navigate to="/my-account" replace />} />
</Routes> </Routes>

View File

@@ -25,6 +25,7 @@ use WooNooW\Frontend\ShopController;
use WooNooW\Frontend\CartController as FrontendCartController; use WooNooW\Frontend\CartController as FrontendCartController;
use WooNooW\Frontend\AccountController; use WooNooW\Frontend\AccountController;
use WooNooW\Frontend\AddressController; use WooNooW\Frontend\AddressController;
use WooNooW\Frontend\WishlistController;
use WooNooW\Frontend\HookBridge; use WooNooW\Frontend\HookBridge;
use WooNooW\Api\Controllers\SettingsController; use WooNooW\Api\Controllers\SettingsController;
use WooNooW\Api\Controllers\CartController as ApiCartController; use WooNooW\Api\Controllers\CartController as ApiCartController;
@@ -127,6 +128,7 @@ class Routes {
FrontendCartController::register_routes(); FrontendCartController::register_routes();
AccountController::register_routes(); AccountController::register_routes();
AddressController::register_routes(); AddressController::register_routes();
WishlistController::register_routes();
HookBridge::register_routes(); HookBridge::register_routes();
}); });
} }

View File

@@ -21,6 +21,7 @@ class CustomerSettingsProvider {
// General // General
'auto_register_members' => get_option('woonoow_auto_register_members', 'no') === 'yes', 'auto_register_members' => get_option('woonoow_auto_register_members', 'no') === 'yes',
'multiple_addresses_enabled' => get_option('woonoow_multiple_addresses_enabled', 'yes') === 'yes', 'multiple_addresses_enabled' => get_option('woonoow_multiple_addresses_enabled', 'yes') === 'yes',
'wishlist_enabled' => get_option('woonoow_wishlist_enabled', 'yes') === 'yes',
// VIP Customer Qualification // VIP Customer Qualification
'vip_min_spent' => floatval(get_option('woonoow_vip_min_spent', 1000)), 'vip_min_spent' => floatval(get_option('woonoow_vip_min_spent', 1000)),
@@ -49,6 +50,10 @@ class CustomerSettingsProvider {
$updated = $updated && update_option('woonoow_multiple_addresses_enabled', $settings['multiple_addresses_enabled'] ? 'yes' : 'no'); $updated = $updated && update_option('woonoow_multiple_addresses_enabled', $settings['multiple_addresses_enabled'] ? 'yes' : 'no');
} }
if (isset($settings['wishlist_enabled'])) {
$updated = $updated && update_option('woonoow_wishlist_enabled', $settings['wishlist_enabled'] ? 'yes' : 'no');
}
// VIP settings // VIP settings
if (isset($settings['vip_min_spent'])) { if (isset($settings['vip_min_spent'])) {
$updated = $updated && update_option('woonoow_vip_min_spent', floatval($settings['vip_min_spent'])); $updated = $updated && update_option('woonoow_vip_min_spent', floatval($settings['vip_min_spent']));

View File

@@ -0,0 +1,176 @@
<?php
namespace WooNooW\Frontend;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
class WishlistController {
/**
* Register REST API routes
*/
public static function register_routes() {
$namespace = 'woonoow/v1';
// Get wishlist
register_rest_route($namespace, '/account/wishlist', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_wishlist'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
// Add to wishlist
register_rest_route($namespace, '/account/wishlist', [
'methods' => 'POST',
'callback' => [__CLASS__, 'add_to_wishlist'],
'permission_callback' => [__CLASS__, 'check_permission'],
'args' => [
'product_id' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
],
],
]);
// Remove from wishlist
register_rest_route($namespace, '/account/wishlist/(?P<product_id>\d+)', [
'methods' => 'DELETE',
'callback' => [__CLASS__, 'remove_from_wishlist'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
// Clear wishlist
register_rest_route($namespace, '/account/wishlist/clear', [
'methods' => 'POST',
'callback' => [__CLASS__, 'clear_wishlist'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
}
/**
* Check if user is logged in
*/
public static function check_permission() {
return is_user_logged_in();
}
/**
* Get wishlist items with product details
*/
public static function get_wishlist(WP_REST_Request $request) {
$user_id = get_current_user_id();
$wishlist = get_user_meta($user_id, 'woonoow_wishlist', true);
if (!$wishlist || !is_array($wishlist)) {
return new WP_REST_Response([], 200);
}
$items = [];
foreach ($wishlist as $item) {
$product_id = $item['product_id'];
$product = wc_get_product($product_id);
if (!$product) {
continue; // Skip if product doesn't exist
}
$items[] = [
'product_id' => $product_id,
'name' => $product->get_name(),
'slug' => $product->get_slug(),
'price' => $product->get_price(),
'regular_price'=> $product->get_regular_price(),
'sale_price' => $product->get_sale_price(),
'image' => wp_get_attachment_url($product->get_image_id()),
'on_sale' => $product->is_on_sale(),
'stock_status' => $product->get_stock_status(),
'type' => $product->get_type(),
'added_at' => $item['added_at'],
];
}
return new WP_REST_Response($items, 200);
}
/**
* Add product to wishlist
*/
public static function add_to_wishlist(WP_REST_Request $request) {
$user_id = get_current_user_id();
$product_id = $request->get_param('product_id');
// Validate product exists
$product = wc_get_product($product_id);
if (!$product) {
return new WP_Error('invalid_product', 'Product not found', ['status' => 404]);
}
$wishlist = get_user_meta($user_id, 'woonoow_wishlist', true);
if (!is_array($wishlist)) {
$wishlist = [];
}
// Check if already in wishlist
foreach ($wishlist as $item) {
if ($item['product_id'] === $product_id) {
return new WP_Error('already_in_wishlist', 'Product already in wishlist', ['status' => 400]);
}
}
// Add to wishlist
$wishlist[] = [
'product_id' => $product_id,
'added_at' => current_time('mysql'),
];
update_user_meta($user_id, 'woonoow_wishlist', $wishlist);
return new WP_REST_Response([
'message' => 'Product added to wishlist',
'count' => count($wishlist),
], 200);
}
/**
* Remove product from wishlist
*/
public static function remove_from_wishlist(WP_REST_Request $request) {
$user_id = get_current_user_id();
$product_id = (int) $request->get_param('product_id');
$wishlist = get_user_meta($user_id, 'woonoow_wishlist', true);
if (!is_array($wishlist)) {
return new WP_Error('empty_wishlist', 'Wishlist is empty', ['status' => 400]);
}
// Remove product from wishlist
$wishlist = array_filter($wishlist, function($item) use ($product_id) {
return $item['product_id'] !== $product_id;
});
// Re-index array
$wishlist = array_values($wishlist);
update_user_meta($user_id, 'woonoow_wishlist', $wishlist);
return new WP_REST_Response([
'message' => 'Product removed from wishlist',
'count' => count($wishlist),
], 200);
}
/**
* Clear entire wishlist
*/
public static function clear_wishlist(WP_REST_Request $request) {
$user_id = get_current_user_id();
delete_user_meta($user_id, 'woonoow_wishlist');
return new WP_REST_Response([
'message' => 'Wishlist cleared',
'count' => 0,
], 200);
}
}