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:
Dwindi Ramadhana
2025-12-26 10:59:48 +07:00
parent 0b08ddefa1
commit 0b2c8a56d6
23 changed files with 1132 additions and 232 deletions

View File

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