import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'; import { api } from '@/lib/api/client'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Plus, Trash2, Edit2, Link as LinkIcon, Search, Copy, CheckCircle, X, ChevronLeft } from 'lucide-react'; import { toast } from 'sonner'; import { Link } from 'react-router-dom'; import { useDebounce } from '@/hooks/useDebounce'; interface Product { id: number; name: string; slug: string; image?: string; price_html?: string; } interface Collection { id: number; title: string; slug: string; description: string; product_ids: number[]; link: string; } export function AffiliateCollections() { const config = (window as any).woonoowCustomer || {}; const enableCuratedCollections = config.affiliateSettings?.enableCuratedCollections !== false; const queryClient = useQueryClient(); const [isFormOpen, setIsFormOpen] = useState(false); const [editingCollection, setEditingCollection] = useState(null); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [selectedProducts, setSelectedProducts] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const debouncedSearch = useDebounce(searchQuery, 300); const [copiedId, setCopiedId] = useState(null); const { data: collections, isLoading: isLoadingCollections } = useQuery ({ queryKey: ['affiliate-collections'], queryFn: async () => { const res: any = await api.get('/account/affiliate/collections'); return Array.isArray(res) ? res : []; } }); const { data: searchResults, isLoading: isSearching } = useQuery({ queryKey: ['collection-product-search', debouncedSearch], queryFn: async () => { if (!debouncedSearch) return []; try { const res: any = await api.get(`/shop/products?search=${encodeURIComponent(debouncedSearch)}&per_page=5`); return res.products || []; } catch (err) { return []; } }, enabled: debouncedSearch.length > 2, placeholderData: keepPreviousData }); // When editing, fetch details of products so we can show their names/images const { data: editingProducts } = useQuery({ queryKey: ['collection-editing-products', editingCollection?.id], queryFn: async () => { if (!editingCollection || editingCollection.product_ids.length === 0) return []; const res: any = await api.get(`/shop/products?include=${editingCollection.product_ids.join(',')}&per_page=20`); return res.products || []; }, enabled: !!editingCollection }); // Pre-fill form when editingProducts is loaded React.useEffect(() => { if (editingCollection && editingProducts) { setTitle(editingCollection.title); setDescription(editingCollection.description); setSelectedProducts(editingProducts); } }, [editingCollection, editingProducts]); const resetForm = () => { setIsFormOpen(false); setEditingCollection(null); setTitle(''); setDescription(''); setSelectedProducts([]); setSearchQuery(''); }; const createMutation = useMutation({ mutationFn: async (data: any) => { return api.post('/account/affiliate/collections', data); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['affiliate-collections'] }); toast.success('Collection created successfully!'); resetForm(); }, onError: (err: any) => { toast.error(err.message || 'Failed to create collection'); } }); const updateMutation = useMutation({ mutationFn: async ({ id, data }: { id: number, data: any }) => { return api.put(`/account/affiliate/collections/${id}`, data); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['affiliate-collections'] }); toast.success('Collection updated successfully!'); resetForm(); }, onError: (err: any) => { toast.error(err.message || 'Failed to update collection'); } }); const deleteMutation = useMutation({ mutationFn: async (id: number) => { return api.delete(`/account/affiliate/collections/${id}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['affiliate-collections'] }); toast.success('Collection deleted!'); } }); const handleSave = () => { if (!title) { toast.error('Title is required'); return; } if (selectedProducts.length > 20) { toast.error('Maximum 20 products allowed per collection'); return; } const data = { title, description, product_ids: selectedProducts.map(p => p.id) }; if (editingCollection) { updateMutation.mutate({ id: editingCollection.id, data }); } else { createMutation.mutate(data); } }; const toggleProduct = (product: Product) => { const exists = selectedProducts.find(p => p.id === product.id); if (exists) { setSelectedProducts(prev => prev.filter(p => p.id !== product.id)); } else { if (selectedProducts.length >= 20) { toast.error('Maximum 20 products allowed'); return; } setSelectedProducts(prev => [...prev, product]); } }; const copyToClipboard = (link: string, id: string) => { navigator.clipboard.writeText(link); setCopiedId(id); toast.success('Link copied to clipboard!'); setTimeout(() => setCopiedId(null), 2000); }; if (isLoadingCollections) return
Loading collections...
; if (!enableCuratedCollections) { return (
Back to Affiliate Dashboard

My Curated Collections

This feature has been disabled by the administrator.

); } return (
Back to Affiliate Dashboard

My Curated Collections

Group your favorite products into a single shareable link.

{!isFormOpen && ( )}
{isFormOpen && (

{editingCollection ? 'Edit Collection' : 'Create New Collection'}

setTitle(e.target.value)} />