'use client' import React, { useState } from 'react' import Header from '../../components/nav/Header' import Footer from '../../components/Footer' import { PlusIcon, PencilIcon, TrashIcon, LinkIcon, PhotoIcon, XMarkIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline' import useAuthStore from '../../store/authStore' import { useRouter } from 'next/navigation' import PageTransitionEffect from '../../components/animation/pageTransitionEffect' import { useAdminAffiliates, type AdminAffiliate } from './hooks/getAffiliates' import { addAffiliate } from './hooks/addAffiliate' import { updateAffiliate } from './hooks/updateAffiliate' import { deleteAffiliate } from './hooks/deleteAffiliate' import AffiliateCropModal from './components/AffiliateCropModal' type Affiliate = AdminAffiliate // Centralized affiliate categories const AFFILIATE_CATEGORIES = [ 'Technology', 'Energy', 'Finance', 'Healthcare', 'Education', 'Travel', 'Retail', 'Construction', 'Food', 'Automotive', 'Fashion', 'Pets' ] as const export default function AffiliateManagementPage() { const router = useRouter() const user = useAuthStore(s => s.user) const isAdmin = !!user && ( (user as any)?.role === 'admin' || (user as any)?.userType === 'admin' || (user as any)?.isAdmin === true || ((user as any)?.roles?.includes?.('admin')) ) const [authChecked, setAuthChecked] = React.useState(false) React.useEffect(() => { if (user === null) { router.replace('/login') return } if (user && !isAdmin) { router.replace('/') return } setAuthChecked(true) }, [user, isAdmin, router]) // Fetch affiliates from API const { affiliates, loading, error, refresh } = useAdminAffiliates() const [searchQuery, setSearchQuery] = useState('') const [categoryFilter, setCategoryFilter] = useState('all') const [showCreateModal, setShowCreateModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) const [selectedAffiliate, setSelectedAffiliate] = useState(null) const [isCreating, setIsCreating] = useState(false) const [isUpdating, setIsUpdating] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const categories = ['all', ...AFFILIATE_CATEGORIES] const filteredAffiliates = affiliates.filter(affiliate => { const matchesSearch = affiliate.name.toLowerCase().includes(searchQuery.toLowerCase()) || affiliate.description.toLowerCase().includes(searchQuery.toLowerCase()) const matchesCategory = categoryFilter === 'all' || affiliate.category === categoryFilter return matchesSearch && matchesCategory }) const handleEdit = (affiliate: Affiliate) => { setSelectedAffiliate(affiliate) setShowEditModal(true) } const handleDelete = (affiliate: Affiliate) => { setSelectedAffiliate(affiliate) setShowDeleteModal(true) } const confirmDelete = async () => { if (!selectedAffiliate) return setIsDeleting(true) try { const result = await deleteAffiliate(selectedAffiliate.id) if (result.ok) { setShowDeleteModal(false) setSelectedAffiliate(null) await refresh() } else { alert(result.message || 'Failed to delete affiliate') } } catch (err) { console.error('Delete error:', err) alert('Failed to delete affiliate') } finally { setIsDeleting(false) } } if (!authChecked) return null return (
{/* Error State */} {error && (

Error loading affiliates: {error}

)} {/* Header */}

Affiliate Management

Manage your affiliate partners and tracking links

{/* Search and Filter */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-900 placeholder-gray-400 focus:ring-2 focus:ring-blue-900 focus:border-transparent" />
{/* Stats */}

Total Affiliates

{affiliates.length}

Active

{affiliates.filter(a => a.isActive).length}

Inactive

{affiliates.filter(a => !a.isActive).length}

{/* Affiliates Grid */}
{loading && (

Loading affiliates...

)} {!loading && filteredAffiliates.map((affiliate) => (
{/* Logo/Image */}
{affiliate.logoUrl ? ( {affiliate.name} ) : (
)}
{affiliate.isActive ? 'Active' : 'Inactive'}
{/* Content */}

{affiliate.name}

{affiliate.category}

{affiliate.description}

{affiliate.commissionRate && (
Commission: {affiliate.commissionRate}
)} {affiliate.url} {/* Actions */}
Status
))} {!loading && filteredAffiliates.length === 0 && (

No affiliates found

{searchQuery || categoryFilter !== 'all' ? 'Try adjusting your search or filter' : 'Get started by adding a new affiliate partner'}

)}
{/* Modals */} {showCreateModal && setShowCreateModal(false)} onCreate={async (newAffiliate) => { setIsCreating(true) try { const result = await addAffiliate({ name: newAffiliate.name, description: newAffiliate.description, url: newAffiliate.url, category: newAffiliate.category, commissionRate: newAffiliate.commissionRate, isActive: newAffiliate.isActive, logoFile: newAffiliate.logoFile }) if (result.ok) { await refresh() setShowCreateModal(false) } else { alert(result.message || 'Failed to create affiliate') } } catch (err) { console.error('Create error:', err) alert('Failed to create affiliate') } finally { setIsCreating(false) } }} />} {showEditModal && selectedAffiliate && ( { setShowEditModal(false) setSelectedAffiliate(null) }} onUpdate={async (updated) => { setIsUpdating(true) try { const result = await updateAffiliate({ id: updated.id, name: updated.name, description: updated.description, url: updated.url, category: updated.category, commissionRate: updated.commissionRate, isActive: updated.isActive, logoFile: updated.logoFile, removeLogo: updated.removeLogo }) if (result.ok) { await refresh() setShowEditModal(false) setSelectedAffiliate(null) } else { alert(result.message || 'Failed to update affiliate') } } catch (err) { console.error('Update error:', err) alert('Failed to update affiliate') } finally { setIsUpdating(false) } }} /> )} {showDeleteModal && selectedAffiliate && ( { if (!isDeleting) { setShowDeleteModal(false) setSelectedAffiliate(null) } }} onConfirm={confirmDelete} /> )}
) } // Create Modal Component function CreateAffiliateModal({ onClose, onCreate }: { onClose: () => void; onCreate: (affiliate: Omit & { logoFile?: File }) => void }) { const [name, setName] = useState('') const [description, setDescription] = useState('') const [url, setUrl] = useState('') const [category, setCategory] = useState(AFFILIATE_CATEGORIES[0]) const [commissionRate, setCommissionRate] = useState('') const [isActive, setIsActive] = useState(true) const [logoFile, setLogoFile] = useState(undefined) const [previewUrl, setPreviewUrl] = useState(null) const [showCropModal, setShowCropModal] = useState(false) const [rawImageUrl, setRawImageUrl] = useState(null) // Cleanup preview URL on unmount React.useEffect(() => { return () => { if (previewUrl) URL.revokeObjectURL(previewUrl) if (rawImageUrl) URL.revokeObjectURL(rawImageUrl) } }, [previewUrl, rawImageUrl]) const handleLogoChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) { console.log('No file selected') return } console.log('File selected:', file.name, file.type, file.size) const allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'] if (!allowed.includes(file.type)) { alert('Invalid image type. Allowed: JPG, PNG, WebP, SVG') e.target.value = '' // Reset input return } if (file.size > 5 * 1024 * 1024) { alert('Image exceeds 5MB limit') e.target.value = '' // Reset input return } // Open crop modal with raw image const url = URL.createObjectURL(file) setRawImageUrl(url) setShowCropModal(true) // Reset input so same file can be selected again e.target.value = '' } const handleCropComplete = (croppedBlob: Blob) => { // Clean up old preview if (previewUrl) URL.revokeObjectURL(previewUrl) if (rawImageUrl) URL.revokeObjectURL(rawImageUrl) // Create file from blob const croppedFile = new File([croppedBlob], 'affiliate-logo.jpg', { type: 'image/jpeg' }) setLogoFile(croppedFile) // Create preview const url = URL.createObjectURL(croppedBlob) setPreviewUrl(url) setRawImageUrl(null) } const handleRemoveLogo = () => { if (previewUrl) URL.revokeObjectURL(previewUrl) setLogoFile(undefined) setPreviewUrl(null) } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() onCreate({ name, description, url, category, commissionRate: commissionRate || undefined, isActive, logoFile }) } return ( <> { setShowCropModal(false) if (rawImageUrl) URL.revokeObjectURL(rawImageUrl) setRawImageUrl(null) }} onCropComplete={handleCropComplete} />

Add New Affiliate

setName(e.target.value)} className="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent" placeholder="e.g., Coffee Equipment Co." />