feat: add SEOHead to all SPA pages for dynamic page titles
Added SEOHead component to: - ThankYou page (both template styles) - Login page - Account/Dashboard - Account/Orders - Account/Downloads - Account/Addresses - Account/Wishlist - Account/Licenses - Account/AccountDetails - Public Wishlist page Also created usePageTitle hook as alternative for non-Helmet usage.
This commit is contained in:
@@ -8,6 +8,7 @@ import { formatPrice } from '@/lib/currency';
|
||||
import { toast } from 'sonner';
|
||||
import { useModules } from '@/hooks/useModules';
|
||||
import { useModuleSettings } from '@/hooks/useModuleSettings';
|
||||
import SEOHead from '@/components/SEOHead';
|
||||
|
||||
interface WishlistItem {
|
||||
product_id: number;
|
||||
@@ -126,119 +127,120 @@ export default function Wishlist() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SEOHead title="Wishlist" description="Your saved products" />
|
||||
{/* 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>
|
||||
<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>
|
||||
{/* 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>
|
||||
|
||||
{/* 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}`)}
|
||||
{/* Actions */}
|
||||
{(wishlistSettings.show_add_to_cart_button ?? true) && (
|
||||
<Button
|
||||
onClick={() => handleAddToCart(item)}
|
||||
disabled={item.stock_status === 'outofstock'}
|
||||
className="w-full"
|
||||
size="sm"
|
||||
>
|
||||
{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 */}
|
||||
{(wishlistSettings.show_add_to_cart_button ?? true) && (
|
||||
<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'
|
||||
<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>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user