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:
Dwindi Ramadhana
2026-01-07 22:51:47 +07:00
parent d7b132d9d9
commit a4a055a98e
10 changed files with 147 additions and 126 deletions

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { toast } from 'sonner';
import { api } from '@/lib/api/client';
import SEOHead from '@/components/SEOHead';
interface AvatarSettings {
allow_custom_avatar: boolean;
@@ -198,6 +199,7 @@ export default function AccountDetails() {
return (
<div>
<SEOHead title="Account Details" description="Edit your account information" />
<h1 className="text-2xl font-bold mb-6">Account Details</h1>
{/* Avatar Section */}

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { MapPin, Plus, Edit, Trash2, Star } from 'lucide-react';
import { api } from '@/lib/api/client';
import { toast } from 'sonner';
import SEOHead from '@/components/SEOHead';
interface Address {
id: number;
@@ -50,10 +51,10 @@ export default function Addresses() {
const loadAddresses = async () => {
try {
const response: any = await api.get('/account/addresses');
// Handle different response structures
let data: Address[] = [];
if (Array.isArray(response)) {
data = response;
} else if (response && typeof response === 'object') {
@@ -121,7 +122,7 @@ export default function Addresses() {
const handleDelete = async (id: number) => {
if (!confirm('Are you sure you want to delete this address?')) return;
try {
await api.delete(`/account/addresses/${id}`);
toast.success('Address deleted successfully');
@@ -153,9 +154,10 @@ export default function Addresses() {
return (
<div>
<SEOHead title="Addresses" description="Manage your shipping and billing addresses" />
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold">Addresses</h1>
<button
<button
onClick={handleAdd}
className="font-[inherit] inline-flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
>
@@ -163,12 +165,12 @@ export default function Addresses() {
Add Address
</button>
</div>
{addresses.length === 0 ? (
<div className="text-center py-12 border rounded-lg">
<MapPin className="w-12 h-12 text-gray-300 mx-auto mb-2" />
<p className="text-gray-600 mb-4">No addresses saved yet</p>
<button
<button
onClick={handleAdd}
className="font-[inherit] inline-flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
>
@@ -185,14 +187,14 @@ export default function Addresses() {
<Star className="w-5 h-5 text-yellow-500 fill-yellow-500" />
</div>
)}
<div className="mb-4">
<h3 className="font-semibold text-lg">{address.label}</h3>
<span className="text-xs text-gray-500 uppercase">
{address.type === 'both' ? 'Billing & Shipping' : address.type}
</span>
</div>
<div className="text-sm text-gray-700 space-y-1 mb-4">
<p className="font-medium">{address.first_name} {address.last_name}</p>
{address.company && <p>{address.company}</p>}
@@ -203,7 +205,7 @@ export default function Addresses() {
{address.phone && <p className="pt-2">Phone: {address.phone}</p>}
{address.email && <p>Email: {address.email}</p>}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleEdit(address)}
@@ -242,7 +244,7 @@ export default function Addresses() {
<h2 className="text-xl font-bold mb-4">
{editingAddress ? 'Edit Address' : 'Add New Address'}
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Label *</label>
@@ -254,7 +256,7 @@ export default function Addresses() {
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Address Type *</label>
<select
@@ -267,7 +269,7 @@ export default function Addresses() {
<option value="shipping">Shipping Only</option>
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">First Name *</label>
@@ -288,7 +290,7 @@ export default function Addresses() {
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Company</label>
<input
@@ -298,7 +300,7 @@ export default function Addresses() {
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Address Line 1 *</label>
<input
@@ -308,7 +310,7 @@ export default function Addresses() {
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Address Line 2</label>
<input
@@ -318,7 +320,7 @@ export default function Addresses() {
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">City *</label>
@@ -339,7 +341,7 @@ export default function Addresses() {
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Postcode *</label>
@@ -360,7 +362,7 @@ export default function Addresses() {
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Email</label>
@@ -381,7 +383,7 @@ export default function Addresses() {
/>
</div>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
@@ -393,7 +395,7 @@ export default function Addresses() {
<label htmlFor="is_default" className="text-sm">Set as default address</label>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
onClick={handleSave}

View File

@@ -1,16 +1,18 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ShoppingBag, Package, MapPin, User } from 'lucide-react';
import SEOHead from '@/components/SEOHead';
export default function Dashboard() {
const user = (window as any).woonoowCustomer?.user;
return (
<div>
<SEOHead title="My Account" description="Manage your account" />
<h1 className="text-2xl font-bold mb-6">
Hello {user?.display_name || 'there'}!
</h1>
<p className="text-gray-600 mb-8">
From your account dashboard you can view your recent orders, manage your shipping and billing addresses, and edit your password and account details.
</p>

View File

@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
import { api } from '@/lib/api/client';
import { toast } from 'sonner';
import { formatPrice } from '@/lib/currency';
import SEOHead from '@/components/SEOHead';
interface DownloadItem {
download_id: string;
@@ -97,6 +98,7 @@ export default function Downloads() {
return (
<div>
<SEOHead title="Downloads" description="Your purchased downloads" />
<h1 className="text-2xl font-bold mb-6">Downloads</h1>
<div className="space-y-4">

View File

@@ -15,6 +15,7 @@ import {
} from '@/components/ui/alert-dialog';
import { Key, Copy, Check, ChevronDown, ChevronUp, Monitor, Globe, Power } from 'lucide-react';
import { toast } from 'sonner';
import SEOHead from '@/components/SEOHead';
interface Activation {
id: number;
@@ -94,6 +95,7 @@ export default function Licenses() {
return (
<div className="space-y-6">
<SEOHead title="Licenses" description="Manage your software licenses" />
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Key className="h-6 w-6" />

View File

@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { Package, Eye } from 'lucide-react';
import { api } from '@/lib/api/client';
import { toast } from 'sonner';
import SEOHead from '@/components/SEOHead';
interface Order {
id: number;
@@ -69,7 +70,7 @@ export default function Orders() {
return (
<div>
<h1 className="text-2xl font-bold mb-6">Orders</h1>
<div className="text-center py-12">
<Package className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-600 mb-4">No orders yet</p>
@@ -86,8 +87,9 @@ export default function Orders() {
return (
<div>
<SEOHead title="Orders" description="View your order history" />
<h1 className="text-2xl font-bold mb-6">Orders</h1>
<div className="space-y-4">
{orders.map((order) => (
<div key={order.id} className="border rounded-lg p-4 hover:border-primary transition-colors">
@@ -100,7 +102,7 @@ export default function Orders() {
{order.status.replace('-', ' ').toUpperCase()}
</span>
</div>
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
{order.items_count} {order.items_count === 1 ? 'item' : 'items'}

View File

@@ -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>
);
}

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
import { toast } from 'sonner';
import Container from '@/components/Layout/Container';
import SEOHead from '@/components/SEOHead';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -115,6 +116,7 @@ export default function Login() {
return (
<Container>
<SEOHead title="Login" description="Sign in to your account" />
<div className="min-h-[60vh] flex items-center justify-center py-12">
<div className="w-full max-w-md">
{/* Back link */}

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useParams, Link, useSearchParams } from 'react-router-dom';
import { useThankYouSettings } from '@/hooks/useAppearanceSettings';
import Container from '@/components/Layout/Container';
import SEOHead from '@/components/SEOHead';
import { CheckCircle, ShoppingBag, Package, Truck, User, LogIn } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { formatPrice } from '@/lib/currency';
@@ -74,6 +75,7 @@ export default function ThankYou() {
if (template === 'receipt') {
return (
<div style={{ backgroundColor }}>
<SEOHead title="Order Confirmed" description={`Order #${order?.number || orderId} confirmed`} />
<Container>
<div className="py-12 max-w-2xl mx-auto">
{/* Receipt Container */}
@@ -351,6 +353,7 @@ export default function ThankYou() {
// Render basic style template (default)
return (
<div style={{ backgroundColor }}>
<SEOHead title="Order Confirmed" description={`Order #${order?.number || orderId} confirmed`} />
<Container>
<div className="py-12 max-w-3xl mx-auto">
{/* Success Header */}

View File

@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { apiClient } from '@/lib/api/client';
import { formatPrice } from '@/lib/currency';
import SEOHead from '@/components/SEOHead';
interface ProductData {
id: number;
@@ -106,6 +107,7 @@ export default function Wishlist() {
return (
<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="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold">My Wishlist</h1>