436 lines
16 KiB
TypeScript
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>
|
|
);
|
|
} |