222 lines
9.5 KiB
TypeScript
222 lines
9.5 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import {
|
|
Code,
|
|
Briefcase,
|
|
Shield,
|
|
Cloud,
|
|
Globe,
|
|
ExternalLink,
|
|
Layers
|
|
} from 'lucide-react';
|
|
import { PageHeader } from '../components/ui/PageHeader';
|
|
import { PageLayout } from '../components/layout/PageLayout';
|
|
import { getEcosystemOfferings } from '../services/ecosystem-api';
|
|
import type { EcosystemOffering } from '../services/ecosystem-api';
|
|
import { BrandLogo } from './admin/EcosystemManagerPage';
|
|
|
|
const iconMap: Record<string, any> = {
|
|
Code,
|
|
Briefcase,
|
|
Shield,
|
|
Cloud,
|
|
Globe
|
|
};
|
|
|
|
export const EcosystemPage: React.FC = () => {
|
|
const [offerings, setOfferings] = useState<EcosystemOffering[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [filter, setFilter] = useState<'ALL' | 'PRODUCT' | 'SERVICE'>('ALL');
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
const data = await getEcosystemOfferings();
|
|
setOfferings(data);
|
|
} catch (err) {
|
|
console.error('Failed to load ecosystem data:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, []);
|
|
|
|
const filteredOfferings = offerings.filter(o => {
|
|
if (filter === 'ALL') return true;
|
|
return o.type === filter;
|
|
});
|
|
|
|
const headerNode = (
|
|
<PageHeader
|
|
title="Ecosystem Explorer"
|
|
subtitle="Unlock access to Tech4Biz's complete network of industry-leading products, platforms, and specialized services."
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<PageLayout header={headerNode}>
|
|
<div className="p-6 space-y-8">
|
|
{/* Segmented controls filter */}
|
|
<div className="flex justify-between items-center border-b border-ink-200 pb-4">
|
|
<div className="flex bg-ink-100 p-0.5 rounded-xl border border-ink-200 overflow-x-auto max-w-full">
|
|
{(['ALL', 'PRODUCT', 'SERVICE'] as const).map((type) => {
|
|
const labels: Record<string, string> = {
|
|
ALL: 'All Solutions',
|
|
PRODUCT: 'Products',
|
|
SERVICE: 'Services'
|
|
};
|
|
return (
|
|
<button
|
|
key={type}
|
|
onClick={() => setFilter(type)}
|
|
className={`px-4 py-2 rounded-lg text-xs font-black tracking-wider uppercase transition-all duration-300 whitespace-nowrap cursor-pointer ${
|
|
filter === type
|
|
? 'bg-ink-0 text-ink-900 shadow-sm border border-ink-200/50'
|
|
: 'text-ink-500 hover:text-ink-900'
|
|
}`}
|
|
>
|
|
{labels[type]}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="text-[10px] font-bold text-ink-400 uppercase tracking-widest hidden sm:block">
|
|
{filteredOfferings.length} Solutions Available
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex flex-1 items-center justify-center py-20">
|
|
<div className="w-8 h-8 border-4 border-ink-900/30 border-t-ink-900 rounded-full animate-spin"></div>
|
|
</div>
|
|
) : (
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key="offerings-grid"
|
|
initial={{ opacity: 0, y: 15 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -15 }}
|
|
transition={{ duration: 0.25 }}
|
|
className="grid grid-cols-1 md:grid-cols-2 gap-8"
|
|
>
|
|
{filteredOfferings.map((offering) => {
|
|
const IconComponent = iconMap[offering.logoIcon] || Globe;
|
|
const isProduct = offering.type === 'PRODUCT';
|
|
|
|
return (
|
|
<div
|
|
key={offering.id}
|
|
className="group flex flex-col justify-between bg-ink-0 border border-ink-200 hover:border-ink-450 rounded-3xl p-6 md:p-8 hover:shadow-xl transition-all duration-500"
|
|
>
|
|
<div>
|
|
{/* Logo and Badges */}
|
|
<div className="flex justify-between items-center mb-6">
|
|
<div className="h-10 flex items-center shrink-0">
|
|
{offering.logoUrl ? (
|
|
<BrandLogo name={offering.logoUrl} className="max-h-7 max-w-[150px] object-contain text-ink-900 dark:text-ink-0" />
|
|
) : (
|
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center text-ink-0 bg-ink-900 shadow-md">
|
|
<IconComponent className="w-5 h-5" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black tracking-widest uppercase border ${
|
|
isProduct
|
|
? 'bg-blue-500/10 text-blue-600 border-blue-500/20'
|
|
: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20'
|
|
}`}>
|
|
<Layers className="w-3 h-3" />
|
|
{offering.type}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Info Copy */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-lg font-black tracking-tight text-ink-900">
|
|
{offering.name}
|
|
</h3>
|
|
<p className="text-xs font-bold text-ink-700 leading-snug">
|
|
{offering.tagline}
|
|
</p>
|
|
<p className="text-xs font-medium leading-relaxed text-ink-500 pt-1">
|
|
{offering.description}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Key Benefits */}
|
|
{offering.benefits && offering.benefits.length > 0 && (
|
|
<div className="pt-6 space-y-2">
|
|
<span className="text-[10px] font-extrabold uppercase tracking-widest text-ink-400">Key Benefits</span>
|
|
<ul className="grid grid-cols-1 gap-2.5 pt-1">
|
|
{offering.benefits.map((benefit, bIdx) => (
|
|
<li key={bIdx} className="flex items-start gap-2.5 text-xs font-medium text-ink-600">
|
|
<span className={`w-1.5 h-1.5 rounded-full shrink-0 mt-1.5 ${
|
|
isProduct ? 'bg-blue-500' : 'bg-emerald-500'
|
|
}`} />
|
|
<span>{benefit}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* Optional Media (GIF, Video, Image) */}
|
|
{offering.mediaUrl && offering.mediaType && offering.mediaType !== 'NONE' && (
|
|
<div className="mt-6 border border-ink-200 rounded-2xl overflow-hidden bg-ink-50 relative aspect-video shadow-sm">
|
|
{offering.mediaType === 'VIDEO' ? (
|
|
offering.mediaUrl.includes('youtube.com') || offering.mediaUrl.includes('youtube-nocookie.com') || offering.mediaUrl.includes('vimeo.com') ? (
|
|
<iframe
|
|
src={offering.mediaUrl}
|
|
title={`${offering.name} Video Preview`}
|
|
className="w-full h-full border-0 absolute inset-0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowFullScreen
|
|
/>
|
|
) : (
|
|
<video
|
|
src={offering.mediaUrl}
|
|
controls
|
|
muted
|
|
loop
|
|
playsInline
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
)
|
|
) : (
|
|
<img
|
|
src={offering.mediaUrl}
|
|
alt={`${offering.name} Screenshot`}
|
|
className="w-full h-full object-cover group-hover:scale-102 transition-transform duration-700"
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* CTA Button */}
|
|
<div className="pt-8 mt-auto">
|
|
<a
|
|
href={offering.websiteUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center justify-center gap-2 w-full px-5 py-3 rounded-xl text-xs font-black tracking-wider uppercase bg-ink-900 text-ink-0 hover:bg-ink-800 hover:shadow-lg transition-all duration-300 cursor-pointer"
|
|
>
|
|
<span>{offering.ctaText || 'Explore Solution'}</span>
|
|
<ExternalLink className="w-3.5 h-3.5" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
)}
|
|
</div>
|
|
</PageLayout>
|
|
);
|
|
};
|
|
|
|
export default EcosystemPage;
|