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:
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '@/lib/api/client';
|
import { api } from '@/lib/api/client';
|
||||||
|
import SEOHead from '@/components/SEOHead';
|
||||||
|
|
||||||
interface AvatarSettings {
|
interface AvatarSettings {
|
||||||
allow_custom_avatar: boolean;
|
allow_custom_avatar: boolean;
|
||||||
@@ -198,6 +199,7 @@ export default function AccountDetails() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<SEOHead title="Account Details" description="Edit your account information" />
|
||||||
<h1 className="text-2xl font-bold mb-6">Account Details</h1>
|
<h1 className="text-2xl font-bold mb-6">Account Details</h1>
|
||||||
|
|
||||||
{/* Avatar Section */}
|
{/* Avatar Section */}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { MapPin, Plus, Edit, Trash2, Star } from 'lucide-react';
|
import { MapPin, Plus, Edit, Trash2, Star } from 'lucide-react';
|
||||||
import { api } from '@/lib/api/client';
|
import { api } from '@/lib/api/client';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import SEOHead from '@/components/SEOHead';
|
||||||
|
|
||||||
interface Address {
|
interface Address {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -153,6 +154,7 @@ export default function Addresses() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<SEOHead title="Addresses" description="Manage your shipping and billing addresses" />
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h1 className="text-2xl font-bold">Addresses</h1>
|
<h1 className="text-2xl font-bold">Addresses</h1>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ShoppingBag, Package, MapPin, User } from 'lucide-react';
|
import { ShoppingBag, Package, MapPin, User } from 'lucide-react';
|
||||||
|
import SEOHead from '@/components/SEOHead';
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const user = (window as any).woonoowCustomer?.user;
|
const user = (window as any).woonoowCustomer?.user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<SEOHead title="My Account" description="Manage your account" />
|
||||||
<h1 className="text-2xl font-bold mb-6">
|
<h1 className="text-2xl font-bold mb-6">
|
||||||
Hello {user?.display_name || 'there'}!
|
Hello {user?.display_name || 'there'}!
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { api } from '@/lib/api/client';
|
import { api } from '@/lib/api/client';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { formatPrice } from '@/lib/currency';
|
import { formatPrice } from '@/lib/currency';
|
||||||
|
import SEOHead from '@/components/SEOHead';
|
||||||
|
|
||||||
interface DownloadItem {
|
interface DownloadItem {
|
||||||
download_id: string;
|
download_id: string;
|
||||||
@@ -97,6 +98,7 @@ export default function Downloads() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<SEOHead title="Downloads" description="Your purchased downloads" />
|
||||||
<h1 className="text-2xl font-bold mb-6">Downloads</h1>
|
<h1 className="text-2xl font-bold mb-6">Downloads</h1>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Key, Copy, Check, ChevronDown, ChevronUp, Monitor, Globe, Power } from 'lucide-react';
|
import { Key, Copy, Check, ChevronDown, ChevronUp, Monitor, Globe, Power } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import SEOHead from '@/components/SEOHead';
|
||||||
|
|
||||||
interface Activation {
|
interface Activation {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -94,6 +95,7 @@ export default function Licenses() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<SEOHead title="Licenses" description="Manage your software licenses" />
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||||
<Key className="h-6 w-6" />
|
<Key className="h-6 w-6" />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { Package, Eye } from 'lucide-react';
|
import { Package, Eye } from 'lucide-react';
|
||||||
import { api } from '@/lib/api/client';
|
import { api } from '@/lib/api/client';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import SEOHead from '@/components/SEOHead';
|
||||||
|
|
||||||
interface Order {
|
interface Order {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -86,6 +87,7 @@ export default function Orders() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<SEOHead title="Orders" description="View your order history" />
|
||||||
<h1 className="text-2xl font-bold mb-6">Orders</h1>
|
<h1 className="text-2xl font-bold mb-6">Orders</h1>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { formatPrice } from '@/lib/currency';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useModules } from '@/hooks/useModules';
|
import { useModules } from '@/hooks/useModules';
|
||||||
import { useModuleSettings } from '@/hooks/useModuleSettings';
|
import { useModuleSettings } from '@/hooks/useModuleSettings';
|
||||||
|
import SEOHead from '@/components/SEOHead';
|
||||||
|
|
||||||
interface WishlistItem {
|
interface WishlistItem {
|
||||||
product_id: number;
|
product_id: number;
|
||||||
@@ -126,119 +127,120 @@ export default function Wishlist() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<SEOHead title="Wishlist" description="Your saved products" />
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">My Wishlist</h1>
|
<h1 className="text-2xl font-bold">My Wishlist</h1>
|
||||||
<p className="text-gray-600 text-sm mt-1">
|
<p className="text-gray-600 text-sm mt-1">
|
||||||
{items.length} {items.length === 1 ? 'item' : 'items'}
|
{items.length} {items.length === 1 ? 'item' : 'items'}
|
||||||
</p>
|
</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>
|
</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 */}
|
{/* Empty State */}
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<div className="text-center py-16 bg-white border rounded-lg">
|
<div className="text-center py-16 bg-white border rounded-lg">
|
||||||
<Heart className="w-16 h-16 mx-auto text-gray-300 mb-4" />
|
<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>
|
<h2 className="text-xl font-semibold mb-2">Your wishlist is empty</h2>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 mb-6">
|
||||||
Save your favorite products to buy them later
|
Save your favorite products to buy them later
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => navigate('/shop')}>
|
<Button onClick={() => navigate('/shop')}>
|
||||||
Browse Products
|
Browse Products
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Wishlist Grid */
|
/* Wishlist Grid */
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.product_id}
|
key={item.product_id}
|
||||||
className="bg-white border rounded-lg overflow-hidden hover:shadow-lg transition-shadow group"
|
className="bg-white border rounded-lg overflow-hidden hover:shadow-lg transition-shadow group"
|
||||||
>
|
>
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
<div className="relative aspect-square bg-gray-100">
|
<div className="relative aspect-square bg-gray-100">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemove(item.product_id)}
|
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"
|
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"
|
title="Remove from wishlist"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4 text-red-600" />
|
<X className="w-4 h-4 text-red-600" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={item.image || '/placeholder.png'}
|
src={item.image || '/placeholder.png'}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
className="w-full h-full object-cover cursor-pointer"
|
className="w-full h-full object-cover cursor-pointer"
|
||||||
onClick={() => navigate(`/product/${item.slug}`)}
|
onClick={() => navigate(`/product/${item.slug}`)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{item.on_sale && (
|
{item.on_sale && (
|
||||||
<div className="absolute top-3 left-3 bg-red-600 text-white text-xs font-bold px-2 py-1 rounded">
|
<div className="absolute top-3 left-3 bg-red-600 text-white text-xs font-bold px-2 py-1 rounded">
|
||||||
SALE
|
SALE
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Actions */}
|
||||||
<div className="p-4">
|
{(wishlistSettings.show_add_to_cart_button ?? true) && (
|
||||||
<h3
|
<Button
|
||||||
className="font-medium text-gray-900 mb-2 line-clamp-2 cursor-pointer hover:text-primary transition-colors"
|
onClick={() => handleAddToCart(item)}
|
||||||
onClick={() => navigate(`/product/${item.slug}`)}
|
disabled={item.stock_status === 'outofstock'}
|
||||||
|
className="w-full"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
{item.name}
|
<ShoppingCart className="w-4 h-4 mr-2" />
|
||||||
</h3>
|
{item.stock_status === 'outofstock'
|
||||||
|
? 'Out of Stock'
|
||||||
{/* Price */}
|
: item.type === 'variable'
|
||||||
<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'
|
|
||||||
? 'Select Options'
|
? 'Select Options'
|
||||||
: 'Add to Cart'}
|
: 'Add to Cart'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
|
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import Container from '@/components/Layout/Container';
|
import Container from '@/components/Layout/Container';
|
||||||
|
import SEOHead from '@/components/SEOHead';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -115,6 +116,7 @@ export default function Login() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
<SEOHead title="Login" description="Sign in to your account" />
|
||||||
<div className="min-h-[60vh] flex items-center justify-center py-12">
|
<div className="min-h-[60vh] flex items-center justify-center py-12">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
{/* Back link */}
|
{/* Back link */}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useParams, Link, useSearchParams } from 'react-router-dom';
|
import { useParams, Link, useSearchParams } from 'react-router-dom';
|
||||||
import { useThankYouSettings } from '@/hooks/useAppearanceSettings';
|
import { useThankYouSettings } from '@/hooks/useAppearanceSettings';
|
||||||
import Container from '@/components/Layout/Container';
|
import Container from '@/components/Layout/Container';
|
||||||
|
import SEOHead from '@/components/SEOHead';
|
||||||
import { CheckCircle, ShoppingBag, Package, Truck, User, LogIn } from 'lucide-react';
|
import { CheckCircle, ShoppingBag, Package, Truck, User, LogIn } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { formatPrice } from '@/lib/currency';
|
import { formatPrice } from '@/lib/currency';
|
||||||
@@ -74,6 +75,7 @@ export default function ThankYou() {
|
|||||||
if (template === 'receipt') {
|
if (template === 'receipt') {
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor }}>
|
<div style={{ backgroundColor }}>
|
||||||
|
<SEOHead title="Order Confirmed" description={`Order #${order?.number || orderId} confirmed`} />
|
||||||
<Container>
|
<Container>
|
||||||
<div className="py-12 max-w-2xl mx-auto">
|
<div className="py-12 max-w-2xl mx-auto">
|
||||||
{/* Receipt Container */}
|
{/* Receipt Container */}
|
||||||
@@ -351,6 +353,7 @@ export default function ThankYou() {
|
|||||||
// Render basic style template (default)
|
// Render basic style template (default)
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor }}>
|
<div style={{ backgroundColor }}>
|
||||||
|
<SEOHead title="Order Confirmed" description={`Order #${order?.number || orderId} confirmed`} />
|
||||||
<Container>
|
<Container>
|
||||||
<div className="py-12 max-w-3xl mx-auto">
|
<div className="py-12 max-w-3xl mx-auto">
|
||||||
{/* Success Header */}
|
{/* Success Header */}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { apiClient } from '@/lib/api/client';
|
import { apiClient } from '@/lib/api/client';
|
||||||
import { formatPrice } from '@/lib/currency';
|
import { formatPrice } from '@/lib/currency';
|
||||||
|
import SEOHead from '@/components/SEOHead';
|
||||||
|
|
||||||
interface ProductData {
|
interface ProductData {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -106,6 +107,7 @@ export default function Wishlist() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<SEOHead title="Wishlist" description="Your saved products" />
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user