Admin
diff --git a/app/admin/merchants/page.tsx b/app/admin/merchants/page.tsx
index be58ef2..63f63ff 100644
--- a/app/admin/merchants/page.tsx
+++ b/app/admin/merchants/page.tsx
@@ -16,13 +16,50 @@ type MerchantAdminDto = {
lastOfferSyncAt: string | null;
};
-function formatDate(value: string | null) {
- if (!value) return "—";
+// Helper: relative time formatting
+function formatRelativeTime(value: string | null): string {
+ if (!value) return "Never";
+ const d = new Date(value);
+ if (Number.isNaN(d.getTime())) return "Invalid date";
+
+ const now = Date.now();
+ const diff = now - d.getTime();
+ const minutes = Math.floor(diff / 60000);
+ const hours = Math.floor(diff / 3600000);
+ const days = Math.floor(diff / 86400000);
+
+ if (minutes < 1) return "Just now";
+ if (minutes < 60) return `${minutes}m ago`;
+ if (hours < 24) return `${hours}h ago`;
+ if (days < 30) return `${days}d ago`;
+ return d.toLocaleDateString();
+}
+
+// Helper: full date formatting for tooltips
+function formatFullDate(value: string | null): string {
+ if (!value) return "Never";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return value;
return d.toLocaleString();
}
+// Helper: truncate URL for display
+function truncateUrl(url: string | null, maxLength = 40): string {
+ if (!url) return "—";
+ if (url.length <= maxLength) return url;
+ return url.substring(0, maxLength) + "...";
+}
+
+// Helper: copy to clipboard
+async function copyToClipboard(text: string) {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
export default function MerchantsAdminPage() {
const [merchants, setMerchants] = useState([]);
const [loading, setLoading] = useState(true);
@@ -31,6 +68,19 @@ export default function MerchantsAdminPage() {
const [importingId, setImportingId] = useState(null);
const [offerSyncId, setOfferSyncId] = useState(null);
const [banner, setBanner] = useState(null);
+ const [copiedUrl, setCopiedUrl] = useState(null);
+ const [openDropdown, setOpenDropdown] = useState(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(() => {
@@ -81,6 +131,15 @@ export default function MerchantsAdminPage() {
);
};
+ // --- copy URL handler ---
+ const handleCopyUrl = async (url: string, id: number) => {
+ const success = await copyToClipboard(url);
+ if (success) {
+ setCopiedUrl(`${id}-${url}`);
+ setTimeout(() => setCopiedUrl(null), 2000);
+ }
+ };
+
// --- save merchant (name/feed URLs/isActive) ---
const handleSave = async (id: number) => {
const merchant = merchants.find((m) => m.id === id);
@@ -124,6 +183,7 @@ export default function MerchantsAdminPage() {
// --- run full import ---
const handleRunFullImport = async (id: number) => {
+ setOpenDropdown(null);
try {
setImportingId(id);
setError(null);
@@ -151,6 +211,7 @@ export default function MerchantsAdminPage() {
// --- run offer sync ---
const handleRunOfferSync = async (id: number) => {
+ setOpenDropdown(null);
try {
setOfferSyncId(id);
setError(null);
@@ -179,9 +240,59 @@ 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 (
-