feat: Newsletter system improvements and validation framework
- Fix: Marketing events now display in Staff notifications tab - Reorganize: Move Coupons to Marketing/Coupons for better organization - Add: Comprehensive email/phone validation with extensible filter hooks - Email validation with regex pattern (xxxx@xxxx.xx) - Phone validation with WhatsApp verification support - Filter hooks for external API integration (QuickEmailVerification, etc.) - Fix: Newsletter template routes now use centralized notification email builder - Add: Validation.php class for reusable validation logic - Add: VALIDATION_HOOKS.md documentation with integration examples - Add: NEWSLETTER_CAMPAIGN_PLAN.md architecture for future campaign system - Fix: API delete method call in Newsletter.tsx (delete -> del) - Remove: Duplicate EmailTemplates.tsx (using notification system instead) - Update: Newsletter controller to use centralized Validation class Breaking changes: - Coupons routes moved from /routes/Coupons to /routes/Marketing/Coupons - Legacy /coupons routes maintained for backward compatibility
This commit is contained in:
0
customer-spa/src/components/Layout/PageLayout.tsx
Normal file
0
customer-spa/src/components/Layout/PageLayout.tsx
Normal file
@@ -21,8 +21,9 @@ export function useWishlist() {
|
||||
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;
|
||||
// Check if wishlist is enabled (default true if not explicitly set to false)
|
||||
const settings = (window as any).woonoowCustomer?.settings;
|
||||
const isEnabled = settings?.wishlist_enabled !== false;
|
||||
const isLoggedIn = (window as any).woonoowCustomer?.user?.isLoggedIn;
|
||||
|
||||
// Load wishlist on mount
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ShoppingCart, User, Search, Menu, X } from 'lucide-react';
|
||||
import { Search, ShoppingCart, User, Menu, X, Heart } from 'lucide-react';
|
||||
import { useLayout } from '../contexts/ThemeContext';
|
||||
import { useCartStore } from '../lib/cart/store';
|
||||
import { useHeaderSettings, useFooterSettings } from '../hooks/useAppearanceSettings';
|
||||
@@ -130,6 +130,14 @@ function ClassicLayout({ children }: BaseLayoutProps) {
|
||||
</a>
|
||||
))}
|
||||
|
||||
{/* Wishlist */}
|
||||
{headerSettings.elements.wishlist && (window as any).woonoowCustomer?.settings?.wishlist_enabled !== false && user?.isLoggedIn && (
|
||||
<Link to="/my-account/wishlist" className="flex items-center gap-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">
|
||||
<Heart className="h-5 w-5" />
|
||||
<span className="hidden lg:block">Wishlist</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Cart */}
|
||||
{headerSettings.elements.cart && (
|
||||
<Link to="/cart" className="flex items-center gap-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">
|
||||
|
||||
@@ -50,41 +50,21 @@ export default function Addresses() {
|
||||
const loadAddresses = async () => {
|
||||
try {
|
||||
const response: any = await api.get('/account/addresses');
|
||||
console.log('API response:', response);
|
||||
console.log('Type of response:', typeof response);
|
||||
console.log('Is array:', Array.isArray(response));
|
||||
console.log('Response keys:', response ? Object.keys(response) : 'null');
|
||||
console.log('Response values:', response ? Object.values(response) : 'null');
|
||||
|
||||
// Handle different response structures
|
||||
let data: Address[] = [];
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
// Direct array response
|
||||
data = response;
|
||||
console.log('Using direct array');
|
||||
} else if (response && typeof response === 'object') {
|
||||
// Log all properties to debug
|
||||
console.log('Checking object properties...');
|
||||
|
||||
// Check common wrapper properties
|
||||
if (Array.isArray(response.data)) {
|
||||
data = response.data;
|
||||
console.log('Using response.data');
|
||||
} else if (Array.isArray(response.addresses)) {
|
||||
data = response.addresses;
|
||||
console.log('Using response.addresses');
|
||||
} else if (response.length !== undefined && typeof response === 'object') {
|
||||
// Might be array-like object, convert to array
|
||||
data = Object.values(response).filter((item: any) => item && typeof item === 'object' && item.id) as Address[];
|
||||
console.log('Converted object to array:', data);
|
||||
} else {
|
||||
console.error('API returned unexpected structure:', response);
|
||||
console.error('Available keys:', Object.keys(response));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final addresses array:', data);
|
||||
setAddresses(data);
|
||||
} catch (error) {
|
||||
console.error('Load addresses error:', error);
|
||||
@@ -124,16 +104,11 @@ export default function Addresses() {
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
console.log('Saving address:', formData);
|
||||
if (editingAddress) {
|
||||
console.log('Updating address ID:', editingAddress.id);
|
||||
const response = await api.put(`/account/addresses/${editingAddress.id}`, formData);
|
||||
console.log('Update response:', response);
|
||||
await api.put(`/account/addresses/${editingAddress.id}`, formData);
|
||||
toast.success('Address updated successfully');
|
||||
} else {
|
||||
console.log('Creating new address');
|
||||
const response = await api.post('/account/addresses', formData);
|
||||
console.log('Create response:', response);
|
||||
await api.post('/account/addresses', formData);
|
||||
toast.success('Address added successfully');
|
||||
}
|
||||
setShowModal(false);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { useCartStore } from '@/lib/cart/store';
|
||||
import { useProductSettings } from '@/hooks/useAppearanceSettings';
|
||||
import { useWishlist } from '@/hooks/useWishlist';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Container from '@/components/Layout/Container';
|
||||
import { ProductCard } from '@/components/ProductCard';
|
||||
@@ -23,6 +24,7 @@ export default function Product() {
|
||||
const [selectedAttributes, setSelectedAttributes] = useState<Record<string, string>>({});
|
||||
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
||||
const { addItem } = useCartStore();
|
||||
const { isEnabled: wishlistEnabled, isInWishlist, toggleWishlist, isLoggedIn } = useWishlist();
|
||||
|
||||
// Fetch product details by slug
|
||||
const { data: product, isLoading, error } = useQuery<ProductType | null>({
|
||||
@@ -40,27 +42,19 @@ export default function Product() {
|
||||
queryFn: async () => {
|
||||
if (!product) return [];
|
||||
|
||||
console.log('[Related Products] Fetching for product:', product.id);
|
||||
console.log('[Related Products] Categories:', product.categories);
|
||||
|
||||
try {
|
||||
if (product.related_ids && product.related_ids.length > 0) {
|
||||
const ids = product.related_ids.slice(0, 4).join(',');
|
||||
console.log('[Related Products] Using related_ids:', ids);
|
||||
const response = await apiClient.get<ProductsResponse>(`/shop/products?include=${ids}`);
|
||||
console.log('[Related Products] Response:', response);
|
||||
return response.products || [];
|
||||
}
|
||||
|
||||
const categoryId = product.categories?.[0]?.term_id || product.categories?.[0]?.id;
|
||||
if (categoryId) {
|
||||
console.log('[Related Products] Using category:', categoryId);
|
||||
const response = await apiClient.get<ProductsResponse>(`/shop/products?category=${categoryId}&per_page=4&exclude=${product.id}`);
|
||||
console.log('[Related Products] Response:', response.products?.length, 'products');
|
||||
return response.products || [];
|
||||
}
|
||||
|
||||
console.log('[Related Products] No category found');
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch related products:', error);
|
||||
@@ -70,15 +64,6 @@ export default function Product() {
|
||||
enabled: !!product?.id && elements.related_products,
|
||||
});
|
||||
|
||||
// Debug logging
|
||||
console.log('[Related Products] Settings:', {
|
||||
enabled: elements.related_products,
|
||||
hasProduct: !!product?.id,
|
||||
queryEnabled: !!product?.id && elements.related_products,
|
||||
relatedProductsData: relatedProducts,
|
||||
relatedProductsLength: relatedProducts?.length
|
||||
});
|
||||
|
||||
// Set initial image when product loads
|
||||
useEffect(() => {
|
||||
if (product && !selectedImage) {
|
||||
@@ -502,10 +487,21 @@ export default function Product() {
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
Add to Cart
|
||||
</button>
|
||||
<button className="w-full h-14 flex items-center justify-center gap-2 bg-white text-gray-900 rounded-xl font-semibold text-base border-2 border-gray-200 hover:border-gray-400 transition-all">
|
||||
<Heart className="h-5 w-5" />
|
||||
Add to Wishlist
|
||||
</button>
|
||||
{wishlistEnabled && (
|
||||
<button
|
||||
onClick={() => product && toggleWishlist(product.id)}
|
||||
className={`w-full h-14 flex items-center justify-center gap-2 rounded-xl font-semibold text-base border-2 transition-all ${
|
||||
product && isInWishlist(product.id)
|
||||
? 'bg-red-50 text-red-600 border-red-200 hover:border-red-400'
|
||||
: 'bg-white text-gray-900 border-gray-200 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
<Heart className={`h-5 w-5 ${
|
||||
product && isInWishlist(product.id) ? 'fill-red-500' : ''
|
||||
}`} />
|
||||
{product && isInWishlist(product.id) ? 'Remove from Wishlist' : 'Add to Wishlist'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -694,7 +690,7 @@ export default function Product() {
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<svg key={star} className={`w-5 h-5 ${star <= (product.average_rating || 0) ? 'text-yellow-400' : 'text-gray-300'} fill-current`} viewBox="0 0 20 20">
|
||||
<svg key={star} className={`w-5 h-5 ${star <= Number(product.average_rating || 0) ? 'text-yellow-400' : 'text-gray-300'} fill-current`} viewBox="0 0 20 20">
|
||||
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" />
|
||||
</svg>
|
||||
))}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface ProductCategory {
|
||||
name: string;
|
||||
slug: string;
|
||||
count?: number;
|
||||
term_id?: number;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
@@ -31,6 +32,11 @@ export interface Product {
|
||||
attributes?: any[];
|
||||
variations?: number[];
|
||||
permalink?: string;
|
||||
related_ids?: number[];
|
||||
virtual?: boolean;
|
||||
downloadable?: boolean;
|
||||
review_count?: number;
|
||||
average_rating?: string;
|
||||
}
|
||||
|
||||
export interface ProductsResponse {
|
||||
|
||||
Reference in New Issue
Block a user