Add new merchant creation form and functionality in MerchantsAdminPage
CI / test (push) Successful in 5s

This commit is contained in:
2026-01-20 19:24:36 -05:00
parent 2ea1f19863
commit 411c3bc05d
2 changed files with 208 additions and 1 deletions
+207
View File
@@ -32,6 +32,17 @@ export default function MerchantsAdminPage() {
const [offerSyncId, setOfferSyncId] = useState<number | null>(null);
const [banner, setBanner] = useState<string | null>(null);
// --- new merchant form state ---
const [showNewMerchantForm, setShowNewMerchantForm] = useState(false);
const [creating, setCreating] = useState(false);
const [newMerchant, setNewMerchant] = useState({
name: "",
avantlinkMid: "",
feedUrl: "",
offerFeedUrl: "",
isActive: true,
});
// --- load merchants ---
useEffect(() => {
const controller = new AbortController();
@@ -179,6 +190,56 @@ export default function MerchantsAdminPage() {
}
};
// --- create new merchant ---
const handleCreateMerchant = async () => {
try {
setCreating(true);
setError(null);
setBanner(null);
const res = await fetch(`${API_BASE_URL}/api/v1/admin/merchants`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: newMerchant.name,
avantlinkMid: newMerchant.avantlinkMid,
feedUrl: newMerchant.feedUrl,
offerFeedUrl: newMerchant.offerFeedUrl || null,
isActive: newMerchant.isActive,
}),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
console.error("Merchant creation failed", res.status, text);
throw new Error(
`Creation failed (${res.status})${text ? `: ${text}` : ""}`
);
}
const created: MerchantAdminDto = await res.json();
setMerchants((prev) => [...prev, created]);
// Reset form
setNewMerchant({
name: "",
avantlinkMid: "",
feedUrl: "",
offerFeedUrl: "",
isActive: true,
});
setShowNewMerchantForm(false);
setBanner("Merchant created successfully.");
} catch (e: any) {
console.error("Error creating merchant", e);
setError(e?.message ?? "Failed to create merchant");
} finally {
setCreating(false);
setTimeout(() => setBanner(null), 3000);
}
};
return (
<main className="min-h-screen bg-black text-zinc-50">
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
@@ -211,6 +272,152 @@ export default function MerchantsAdminPage() {
</div>
)}
{/* Add New Merchant Section */}
<section className="mb-6 rounded-lg border border-zinc-800 bg-zinc-950/70 p-4">
{!showNewMerchantForm ? (
<button
type="button"
onClick={() => setShowNewMerchantForm(true)}
className="inline-flex items-center gap-2 rounded-md bg-amber-400 px-4 py-2 text-sm font-semibold text-black hover:bg-amber-300 transition-colors"
>
<span className="text-lg">+</span>
Add New Merchant
</button>
) : (
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-zinc-100">
Add New Merchant
</h2>
<button
type="button"
onClick={() => {
setShowNewMerchantForm(false);
setNewMerchant({
name: "",
avantlinkMid: "",
feedUrl: "",
offerFeedUrl: "",
isActive: true,
});
}}
className="text-zinc-400 hover:text-zinc-200 text-sm"
>
Cancel
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-zinc-400 mb-1">
Merchant Name *
</label>
<input
type="text"
value={newMerchant.name}
onChange={(e) =>
setNewMerchant((prev) => ({ ...prev, name: e.target.value }))
}
placeholder="e.g., Brownells"
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:border-amber-400/70 focus:outline-none focus:ring-0"
/>
</div>
<div>
<label className="block text-xs font-medium text-zinc-400 mb-1">
AvantLink MID *
</label>
<input
type="text"
value={newMerchant.avantlinkMid}
onChange={(e) =>
setNewMerchant((prev) => ({
...prev,
avantlinkMid: e.target.value,
}))
}
placeholder="e.g., 12345"
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:border-amber-400/70 focus:outline-none focus:ring-0"
/>
</div>
<div className="md:col-span-2">
<label className="block text-xs font-medium text-zinc-400 mb-1">
Feed URL *
</label>
<input
type="text"
value={newMerchant.feedUrl}
onChange={(e) =>
setNewMerchant((prev) => ({ ...prev, feedUrl: e.target.value }))
}
placeholder="https://..."
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:border-amber-400/70 focus:outline-none focus:ring-0"
/>
</div>
<div className="md:col-span-2">
<label className="block text-xs font-medium text-zinc-400 mb-1">
Offer Feed URL (optional)
</label>
<input
type="text"
value={newMerchant.offerFeedUrl}
onChange={(e) =>
setNewMerchant((prev) => ({
...prev,
offerFeedUrl: e.target.value,
}))
}
placeholder="https://..."
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:border-amber-400/70 focus:outline-none focus:ring-0"
/>
</div>
<div className="md:col-span-2">
<label className="inline-flex items-center gap-2 text-sm text-zinc-300">
<input
type="checkbox"
checked={newMerchant.isActive}
onChange={(e) =>
setNewMerchant((prev) => ({
...prev,
isActive: e.target.checked,
}))
}
className="h-4 w-4 rounded border-zinc-600 bg-zinc-900 text-amber-400 focus:ring-amber-400"
/>
Active
</label>
</div>
</div>
<div className="mt-4 flex gap-3">
<button
type="button"
onClick={handleCreateMerchant}
disabled={
creating ||
!newMerchant.name ||
!newMerchant.avantlinkMid ||
!newMerchant.feedUrl
}
className={`rounded-md px-4 py-2 text-sm font-semibold transition-colors ${
creating ||
!newMerchant.name ||
!newMerchant.avantlinkMid ||
!newMerchant.feedUrl
? "bg-amber-400/20 text-amber-200 cursor-not-allowed"
: "bg-amber-400 text-black hover:bg-amber-300"
}`}
>
{creating ? "Creating…" : "Create Merchant"}
</button>
</div>
</div>
)}
</section>
{/* Table */}
<section className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-3 md:p-4">
{loading ? (
+1 -1
View File
@@ -303,7 +303,7 @@ export default function PartsBrowseClient(props: {
end: Math.min(currentPage * PAGE_SIZE, serverTotalElements),
};
const headingTitle = props.title ?? `${partRole.replaceAll("-", " ")} parts`;
const headingTitle = props.title ?? `${partRole.replaceAll("-", " ")}s`;
const headingSubtitle = props.subtitle ?? "Browse available parts.";
// ----------------------------