Your business has been registered successfully.
)}
@@ -74,7 +86,7 @@ export default function RegisterBusinessModal({ onClose }) {
type="button"
onClick={onClose}
disabled={status === 'loading'}
- className="flex-[0.8] py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-white text-sm font-medium transition disabled:opacity-50"
+ className="flex-[0.8] py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium transition disabled:opacity-50"
>
Cancel
diff --git a/client/src/components/Sidebar.jsx b/client/src/components/Sidebar.jsx
index baea359..b4bf56c 100644
--- a/client/src/components/Sidebar.jsx
+++ b/client/src/components/Sidebar.jsx
@@ -49,7 +49,7 @@ function StageMarker({ done, active, enabled }) {
}
if (active) {
- return
;
}
if (!enabled) {
@@ -164,7 +164,7 @@ export default function Sidebar() {
className={`flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors duration-150 ${
item.active
? 'bg-gray-100/70 text-gray-800'
- : 'text-gray-500 hover:text-gray-800 hover:bg-white'
+ : 'text-gray-500 hover:text-gray-800 hover:bg-page-bg'
}`}
>
{SVG_ICONS[item.id]}
@@ -203,13 +203,13 @@ export default function Sidebar() {
{item.substeps.map((substep) => (
- {substep.active &&
}
+ {substep.active &&
}
{substep.label}
diff --git a/client/src/components/TestSmsModal.jsx b/client/src/components/TestSmsModal.jsx
index 98aece9..1770b59 100644
--- a/client/src/components/TestSmsModal.jsx
+++ b/client/src/components/TestSmsModal.jsx
@@ -64,7 +64,7 @@ export default function TestSmsModal({ businessId, template, onClose }) {
type="button"
onClick={onClose}
disabled={sending}
- className="flex-1 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-white text-sm font-medium transition disabled:opacity-50"
+ className="flex-1 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium transition disabled:opacity-50"
>
Cancel
diff --git a/client/src/index.css b/client/src/index.css
index 56acdf9..02544f4 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -53,6 +53,40 @@ body {
-webkit-font-smoothing: antialiased;
}
+button,
+[role="button"] {
+ transition:
+ background-color 160ms ease,
+ border-color 160ms ease,
+ color 160ms ease,
+ box-shadow 160ms ease,
+ opacity 160ms ease,
+ transform 160ms ease;
+}
+
+button:not(:disabled),
+[role="button"]:not([aria-disabled="true"]) {
+ cursor: pointer;
+}
+
+button:not(:disabled):hover,
+[role="button"]:not([aria-disabled="true"]):hover {
+ box-shadow: 0 10px 22px -18px rgba(15, 23, 42, 0.35);
+}
+
+button:disabled,
+[role="button"][aria-disabled="true"] {
+ cursor: not-allowed;
+}
+
+button:focus-visible,
+[role="button"]:focus-visible {
+ outline: none;
+ box-shadow:
+ 0 0 0 3px rgba(56, 56, 196, 0.14),
+ 0 10px 22px -18px rgba(15, 23, 42, 0.35);
+}
+
::-webkit-scrollbar {
width: 6px;
}
diff --git a/client/src/pages/Brand.jsx b/client/src/pages/Brand.jsx
index d512498..cb09c97 100644
--- a/client/src/pages/Brand.jsx
+++ b/client/src/pages/Brand.jsx
@@ -25,7 +25,7 @@ function DeleteConfirmModal({ brandName, onCancel, onConfirm, deleting }) {
Cancel
@@ -116,7 +116,7 @@ export default function Brand() {
setShowDeleteConfirm(true)}
- className="shrink-0 text-xs text-red-600 hover:text-gray-700 hover:bg-white border border-gray-200 px-3 py-2 rounded-lg font-medium transition"
+ className="shrink-0 rounded-lg border border-gray-200 px-3 py-2 text-xs font-medium text-red-600 transition hover:border-red-200 hover:bg-red-50 hover:text-red-700"
>
Delete Brand
@@ -182,7 +182,7 @@ export default function Brand() {
{card.icon}
{card.label}
diff --git a/client/src/pages/Businesses.jsx b/client/src/pages/Businesses.jsx
index 72e1664..13a3676 100644
--- a/client/src/pages/Businesses.jsx
+++ b/client/src/pages/Businesses.jsx
@@ -12,6 +12,110 @@ import {
getBusinessTagline,
} from '../utils/businessProfile';
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : '';
+}
+
+function normalizeUniqueStrings(value) {
+ if (!Array.isArray(value)) return [];
+
+ const seen = new Set();
+ return value
+ .map((entry) => normalizeText(entry))
+ .filter((entry) => {
+ if (!entry || seen.has(entry)) return false;
+ seen.add(entry);
+ return true;
+ });
+}
+
+function extractCdnUrls(business) {
+ return normalizeUniqueStrings(business?.relevantImagePaths);
+}
+
+function normalizeScrapeLinks(value) {
+ if (!Array.isArray(value)) return [];
+
+ const seen = new Set();
+ return value
+ .map((entry) => {
+ if (typeof entry === 'string') {
+ const href = normalizeText(entry);
+ return href ? { href, label: href } : null;
+ }
+
+ if (!entry || typeof entry !== 'object') return null;
+
+ const href = normalizeText(entry.href || entry.url || entry.link);
+ if (!href) return null;
+
+ const label = normalizeText(entry.text || entry.title || entry.label || href);
+ return { href, label };
+ })
+ .filter((entry) => {
+ if (!entry || seen.has(entry.href)) return false;
+ seen.add(entry.href);
+ return true;
+ });
+}
+
+function formatPrettyJson(value) {
+ if (value == null) return '';
+
+ if (typeof value === 'string') {
+ try {
+ return JSON.stringify(JSON.parse(value), null, 2);
+ } catch {
+ return value;
+ }
+ }
+
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return String(value);
+ }
+}
+
+function CdnGallery({ urls, compact = false, showLabels = true, clickable = true }) {
+ if (!urls.length) return null;
+
+ return (
+
+ {urls.map((url, index) => {
+ const Wrapper = clickable ? 'a' : 'div';
+ const wrapperProps = clickable
+ ? { href: url, target: '_blank', rel: 'noreferrer' }
+ : {};
+
+ return (
+
+
+
{
+ event.currentTarget.style.opacity = '0.35';
+ }}
+ />
+
+ {showLabels && (
+
+ )}
+
+ );
+ })}
+
+ );
+}
+
function DeleteConfirmModal({ businessName, onCancel, onConfirm, deleting }) {
return (
@@ -27,7 +131,7 @@ function DeleteConfirmModal({ businessName, onCancel, onConfirm, deleting }) {
Cancel
@@ -49,35 +153,137 @@ function BusinessCreatedModal({ business, onClose }) {
const domain = getBusinessDomain(business);
const tagline = getBusinessTagline(business);
const image = getBusinessImage(business);
+ const cdnUrls = extractCdnUrls(business?.scrapeArtifacts?.cdnUrls?.length ? { relevantImagePaths: business.scrapeArtifacts.cdnUrls } : business);
+ const links = normalizeScrapeLinks(business?.scrapeArtifacts?.links);
+ const prettyJson = useMemo(() => formatPrettyJson(business?.scrapeArtifacts?.json), [business]);
+ const [viewportWidth, setViewportWidth] = useState(() => (
+ typeof window === 'undefined' ? 1200 : window.innerWidth
+ ));
+
+ useEffect(() => {
+ if (typeof window === 'undefined') return undefined;
+
+ const handleResize = () => setViewportWidth(window.innerWidth);
+ handleResize();
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ const preferredWidth = prettyJson
+ ? 1080
+ : cdnUrls.length > 0 || links.length > 0
+ ? 860
+ : 560;
+ const modalWidth = Math.max(320, Math.min(viewportWidth - 32, preferredWidth));
return (
-
-
-
✓
-
Business Added!
-
Your business is ready for onboarding.
-
-
-
- {image ? (
-
- ) : (
-
{name?.[0]?.toUpperCase() || 'B'}
- )}
-
-
-
{name}
- {domain &&
{domain}
}
- {tagline &&
{tagline}
}
+
+
+
+
+
Business created
+
{name}
+
+ {domain
+ ? `Scrape completed for ${domain}. Review the captured assets below before moving on.`
+ : 'Scrape completed. Review the captured assets below before moving on.'}
+
+
+
+ Close
+
+
+
+
+
+
+
+ {image ? (
+
+ ) : (
+
{name?.[0]?.toUpperCase() || 'B'}
+ )}
+
+
+
{name}
+ {domain &&
{domain}
}
+ {tagline &&
{tagline}
}
+
+
+ {cdnUrls.length} image{cdnUrls.length === 1 ? '' : 's'}
+
+
+ {links.length} link{links.length === 1 ? '' : 's'}
+
+
+
+
+ {cdnUrls.length > 0 && (
+
+
+
Images
+
Captured storefront images are available below.
+
+
+
+ )}
+
+ {prettyJson && (
+
+
+
Captured Data
+
Raw storefront data captured during onboarding.
+
+
+
+ )}
+
+ {links.length > 0 && (
+
+
+
Links
+
Every discovered storefront link is available below.
+
+
+
+ )}
+
+
+
+
+ Continue
+
-
- Done
-
);
@@ -88,16 +294,14 @@ function StatusBadge({ status }) {
return (
{isScraped ? 'Scraped' : 'Not Scraped Yet'}
@@ -121,12 +325,35 @@ function UnifiedBusinessCard({
const domain = getBusinessDomain(entity);
const tagline = getBusinessTagline(entity);
const isScraped = item.status === 'scraped';
+ const cdnUrls = extractCdnUrls(item.business);
const isOpening = isScraped && selectingBusinessId === businessId;
const isImporting = !isScraped && creatingSalesChannelId === channelId;
const hasWebsiteUrl = Boolean(item.channel?.websiteUrl);
+ const canOpenBusiness = isScraped && item.business && !isOpening;
+
+ function handleCardClick() {
+ if (!canOpenBusiness) return;
+ onSelect(item.business);
+ }
+
+ function handleCardKeyDown(event) {
+ if (!canOpenBusiness) return;
+
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ onSelect(item.business);
+ }
+ }
return (
-
+
@@ -163,21 +390,29 @@ function UnifiedBusinessCard({
A website URL could not be derived automatically for this sales channel.
)}
+
+ {isScraped && cdnUrls.length > 0 && (
+
+
+
Images
+
+ {cdnUrls.length} image{cdnUrls.length === 1 ? '' : 's'}
+
+
+
+
+ )}
{isScraped ? (
<>
onSelect(item.business)}
- disabled={isOpening}
- >
- {isOpening ? 'Opening…' : 'Manage →'}
-
-
onDelete(item.business)}
- className="text-xs text-gray-600 hover:text-gray-700 font-medium transition"
+ onClick={(event) => {
+ event.stopPropagation();
+ onDelete(item.business);
+ }}
+ className="rounded-md px-2.5 py-1.5 text-xs font-medium text-gray-600 transition hover:bg-red-50 hover:text-red-600 focus:outline-none focus:ring-2 focus:ring-red-200"
>
Delete
@@ -320,6 +555,18 @@ export default function Businesses() {
useEffect(() => { load(); }, [load]);
+ async function handleBusinessCreated(created) {
+ setShowModal(false);
+ setCreatedBusiness(created);
+
+ try {
+ await Promise.all([loadBusinesses(), loadSalesChannels()]);
+ setSalesChannelsStatus('success');
+ } catch (err) {
+ setError(err.response?.data?.error || 'Business was created, but the business list could not be refreshed.');
+ }
+ }
+
async function handleSelect(biz) {
setSelectingBusinessId(biz.businessId);
setError('');
@@ -350,9 +597,7 @@ export default function Businesses() {
applicationId,
websiteUrl: channel.websiteUrl,
});
- setCreatedBusiness(res.data);
- await Promise.all([loadBusinesses(), loadSalesChannels()]);
- setSalesChannelsStatus('success');
+ await handleBusinessCreated(res.data);
} catch (err) {
setError(err.response?.data?.error || 'Failed to add business from sales channel');
} finally {
@@ -461,7 +706,7 @@ export default function Businesses() {
Use the manual fallback only if you need to set up a storefront URL directly.
setShowModal(true)}
- className="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-white text-sm font-medium transition"
+ className="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium transition"
>
Use website URL fallback
@@ -501,7 +746,10 @@ export default function Businesses() {
{showModal && (
-
{ setShowModal(false); load(); }} />
+ { setShowModal(false); load(); }}
+ onSuccess={handleBusinessCreated}
+ />
)}
{createdBusiness && setCreatedBusiness(null)} />}
{deleteTarget && (
diff --git a/client/src/pages/Events.jsx b/client/src/pages/Events.jsx
index 9cc12a9..5eee825 100644
--- a/client/src/pages/Events.jsx
+++ b/client/src/pages/Events.jsx
@@ -56,13 +56,13 @@ const EVENT_GROUPS = [
id: 'fulfillment',
label: 'Order & Fulfillment',
description: 'Core order confirmation, allocation, packing, and dispatch readiness stages.',
- defaultExpanded: true,
+ defaultExpanded: false,
},
{
id: 'delivery',
label: 'Delivery Journey',
description: 'Courier pickup, in-transit updates, and final handover milestones.',
- defaultExpanded: true,
+ defaultExpanded: false,
},
{
id: 'cancellations',
@@ -101,22 +101,73 @@ const DEFAULT_EXPANDED_GROUPS = EVENT_GROUPS.reduce((acc, group) => {
}, {});
const EVENT_TEMPLATE_STATUS_CONFIG = {
unselected: {
- label: 'No template selected',
- wrapper: 'border-gray-200 bg-white text-gray-500',
+ label: 'Not Selected',
+ badge: 'border-gray-200 bg-white text-gray-500',
dot: 'bg-gray-400',
},
pending_whitelisting: {
label: 'Pending Whitelisting',
- wrapper: 'border-gray-200 bg-white text-gray-700',
- dot: 'bg-white0',
+ badge: 'border-amber-200 bg-amber-50 text-amber-700',
+ dot: 'bg-amber-500',
},
whitelisted: {
label: 'Published',
- wrapper: 'border-gray-200 bg-white text-gray-700',
- dot: 'bg-white0',
+ badge: 'border-emerald-200 bg-emerald-50 text-emerald-700',
+ dot: 'bg-emerald-500',
},
};
+const EVENT_GROUP_STYLE_CONFIG = {
+ fulfillment: {
+ markerShell: 'border-slate-200 bg-slate-50',
+ markerDot: 'bg-slate-500',
+ },
+ delivery: {
+ markerShell: 'border-sky-200 bg-sky-50',
+ markerDot: 'bg-sky-500',
+ },
+ cancellations: {
+ markerShell: 'border-rose-200 bg-rose-50',
+ markerDot: 'bg-rose-500',
+ },
+ returns: {
+ markerShell: 'border-indigo-200 bg-indigo-50',
+ markerDot: 'bg-indigo-500',
+ },
+ refunds: {
+ markerShell: 'border-emerald-200 bg-emerald-50',
+ markerDot: 'bg-emerald-500',
+ },
+ rto: {
+ markerShell: 'border-fuchsia-200 bg-fuchsia-50',
+ markerDot: 'bg-fuchsia-500',
+ },
+ custom: {
+ markerShell: 'border-indigo-200 bg-indigo-50',
+ markerDot: 'bg-indigo-500',
+ },
+};
+
+function normalizeTemplateStatus(status) {
+ return status === 'whitelisted' ? 'whitelisted' : 'pending_whitelisting';
+}
+
+function buildSelectedTemplatePreview(template = {}) {
+ const selectedTemplate = String(template?.selectedTemplate || '').trim();
+ if (!selectedTemplate) return null;
+
+ return {
+ eventSlug: String(template?.eventSlug || '').trim(),
+ selectedTemplate,
+ status: normalizeTemplateStatus(template?.status),
+ templateId: String(template?.templateId || '').trim(),
+ variableMap: template?.variableMap && typeof template.variableMap === 'object'
+ ? template.variableMap
+ : {},
+ curlProfileId: String(template?.curlProfileId || '').trim(),
+ };
+}
+
function getEventGroupId(event) {
const slug = String(event?.slug || '');
@@ -212,17 +263,16 @@ function buildTemplateUiState(templates = []) {
const nextVariants = {};
const nextGenState = {};
const nextTemplateStatusBySlug = {};
+ const nextSelectedTemplateBySlug = {};
templates.forEach((template) => {
if (!template?.eventSlug) return;
if (template.selectedTemplate) {
- if (template.status === 'whitelisted') {
- nextTemplateStatusBySlug[template.eventSlug] = 'whitelisted';
- } else {
- nextTemplateStatusBySlug[template.eventSlug] = 'pending_whitelisting';
- }
+ const normalizedStatus = normalizeTemplateStatus(template.status);
+ nextTemplateStatusBySlug[template.eventSlug] = normalizedStatus;
nextGenState[template.eventSlug] = 'selected';
+ nextSelectedTemplateBySlug[template.eventSlug] = buildSelectedTemplatePreview(template);
return;
}
@@ -232,7 +282,7 @@ function buildTemplateUiState(templates = []) {
}
});
- return { nextVariants, nextGenState, nextTemplateStatusBySlug };
+ return { nextVariants, nextGenState, nextTemplateStatusBySlug, nextSelectedTemplateBySlug };
}
export default function Events() {
@@ -253,6 +303,7 @@ export default function Events() {
const [openVariableMenuKey, setOpenVariableMenuKey] = useState('');
const [activeCaretVariantKey, setActiveCaretVariantKey] = useState('');
const [templateStatusBySlug, setTemplateStatusBySlug] = useState({});
+ const [selectedTemplateBySlug, setSelectedTemplateBySlug] = useState({});
const [error, setError] = useState('');
const [readyToGenerate, setReadyToGenerate] = useState(false);
@@ -283,13 +334,19 @@ export default function Events() {
]);
const templates = templatesRes.data.templates || [];
- const { nextVariants, nextGenState, nextTemplateStatusBySlug } = buildTemplateUiState(templates);
+ const {
+ nextVariants,
+ nextGenState,
+ nextTemplateStatusBySlug,
+ nextSelectedTemplateBySlug,
+ } = buildTemplateUiState(templates);
setEvents(eventsRes.data.events || []);
setReadyToGenerate(!!activeProfileRes.data?.activeProfile?.rawCurl);
setVariants(nextVariants);
setGenState(nextGenState);
setTemplateStatusBySlug(nextTemplateStatusBySlug);
+ setSelectedTemplateBySlug(nextSelectedTemplateBySlug);
setVariantDrafts((currentDrafts) => syncDraftsWithVariants(currentDrafts, nextVariants));
} catch {
setError('Failed to load events');
@@ -353,6 +410,11 @@ export default function Events() {
delete nextStatuses[slug];
return nextStatuses;
});
+ setSelectedTemplateBySlug((currentTemplates) => {
+ const nextTemplates = { ...currentTemplates };
+ delete nextTemplates[slug];
+ return nextTemplates;
+ });
setGenState((state) => ({ ...state, [slug]: 'done' }));
} catch (err) {
setError(err.response?.data?.error || 'Generation failed');
@@ -413,7 +475,8 @@ export default function Events() {
setError('');
try {
- await apiClient.post(`/api/businesses/${businessId}/templates/${slug}/select`, { selectedVariant: variant });
+ const res = await apiClient.post(`/api/businesses/${businessId}/templates/${slug}/select`, { selectedVariant: variant });
+ const selectedTemplatePreview = buildSelectedTemplatePreview(res.data);
await refreshOnboardingState(businessId).catch(() => null);
setVariants((currentVariants) => ({ ...currentVariants, [slug]: [] }));
setVariantDrafts((currentDrafts) => removeDraftsForSlug(currentDrafts, slug));
@@ -421,6 +484,10 @@ export default function Events() {
setActiveCaretVariantKey('');
setGenState((state) => ({ ...state, [slug]: 'selected' }));
setTemplateStatusBySlug((currentStatuses) => ({ ...currentStatuses, [slug]: 'pending_whitelisting' }));
+ setSelectedTemplateBySlug((currentTemplates) => ({
+ ...currentTemplates,
+ [slug]: selectedTemplatePreview,
+ }));
if (shouldAutoAdvance) {
navigate(`/${businessId}/templates?event=${encodeURIComponent(slug)}`);
}
@@ -555,7 +622,7 @@ export default function Events() {
setShowAddForm((visible) => !visible)}
- className="px-4 py-2 rounded-lg bg-white border border-gray-300 text-sm text-gray-700 font-semibold hover:bg-white transition"
+ className="px-4 py-2 rounded-lg bg-white border border-gray-300 text-sm text-gray-700 font-semibold hover:bg-gray-50 hover:border-gray-400 transition"
>
{showAddForm ? 'Cancel' : '+ Add Event'}
@@ -605,37 +672,31 @@ export default function Events() {
{groupedEvents.map((group) => {
const isExpanded = searchTerm.trim() ? true : !!expandedGroups[group.id];
+ const groupStyle = EVENT_GROUP_STYLE_CONFIG[group.id] || EVENT_GROUP_STYLE_CONFIG.custom;
return (
toggleGroup(group.id)}
- className="flex w-full items-start justify-between gap-4 px-6 py-5 text-left transition hover:bg-white"
+ className="group flex w-full items-start justify-between gap-4 px-6 py-5 text-left transition hover:bg-gray-50"
>
-
+
+
+
{group.label}
-
+
{group.events.length} events
{group.description}
-
+
@@ -645,307 +706,323 @@ export default function Events() {
{isExpanded && (
- {group.events.map((event) => {
- const state = genState[event.slug] || 'idle';
- const eventVariants = variants[event.slug] || [];
- const templateStatus = templateStatusBySlug[event.slug] || 'unselected';
- const statusConfig = EVENT_TEMPLATE_STATUS_CONFIG[templateStatus] || EVENT_TEMPLATE_STATUS_CONFIG.unselected;
- const canViewTemplate = templateStatus !== 'unselected';
+ {group.events.map((event) => {
+ const state = genState[event.slug] || 'idle';
+ const eventVariants = variants[event.slug] || [];
+ const templateStatus = templateStatusBySlug[event.slug] || 'unselected';
+ const statusConfig = EVENT_TEMPLATE_STATUS_CONFIG[templateStatus] || EVENT_TEMPLATE_STATUS_CONFIG.unselected;
+ const selectedTemplatePreview = selectedTemplateBySlug[event.slug] || null;
+ const canViewTemplate = templateStatus !== 'unselected';
- return (
-
-
-
- {event.isDefault ? (
-
- ) : (
-
handleDelete(event.slug)}
- className="mt-0.5 w-6 h-6 rounded-full bg-white hover:bg-red-100 flex items-center justify-center border border-gray-200 text-gray-600 transition shrink-0"
- title="Delete event"
- >
-
-
- )}
-
-
{event.label}
-
-
-
-
-
-
-
- {canViewTemplate && (
- navigate(`/${businessId}/templates?event=${encodeURIComponent(event.slug)}`)}
- className="px-3.5 py-2 rounded-lg bg-white border border-gray-300 text-sm font-medium text-gray-700 hover:bg-white transition "
- >
- View in Templates
-
- )}
- handleGenerate(event.slug)}
- disabled={state === 'loading' || !readyToGenerate}
- className={`px-4 py-2 rounded-lg text-sm font-medium transition flex items-center gap-2 disabled:opacity-50 ${
- state === 'done' || state === 'selected'
- ? 'bg-white border border-gray-300 text-gray-700 hover:bg-white'
- : 'bg-white border border-gray-200 text-primary-dark hover:bg-indigo-100'
- }`}
- >
- {state === 'loading' ? (
- <> Generating…>
- ) : state === 'done' || state === 'selected' ? (
- <>↺ Regenerate>
- ) : (
- <>⚡ Generate Template>
- )}
-
-
-
-
- {eventVariants.length > 0 && (
-
-
Review, edit, and choose a variant
-
- {eventVariants.map((variant, index) => {
- const variantKey = getVariantKey(event.slug, index);
- const draft = variantDrafts[variantKey] || createVariantDraft(variant);
- const currentText = draft.currentText;
- const originalText = draft.originalText;
- const validationStatus = draft.validationStatus;
- const currentMatchesCheckedText = draft.lastCheckedText === currentText;
- const isEdited = currentText !== originalText;
- const dltTokenCount = countDltTokens(currentText);
- const invalidDltTokens = getInvalidDltTokens(currentText);
- const hasMalformedDltToken = hasMalformedDltFragments(currentText);
- const hasInvalidPlaceholder = invalidDltTokens.length > 0 || hasMalformedDltToken;
- const tooLong = currentText.length > MAX_SMS_LENGTH;
- const isSelectingThis = selectingVariantKey === variantKey;
- const isSelectingAnotherVariant = !!selectingVariantKey
- && selectingVariantKey !== variantKey
- && selectingVariantKey.startsWith(`${event.slug}:`);
- const canRunCheck = isEdited && !tooLong && !hasInvalidPlaceholder && validationStatus !== 'checking';
- const canUseEdited = isEdited
- && validationStatus === 'approved'
- && currentMatchesCheckedText
- && !tooLong
- && !hasInvalidPlaceholder;
- const canInsertVariable = activeCaretVariantKey === variantKey;
-
- return (
-
-
-
-
- {isEdited ? 'Edited Draft' : 'Original Draft'}
-
-
- {validationStatus === 'checking' && (
-
- Checking edit…
-
- )}
-
- {validationStatus === 'approved' && currentMatchesCheckedText && (
-
- Edit passed check
-
- )}
-
- {validationStatus === 'rejected' && currentMatchesCheckedText && (
-
- Needs changes
-
- )}
-
-
-
{
- if (node) variableMenuRefs.current[variantKey] = node;
- else delete variableMenuRefs.current[variantKey];
- }}
- >
-
e.preventDefault()}
- onClick={() => handleVariableMenuToggle(variantKey)}
- disabled={!canInsertVariable}
- className="text-xs px-3 py-2 rounded-md bg-white border border-gray-200 text-primary-dark font-semibold hover:bg-white transition disabled:opacity-50 disabled:cursor-not-allowed"
- >
- # Add Variable
-
-
- {openVariableMenuKey === variantKey && (
-
-
-
Insert DLT Variable
+ return (
+
+
+
+ {event.isDefault ? (
+
-
- {DLT_VARIABLE_OPTIONS.map((option) => (
- e.preventDefault()}
- onClick={() => insertVariableToken(event.slug, index, option.token)}
- className="w-full px-4 py-2 text-left hover:bg-white transition flex items-center justify-between gap-3"
- >
- {option.label}
- {option.token}
-
- ))}
-
-
- )}
-
-
-
-
+
+ {eventVariants.length > 0 && (
+
+
Review, edit, and choose a variant
+
+ {eventVariants.map((variant, index) => {
+ const variantKey = getVariantKey(event.slug, index);
+ const draft = variantDrafts[variantKey] || createVariantDraft(variant);
+ const currentText = draft.currentText;
+ const originalText = draft.originalText;
+ const validationStatus = draft.validationStatus;
+ const currentMatchesCheckedText = draft.lastCheckedText === currentText;
+ const isEdited = currentText !== originalText;
+ const dltTokenCount = countDltTokens(currentText);
+ const invalidDltTokens = getInvalidDltTokens(currentText);
+ const hasMalformedDltToken = hasMalformedDltFragments(currentText);
+ const hasInvalidPlaceholder = invalidDltTokens.length > 0 || hasMalformedDltToken;
+ const tooLong = currentText.length > MAX_SMS_LENGTH;
+ const isSelectingThis = selectingVariantKey === variantKey;
+ const isSelectingAnotherVariant = !!selectingVariantKey
+ && selectingVariantKey !== variantKey
+ && selectingVariantKey.startsWith(`${event.slug}:`);
+ const canRunCheck = isEdited && !tooLong && !hasInvalidPlaceholder && validationStatus !== 'checking';
+ const canUseEdited = isEdited
+ && validationStatus === 'approved'
+ && currentMatchesCheckedText
+ && !tooLong
+ && !hasInvalidPlaceholder;
+ const canInsertVariable = activeCaretVariantKey === variantKey;
+
+ return (
+
+
+
+
+ {isEdited ? 'Edited Draft' : 'Original Draft'}
+
+
+ {validationStatus === 'checking' && (
+
+ Checking edit…
+
+ )}
+
+ {validationStatus === 'approved' && currentMatchesCheckedText && (
+
+ Edit passed check
+
+ )}
+
+ {validationStatus === 'rejected' && currentMatchesCheckedText && (
+
+ Needs changes
+
+ )}
+
+
+
{
+ if (node) variableMenuRefs.current[variantKey] = node;
+ else delete variableMenuRefs.current[variantKey];
+ }}
+ >
+
e.preventDefault()}
+ onClick={() => handleVariableMenuToggle(variantKey)}
+ disabled={!canInsertVariable}
+ className="text-xs px-3 py-2 rounded-md bg-white border border-gray-200 text-primary-dark font-semibold hover:bg-gray-50 hover:border-gray-300 transition disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ # Add Variable
+
+
+ {openVariableMenuKey === variantKey && (
+
+
+
+ {DLT_VARIABLE_OPTIONS.map((option) => (
+ e.preventDefault()}
+ onClick={() => insertVariableToken(event.slug, index, option.token)}
+ className="flex w-full items-center justify-between gap-3 px-4 py-2 text-left transition hover:bg-gray-50"
+ >
+ {option.label}
+ {option.token}
+
+ ))}
+
+
+ )}
+
+
+
+
+ );
+ })}
+
+
)}
-
- );
- })}
-
-
- )}
-
- );
- })}
+ );
+ })}
)}
diff --git a/client/src/pages/GlobalSms.jsx b/client/src/pages/GlobalSms.jsx
index bc8fa31..9749a6f 100644
--- a/client/src/pages/GlobalSms.jsx
+++ b/client/src/pages/GlobalSms.jsx
@@ -65,6 +65,8 @@ export default function GlobalSms() {
}, [loadProfiles]);
const activeProfile = profiles.find(p => p.id === activeProfileId) || null;
+ const hasProfiles = profiles.length > 0;
+ const isCreatingFirstProfile = !hasProfiles && !editingId;
const pData = activeProfile?.provider || {};
const missingFields = [];
if (activeProfile && !pData.providerName) missingFields.push('providerName');
@@ -303,104 +305,140 @@ export default function GlobalSms() {
)}
- {/* Profiles List */}
-
-
All Profiles
- {profiles.length > 0 ? (
- profiles.map(p => {
+ {hasProfiles && (
+
+
All Profiles
+ {profiles.map(p => {
const isActive = p.id === activeProfileId;
return (
-
-
-
-
{p.name}
- {isActive && (
-
- Active Profile
-
+
+
+
+
+
{p.name}
+ {isActive && (
+
+ Active Profile
+
+ )}
+ {p.isDefault && !isActive && (
+
+ Default
+
+ )}
+
+
+ Updated: {new Date(p.updatedAt).toLocaleString()}
+
+
+
+
+ {!isActive && (
+ handleActivate(p.id)}
+ className="rounded-lg bg-primary-blue px-4 py-2 text-sm font-semibold text-white transition hover:bg-primary-dark"
+ >
+ Use this cURL
+
)}
- {p.isDefault && !isActive && (
-
- Default
-
+ handleEditClick(p)}
+ className="rounded-lg border border-border-main px-3 py-2 text-sm font-medium text-text-muted transition hover:border-primary-blue hover:bg-page-bg hover:text-primary-blue"
+ >
+ Edit
+
+ {profiles.length > 1 && (
+ handleDelete(p.id)}
+ className="rounded-lg border border-border-main px-3 py-2 text-sm font-medium text-text-muted transition hover:border-error-text hover:bg-red-50 hover:text-error-text"
+ >
+ Delete
+
)}
-
Updated: {new Date(p.updatedAt).toLocaleString()}
-
- {p.rawCurl}
-
-
-
- {!isActive && (
- handleActivate(p.id)}
- className="px-4 py-2 bg-primary-blue hover:bg-primary-dark text-white text-sm font-semibold rounded-lg transition"
- >
- Use this cURL
-
- )}
- handleEditClick(p)}
- className="px-3 py-2 border border-border-main text-text-muted hover:text-primary-blue hover:border-primary-blue rounded-lg text-sm font-medium transition"
- >
- Edit
-
- {profiles.length > 1 && (
- handleDelete(p.id)}
- className="px-3 py-2 border border-border-main text-text-muted hover:text-error-text hover:border-error-text hover:bg-white rounded-lg text-sm font-medium transition"
- >
- Delete
-
- )}
);
- })
- ) : (
-
-
No cURL profiles configured yet.
-
- )}
-
+ })}
+
+ )}
{/* Inline Form (Create / Edit) */}
-
-
-
- {editingId ? 'Edit Profile' : 'Add New Profile'}
-
+
+
+
+
+ {editingId ? 'Edit Profile' : isCreatingFirstProfile ? 'Create Your First cURL Profile' : 'Add New Profile'}
+
+
+ Give this profile a recognizable name, then paste the full provider cURL command below.
+
+
{editingId && (
-
+
Switch to Add New
)}
-
+
+ {isCreatingFirstProfile && (
+
+
Start by adding a cURL profile
+
+ This becomes the base for validating provider details and unlocking event template generation.
+
+
+ )}