profit-planet-frontend/src/app/affiliate-links/page.tsx
2026-01-14 01:22:08 +01:00

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