217 lines
8.7 KiB
TypeScript
217 lines
8.7 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState, useMemo } from 'react'
|
|
import PageLayout from '../components/PageLayout'
|
|
import Waves from '../components/waves'
|
|
|
|
type Affiliate = {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
url: string
|
|
logoUrl?: string
|
|
category: string
|
|
commissionRate?: string
|
|
}
|
|
|
|
// Fallback placeholder image
|
|
const PLACEHOLDER_IMAGE = 'https://images.unsplash.com/photo-1557804506-669a67965ba0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3270&q=80'
|
|
|
|
export default function AffiliateLinksPage() {
|
|
const [affiliates, setAffiliates] = useState<Affiliate[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
// NEW: selected category
|
|
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
|
|
|
useEffect(() => {
|
|
async function fetchAffiliates() {
|
|
try {
|
|
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'
|
|
const res = await fetch(`${BASE_URL}/api/affiliates/active`)
|
|
|
|
if (!res.ok) {
|
|
throw new Error('Failed to fetch affiliates')
|
|
}
|
|
|
|
const data = await res.json()
|
|
const activeAffiliates = data.data || []
|
|
|
|
setAffiliates(activeAffiliates.map((item: any) => ({
|
|
id: String(item.id),
|
|
name: String(item.name || 'Partner'),
|
|
description: String(item.description || ''),
|
|
url: String(item.url || '#'),
|
|
logoUrl: item.logoUrl || PLACEHOLDER_IMAGE,
|
|
category: String(item.category || 'Other'),
|
|
commissionRate: item.commission_rate ? String(item.commission_rate) : undefined
|
|
})))
|
|
} catch (err) {
|
|
console.error('Error loading affiliates:', err)
|
|
setError('Failed to load affiliate partners')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchAffiliates()
|
|
}, [])
|
|
|
|
const posts = affiliates.map(affiliate => ({
|
|
id: affiliate.id,
|
|
title: affiliate.name,
|
|
href: affiliate.url,
|
|
description: affiliate.description,
|
|
imageUrl: affiliate.logoUrl || PLACEHOLDER_IMAGE,
|
|
category: { title: affiliate.category, href: '#' },
|
|
commissionRate: affiliate.commissionRate
|
|
}))
|
|
|
|
// NEW: fixed categories from the provided image, merged with backend ones
|
|
const categories = useMemo(() => {
|
|
const fromImage = [
|
|
'Technology',
|
|
'Energy',
|
|
'Finance',
|
|
'Healthcare',
|
|
'Education',
|
|
'Travel',
|
|
'Retail',
|
|
'Construction',
|
|
'Food',
|
|
'Automotive',
|
|
'Fashion',
|
|
'Pets',
|
|
]
|
|
const set = new Set<string>(fromImage)
|
|
affiliates.forEach(a => { if (a.category) set.add(a.category) })
|
|
return ['all', ...Array.from(set)]
|
|
}, [affiliates])
|
|
|
|
return (
|
|
<PageLayout>
|
|
<div
|
|
className="relative w-full flex flex-col min-h-screen overflow-hidden"
|
|
style={{ backgroundImage: 'none', background: 'none' }}
|
|
>
|
|
<Waves
|
|
className="pointer-events-none"
|
|
lineColor="#0f172a"
|
|
backgroundColor="rgba(245, 245, 240, 1)"
|
|
waveSpeedX={0.02}
|
|
waveSpeedY={0.01}
|
|
waveAmpX={40}
|
|
waveAmpY={20}
|
|
friction={0.9}
|
|
tension={0.01}
|
|
maxCursorMove={120}
|
|
xGap={12}
|
|
yGap={36}
|
|
/>
|
|
|
|
<div className="relative z-10 min-h-screen flex flex-col">
|
|
<main className="flex-1 max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8 w-full">
|
|
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-6 sm:p-8">
|
|
{/* Header (aligned with management pages) */}
|
|
<header className="flex flex-col gap-4 mb-8">
|
|
<div>
|
|
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Affiliate Partners</h1>
|
|
<p className="text-lg text-blue-700 mt-2">
|
|
Discover our trusted partners and earn commissions through affiliate links.
|
|
</p>
|
|
</div>
|
|
{/* NEW: Category filter */}
|
|
<div className="flex items-center gap-2">
|
|
<label className="text-sm text-blue-900 font-medium">Filter by category:</label>
|
|
<select
|
|
value={selectedCategory}
|
|
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
className="rounded-md border border-blue-200 bg-white px-3 py-1.5 text-sm text-blue-900 shadow-sm"
|
|
>
|
|
{categories.map(c => (
|
|
<option key={c} value={c}>{c === 'all' ? 'All' : c}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</header>
|
|
|
|
{/* States */}
|
|
{loading && (
|
|
<div className="mx-auto max-w-2xl text-center">
|
|
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-indigo-400 border-b-transparent" />
|
|
<p className="mt-4 text-sm text-gray-600">Loading affiliate partners...</p>
|
|
</div>
|
|
)}
|
|
|
|
{error && !loading && (
|
|
<div className="mx-auto max-w-2xl rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 text-center">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && posts.length === 0 && (
|
|
<div className="mx-auto max-w-2xl text-center text-sm text-gray-600">
|
|
No affiliate partners available at the moment.
|
|
</div>
|
|
)}
|
|
|
|
{/* Cards (aligned to white panels, border, shadow) */}
|
|
{!loading && !error && posts.length > 0 && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
{posts.map((post) => {
|
|
// NEW: highlight when matches selected category (keep all visible)
|
|
const isHighlighted = selectedCategory !== 'all' && post.category.title === selectedCategory
|
|
return (
|
|
<article
|
|
key={post.id}
|
|
className={`rounded-2xl bg-white border shadow-lg overflow-hidden flex flex-col transition
|
|
${isHighlighted ? 'border-2 border-indigo-400 ring-2 ring-indigo-200' : 'border-gray-100'}`}
|
|
>
|
|
<div className="relative">
|
|
<img alt="" src={post.imageUrl} className="aspect-video w-full object-cover" />
|
|
</div>
|
|
<div className="p-6 flex-1 flex flex-col">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<h3 className="text-xl font-semibold text-blue-900">{post.title}</h3>
|
|
{post.commissionRate && (
|
|
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium border border-indigo-200 bg-indigo-50 text-indigo-700">
|
|
{post.commissionRate}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="mt-2 flex flex-wrap gap-2 text-xs">
|
|
<a
|
|
href={post.category.href}
|
|
className={`inline-flex items-center rounded-full px-2 py-0.5 border text-blue-900
|
|
${isHighlighted ? 'border-indigo-300 bg-indigo-50' : 'border-blue-200 bg-blue-50'}`}
|
|
>
|
|
{post.category.title}
|
|
</a>
|
|
</div>
|
|
<p className="mt-3 text-sm text-gray-700 line-clamp-4">{post.description}</p>
|
|
<div className="mt-5 flex items-center justify-between">
|
|
<a
|
|
href={post.href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium shadow transition"
|
|
>
|
|
Visit Affiliate Link
|
|
</a>
|
|
<span className="text-[11px] text-gray-500">
|
|
External partner website.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</PageLayout>
|
|
)
|
|
} |