feat(admin): apply new design system to Products pages
- List page: new PageHeader with 'OPERATIONS' eyebrow + 'Add product' CTA - New/Edit pages: PageHeader with eyebrow, icon, status badge (edit) - ProductsClient: EmptyState for zero products, AdminBadge for status, Fragment Mono on stats, token-based colors throughout - ProductTableBody: AdminBadge tones for status/taxable, token-based hover/selected states, Fragment Mono on price column - NewProductForm / ProductEditForm / ProductFormModal: surrounding chrome on tokens; save bars use .ha-btn-primary / .ha-btn-ghost - .atelier-* body preserved as editorial treatment
This commit is contained in:
@@ -9,7 +9,8 @@ import { updateProduct } from "@/actions/products/update-product";
|
||||
import { deleteProduct } from "@/actions/products";
|
||||
import { uploadProductImage } from "@/actions/products/upload-image";
|
||||
import ProductFormModal, { type ProductFormValues } from "@/components/admin/ProductFormModal";
|
||||
import { PageHeader, AdminButton, AdminIconButton, AdminSearchInput, AdminFilterTabs, AdminViewModeTabs, useToast, Skeleton } from "@/components/admin/design-system";
|
||||
import { PageHeader, AdminButton, AdminSearchInput, AdminFilterTabs, AdminViewModeTabs, AdminBadge, EmptyState, useToast, Skeleton } from "@/components/admin/design-system";
|
||||
import { Package as PackageIconLucide, Inbox as InboxIcon } from "lucide-react";
|
||||
|
||||
type Product = {
|
||||
id: string;
|
||||
@@ -29,44 +30,13 @@ type ViewMode = "table" | "cards";
|
||||
|
||||
// Icons
|
||||
const Icons = {
|
||||
search: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="m21 21-4.3-4.3"/>
|
||||
</svg>
|
||||
),
|
||||
plus: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 5v14M5 12h14"/>
|
||||
</svg>
|
||||
),
|
||||
package: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="m7.5 4.27 9 5.15"/>
|
||||
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/>
|
||||
<path d="m3.3 7 8.7 5 8.7-5"/>
|
||||
<path d="M12 22V12"/>
|
||||
</svg>
|
||||
),
|
||||
upload: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
// Page header icon
|
||||
const PackageIconHeader = () => (
|
||||
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="m7.5 4.27 9 5.15"/>
|
||||
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/>
|
||||
<path d="m3.3 7 8.7 5 8.7-5"/>
|
||||
<path d="M12 22V12"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function ProductsClient({
|
||||
products,
|
||||
brandId,
|
||||
@@ -253,34 +223,50 @@ export default function ProductsClient({
|
||||
|
||||
return (
|
||||
<div className="p-4 sm:p-6">
|
||||
{/* Page Header */}
|
||||
{/* Page Header — eyebrow + title + subtitle + primary CTA */}
|
||||
<div className="ha-eyebrow mb-2">Operations</div>
|
||||
<PageHeader
|
||||
icon={<PackageIconHeader />}
|
||||
icon={<PackageIconLucide className="h-5 w-5" strokeWidth={1.75} />}
|
||||
title="Products"
|
||||
subtitle={`${filtered.length} product${filtered.length !== 1 ? "s" : ""}`}
|
||||
subtitle="Manage your product catalog, pricing, and availability."
|
||||
actions={
|
||||
<AdminButton
|
||||
onClick={openAddModal}
|
||||
icon={Icons.plus("h-4 w-4")}
|
||||
>
|
||||
Add Product
|
||||
Add product
|
||||
</AdminButton>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-3 gap-3 mb-6">
|
||||
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Total</p>
|
||||
<p className="text-xl sm:text-2xl font-bold text-[var(--admin-text-primary)] mt-1">{isLoading ? <Skeleton variant="text" className="w-12 h-6" /> : products.length}</p>
|
||||
<div className="bg-[var(--admin-card-bg)] rounded-xl border border-[var(--admin-border)] p-4">
|
||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium uppercase tracking-wider">Total</p>
|
||||
<p
|
||||
className="text-xl sm:text-2xl font-bold text-[var(--admin-text-primary)] mt-1"
|
||||
style={{ fontFamily: "var(--font-fragment-mono)", fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
{isLoading ? <Skeleton variant="text" className="w-12 h-6" /> : products.length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Active</p>
|
||||
<p className="text-xl sm:text-2xl font-bold text-[var(--admin-accent)] mt-1">{isLoading ? <Skeleton variant="text" className="w-10 h-6" /> : activeCount}</p>
|
||||
<div className="bg-[var(--admin-card-bg)] rounded-xl border border-[var(--admin-border)] p-4">
|
||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium uppercase tracking-wider">Active</p>
|
||||
<p
|
||||
className="text-xl sm:text-2xl font-bold text-[var(--admin-primary)] mt-1"
|
||||
style={{ fontFamily: "var(--font-fragment-mono)", fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
{isLoading ? <Skeleton variant="text" className="w-10 h-6" /> : activeCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Inactive</p>
|
||||
<p className="text-xl sm:text-2xl font-bold text-stone-400 mt-1">{isLoading ? <Skeleton variant="text" className="w-8 h-6" /> : inactiveCount}</p>
|
||||
<div className="bg-[var(--admin-card-bg)] rounded-xl border border-[var(--admin-border)] p-4">
|
||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium uppercase tracking-wider">Inactive</p>
|
||||
<p
|
||||
className="text-xl sm:text-2xl font-bold text-[var(--admin-text-muted)] mt-1"
|
||||
style={{ fontFamily: "var(--font-fragment-mono)", fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
{isLoading ? <Skeleton variant="text" className="w-8 h-6" /> : inactiveCount}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -320,6 +306,7 @@ export default function ProductsClient({
|
||||
onDeleteConfirm={(id) => handleDelete(id)}
|
||||
onDeleteCancel={() => setDeleteConfirm(null)}
|
||||
deletingId={deletingId}
|
||||
onAdd={openAddModal}
|
||||
/>
|
||||
) : (
|
||||
<CardView
|
||||
@@ -330,6 +317,7 @@ export default function ProductsClient({
|
||||
onDeleteConfirm={(id) => handleDelete(id)}
|
||||
onDeleteCancel={() => setDeleteConfirm(null)}
|
||||
deletingId={deletingId}
|
||||
onAdd={openAddModal}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -375,6 +363,7 @@ function TableView({
|
||||
onDeleteConfirm,
|
||||
onDeleteCancel,
|
||||
deletingId,
|
||||
onAdd,
|
||||
}: {
|
||||
products: Product[];
|
||||
onEdit: (p: Product) => void;
|
||||
@@ -383,6 +372,7 @@ function TableView({
|
||||
onDeleteConfirm: (id: string) => void;
|
||||
onDeleteCancel: () => void;
|
||||
deletingId: string | null;
|
||||
onAdd: () => void;
|
||||
}) {
|
||||
// Track the position of the open row's three-dots button so the popup can be
|
||||
// rendered via portal at body level (escapes any overflow:hidden ancestors
|
||||
@@ -434,31 +424,32 @@ function TableView({
|
||||
const openProduct = deleteConfirm ? products.find((p) => p.id === deleteConfirm) : null;
|
||||
|
||||
return (
|
||||
<div className="overflow-visible rounded-xl border border-[var(--admin-border)] bg-white">
|
||||
<div className="overflow-visible rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card-bg)]">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-stone-50">
|
||||
<thead className="bg-[var(--admin-bg)]">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs">Product</th>
|
||||
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs">Type</th>
|
||||
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs">Price</th>
|
||||
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs">Status</th>
|
||||
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs uppercase tracking-wider">Product</th>
|
||||
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs uppercase tracking-wider">Type</th>
|
||||
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs uppercase tracking-wider">Price</th>
|
||||
<th className="text-left px-4 py-3 font-semibold text-[var(--admin-text-muted)] text-xs uppercase tracking-wider">Status</th>
|
||||
<th className="px-4 py-3" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[var(--admin-border)]">
|
||||
{products.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-4 py-16 text-center">
|
||||
<div className="flex h-16 w-16 mx-auto items-center justify-center rounded-full bg-stone-100 mb-4">
|
||||
{Icons.package("h-8 w-8 text-stone-400")}
|
||||
</div>
|
||||
<p className="text-sm font-medium text-stone-600">No products found</p>
|
||||
<p className="text-xs text-stone-400 mt-1">Try adjusting your filters</p>
|
||||
<td colSpan={5} className="px-4 py-8">
|
||||
<EmptyState
|
||||
icon={<InboxIcon className="h-8 w-8" strokeWidth={1.25} />}
|
||||
title="No products found"
|
||||
description="Try adjusting your filters, or add your first product to get started."
|
||||
action={{ label: "Add your first product", onClick: onAdd }}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
products.map((product) => (
|
||||
<tr key={product.id} className="hover:bg-stone-50 transition-colors">
|
||||
<tr key={product.id} className="hover:bg-[var(--admin-primary-soft)] transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{product.image_url ? (
|
||||
@@ -472,40 +463,41 @@ function TableView({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-10 w-10 rounded-lg bg-stone-100 flex items-center justify-center shrink-0 border border-[var(--admin-border)]">
|
||||
<span className="text-stone-400">□</span>
|
||||
<div className="h-10 w-10 rounded-lg bg-[var(--admin-bg-subtle)] flex items-center justify-center shrink-0 border border-[var(--admin-border)]">
|
||||
<span className="text-[var(--admin-text-muted)]">□</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<button
|
||||
onClick={() => onEdit(product)}
|
||||
className="font-semibold text-[var(--admin-text-primary)] hover:text-[var(--admin-accent)] transition-colors text-left"
|
||||
className="font-semibold text-[var(--admin-text-primary)] hover:text-[var(--admin-primary)] transition-colors text-left"
|
||||
>
|
||||
{product.name}
|
||||
</button>
|
||||
<div className="text-xs text-stone-500 line-clamp-1">
|
||||
{product.description || <span className="italic text-stone-400">No description</span>}
|
||||
<div className="text-xs text-[var(--admin-text-muted)] line-clamp-1">
|
||||
{product.description || <span className="italic text-[var(--admin-text-muted)]">No description</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3">
|
||||
<span className="rounded-md bg-stone-100 px-2 py-0.5 text-[10px] font-semibold uppercase text-stone-600">
|
||||
<span className="rounded-md bg-[var(--admin-bg-subtle)] px-2 py-0.5 text-[10px] font-semibold uppercase text-[var(--admin-text-secondary)]">
|
||||
{product.type}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 font-mono text-sm font-semibold text-[var(--admin-text-primary)]">
|
||||
<td
|
||||
className="px-4 py-3 text-sm font-semibold text-[var(--admin-text-primary)]"
|
||||
style={{ fontFamily: "var(--font-fragment-mono)", fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
${Number(product.price).toFixed(2)}
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[10px] font-semibold ${
|
||||
product.active ? "bg-[var(--admin-accent)]/10 text-[var(--admin-accent)]" : "bg-stone-100 text-stone-500"
|
||||
}`}>
|
||||
<AdminBadge tone={product.active ? "success" : "neutral"} dot>
|
||||
{product.active ? "Active" : "Inactive"}
|
||||
</span>
|
||||
</AdminBadge>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 text-right">
|
||||
@@ -520,7 +512,7 @@ function TableView({
|
||||
<button
|
||||
ref={(el) => { buttonRefs.current[product.id] = el; }}
|
||||
onClick={() => onDelete(product.id)}
|
||||
className="rounded-lg px-2 py-1.5 text-xs text-[var(--admin-text-muted)] hover:text-red-600 hover:bg-red-50 transition-colors"
|
||||
className="rounded-lg px-2 py-1.5 text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-danger)] hover:bg-[var(--admin-danger-soft)] transition-colors"
|
||||
aria-label="Product actions"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
@@ -548,19 +540,19 @@ function TableView({
|
||||
<div
|
||||
role="dialog"
|
||||
aria-label="Confirm delete"
|
||||
className="fixed z-[70] w-72 rounded-xl bg-white border border-[var(--admin-border)] shadow-xl p-4"
|
||||
className="fixed z-[70] w-72 rounded-xl bg-[var(--admin-card-bg)] border border-[var(--admin-border)] shadow-xl p-4"
|
||||
style={{ top: menuPos.top, left: menuPos.left, transform: "translateX(-100%)" }}
|
||||
>
|
||||
<p className="text-sm font-semibold text-stone-900">
|
||||
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">
|
||||
Delete "{openProduct.name}"?
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-stone-500">
|
||||
<p className="mt-1 text-xs text-[var(--admin-text-muted)]">
|
||||
This will remove the product. If attached to orders, it will be hidden.
|
||||
</p>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button
|
||||
onClick={onDeleteCancel}
|
||||
className="flex-1 rounded-lg border border-[var(--admin-border)] bg-white px-3 py-2 text-xs font-semibold text-stone-700 hover:bg-stone-50"
|
||||
className="flex-1 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2 text-xs font-semibold text-[var(--admin-text-primary)] hover:bg-[var(--admin-bg)]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -592,6 +584,7 @@ function CardView({
|
||||
onDeleteConfirm,
|
||||
onDeleteCancel,
|
||||
deletingId,
|
||||
onAdd,
|
||||
}: {
|
||||
products: Product[];
|
||||
onEdit: (p: Product) => void;
|
||||
@@ -600,25 +593,27 @@ function CardView({
|
||||
onDeleteConfirm: (id: string) => void;
|
||||
onDeleteCancel: () => void;
|
||||
deletingId: string | null;
|
||||
onAdd: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{products.length === 0 ? (
|
||||
<div className="col-span-full text-center py-16 rounded-xl border border-[var(--admin-border)] bg-white">
|
||||
<div className="flex h-16 w-16 mx-auto items-center justify-center rounded-full bg-stone-100 mb-4">
|
||||
{Icons.package("h-8 w-8 text-stone-400")}
|
||||
</div>
|
||||
<p className="text-sm font-medium text-stone-600">No products found</p>
|
||||
<p className="text-xs text-stone-400 mt-1">Try adjusting your filters</p>
|
||||
<div className="col-span-full rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card-bg)]">
|
||||
<EmptyState
|
||||
icon={<InboxIcon className="h-8 w-8" strokeWidth={1.25} />}
|
||||
title="No products found"
|
||||
description="Try adjusting your filters, or add your first product to get started."
|
||||
action={{ label: "Add your first product", onClick: onAdd }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
products.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="group relative rounded-xl border border-[var(--admin-border)] bg-white hover:shadow-md hover:border-[var(--admin-accent)]/30 transition-all overflow-hidden"
|
||||
className="group relative rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card-bg)] hover:shadow-md hover:border-[var(--admin-primary)]/40 transition-all overflow-hidden"
|
||||
>
|
||||
{/* Image */}
|
||||
<div className="relative h-40 bg-stone-100">
|
||||
<div className="relative h-40 bg-[var(--admin-bg-subtle)]">
|
||||
{product.image_url ? (
|
||||
<Image
|
||||
src={product.image_url}
|
||||
@@ -630,16 +625,14 @@ function CardView({
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-4xl text-stone-300">□</span>
|
||||
<span className="text-4xl text-[var(--admin-text-muted)]">□</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Status badge */}
|
||||
<div className="absolute top-3 right-3">
|
||||
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-[10px] font-semibold shadow-sm ${
|
||||
product.active ? "bg-[var(--admin-accent)] text-white" : "bg-stone-500 text-white"
|
||||
}`}>
|
||||
<AdminBadge tone={product.active ? "success" : "neutral"} dot>
|
||||
{product.active ? "Active" : "Inactive"}
|
||||
</span>
|
||||
</AdminBadge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -649,27 +642,30 @@ function CardView({
|
||||
onClick={() => onEdit(product)}
|
||||
className="block w-full text-left"
|
||||
>
|
||||
<h3 className="font-semibold text-[var(--admin-text-primary)] group-hover:text-[var(--admin-accent)] transition-colors line-clamp-1">
|
||||
<h3 className="font-semibold text-[var(--admin-text-primary)] group-hover:text-[var(--admin-primary)] transition-colors line-clamp-1">
|
||||
{product.name}
|
||||
</h3>
|
||||
<p className="text-xs text-stone-500 mt-1 line-clamp-2 h-8">
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mt-1 line-clamp-2 h-8">
|
||||
{product.description || <span className="italic">No description</span>}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-between mt-4 pt-3 border-t border-stone-100">
|
||||
<div className="flex items-center justify-between mt-4 pt-3 border-t border-[var(--admin-border-light)]">
|
||||
<div>
|
||||
<span className="text-xs text-stone-500">Price</span>
|
||||
<p className="text-lg font-bold text-[var(--admin-text-primary)] font-mono">
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">Price</span>
|
||||
<p
|
||||
className="text-lg font-bold text-[var(--admin-text-primary)]"
|
||||
style={{ fontFamily: "var(--font-fragment-mono)", fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
${Number(product.price).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="rounded-md bg-stone-100 px-2 py-0.5 text-[10px] font-semibold uppercase text-stone-600">
|
||||
<span className="rounded-md bg-[var(--admin-bg-subtle)] px-2 py-0.5 text-[10px] font-semibold uppercase text-[var(--admin-text-secondary)]">
|
||||
{product.type}
|
||||
</span>
|
||||
{product.is_taxable && (
|
||||
<span className="rounded-md bg-amber-50 px-2 py-0.5 text-[10px] font-semibold text-amber-700 border border-amber-200">
|
||||
<span className="rounded-md bg-[var(--admin-accent-soft)] px-2 py-0.5 text-[10px] font-semibold text-[var(--admin-accent)] border border-[var(--admin-accent)]">
|
||||
Tax
|
||||
</span>
|
||||
)}
|
||||
@@ -687,7 +683,7 @@ function CardView({
|
||||
</AdminButton>
|
||||
<button
|
||||
onClick={() => onDelete(product.id)}
|
||||
className="rounded-lg border border-[var(--admin-border)] px-3 py-2 text-xs font-semibold text-[var(--admin-text-muted)] hover:bg-red-50 hover:text-red-600 hover:border-red-200 transition-colors"
|
||||
className="rounded-lg border border-[var(--admin-border)] px-3 py-2 text-xs font-semibold text-[var(--admin-text-muted)] hover:bg-[var(--admin-danger-soft)] hover:text-[var(--admin-danger)] hover:border-[var(--admin-danger)] transition-colors"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
@@ -698,25 +694,25 @@ function CardView({
|
||||
|
||||
{/* Delete confirm */}
|
||||
{deleteConfirm === product.id && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-xl shadow-xl p-4 max-w-[280px] mx-4">
|
||||
<p className="text-sm font-semibold text-stone-900">
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm" style={{ backgroundColor: "color-mix(in srgb, var(--admin-text-primary) 50%, transparent)" }}>
|
||||
<div className="bg-[var(--admin-card-bg)] rounded-xl shadow-xl p-4 max-w-[280px] mx-4">
|
||||
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">
|
||||
Delete "{product.name}"?
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-stone-500">
|
||||
<p className="mt-1 text-xs text-[var(--admin-text-muted)]">
|
||||
This will remove the product. If attached to orders, it will be hidden.
|
||||
</p>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button
|
||||
onClick={onDeleteCancel}
|
||||
className="flex-1 rounded-lg border border-[var(--admin-border)] px-3 py-2 text-xs font-semibold text-stone-700 hover:bg-stone-50"
|
||||
className="flex-1 rounded-lg border border-[var(--admin-border)] px-3 py-2 text-xs font-semibold text-[var(--admin-text-primary)] hover:bg-[var(--admin-bg)]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteConfirm(product.id)}
|
||||
disabled={deletingId === product.id}
|
||||
className="flex-1 rounded-lg bg-red-600 px-3 py-2 text-xs font-bold text-white disabled:opacity-50 hover:bg-red-500"
|
||||
className="flex-1 rounded-lg bg-[var(--admin-danger)] px-3 py-2 text-xs font-bold text-white disabled:opacity-50 hover:bg-[var(--admin-danger-hover)]"
|
||||
>
|
||||
{deletingId === product.id ? "..." : "Delete"}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user