Files
verify/frontend/components/BrandsPanel.tsx
Mohamed Mathar Irfan ed6610d5d8 Initial project upload
2026-07-28 17:57:02 +05:30

585 lines
14 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import {
Plus,
Pencil,
Trash2,
X,
Package,
Eye,
} from "lucide-react";
import { api } from "@/lib/api";
export default function BrandsPanel() {
const empty = {
name: "",
logo_url: "",
description: "",
};
const [brands, setBrands] = useState<any[]>([]);
const [show, setShow] = useState(false);
const [editing, setEditing] = useState<any>(null);
const [form, setForm] = useState(empty);
const [logoFile, setLogoFile] = useState<File | null>(null);
const [viewOpen, setViewOpen] = useState(false);
const [brandProducts, setBrandProducts] = useState<any[]>([]);
const [brandName, setBrandName] = useState("");
async function loadBrands() {
try {
const data = await api("/brands");
setBrands(data);
} catch (err) {
console.error(err);
}
}
useEffect(() => {
loadBrands();
}, []);
function openAdd() {
setEditing(null);
setForm(empty);
setShow(true);
}
async function viewProducts(brand: any) {
try {
const data = await api(`/brands/${brand.id}`);
setBrandName(data.name);
setBrandProducts(data.products || []);
setViewOpen(true);
} catch (err) {
console.error(err);
}
}
function openEdit(brand: any) {
setEditing(brand);
setForm({
name: brand.name ?? "",
logo_url: brand.logo_url ?? "",
description: brand.description ?? "",
});
setShow(true);
}
async function uploadLogo() {
if (!logoFile) return form.logo_url;
const data = new FormData();
data.append("file", logoFile);
const res = await fetch("http://localhost:8000/brands/upload-logo", {
method: "POST",
body: data,
});
const json = await res.json();
return json.url;
}
async function save() {
const logo_url = await uploadLogo();
const body = {
...form,
logo_url,
};
if (editing) {
await api(`/brands/${editing.id}`, {
method: "PUT",
body: JSON.stringify(body),
});
} else {
await api("/brands", {
method: "POST",
body: JSON.stringify(body),
});
}
setShow(false);
loadBrands();
}
async function remove(id: string) {
if (!confirm("Delete this brand?")) return;
await api(`/brands/${id}`, {
method: "DELETE",
});
loadBrands();
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<Package className="w-6 h-6 text-brand-600" />
Brands
</h2>
<p className="text-sm text-slate-500 mt-1">
Manage all organization brands.
</p>
</div>
<button
onClick={openAdd}
className="flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2 text-white font-medium hover:bg-brand-700 transition"
>
<Plus size={18} />
Add Brand
</button>
</div>
{/* Table */}
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<table className="min-w-full">
<thead className="bg-slate-50 border-b">
<tr>
<th className="px-6 py-4 text-left text-sm font-semibold text-slate-600">
Brand
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-slate-600">
Description
</th>
<th className="px-6 py-4 text-center text-sm font-semibold text-slate-600">
Products
</th>
<th className="px-6 py-4 text-center text-sm font-semibold text-slate-600">
Actions
</th>
</tr>
</thead>
<tbody>
{brands.length === 0 && (
<tr>
<td
colSpan={4}
className="py-16 text-center text-slate-400"
>
No brands found.
</td>
</tr>
)}
{brands.map((brand) => (
<tr
key={brand.id}
className="border-b last:border-b-0 hover:bg-slate-50 transition"
>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-brand-100 flex items-center justify-center">
{brand.logo_url ? (
<img
src={brand.logo_url}
alt={brand.name}
className="h-10 w-10 rounded-lg object-cover"
/>
) : (
<Package className="h-5 w-5 text-brand-600" />
)}
</div>
<div>
<div className="font-semibold text-slate-800">
{brand.name}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 text-sm text-slate-600 max-w-md">
{brand.description || "-"}
</td>
<td className="px-6 py-4 text-center">
<span className="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold">
{brand.product_count}
</span>
</td>
<td className="px-6 py-4">
<div className="flex justify-center gap-2">
<button
onClick={() => viewProducts(brand)}
className="rounded-lg border border-slate-200 p-2 hover:bg-green-50 hover:text-green-600 transition"
>
<Eye size={18} />
</button>
<button
onClick={() => openEdit(brand)}
className="rounded-lg border border-slate-200 p-2 hover:bg-blue-50 hover:text-blue-600 transition"
>
<Pencil size={18} />
</button>
<button
onClick={() => remove(brand.id)}
className="rounded-lg border border-slate-200 p-2 hover:bg-red-50 hover:text-red-600 transition"
>
<Trash2 size={18} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* view popup */}
{viewOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="w-full max-w-6xl rounded-2xl bg-white shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between border-b px-6 py-4">
<div>
<h2 className="text-xl font-bold text-slate-800">
{brandName} Products
</h2>
<p className="text-sm text-slate-500">
Total Products : {brandProducts.length}
</p>
</div>
<button
onClick={() => setViewOpen(false)}
className="rounded-lg p-2 hover:bg-slate-100"
>
<X size={20} />
</button>
</div>
{/* Body */}
<div className="max-h-[70vh] overflow-auto">
{brandProducts.length === 0 ? (
<div className="py-20 text-center text-slate-500">
<Package className="mx-auto mb-3 h-10 w-10 text-slate-300" />
No Products Found
</div>
) : (
<table className="min-w-full">
<thead className="sticky top-0 bg-slate-50">
<tr>
<th className="px-5 py-4 text-left text-sm font-semibold">
Image
</th>
<th className="px-5 py-4 text-left text-sm font-semibold">
Product
</th>
<th className="px-5 py-4 text-left text-sm font-semibold">
SKU
</th>
<th className="px-5 py-4 text-center text-sm font-semibold">
QRs
</th>
<th className="px-5 py-4 text-center text-sm font-semibold">
Status
</th>
</tr>
</thead>
<tbody>
{brandProducts.map((p: any) => (
<tr
key={p.id}
className="border-b hover:bg-slate-50"
>
{/* Product Image */}
<td className="px-5 py-4">
<div className="h-14 w-14 overflow-hidden rounded-lg border bg-slate-100">
{p.image_url ? (
<img
src={p.image_url}
alt={p.name}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full items-center justify-center">
<Package className="h-6 w-6 text-slate-400" />
</div>
)}
</div>
</td>
{/* Product */}
<td className="px-5 py-4">
<div className="font-semibold text-slate-800">
{p.name}
</div>
</td>
{/* Brand */}
{/* SKU */}
<td className="px-5 py-4 text-slate-600">
{p.sku || "-"}
</td>
{/* QR Count */}
<td className="px-5 py-4 text-center">
<span className="rounded-full bg-purple-100 px-3 py-1 text-xs font-bold text-purple-700">
{p.qr_count}
</span>
</td>
{/* Status */}
<td className="px-5 py-4 text-center">
<span
className={`rounded-full px-3 py-1 text-xs font-semibold ${
p.status === "active"
? "bg-green-100 text-green-700"
: p.status === "draft"
? "bg-yellow-100 text-yellow-700"
: "bg-red-100 text-red-700"
}`}
>
{p.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Footer */}
<div className="flex justify-end border-t px-6 py-4">
<button
onClick={() => setViewOpen(false)}
className="rounded-lg bg-brand-600 px-5 py-2 font-medium text-white hover:bg-brand-700"
>
Close
</button>
</div>
</div>
</div>
)}
{/* Modal */}
{show && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="w-full max-w-lg rounded-xl bg-white shadow-xl">
<div className="flex items-center justify-between border-b px-6 py-4">
<h3 className="text-lg font-bold">
{editing ? "Edit Brand" : "Add Brand"}
</h3>
<button
onClick={() => setShow(false)}
className="rounded-lg p-2 hover:bg-slate-100"
>
<X size={18} />
</button>
</div>
<div className="space-y-4 p-6">
<div>
<label className="mb-1 block text-sm font-medium">
Brand Name
</label>
<input
value={form.name}
onChange={(e) =>
setForm({ ...form, name: e.target.value })
}
className="w-full rounded-lg border border-slate-300 px-3 py-2 focus:border-brand-500 focus:outline-none"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">
Brand Logo
</label>
<input
type="file"
accept="image/*"
onChange={(e) => {
if (e.target.files?.length) {
setLogoFile(e.target.files[0]);
}
}}
className="w-full rounded-lg border border-slate-300 px-3 py-2"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">
Description
</label>
<textarea
rows={4}
value={form.description}
onChange={(e) =>
setForm({
...form,
description: e.target.value,
})
}
className="w-full rounded-lg border border-slate-300 px-3 py-2 focus:border-brand-500 focus:outline-none"
/>
</div>
</div>
<div className="flex justify-end gap-3 border-t px-6 py-4">
<button
onClick={() => setShow(false)}
className="rounded-lg border border-slate-300 px-5 py-2 hover:bg-slate-100"
>
Cancel
</button>
<button
onClick={save}
className="rounded-lg bg-brand-600 px-5 py-2 font-medium text-white hover:bg-brand-700"
>
{editing ? "Update Brand" : "Create Brand"}
</button>
</div>
</div>
</div>
)}
</div>
);
}