fix: react-doctor → 64/100 — remove dead AdminStatusBadge/AdminCountBadge, add dialog semantics
This commit is contained in:
@@ -29,7 +29,10 @@ type Props = {
|
|||||||
|
|
||||||
export default function ImportCenterClient({ brandId: initialBrandId, brandName: initialBrandName, brands = [], isPlatformAdmin = false }: Props) {
|
export default function ImportCenterClient({ brandId: initialBrandId, brandName: initialBrandName, brands = [], isPlatformAdmin = false }: Props) {
|
||||||
const [activeBrandId, setActiveBrandId] = useState(initialBrandId);
|
const [activeBrandId, setActiveBrandId] = useState(initialBrandId);
|
||||||
const [activeBrandName, setActiveBrandName] = useState(initialBrandName);
|
// Brand name is derived from the brands list — no separate state, so
|
||||||
|
// updates to it don't trigger an extra render of the whole component.
|
||||||
|
const activeBrandName =
|
||||||
|
brands.find((b) => b.id === activeBrandId)?.name ?? initialBrandName;
|
||||||
const [fileName, setFileName] = useState("");
|
const [fileName, setFileName] = useState("");
|
||||||
const [base64Data, setBase64Data] = useState("");
|
const [base64Data, setBase64Data] = useState("");
|
||||||
const [analysis, setAnalysis] = useState<ImportAnalysis | null>(null);
|
const [analysis, setAnalysis] = useState<ImportAnalysis | null>(null);
|
||||||
@@ -45,8 +48,6 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
|||||||
|
|
||||||
function handleBrandChange(newBrandId: string) {
|
function handleBrandChange(newBrandId: string) {
|
||||||
setActiveBrandId(newBrandId);
|
setActiveBrandId(newBrandId);
|
||||||
const found = brands.find((b) => b.id === newBrandId);
|
|
||||||
if (found) setActiveBrandName(found.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFile(file: File) {
|
function handleFile(file: File) {
|
||||||
@@ -178,9 +179,17 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
|||||||
{step === "upload" && (
|
{step === "upload" && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onDragOver={(e) => { e.preventDefault(); }}
|
onDragOver={(e) => { e.preventDefault(); }}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
inputRef.current?.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="flex flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed border-[var(--admin-border)] bg-white p-12 cursor-pointer hover:border-[var(--admin-accent)] hover:bg-[var(--admin-bg)] transition-colors"
|
className="flex flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed border-[var(--admin-border)] bg-white p-12 cursor-pointer hover:border-[var(--admin-accent)] hover:bg-[var(--admin-bg)] transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="h-10 w-10 text-[var(--admin-text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="h-10 w-10 text-[var(--admin-text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
|||||||
@@ -473,9 +473,6 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
|
|||||||
const [showAddModal, setShowAddModal] = useState(false);
|
const [showAddModal, setShowAddModal] = useState(false);
|
||||||
const [addSearch, setAddSearch] = useState("");
|
const [addSearch, setAddSearch] = useState("");
|
||||||
const [customIntegrations, setCustomIntegrations] = useState<CustomIntegration[]>([]);
|
const [customIntegrations, setCustomIntegrations] = useState<CustomIntegration[]>([]);
|
||||||
const [showCustomForm, setShowCustomForm] = useState(false);
|
|
||||||
const [newCustomName, setNewCustomName] = useState("");
|
|
||||||
const [newCustomType, setNewCustomType] = useState<"ai_provider" | "payment_processor" | "other">("other");
|
|
||||||
|
|
||||||
// Integrations shown in the grid — OpenAI card is hidden since it's now handled by AIProviderPanel
|
// Integrations shown in the grid — OpenAI card is hidden since it's now handled by AIProviderPanel
|
||||||
const gridIntegrations = INTEGRATIONS.filter((i) => i.id !== "openai");
|
const gridIntegrations = INTEGRATIONS.filter((i) => i.id !== "openai");
|
||||||
@@ -498,19 +495,17 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
|
|||||||
setAddSearch("");
|
setAddSearch("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAddCustom() {
|
function handleAddCustom(name: string, type: "ai_provider" | "payment_processor" | "other") {
|
||||||
if (!newCustomName.trim()) return;
|
if (!name.trim()) return;
|
||||||
const newInt: CustomIntegration = {
|
const newInt: CustomIntegration = {
|
||||||
id: `custom_${Date.now()}`,
|
id: `custom_${Date.now()}`,
|
||||||
name: newCustomName,
|
name: name.trim(),
|
||||||
type: newCustomType,
|
type,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
credentials: [],
|
credentials: [],
|
||||||
syncOptions: [],
|
syncOptions: [],
|
||||||
};
|
};
|
||||||
setCustomIntegrations((prev) => [...prev, newInt]);
|
setCustomIntegrations((prev) => [...prev, newInt]);
|
||||||
setNewCustomName("");
|
|
||||||
setShowCustomForm(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -608,7 +603,16 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
|
|||||||
|
|
||||||
{/* Add integration modal */}
|
{/* Add integration modal */}
|
||||||
{showAddModal && (
|
{showAddModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setShowAddModal(false)}>
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Add Integration"
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
onClick={() => setShowAddModal(false)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Escape") setShowAddModal(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="w-full max-w-lg rounded-2xl bg-zinc-900 border border-zinc-700 p-6 shadow-xl" onClick={(e) => e.stopPropagation()}>
|
<div className="w-full max-w-lg rounded-2xl bg-zinc-900 border border-zinc-700 p-6 shadow-xl" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<h2 className="text-lg font-bold text-zinc-100">Add Integration</h2>
|
<h2 className="text-lg font-bold text-zinc-100">Add Integration</h2>
|
||||||
|
|||||||
@@ -372,7 +372,16 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|||||||
|
|
||||||
{/* Edit Modal */}
|
{/* Edit Modal */}
|
||||||
{editHg && (
|
{editHg && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setEditHg(null)}>
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Edit Headgate"
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
onClick={() => setEditHg(null)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Escape") setEditHg(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]" onClick={(e) => e.stopPropagation()}>
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--admin-border)]">
|
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--admin-border)]">
|
||||||
<p className="font-bold text-[var(--admin-text-primary)]">Edit Headgate</p>
|
<p className="font-bold text-[var(--admin-text-primary)]">Edit Headgate</p>
|
||||||
@@ -512,7 +521,16 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Headgate details"
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
onClick={onClose}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]" onClick={(e) => e.stopPropagation()}>
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@@ -138,56 +138,3 @@ export default function AdminBadge({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status badge with predefined statuses
|
|
||||||
type AdminStatusBadgeProps = {
|
|
||||||
status: "active" | "inactive" | "pending" | "draft" | "completed" | "cancelled";
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reuse the new `tone` prop directly so the status pill text style always
|
|
||||||
// matches the active AdminBadge palette.
|
|
||||||
const statusConfig: Record<string, { tone: BadgeTone; dot: boolean; label: string }> = {
|
|
||||||
active: { tone: "success", dot: true, label: "Active" },
|
|
||||||
inactive: { tone: "neutral", dot: true, label: "Inactive" },
|
|
||||||
pending: { tone: "warning", dot: true, label: "Pending" },
|
|
||||||
draft: { tone: "neutral", dot: true, label: "Draft" },
|
|
||||||
completed: { tone: "success", dot: true, label: "Completed" },
|
|
||||||
cancelled: { tone: "danger", dot: true, label: "Cancelled" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AdminStatusBadge({ status, className = "" }: AdminStatusBadgeProps) {
|
|
||||||
const config = statusConfig[status] || statusConfig.inactive;
|
|
||||||
return (
|
|
||||||
<AdminBadge tone={config.tone} dot={config.dot} className={className}>
|
|
||||||
{config.label}
|
|
||||||
</AdminBadge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count badge (circular)
|
|
||||||
type AdminCountBadgeProps = {
|
|
||||||
count: number;
|
|
||||||
tone?: BadgeTone;
|
|
||||||
/** @deprecated Use `tone` instead. */
|
|
||||||
variant?: BadgeVariant;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AdminCountBadge({
|
|
||||||
count,
|
|
||||||
tone,
|
|
||||||
variant = "default",
|
|
||||||
className = "",
|
|
||||||
}: AdminCountBadgeProps) {
|
|
||||||
const resolvedTone: BadgeTone = tone ?? variantToTone[variant];
|
|
||||||
const { bg, text } = toneStyles[resolvedTone];
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded-full px-1.5 text-xs font-bold ${className}`}
|
|
||||||
style={{ backgroundColor: bg, color: text }}
|
|
||||||
>
|
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export { default as AdminEmptyState } from "./AdminEmptyState";
|
|||||||
export { default as AdminDeleteConfirm, useDeleteConfirm } from "./AdminDeleteConfirm";
|
export { default as AdminDeleteConfirm, useDeleteConfirm } from "./AdminDeleteConfirm";
|
||||||
export { default as AdminActionMenu, AdminActionButton } from "./AdminActionMenu";
|
export { default as AdminActionMenu, AdminActionButton } from "./AdminActionMenu";
|
||||||
export { default as AdminPagination, AdminSimplePagination } from "./AdminPagination";
|
export { default as AdminPagination, AdminSimplePagination } from "./AdminPagination";
|
||||||
export { default as AdminBadge, AdminStatusBadge, AdminCountBadge } from "./AdminBadge";
|
export { default as AdminBadge } from "./AdminBadge";
|
||||||
|
|
||||||
// Phase 2 pattern primitives (admin-level, not in design-system/ folder)
|
// Phase 2 pattern primitives (admin-level, not in design-system/ folder)
|
||||||
// Re-exported here so callers can `import { KPIStat, EmptyState, LoadingState } from "@/components/admin/design-system"`.
|
// Re-exported here so callers can `import { KPIStat, EmptyState, LoadingState } from "@/components/admin/design-system"`.
|
||||||
|
|||||||
Reference in New Issue
Block a user