2025-12-26 13:12:37 +00:00

436 lines
16 KiB
TypeScript

'use client';
import { getAccessToken_client } from '@/utils/apiHelper_client';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import React, { useState, useEffect } from 'react';
type Brand = { id: string; name: string; logo?: string; dropship: boolean; };
type BrandIndividual = {
name: string;
};
type BrandResponse = {
meta: {
eta: string;
count: number;
generatedAt: string;
};
data: string[]; // API returns a list of brand names
};
async function fetchBrands(): Promise<BrandResponse> {
const resp = await fetch('https://motorstate.data4autos.com/api/data/brands', {
cache: 'no-store',
});
if (!resp.ok) {
throw new Error(`Failed to fetch brands: ${resp.statusText}`);
}
const data = await resp.json();
return data as BrandResponse;
}
export default function BrandsClient() {
const router = useRouter()
const [brands, setBrands] = useState<BrandIndividual[]>([]);
const [search, setSearch] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [toast, setToast] = useState('');
const [isScrolled, setIsScrolled] = useState(false);
const [showDropshipOnly, setShowDropshipOnly] = useState(false);
const [payment, setPayment] = useState<any>(null);
const userId = sessionStorage.getItem('USERID');
interface BrandLogoMap {
[key: string]: {
logo: string;
};
}
const [brandLogos, setBrandLogos] = useState<BrandLogoMap>({});
useEffect(() => {
fetch("/data/brandMap.json")
.then(res => res.json())
.then(data => {
const normalized: BrandLogoMap = {};
Object.keys(data).forEach(key => {
const cleanKey = key.trim().toLowerCase();
normalized[cleanKey] = data[key];
});
setBrandLogos(normalized);
});
}, []);
useEffect(() => {
const role = localStorage.getItem("user_role");
const sessionId = localStorage.getItem("payment_session");
// ✅ Admins and Partners can access directly (skip payment check)
if (role === "admin" || role === "partner") {
return;
}
// 🚫 If no payment session, redirect to pricing
if (!sessionId) {
router.push("/pricing");
return;
}
// ✅ Otherwise, check payment details
const fetchPaymentDetails = async () => {
try {
const res: any = await axios.get(
"https://ebay.backend.data4autos.com/api/payment/details",
{ params: { session_id: sessionId } }
);
setPayment(res.data.payment);
} catch (err) {
console.error("Error fetching payment details:", err);
}
};
fetchPaymentDetails();
}, [router]);
useEffect(() => {
const fetchUserBrands = async () => {
try {
//console.log('Fetching access token...'); // Debugging line
const accessToken = await getAccessToken_client();
//console.log('Access Token:', accessToken); // Debugging line
const brandResponse = await fetchBrands();
// Convert string[] → BrandIndividual[]
const mappedBrands: BrandIndividual[] = brandResponse.data.map(
(name) => ({ name })
);
setBrands(mappedBrands);
const res = await fetch(`https://ebay.backend.data4autos.com/api/motorstate/brands/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// Optionally add Authorization: `Bearer ${accessToken}` if needed
},
});
const data = await res.json();
console.log('GET response:', data);
// Extract selected brand IDs from the response
const userSelectedIds = data.map((b: any) => String(b.name)); // brandid from your response
setSelectedIds(userSelectedIds);
// Optional: show toast
setToast(`Loaded ${userSelectedIds.length} selected brands`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error fetching brands:', error);
setToast('Failed to load user brands');
setTimeout(() => setToast(''), 4000);
}
};
fetchUserBrands();
}, []);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const filteredBrands = brands.filter((b) => {
const matchesSearch = b.name.toLowerCase().includes(search.toLowerCase());
const matchesDropship = showDropshipOnly ? b.name : true;
return matchesSearch && matchesDropship;
});
const allFilteredSelected = filteredBrands.length > 0 && filteredBrands.every((b) => selectedIds.includes(b.name));
const toggleSelect = (id: string) => {
setSelectedIds((prev) => (prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]));
};
const toggleSelectAll = () => {
const ids = filteredBrands.map((b) => b.name);
if (allFilteredSelected) {
setSelectedIds((prev) => prev.filter((id) => !ids.includes(id)));
} else {
setSelectedIds((prev) => Array.from(new Set([...prev, ...ids])));
}
};
const getSelectedStatusText = () => {
if (selectedIds.length === 0) return 'No brands selected';
if (selectedIds.length === 1) return '1 brand selected';
return `${selectedIds.length} brands selected`;
};
//const userId = sessionStorage.getItem('USERID'); // dynamic user id
const handleSave = async () => {
const payload = {
userid: userId,
brands: brands
.filter((b) => selectedIds.includes(b.name))
.map((b) => ({
id: b.name,
name: b.name,
})),
};
try {
const res = await fetch('https://ebay.backend.data4autos.com/api/motorstate/brands/bulk-insert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await res.json();
setToast(`${data.message} (Code: ${data.code}, User: ${data.userid})`);
setTimeout(() => setToast(''), 4000);
} catch (error) {
console.error('Error saving brands:', error);
setToast('Failed to save collections. Please try again.');
setTimeout(() => setToast(''), 4000);
}
};
const getFilterStatusText = () => {
if (filteredBrands.length === brands.length && !search && !showDropshipOnly) {
return `Showing all ${brands.length} brands`;
}
let status = `Showing ${filteredBrands.length} of ${brands.length} brands`;
if (search && showDropshipOnly) {
status += ` matching "${search}" and dropship only`;
} else if (search) {
status += ` matching "${search}"`;
} else if (showDropshipOnly) {
status += ` (dropship only)`;
}
return status;
};
return (
<div className="bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20">
{/* Sticky Header (below default Header) */}
<div
className={`sticky top-14 z-10 transition-all duration-300 ${isScrolled ? 'bg-white/95 backdrop-blur-md shadow-lg py-3' : 'bg-white/80 backdrop-blur-sm py-4'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-col">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-[#00d1ff]">
Data4Autos MotorState Brands
</h1>
<p className="text-sm text-gray-500 mt-1">{getFilterStatusText()}</p>
<p className="text-sm font-medium text-[#00d1ff] mt-1">{getSelectedStatusText()}</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search brands…"
className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
<div className="flex flex-wrap items-center gap-3">
{/* <label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input
type="checkbox"
checked={showDropshipOnly}
onChange={() => setShowDropshipOnly(!showDropshipOnly)}
className="sr-only"
/>
<div className={`block w-10 h-6 rounded-full transition-colors ${showDropshipOnly ? 'bg-[#00d1ff]' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${showDropshipOnly ? 'transform translate-x-4' : ''
}`}
></div>
</div>
Dropship Only
</label> */}
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 cursor-pointer select-none">
<div className="relative">
<input type="checkbox" checked={allFilteredSelected} onChange={toggleSelectAll} className="sr-only" />
<div className={`block w-10 h-6 rounded-full transition-colors ${allFilteredSelected ? 'bg-[#00d1ff]' : 'bg-gray-300'}`}></div>
<div
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${allFilteredSelected ? 'transform translate-x-4' : ''
}`}
></div>
</div>
Select All
</label>
<button
onClick={handleSave}
className="px-5 py-2.5 bg-[#00d1ff] text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:-translate-y-0.5 disabled:opacity-50 disabled:transform-none disabled:cursor-not-allowed flex items-center gap-2 shadow-md hover:shadow-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Save Collections
</button>
</div>
</div>
</div>
</div>
</div>
{/* Brand Grid */}
<div className={`pb-12 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto ${isScrolled ? 'pt-[100px]' : 'pt-16'}`}>
{filteredBrands.length > 0 ? (
<div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-5">
{[...filteredBrands]
.sort((a, b) => {
const aSelected = selectedIds.includes(a.name);
const bSelected = selectedIds.includes(b.name);
if (aSelected && !bSelected) return -1;
if (!aSelected && bSelected) return 1;
return 0;
})
.map((brand, index) => (
<div
key={brand.name}
className="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1.5 relative group"
style={{ animationDelay: `${index * 0.05}s` }}
onClick={() => toggleSelect(brand.name)}
>
<div className="absolute top-3 right-3 z-10">
<label className="inline-flex items-center">
<input
type="checkbox"
checked={selectedIds.includes(brand.name)}
onChange={() => toggleSelect(brand.name)}
className="absolute opacity-0 h-0 w-0"
/>
<span
className={`checkmark w-6 h-6 rounded-md border-2 flex items-center justify-center transition-all ${selectedIds.includes(brand.name) ? 'bg-[#00d1ff] border-[#00d1ff]' : 'bg-white border-gray-300 group-hover:border-blue-400'
}`}
>
{selectedIds.includes(brand.name) && (
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7"></path>
</svg>
)}
</span>
</label>
</div>
<div className="p-5 flex flex-col items-center h-full">
<div className="w-28 h-28 flex items-center justify-center p-2 bg-gray-50 rounded-lg mb-4">
{/* <img
src={brand.name || 'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png'}
alt={brand.name}
className="max-w-full max-h-full object-contain"
/> */}
<img
src={
brandLogos[brand.name.trim().toLowerCase()]?.logo ??
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
className="max-w-full max-h-full object-contain"
/>
</div>
<p className="text-center font-medium text-gray-800 mt-auto">{brand.name}</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-20">
<div className="inline-block p-4 bg-white rounded-xl shadow-md">
<svg className="w-16 h-16 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 className="mt-4 text-xl font-medium text-gray-700">No brands found</h3>
<p className="mt-2 text-gray-500">Try adjusting your search query or filter settings</p>
</div>
</div>
)}
</div>
{/* Toast Notification */}
{toast && (
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl z-30 animate-fade-in-up">
<div className="flex items-center gap-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<span className="font-medium">{toast}</span>
</div>
</div>
)}
{/* Custom Animations */}
<style jsx global>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
.checkmark {
transition: all 0.2s ease;
}
`}</style>
</div>
);
}