diff --git a/.env.production b/.env.production index 9b1b816..9b1f615 100644 --- a/.env.production +++ b/.env.production @@ -2,7 +2,7 @@ NEXT_PUBLIC_API_BASE_URL=http://localhost:8080 # Middleware to limited site to only root page -NEXT_PUBLIC_LAUNCH_ONLY_ROOT=false +NEXT_PUBLIC_LAUNCH_ONLY_ROOT=true NEXT_PUBLIC_SHORTLINK_BASE_URL=https://battl.build diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index b01090c..d787cd0 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -149,7 +149,7 @@ export default function AdminLayout({ return ( // ✅ prevent browser-level horizontal scroll from any wide children -
+
setCollapsed((v) => !v)} @@ -157,8 +157,11 @@ export default function AdminLayout({ /> {/* ✅ min-w-0 is the critical fix: lets the main column shrink */} -
-
+
+

Admin diff --git a/app/admin/merchants/page.tsx b/app/admin/merchants/page.tsx index 690fe09..39d7c29 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,8 @@ 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); @@ -92,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); @@ -135,6 +183,7 @@ export default function MerchantsAdminPage() { // --- run full import --- const handleRunFullImport = async (id: number) => { + setOpenDropdown(null); try { setImportingId(id); setError(null); @@ -162,6 +211,7 @@ export default function MerchantsAdminPage() { // --- run offer sync --- const handleRunOfferSync = async (id: number) => { + setOpenDropdown(null); try { setOfferSyncId(id); setError(null); @@ -242,7 +292,7 @@ export default function MerchantsAdminPage() { return (

-
+
{/* Header */}
@@ -418,135 +468,252 @@ export default function MerchantsAdminPage() { )} - {/* Table */} -
+ {/* Improved Table */} +
{loading ? ( -

Loading merchants…

+

Loading merchants…

) : merchants.length === 0 ? ( -

- No merchants found. Seed some merchants in the backend. +

+ No merchants found. Use the "Add New Merchant" button above.

) : (
- + - - - - - - - - + + + + + + + - + {merchants.map((m) => ( - - + {/* Merchant Name */} + - - - - - - - - - + + {/* Actions Dropdown */} + @@ -559,4 +726,4 @@ export default function MerchantsAdminPage() { ); -} \ No newline at end of file +} diff --git a/components/AdminLeftNavigation.tsx b/components/AdminLeftNavigation.tsx index bf2b10e..b808bf8 100644 --- a/components/AdminLeftNavigation.tsx +++ b/components/AdminLeftNavigation.tsx @@ -46,9 +46,10 @@ export default function AdminLeftNavigation({ return (
MerchantAvantLink MIDFeed URLOffer Feed URLActiveLast Full ImportLast Offer SyncActionsMerchantMIDFeed URLOffer URLStatusLast ImportActions
+
updateMerchantField(m.id, "name", e.target.value) } - className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-sm text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" + className="w-full min-w-[140px] rounded border border-zinc-700 bg-zinc-900 px-2 py-1.5 text-sm font-medium text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" /> + {/* AvantLink MID */} + updateMerchantField(m.id, "avantlinkMid", e.target.value) } - className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-sm text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" + className="w-full min-w-[80px] rounded border border-zinc-700 bg-zinc-900 px-2 py-1.5 text-xs font-mono text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" /> - - updateMerchantField(m.id, "feedUrl", e.target.value) - } - className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs md:text-sm text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" - /> - - - updateMerchantField(m.id, "offerFeedUrl", e.target.value || null) - } - className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs md:text-sm text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" - /> - - +
- updateMerchantField(m.id, "isActive", e.target.checked) + updateMerchantField(m.id, "feedUrl", e.target.value) } - className="h-4 w-4 rounded border-zinc-600 bg-zinc-900 text-amber-400 focus:ring-amber-400" + title={m.feedUrl ?? ""} + className="flex-1 min-w-[180px] rounded border border-zinc-700 bg-zinc-900 px-2 py-1.5 text-xs font-mono text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" /> - Active - + +
- {formatDate(m.lastFullImportAt)} + {/* Offer Feed URL with copy button */} + +
+ + updateMerchantField(m.id, "offerFeedUrl", e.target.value || null) + } + placeholder="—" + title={m.offerFeedUrl ?? "None"} + className="flex-1 min-w-[180px] rounded border border-zinc-700 bg-zinc-900 px-2 py-1.5 text-xs font-mono text-zinc-50 placeholder:text-zinc-600 focus:border-amber-400/70 focus:outline-none focus:ring-0" + /> + {m.offerFeedUrl && ( + + )} +
- {formatDate(m.lastOfferSyncAt)} + {/* Status Badge (replacing checkbox) */} + + -
+ {/* Last Import with tooltip */} +
+
+ + {formatRelativeTime(m.lastFullImportAt)} + + + Offers: {formatRelativeTime(m.lastOfferSyncAt)} + +
+
+
+ {/* Primary Save Button */} - + {/* Dropdown Menu */} +
+ - + {openDropdown === m.id && ( + <> +
setOpenDropdown(null)} + /> +
+
+ + +
+
+ + )} +