721 lines
27 KiB
TypeScript
721 lines
27 KiB
TypeScript
'use client';
|
||
|
||
import axios from 'axios';
|
||
import { useRouter } from 'next/navigation';
|
||
import React, { useEffect, useMemo, useState } from 'react';
|
||
|
||
/** ================== Config ================== */
|
||
const BACKEND_BASE =
|
||
process.env.NEXT_PUBLIC_BACKEND_BASEURL || 'https://ebay.backend.data4autos.com';
|
||
|
||
// Your GET route that lists from tbl_user_products_mapping
|
||
// Example: GET {BACKEND_BASE}/api/user-products-mapping?userid=...&page=1&pageSize=50&search=...
|
||
const ENDPOINT_PRODUCTS = `${BACKEND_BASE}/api/motorstate/user-products`;
|
||
|
||
// Optional: where you queue selected products (same as your other page)
|
||
const ENDPOINT_QUEUE = `${BACKEND_BASE}/api/ebay/withdraw-offer`;
|
||
|
||
/** ================== Types ================== */
|
||
type ProductRow = {
|
||
trand_id: number;
|
||
user_id: string;
|
||
id: string | null; // Turn14 item id
|
||
sku: string | null;
|
||
imgSrc: string | null;
|
||
name: string | null;
|
||
partNumber: string | null;
|
||
category: string | null;
|
||
subcategory: string | null;
|
||
price: number | string | null; // handle either
|
||
inventory: number | string | null;
|
||
description: string | null;
|
||
offer_status: string | null;
|
||
offer_offerId: string | null;
|
||
offer_listingId: string | null;
|
||
offer_categoryId: string | null;
|
||
offer_url: string | null;
|
||
offer_error: string | null;
|
||
created_at: string;
|
||
updated_at: string;
|
||
};
|
||
|
||
type ApiListResp = {
|
||
code: 'PRODUCT_MAPPING_LIST';
|
||
page: number;
|
||
pageSize: number;
|
||
total: number;
|
||
items: ProductRow[];
|
||
};
|
||
|
||
type Filters = {
|
||
inStockOnly: boolean;
|
||
priceMin: number;
|
||
category: string;
|
||
subcategory: string;
|
||
offer_status: string;
|
||
};
|
||
|
||
/** ================== Helpers ================== */
|
||
const toNumber = (val: unknown): number => {
|
||
if (typeof val === 'number') return Number.isFinite(val) ? val : 0;
|
||
if (typeof val === 'string') {
|
||
const cleaned = val.replace(/[^0-9.\-]/g, '');
|
||
const n = parseFloat(cleaned);
|
||
return Number.isFinite(n) ? n : 0;
|
||
}
|
||
return 0;
|
||
};
|
||
|
||
const quantityToList = (qty: number) => {
|
||
if (qty <= 1) return 1;
|
||
if (qty >= 2 && qty <= 8) return qty;
|
||
return 8;
|
||
};
|
||
|
||
/** ================== Component ================== */
|
||
const ManageAddedProducts: React.FC = () => {
|
||
// Session values (only available client-side)
|
||
const userId =
|
||
typeof window !== 'undefined' ? sessionStorage.getItem('USERID') : null;
|
||
const EBAYSTOREID =
|
||
typeof window !== 'undefined' ? sessionStorage.getItem('EBAYSTOREID') : null;
|
||
|
||
const router = useRouter()
|
||
|
||
// Data state
|
||
const [items, setItems] = useState<ProductRow[]>([]);
|
||
const [page, setPage] = useState<number>(1);
|
||
const [pageSize, setPageSize] = useState<number>(48);
|
||
const [total, setTotal] = useState<number>(0);
|
||
|
||
// UI state
|
||
const [isScrolled, setIsScrolled] = useState(false);
|
||
const [loading, setLoading] = useState(false);
|
||
const [toast, setToast] = useState('');
|
||
const [toastActive, setToastActive] = useState(false);
|
||
|
||
// Filters / query
|
||
const [searchText, setSearchText] = useState('');
|
||
const [filters, setFilters] = useState<Filters>({
|
||
inStockOnly: false,
|
||
priceMin: 0,
|
||
category: '',
|
||
subcategory: '',
|
||
offer_status: '',
|
||
});
|
||
|
||
// Selection
|
||
const [selected, setSelected] = useState<Record<number, ProductRow>>({});
|
||
const [payment, setPayment] = useState<any>(null);
|
||
|
||
|
||
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]);
|
||
|
||
|
||
/** Scroll shadow effect */
|
||
useEffect(() => {
|
||
const onScroll = () => setIsScrolled(window.scrollY > 10);
|
||
window.addEventListener('scroll', onScroll);
|
||
return () => window.removeEventListener('scroll', onScroll);
|
||
}, []);
|
||
|
||
/** Data loader */
|
||
useEffect(() => {
|
||
if (!userId) return;
|
||
|
||
const load = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const params = new URLSearchParams({
|
||
userid: userId,
|
||
page: String(page),
|
||
pageSize: String(pageSize),
|
||
});
|
||
if (searchText) params.set('search', searchText);
|
||
if (filters.category) params.set('category', filters.category);
|
||
if (filters.subcategory) params.set('subcategory', filters.subcategory);
|
||
if (filters.offer_status) params.set('offer_status', filters.offer_status);
|
||
|
||
const url = `${ENDPOINT_PRODUCTS}?${params.toString()}`;
|
||
const res = await fetch(url, { method: 'GET' });
|
||
const data: ApiListResp = await res.json();
|
||
|
||
setItems(data.items || []);
|
||
setTotal(data.total || 0);
|
||
setToast(`Loaded ${data.items?.length || 0} items (Total: ${data.total || 0})`);
|
||
setTimeout(() => setToast(''), 3000);
|
||
} catch (e) {
|
||
console.error(e);
|
||
setToast('Failed to load products');
|
||
setTimeout(() => setToast(''), 3000);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
load();
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [userId, page, pageSize, searchText, filters.category, filters.subcategory, filters.offer_status]);
|
||
|
||
/** Dropdown options (use Array.from to avoid TS downlevel iteration issue) */
|
||
const categoryOptions = useMemo(() => {
|
||
const s = new Set<string>();
|
||
items.forEach((i) => i.category && s.add(i.category));
|
||
return [''].concat(Array.from(s).sort());
|
||
}, [items]);
|
||
|
||
const subcategoryOptions = useMemo(() => {
|
||
const s = new Set<string>();
|
||
items.forEach((i) => {
|
||
if (!filters.category || i.category === filters.category) {
|
||
i.subcategory && s.add(i.subcategory);
|
||
}
|
||
});
|
||
return [''].concat(Array.from(s).sort());
|
||
}, [items, filters.category]);
|
||
|
||
const offerStatusOptions = useMemo(() => {
|
||
const s = new Set<string>();
|
||
items.forEach((i) => i.offer_status && s.add(i.offer_status));
|
||
return [''].concat(Array.from(s).sort());
|
||
}, [items]);
|
||
|
||
/** Client-side filters (in-stock & price floor + search) */
|
||
const filteredItems = useMemo(() => {
|
||
return items.filter((it) => {
|
||
const inv = toNumber(it.inventory);
|
||
const price = toNumber(it.price);
|
||
if (filters.inStockOnly && inv <= 0) return false;
|
||
if (price < filters.priceMin) return false;
|
||
if (searchText) {
|
||
const hay = `${it.id ?? ''} ${it.sku ?? ''} ${it.name ?? ''} ${it.partNumber ?? ''} ${it.category ?? ''} ${it.subcategory ?? ''}`.toLowerCase();
|
||
if (!hay.includes(searchText.toLowerCase())) return false;
|
||
}
|
||
return true;
|
||
});
|
||
}, [items, filters.inStockOnly, filters.priceMin, searchText]);
|
||
|
||
/** Selection helpers */
|
||
const selectedCount = useMemo(() => Object.keys(selected).length, [selected]);
|
||
const isSelected = (row: ProductRow) => Boolean(selected[row.trand_id]);
|
||
|
||
const toggleSelect = (row: ProductRow) => {
|
||
setSelected((prev) => {
|
||
const copy = { ...prev };
|
||
if (copy[row.trand_id]) delete copy[row.trand_id];
|
||
else copy[row.trand_id] = row;
|
||
return copy;
|
||
});
|
||
};
|
||
|
||
const toggleSelectAllVisible = () => {
|
||
setSelected((prev) => {
|
||
const copy = { ...prev };
|
||
const allVisibleSelected =
|
||
filteredItems.length > 0 &&
|
||
filteredItems.every((it) => copy[it.trand_id]);
|
||
if (allVisibleSelected) {
|
||
filteredItems.forEach((it) => {
|
||
if (copy[it.trand_id]) delete copy[it.trand_id];
|
||
});
|
||
} else {
|
||
filteredItems.forEach((it) => (copy[it.trand_id] = it));
|
||
}
|
||
return copy;
|
||
});
|
||
};
|
||
|
||
const clearVisibleSelection = () => {
|
||
setSelected((prev) => {
|
||
const copy = { ...prev };
|
||
filteredItems.forEach((it) => {
|
||
if (copy[it.trand_id]) delete copy[it.trand_id];
|
||
});
|
||
return copy;
|
||
});
|
||
};
|
||
|
||
/** Queue selected products (mirrors your other page’s payload shape) */
|
||
const handleAddSelectedProducts = async () => {
|
||
const picked = Object.values(selected);
|
||
|
||
const queuePayload = picked.map(x => x.offer_offerId)
|
||
console.log('Adding products:', queuePayload);
|
||
|
||
|
||
setToast(`Queued ${queuePayload.length} product(s)`);
|
||
try {
|
||
const res = await fetch(ENDPOINT_QUEUE, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
userKey: EBAYSTOREID,
|
||
products: queuePayload,
|
||
userId,
|
||
}),
|
||
});
|
||
const data = await res.json();
|
||
console.log('Queue response:', data);
|
||
setToastActive(true);
|
||
setTimeout(() => setToastActive(false), 2500);
|
||
} catch (e) {
|
||
console.error(e);
|
||
setToast('Failed to queue products');
|
||
}
|
||
};
|
||
|
||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||
|
||
/** ================== Render ================== */
|
||
return (
|
||
<div className="bg-gradient-to-br from-[#00d1ff]/10 via-white to-[#00d1ff]/20 min-h-screen">
|
||
{/* Sticky 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-[#00d1ff] bg-clip-text text-[#00d1ff]">
|
||
Data4Autos – Your Listed Products
|
||
</h1>
|
||
<p className="text-sm text-gray-500 mt-1">
|
||
Showing {filteredItems.length} of {items.length} (Total in DB: {total})
|
||
</p>
|
||
<p className="text-sm font-medium text-[#00d1ff] mt-1">
|
||
Selected: <span className="font-semibold">{selectedCount}</span>
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center w-full md:w-auto">
|
||
{/* Search */}
|
||
<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={searchText}
|
||
onChange={(e) => {
|
||
setPage(1);
|
||
setSearchText(e.target.value);
|
||
}}
|
||
placeholder="Search (sku, name, id)…"
|
||
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>
|
||
|
||
{/* In-stock-only pill toggle */}
|
||
<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={filters.inStockOnly}
|
||
onChange={() =>
|
||
setFilters((f) => ({ ...f, inStockOnly: !f.inStockOnly }))
|
||
}
|
||
className="sr-only"
|
||
/>
|
||
<div
|
||
className={`block w-10 h-6 rounded-full transition-colors ${filters.inStockOnly ? 'bg-[#00d1ff]' : 'bg-gray-300'
|
||
}`}
|
||
></div>
|
||
<div
|
||
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${filters.inStockOnly ? 'transform translate-x-4' : ''
|
||
}`}
|
||
></div>
|
||
</div>
|
||
In Stock Only
|
||
</label>
|
||
|
||
<button
|
||
onClick={handleAddSelectedProducts}
|
||
disabled={selectedCount === 0}
|
||
className="px-5 py-2.5 bg-[#00d1ff] to-purple-600 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>
|
||
Unlist {selectedCount} Product{selectedCount === 1 ? '' : 's'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Global Filters */}
|
||
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||
{/* Category */}
|
||
<label className="text-sm text-gray-700 flex items-center">
|
||
<span className="mr-2 whitespace-nowrap">Category:</span>
|
||
<select
|
||
value={filters.category}
|
||
onChange={(e) => {
|
||
setPage(1);
|
||
setFilters((f) => ({
|
||
...f,
|
||
category: e.target.value,
|
||
subcategory: '', // reset when category changes
|
||
}));
|
||
}}
|
||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
|
||
>
|
||
{categoryOptions.map((c) => (
|
||
<option key={c} value={c}>
|
||
{c || 'All'}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
|
||
{/* Subcategory */}
|
||
<label className="text-sm text-gray-700 flex items-center">
|
||
<span className="mr-2 whitespace-nowrap">Subcategory:</span>
|
||
<select
|
||
value={filters.subcategory}
|
||
onChange={(e) => {
|
||
setPage(1);
|
||
setFilters((f) => ({ ...f, subcategory: e.target.value }));
|
||
}}
|
||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
|
||
>
|
||
{subcategoryOptions.map((s) => (
|
||
<option key={s} value={s}>
|
||
{s || 'All'}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
|
||
{/* Offer Status */}
|
||
<label className="text-sm text-gray-700 flex items-center">
|
||
<span className="mr-2 whitespace-nowrap">Offer Status:</span>
|
||
<select
|
||
value={filters.offer_status}
|
||
onChange={(e) => {
|
||
setPage(1);
|
||
setFilters((f) => ({ ...f, offer_status: e.target.value }));
|
||
}}
|
||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full"
|
||
>
|
||
{offerStatusOptions.map((s) => (
|
||
<option key={s} value={s}>
|
||
{s || 'All'}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
|
||
{/* Price Floor */}
|
||
<div className="text-sm text-gray-700">
|
||
<div className="mb-1 font-medium">Price Floor</div>
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
type="range"
|
||
min={0}
|
||
max={5000}
|
||
step={5}
|
||
value={filters.priceMin}
|
||
onChange={(e) =>
|
||
setFilters((f) => ({ ...f, priceMin: toNumber(e.target.value) }))
|
||
}
|
||
className="w-full"
|
||
aria-label="Minimum price"
|
||
/>
|
||
<input
|
||
type="number"
|
||
min={0}
|
||
value={filters.priceMin}
|
||
onChange={(e) =>
|
||
setFilters((f) => ({ ...f, priceMin: toNumber(e.target.value) }))
|
||
}
|
||
className="w-24 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
<div className="text-xs text-gray-500 mt-1">
|
||
Showing items with price ≥ ${filters.priceMin}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Pagination Controls */}
|
||
<div className="mt-3 flex items-center gap-3 text-sm text-gray-700">
|
||
<div>
|
||
Page <span className="font-semibold">{page}</span> of{' '}
|
||
<span className="font-semibold">{Math.max(1, Math.ceil(total / pageSize))}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
className="px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
|
||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||
disabled={page <= 1 || loading}
|
||
>
|
||
Prev
|
||
</button>
|
||
<button
|
||
className="px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
|
||
onClick={() => setPage((p) => p + 1)}
|
||
disabled={page >= Math.max(1, Math.ceil(total / pageSize)) || loading}
|
||
>
|
||
Next
|
||
</button>
|
||
<label className="ml-3">
|
||
<span className="mr-2">Page size</span>
|
||
<select
|
||
className="border rounded px-2 py-1"
|
||
value={pageSize}
|
||
onChange={(e) => {
|
||
setPage(1);
|
||
setPageSize(Math.max(1, Math.min(200, Number(e.target.value) || 48)));
|
||
}}
|
||
>
|
||
{[24, 36, 48, 60, 96, 120, 200].map((n) => (
|
||
<option key={n} value={n}>
|
||
{n}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Grid */}
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 pb-16">
|
||
{loading ? (
|
||
<div className="py-20 text-center text-gray-500">Loading products…</div>
|
||
) : filteredItems.length === 0 ? (
|
||
<div className="flex items-center justify-center py-24 bg-gradient-to-b from-gray-50 to-gray-100">
|
||
<div className="relative text-center bg-white/80 backdrop-blur-md border border-white/40 shadow-2xl rounded-3xl p-10 transition-all duration-500 hover:scale-[1.02]">
|
||
{/* Icon Circle */}
|
||
<div className="w-20 h-20 mx-auto flex items-center justify-center rounded-full bg-[#00d1ff] shadow-lg">
|
||
<svg
|
||
className="w-10 h-10 text-white"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth="2"
|
||
d="M9 12h6m2 0a8 8 0 11-16 0 8 8 0 0116 0z"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
|
||
{/* Text */}
|
||
<h3 className="mt-6 text-2xl font-semibold text-gray-800 tracking-tight">
|
||
No Products Found
|
||
</h3>
|
||
<p className="mt-3 text-gray-500 max-w-sm mx-auto">
|
||
No products match the current filters. Try adjusting your search or filters to find what you need.
|
||
</p>
|
||
|
||
{/* Button */}
|
||
{/* <button className="mt-6 px-6 py-3 bg-[#00d1ff] text-white rounded-xl shadow-md hover:shadow-lg transition-transform duration-300 hover:scale-105">
|
||
Reset Filters
|
||
</button> */}
|
||
|
||
{/* Decorative Blur Circle */}
|
||
<div className="absolute -top-10 -right-10 w-32 h-32 bg-[#00d1ff] rounded-full blur-3xl"></div>
|
||
<div className="absolute -bottom-10 -left-10 w-32 h-32 bg-[#00d1ff] rounded-full blur-3xl"></div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="mb-3 flex items-center justify-between">
|
||
<div className="text-xs text-gray-600">
|
||
Showing <span className="font-semibold">{filteredItems.length}</span> of{' '}
|
||
<span className="font-semibold">{items.length}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={toggleSelectAllVisible}
|
||
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
|
||
>
|
||
{filteredItems.length > 0 &&
|
||
filteredItems.every((it) => selected[it.trand_id])
|
||
? 'Unselect All Visible'
|
||
: 'Select All Visible'}
|
||
</button>
|
||
{Object.values(selected).some((r) =>
|
||
filteredItems.find((fi) => fi.trand_id === r.trand_id)
|
||
) && (
|
||
<button
|
||
onClick={clearVisibleSelection}
|
||
className="text-xs px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50"
|
||
>
|
||
Clear Visible
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||
{filteredItems.map((it) => {
|
||
const img =
|
||
it.imgSrc ||
|
||
'https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png';
|
||
const inv = toNumber(it.inventory);
|
||
const price = toNumber(it.price);
|
||
const willList = quantityToList(inv);
|
||
|
||
return (
|
||
<div key={it.trand_id} className="border border-gray-200 rounded-lg p-3 hover:shadow-sm bg-white">
|
||
{/* Select */}
|
||
<div className="flex items-center justify-between mb-2">
|
||
<label className="flex items-center gap-2">
|
||
<input
|
||
type="checkbox"
|
||
checked={Boolean(selected[it.trand_id])}
|
||
onChange={() => toggleSelect(it)}
|
||
className="h-4 w-4 rounded border-gray-300 text-[#00d1ff] focus:ring-blue-500"
|
||
/>
|
||
<span className="text-xs text-gray-600">Select</span>
|
||
</label>
|
||
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
|
||
Will list: {willList}
|
||
</span>
|
||
</div>
|
||
|
||
<img
|
||
src={img}
|
||
alt={it.name || 'Product'}
|
||
className="w-full h-24 object-contain mb-2 bg-gray-50 rounded"
|
||
/>
|
||
|
||
<div className="text-sm font-medium line-clamp-2">{it.name || 'Unnamed Product'}</div>
|
||
|
||
<div className="text-xs text-gray-600 mt-1">
|
||
Part #: <span className="font-medium">{it.partNumber || '-'}</span>
|
||
</div>
|
||
|
||
<div className="text-xs text-gray-600">
|
||
{it.category || '-'} > {it.subcategory || '-'}
|
||
</div>
|
||
|
||
<div className="text-xs">
|
||
Price:{' '}
|
||
<span className="font-semibold">
|
||
{Number.isFinite(price) && price > 0 ? `$${price.toFixed(2)}` : '-'}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="text-xs text-gray-700">
|
||
Inventory: <span className="font-semibold">{inv}</span>
|
||
</div>
|
||
|
||
{it.description && (
|
||
<p className="mt-2 text-xs text-gray-700 line-clamp-3">{it.description}</p>
|
||
)}
|
||
|
||
{/* Offer / URL badges */}
|
||
<div className="mt-2 flex flex-wrap gap-1">
|
||
{it.offer_status && (
|
||
<span className="text-[10px] px-2 py-0.5 rounded bg-blue-50 text-blue-700 border border-blue-200">
|
||
{it.offer_status}
|
||
</span>
|
||
)}
|
||
{it.offer_url && (
|
||
<a
|
||
href={it.offer_url}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="text-[10px] px-2 py-0.5 rounded bg-emerald-50 text-emerald-700 border border-emerald-200"
|
||
>
|
||
Listing
|
||
</a>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* Toasts */}
|
||
{toast && (
|
||
<div className="fixed bottom-6 right-6 bg-gradient-to-r from-slate-800 to-black 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>
|
||
)}
|
||
{toastActive && (
|
||
<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">Products Unlisted successfully!</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Tiny animation helper */}
|
||
<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;
|
||
}
|
||
`}</style>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ManageAddedProducts;
|