diff --git a/Dockerfile b/Dockerfile index da5775e..ef786a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ RUN npm ci --omit=dev COPY server/index.js ./ COPY server/fdk.js ./ +COPY server/postgresFdkStorage.js ./ COPY server/config ./config COPY server/routes ./routes COPY server/services ./services diff --git a/client/src/App.jsx b/client/src/App.jsx index 981889f..b5757a8 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -18,14 +18,14 @@ function SubLayout({ children }) { {hasGlobalSms && ( )} -
+
{children}
@@ -42,7 +42,7 @@ function BusinessGuard({ children, isGlobalSmsRoute }) { if (loading) { return (
-
+
); } diff --git a/client/src/components/ImagePicker.jsx b/client/src/components/ImagePicker.jsx index df8978e..a9931cc 100644 --- a/client/src/components/ImagePicker.jsx +++ b/client/src/components/ImagePicker.jsx @@ -24,7 +24,7 @@ export default function ImagePicker({ currentImage, onSelect }) { } if (images.length === 0) { - return
No images available for this brand.
; + return
No images available for this brand.
; } return ( @@ -37,14 +37,14 @@ export default function ImagePicker({ currentImage, onSelect }) { onClick={() => onSelect(img.url)} className={`relative rounded-lg overflow-hidden border-2 aspect-video transition-all ${ isSelected - ? 'border-primary-blue ring-2 ring-primary-blue ring-opacity-50 shadow-md' - : 'border-transparent hover:border-gray-300 opacity-80 hover:opacity-100 shadow-sm' + ? 'border-primary-blue -blue ' + : 'border-transparent hover:border-gray-300 opacity-80 hover:opacity-100 ' }`} > {`brand-pic-${i}`}
{isSelected && ( -
+
)} diff --git a/client/src/components/RegisterBusinessModal.jsx b/client/src/components/RegisterBusinessModal.jsx index 428ebd1..151b9fc 100644 --- a/client/src/components/RegisterBusinessModal.jsx +++ b/client/src/components/RegisterBusinessModal.jsx @@ -26,16 +26,16 @@ export default function RegisterBusinessModal({ onClose }) { return (
-
+
{status === 'success' && (
-
-

Business Added!

+
+

Business Added!

Your business has been registered successfully.

@@ -45,7 +45,7 @@ export default function RegisterBusinessModal({ onClose }) { {(status === 'idle' || status === 'loading' || status === 'error') && ( <>
-

Add a Business

+

Add a Business

Enter the storefront website URL and we'll scrape it to detect the brand and set up your business.

@@ -60,13 +60,13 @@ export default function RegisterBusinessModal({ onClose }) { onChange={(e) => setUrl(e.target.value)} placeholder="https://yourstore.com" disabled={status === 'loading'} - className="w-full px-4 py-2.5 rounded-lg bg-white border border-gray-300 text-gray-900 placeholder-gray-400 font-medium focus:outline-none focus:ring-2 focus:ring-primary-blue focus:border-transparent transition disabled:opacity-50 text-sm shadow-sm" + className="w-full px-4 py-2 rounded-lg bg-white border border-gray-300 text-gray-800 placeholder-gray-400 font-medium focus:outline-none focus:ring-2 focus:ring-primary-blue focus:border-transparent transition disabled:opacity-50 text-sm " required />
{status === 'error' && ( -

{error}

+

{error}

)}
@@ -74,14 +74,14 @@ export default function RegisterBusinessModal({ onClose }) { type="button" onClick={onClose} disabled={status === 'loading'} - className="flex-[0.8] py-2.5 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 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-white text-sm font-medium transition disabled:opacity-50" > Cancel {activeBusiness && (
-
+
{activeBusiness.brandName?.[0]?.toUpperCase() || 'B'}
-

{activeBusiness.brandName}

+

{activeBusiness.brandName}

{activeBusiness.domain}

@@ -161,10 +161,10 @@ export default function Sidebar() { {item.enabled ? ( {SVG_ICONS[item.id]} @@ -186,7 +186,7 @@ export default function Sidebar() { ) : (
{SVG_ICONS[item.id]} {item.label} @@ -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 155b184..98aece9 100644 --- a/client/src/components/TestSmsModal.jsx +++ b/client/src/components/TestSmsModal.jsx @@ -28,11 +28,11 @@ export default function TestSmsModal({ businessId, template, onClose }) { return (
-
-
+
+
📱
-

Test SMS

+

Test SMS

Enter a phone number to send a real test SMS for {template.eventLabel || template.eventSlug.replace(/_/g, ' ')}

@@ -40,7 +40,7 @@ export default function TestSmsModal({ businessId, template, onClose }) { {!result ? (
{error && ( -
+
{error}
)} @@ -52,7 +52,7 @@ export default function TestSmsModal({ businessId, template, onClose }) { value={toNumber} onChange={e => setToNumber(e.target.value)} placeholder="e.g. 919876543210 (with country code)" - className="w-full px-4 py-2.5 rounded-lg bg-gray-50 border border-gray-300 font-mono text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" + className="w-full px-4 py-2 rounded-lg bg-white border border-gray-300 font-mono text-gray-800 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" autoFocus required /> @@ -64,14 +64,14 @@ export default function TestSmsModal({ businessId, template, onClose }) { type="button" onClick={onClose} disabled={sending} - className="flex-1 py-2.5 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium transition disabled:opacity-50" + 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" > Cancel @@ -79,21 +79,21 @@ export default function TestSmsModal({ businessId, template, onClose }) { ) : (
-
+
{result.success ? '✓ SMS sent successfully!' : '✗ SMS send failed'} {result.statusCode && HTTP {result.statusCode}}
{result.response && (
-
+                
                   {typeof result.response === 'string' ? result.response : JSON.stringify(result.response, null, 2)}
                 
)} diff --git a/client/src/components/WhitelistModal.jsx b/client/src/components/WhitelistModal.jsx index f28d9e8..a547ee1 100644 --- a/client/src/components/WhitelistModal.jsx +++ b/client/src/components/WhitelistModal.jsx @@ -104,8 +104,8 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC return (
-
-
+
+
@@ -127,7 +127,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC )} {error && ( -
+
{error}
)} @@ -141,7 +141,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC type="text" value={providerForm.providerName} onChange={e => setProviderForm(prev => ({ ...prev, providerName: e.target.value }))} - className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" + className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" placeholder="e.g. MSG91" autoFocus required @@ -156,7 +156,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC type="text" value={providerForm.senderId} onChange={e => setProviderForm(prev => ({ ...prev, senderId: e.target.value.toUpperCase() }))} - className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main font-mono uppercase text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" + className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main font-mono uppercase text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" placeholder="6 CHARS" maxLength={6} required @@ -171,7 +171,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC type="text" value={providerForm.dltEntityId} onChange={e => setProviderForm(prev => ({ ...prev, dltEntityId: e.target.value }))} - className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" + className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" placeholder="19-digit DLT PE ID" required /> @@ -183,7 +183,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC type="button" onClick={onClose} disabled={savingProvider} - className="flex-1 py-2.5 rounded-lg border border-border-main text-text-primary hover:bg-page-bg text-sm font-medium transition disabled:opacity-50" + className="flex-1 py-2 rounded-lg border border-border-main text-text-primary hover:bg-page-bg text-sm font-medium transition disabled:opacity-50" > Cancel @@ -195,7 +195,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC if (field === 'dltEntityId') return !providerForm.dltEntityId.trim(); return false; })} - className="flex-1 py-2.5 rounded-lg bg-primary-blue hover:bg-primary-dark text-white text-sm font-semibold transition shadow-sm disabled:opacity-50 flex items-center justify-center gap-2" + className="flex-1 py-2 rounded-lg bg-primary-blue hover:bg-primary-dark text-white text-sm font-semibold transition disabled:opacity-50 flex items-center justify-center gap-2" > {savingProvider ? <> Saving… : 'Save Details'} @@ -210,7 +210,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC value={templateId} onChange={e => setTemplateId(e.target.value)} placeholder="e.g. 1234567890987654321" - className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" + className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" autoFocus required /> @@ -223,7 +223,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC value={toNumber} onChange={e => setToNumber(e.target.value)} placeholder="e.g. 919876543210" - className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" + className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm" required />

This sends the publish-triggering SMS request.

@@ -234,14 +234,14 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC type="button" onClick={onClose} disabled={publishing} - className="flex-1 py-2.5 rounded-lg border border-border-main text-text-primary hover:bg-page-bg text-sm font-medium transition disabled:opacity-50" + className="flex-1 py-2 rounded-lg border border-border-main text-text-primary hover:bg-page-bg text-sm font-medium transition disabled:opacity-50" > Cancel diff --git a/client/src/pages/Brand.jsx b/client/src/pages/Brand.jsx index b87b026..d512498 100644 --- a/client/src/pages/Brand.jsx +++ b/client/src/pages/Brand.jsx @@ -13,26 +13,26 @@ const NAV_CARDS = [ function DeleteConfirmModal({ brandName, onCancel, onConfirm, deleting }) { return (
-
-
+
+
🗑
-

Delete Brand?

+

Delete Brand?

- This will permanently delete {brandName} and all associated events, templates, and images. This cannot be undone. + This will permanently delete {brandName} and all associated events, templates, and images. This cannot be undone.

@@ -64,8 +64,8 @@ export default function Brand() { if (loading) { return ( -
-
+
+
); } @@ -73,18 +73,18 @@ export default function Brand() { // — WELCOME SCREEN — if (!brand) { return ( -
+
-
+
S
-

SMS Template Extension

+

SMS Template Extension

Generate TRAI-compliant SMS templates for your Fynd store. We'll scrape your website and use AI to extract your brand context automatically.

@@ -96,17 +96,17 @@ export default function Brand() { // — BRAND DETAIL PAGE — return ( -
+
{/* Brand header card */} -
+
-

{brand.brandName}

+

{brand.brandName}

{brand.domain}

- + {brand.tone} @@ -116,7 +116,7 @@ export default function Brand() {
@@ -142,7 +142,7 @@ export default function Brand() { {brand.colors.map((c, i) => (
@@ -156,11 +156,11 @@ export default function Brand() { {/* Brand images */} {brand.relevantImagePaths?.length > 0 && ( -
+

Brand Images

{brand.relevantImagePaths.map((url, i) => ( -
+
{`brand
{card.icon}
-

{card.label}

+

{card.label}

{card.desc}

))} diff --git a/client/src/pages/Businesses.jsx b/client/src/pages/Businesses.jsx index b4a1d58..72e1664 100644 --- a/client/src/pages/Businesses.jsx +++ b/client/src/pages/Businesses.jsx @@ -5,36 +5,36 @@ import { useBusiness } from '../context/BusinessContext'; import RegisterBusinessModal from '../components/RegisterBusinessModal'; import { fetchActiveSalesChannels } from '../utils/fyndSalesChannels'; import { + getApplicationId, getBusinessDomain, getBusinessImage, getBusinessName, getBusinessTagline, - getChannelId, } from '../utils/businessProfile'; function DeleteConfirmModal({ businessName, onCancel, onConfirm, deleting }) { return (
-
-
+
+
🗑
-

Delete Business?

+

Delete Business?

- This will permanently delete {businessName} and all its events, templates, and images. This cannot be undone. + This will permanently delete {businessName} and all its events, templates, and images. This cannot be undone.

@@ -52,13 +52,13 @@ function BusinessCreatedModal({ business, onClose }) { return (
-
-
-

Business Added!

+
+
+

Business Added!

Your business is ready for onboarding.

-
+
-
+
{image ? ( {name} ) : ( @@ -74,7 +74,7 @@ function BusinessCreatedModal({ business, onClose }) {
@@ -83,37 +83,125 @@ function BusinessCreatedModal({ business, onClose }) { ); } -function SalesChannelCard({ channel, disabled, onImport }) { - const name = getBusinessName(channel); - const domain = getBusinessDomain(channel); - const image = getBusinessImage(channel); +function StatusBadge({ status }) { + const isScraped = status === 'scraped'; return ( -
-
-
- {image ? ( - {name} - ) : ( - {name?.[0]?.toUpperCase() || 'S'} - )} -
-
-

{name}

-

{domain || 'Domain unavailable'}

+ + + {isScraped ? 'Scraped' : 'Not Scraped Yet'} + + ); +} + +function UnifiedBusinessCard({ + item, + selectingBusinessId, + creatingSalesChannelId, + onSelect, + onImport, + onDelete, + onFallback, +}) { + const entity = item.business || item.channel; + const businessId = item.business?.businessId || ''; + const channelId = getApplicationId(item.channel); + const image = getBusinessImage(entity); + const name = getBusinessName(entity); + const domain = getBusinessDomain(entity); + const tagline = getBusinessTagline(entity); + const isScraped = item.status === 'scraped'; + const isOpening = isScraped && selectingBusinessId === businessId; + const isImporting = !isScraped && creatingSalesChannelId === channelId; + const hasWebsiteUrl = Boolean(item.channel?.websiteUrl); + + return ( +
+
+
+
+
+ {image ? ( + {name} + ) : ( + + {name?.[0]?.toUpperCase() || 'B'} + + )} +
+
+

{name}

+ {domain && ( +

{domain}

+ )} + {tagline && ( +

{tagline}

+ )} +
+
+
+ + {isScraped && item.business?.createdAt && ( +

+ Added {new Date(item.business.createdAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })} +

+ )} + + {!isScraped && !hasWebsiteUrl && ( +

+ A website URL could not be derived automatically for this sales channel. +

+ )}
-
- - {channel.websiteUrl ? 'Ready to scrape' : 'Use manual URL fallback'} - - + +
+ {isScraped ? ( + <> + + + + ) : ( + <> + + + {hasWebsiteUrl ? 'Ready to scrape' : 'Needs manual URL'} + + + )}
); @@ -135,29 +223,65 @@ export default function Businesses() { const [deleting, setDeleting] = useState(false); const [error, setError] = useState(''); - const configuredApplicationIds = useMemo(() => ( - new Set( + const showUnifiedSalesChannelView = salesChannelsStatus === 'success'; + + const unifiedEntries = useMemo(() => { + const matchedBusinessIds = new Set(); + const businessByApplicationId = new Map( businesses - .map((business) => String(business?.applicationId || '').trim()) - .filter(Boolean) - ) - ), [businesses]); + .map((business) => [getApplicationId(business), business]) + .filter(([applicationId]) => Boolean(applicationId)) + ); - const availableSalesChannels = useMemo(() => ( - salesChannels.filter((channel) => !configuredApplicationIds.has(getChannelId(channel))) - ), [configuredApplicationIds, salesChannels]); - const showSalesChannelsSection = salesChannelsStatus === 'success' && availableSalesChannels.length > 0; + const mergedEntries = salesChannels.map((channel, index) => { + const applicationId = getApplicationId(channel); + const business = applicationId ? businessByApplicationId.get(applicationId) || null : null; - const filteredSalesChannels = useMemo(() => { - const query = salesChannelQuery.trim().toLowerCase(); - if (!query) return availableSalesChannels; + if (business?.businessId) { + matchedBusinessIds.add(business.businessId); + } - return availableSalesChannels.filter((channel) => { - const name = String(getBusinessName(channel) || '').toLowerCase(); - const domain = String(getBusinessDomain(channel) || '').toLowerCase(); - return name.includes(query) || domain.includes(query); + return { + key: `channel:${applicationId || channel.name || channel.domain || index}`, + status: business ? 'scraped' : 'not_scraped', + applicationId, + business, + channel, + }; }); - }, [availableSalesChannels, salesChannelQuery]); + + const standaloneBusinesses = businesses + .filter((business) => !matchedBusinessIds.has(business.businessId)) + .map((business) => ({ + key: `business:${business.businessId}`, + status: 'scraped', + applicationId: getApplicationId(business), + business, + channel: null, + })); + + return [...mergedEntries, ...standaloneBusinesses].sort((left, right) => { + if (left.status !== right.status) { + return left.status === 'not_scraped' ? -1 : 1; + } + + return getBusinessName(left.business || left.channel) + .localeCompare(getBusinessName(right.business || right.channel)); + }); + }, [businesses, salesChannels]); + + const filteredUnifiedEntries = useMemo(() => { + const query = salesChannelQuery.trim().toLowerCase(); + if (!query) return unifiedEntries; + + return unifiedEntries.filter((entry) => { + const entity = entry.business || entry.channel; + const name = String(getBusinessName(entity) || '').toLowerCase(); + const domain = String(getBusinessDomain(entity) || '').toLowerCase(); + const tagline = String(getBusinessTagline(entity) || '').toLowerCase(); + return name.includes(query) || domain.includes(query) || tagline.includes(query); + }); + }, [unifiedEntries, salesChannelQuery]); const loadBusinesses = useCallback(async () => { const res = await apiClient.get('/api/businesses'); @@ -210,11 +334,11 @@ export default function Businesses() { } async function handleCreateFromSalesChannel(channel) { - const applicationId = getChannelId(channel); + const applicationId = getApplicationId(channel); if (!applicationId) return; if (!channel.websiteUrl) { - setError('A website URL could not be derived from this sales channel. Please use Add Business and enter the URL manually.'); + setError('A website URL could not be derived from this sales channel. Please use the fallback URL flow to continue.'); return; } @@ -252,162 +376,132 @@ export default function Businesses() { if (loading) { return ( -
-
+
+
); } return ( -
+
-

- {businesses.length > 0 ? 'Your Businesses' : 'Set Up Your First Business'} +

+ {showUnifiedSalesChannelView ? 'Your Sales Channels' : (businesses.length > 0 ? 'Your Businesses' : 'Set Up Your First Business')}

- {showSalesChannelsSection - ? 'Import from an active sales channel when available, or use the website URL fallback to scrape manually.' + {showUnifiedSalesChannelView + ? 'View every connected sales channel in one place and scrape the ones that are not onboarded yet.' : 'Add a storefront URL and we’ll scrape it to set up your business.'}

- + {!showUnifiedSalesChannelView && ( + + )}
{error && ( -
+
{error} - +
)} -
-
-

Configured Businesses

-

Select a business to manage its SMS templates.

-
- - {businesses.length > 0 ? ( -
- {businesses.map((biz) => ( -
- -
- Click to manage → - -
-
- ))} -
- ) : ( -
-

No configured businesses yet.

-

Use Add Business to enter a storefront URL and get started.

-
- )} -
- - {showSalesChannelsSection && ( -
-
-
-

Active Sales Channels

-

These are pulled directly from Commerce and can be scraped into businesses with one click.

-
- -
-
+ {showUnifiedSalesChannelView ? ( +
- - setSalesChannelQuery(e.target.value)} - placeholder="Search by channel name or domain" - className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm text-gray-900 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-blue focus:border-transparent" - /> +

Sales Channels

+

Scraped businesses and active sales channels are shown together here.

-
- {filteredSalesChannels.map((channel) => { - const channelId = getChannelId(channel); - return ( - handleCreateFromSalesChannel(channel)} + {unifiedEntries.length > 0 ? ( +
+
+ + setSalesChannelQuery(e.target.value)} + placeholder="Search by name, domain, or description" + className="w-full rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-800 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-blue focus:border-transparent" /> - ); - })} -
+
- {filteredSalesChannels.length === 0 && ( -
-

No active sales channels matched your search.

-

Use the website URL fallback if you want to scrape a storefront directly.

+
+ {filteredUnifiedEntries.map((item) => ( + setShowModal(true)} + /> + ))} +
+ + {filteredUnifiedEntries.length === 0 && ( +
+

No sales channels matched your search.

+

Try a different name or domain.

+
+ )} +
+ ) : ( +
+

No sales channels are available yet.

+

Use the manual fallback only if you need to set up a storefront URL directly.

+
)} -
-
+
+ ) : ( +
+
+

Configured Businesses

+

Select a business to manage its SMS templates.

+
+ + {businesses.length > 0 ? ( +
+ {businesses.map((biz) => ( + setShowModal(true)} + /> + ))} +
+ ) : ( +
+

No configured businesses yet.

+

Use Add Business to enter a storefront URL and get started.

+
+ )} +
)}
{showModal && ( - { setShowModal(false); loadBusinesses(); }} /> + { setShowModal(false); load(); }} /> )} {createdBusiness && setCreatedBusiness(null)} />} {deleteTarget && ( diff --git a/client/src/pages/Events.jsx b/client/src/pages/Events.jsx index 43d7cdb..9cc12a9 100644 --- a/client/src/pages/Events.jsx +++ b/client/src/pages/Events.jsx @@ -102,18 +102,18 @@ const DEFAULT_EXPANDED_GROUPS = EVENT_GROUPS.reduce((acc, group) => { const EVENT_TEMPLATE_STATUS_CONFIG = { unselected: { label: 'No template selected', - wrapper: 'border-gray-200 bg-gray-50 text-gray-500', + wrapper: 'border-gray-200 bg-white text-gray-500', dot: 'bg-gray-400', }, pending_whitelisting: { label: 'Pending Whitelisting', - wrapper: 'border-amber-200 bg-amber-50 text-amber-700', - dot: 'bg-amber-500', + wrapper: 'border-gray-200 bg-white text-gray-700', + dot: 'bg-white0', }, whitelisted: { label: 'Published', - wrapper: 'border-green-200 bg-green-50 text-green-700', - dot: 'bg-green-500', + wrapper: 'border-gray-200 bg-white text-gray-700', + dot: 'bg-white0', }, }; @@ -507,7 +507,7 @@ export default function Events() { if (loading) { return (
-
+
); } @@ -519,7 +519,7 @@ export default function Events() {
-

Events

+

Events

Generate SMS templates for each order event.

@@ -533,7 +533,7 @@ export default function Events() { value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} placeholder="Search events" - className="w-full rounded-lg border border-gray-300 bg-white py-3 pl-11 pr-10 text-sm font-medium text-gray-900 placeholder-gray-400 shadow-sm transition focus:border-primary-blue focus:outline-none focus:ring-2 focus:ring-indigo-100" + className="w-full rounded-lg border border-gray-300 bg-white py-2 pl-11 pr-10 text-sm font-medium text-gray-800 placeholder-gray-400 transition focus:border-primary-blue focus:outline-none focus:ring-2 focus:ring-indigo-100" /> {searchTerm && ( @@ -564,32 +564,32 @@ export default function Events() {
{!readyToGenerate && ( -
+
⚠️ Set up and activate a cURL profile before generating templates.
)} {error && ( -
+
{error} - +
)} {showAddForm && ( -
+ setNewLabel(e.target.value)} placeholder="Event name (e.g. Return Initiated)" - className="flex-1 px-4 py-2.5 rounded-lg bg-white border border-gray-300 text-gray-900 placeholder-gray-400 font-medium focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm shadow-sm" + className="flex-1 px-4 py-2 rounded-lg bg-white border border-gray-300 text-gray-800 placeholder-gray-400 font-medium focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm " autoFocus /> @@ -597,8 +597,8 @@ export default function Events() { )} {groupedEvents.length === 0 ? ( -
-

No events match your search.

+
+

No events match your search.

Try a different keyword or clear the search to see the full lifecycle list.

) : ( @@ -607,26 +607,26 @@ export default function Events() { const isExpanded = searchTerm.trim() ? true : !!expandedGroups[group.id]; return ( -
+
{isExpanded && ( -
+
{group.events.map((event) => { const state = genState[event.slug] || 'idle'; @@ -653,7 +653,7 @@ export default function Events() { const canViewTemplate = templateStatus !== 'unselected'; return ( -
+
{event.isDefault ? ( @@ -663,14 +663,14 @@ export default function Events() { ) : ( )}
-

{event.label}

+

{event.label}

@@ -678,7 +678,7 @@ export default function Events() { @@ -686,7 +686,7 @@ export default function Events() { @@ -694,10 +694,10 @@ export default function Events() {
{eventVariants.length > 0 && ( -
+

Review, edit, and choose a variant

{eventVariants.map((variant, index) => { @@ -743,9 +743,9 @@ export default function Events() { return (
@@ -753,26 +753,26 @@ export default function Events() {
{isEdited ? 'Edited Draft' : 'Original Draft'} {validationStatus === 'checking' && ( - + Checking edit… )} {validationStatus === 'approved' && currentMatchesCheckedText && ( - + Edit passed check )} {validationStatus === 'rejected' && currentMatchesCheckedText && ( - + Needs changes )} @@ -790,14 +790,14 @@ export default function Events() { onMouseDown={(e) => e.preventDefault()} onClick={() => handleVariableMenuToggle(variantKey)} disabled={!canInsertVariable} - className="text-xs px-3 py-2 rounded-md bg-white border border-refresh-active text-primary-dark font-semibold hover:bg-refresh-hover transition disabled:opacity-50 disabled:cursor-not-allowed" + 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

@@ -807,7 +807,7 @@ export default function Events() { type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => insertVariableToken(event.slug, index, option.token)} - className="w-full px-4 py-3 text-left hover:bg-refresh-hover transition flex items-center justify-between gap-3" + className="w-full px-4 py-2 text-left hover:bg-white transition flex items-center justify-between gap-3" > {option.label} {option.token} @@ -831,10 +831,10 @@ export default function Events() { onSelect={(e) => trackTextareaSelection(variantKey, e.target)} onKeyUp={(e) => trackTextareaSelection(variantKey, e.target)} rows={4} - className={`w-full rounded-lg border px-4 py-3 text-sm text-gray-800 font-mono leading-relaxed resize-y focus:outline-none focus:ring-2 ${ + className={`w-full rounded-lg border px-4 py-2 text-sm text-gray-800 font-mono leading-relaxed resize-y focus:outline-none focus:ring-2 ${ isEdited - ? 'border-amber-200 bg-amber-50/40 focus:ring-amber-200 focus:border-amber-300' - : 'border-gray-200 bg-gray-50 focus:ring-indigo-100 focus:border-primary-blue' + ? 'border-gray-200 bg-white/40 focus:ring-amber-200 focus:border-amber-300' + : 'border-gray-200 bg-white focus:ring-indigo-100 focus:border-primary-blue' }`} /> @@ -842,12 +842,12 @@ export default function Events() {
{currentText.length} / {MAX_SMS_LENGTH} - + DLT vars: {dltTokenCount}
@@ -855,7 +855,7 @@ export default function Events() { {isEdited && ( @@ -863,33 +863,33 @@ export default function Events() {
{isEdited && ( -
+

Original generated version

{originalText}

)} {invalidDltTokens.length > 0 && ( -
+
Unsupported DLT variable token{invalidDltTokens.length > 1 ? 's' : ''}: {invalidDltTokens.join(', ')}. Use only {DLT_VARIABLE_OPTIONS.map((option) => option.token).join(', ')}.
)} {hasMalformedDltToken && invalidDltTokens.length === 0 && ( -
+
Finish or remove incomplete DLT placeholder text before checking or selecting this edit.
)} {tooLong && ( -
+
Shorten this template to {MAX_SMS_LENGTH} characters or less before checking or using the edited version.
)} {validationStatus === 'rejected' && currentMatchesCheckedText && draft.why && ( -
+
Why it did not pass: {draft.why}
)} @@ -908,7 +908,7 @@ export default function Events() { diff --git a/client/src/pages/GlobalSms.jsx b/client/src/pages/GlobalSms.jsx index fe75403..bc8fa31 100644 --- a/client/src/pages/GlobalSms.jsx +++ b/client/src/pages/GlobalSms.jsx @@ -195,31 +195,31 @@ export default function GlobalSms() {
{error && ( -
+
{error}
)} {success && ( -
+
{success} - +
)} {/* Active Profile Setup Review Block */} {activeProfile && ( -
+

Active Setup: {activeProfile.name}

{isSetupComplete ? ( - Setup Complete + Setup Complete ) : ( - Missing Information + Missing Information )}
-
+

Parsed Provider Data:

    @@ -243,7 +243,7 @@ export default function GlobalSms() {
{!isSetupComplete && ( -
+

Please fill in the missing fields:

{missingFields.includes('providerName') && ( @@ -280,7 +280,7 @@ export default function GlobalSms() { @@ -293,7 +293,7 @@ export default function GlobalSms() {

Your active cURL profile is fully configured.

@@ -310,17 +310,17 @@ export default function GlobalSms() { profiles.map(p => { const isActive = p.id === activeProfileId; return ( -
+

{p.name}

{isActive && ( - + Active Profile )} {p.isDefault && !isActive && ( - + Default )} @@ -334,7 +334,7 @@ export default function GlobalSms() { {!isActive && ( @@ -348,7 +348,7 @@ export default function GlobalSms() { {profiles.length > 1 && ( @@ -365,7 +365,7 @@ export default function GlobalSms() {
{/* Inline Form (Create / Edit) */} -
+

{editingId ? 'Edit Profile' : 'Add New Profile'} @@ -376,7 +376,7 @@ export default function GlobalSms() { )}

-
+
@@ -385,7 +385,7 @@ export default function GlobalSms() { value={formName} onChange={e => setFormName(e.target.value)} placeholder="e.g. Production SMS, Staging Twilio" - className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue transition text-sm" + className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue transition text-sm" required />
@@ -395,7 +395,7 @@ export default function GlobalSms() { value={formCurl} onChange={e => setFormCurl(e.target.value)} placeholder="curl --request POST --url ..." - className="w-full h-40 px-4 py-3 rounded-lg font-mono text-sm bg-page-bg border border-border-main text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue transition resize-none leading-relaxed" + className="w-full h-40 px-4 py-2 rounded-lg font-mono text-sm bg-page-bg border border-border-main text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue transition resize-none leading-relaxed" required spellCheck="false" /> @@ -415,7 +415,7 @@ export default function GlobalSms() { @@ -424,7 +424,7 @@ export default function GlobalSms() { type="button" onClick={handleAddClick} disabled={saving} - className="px-5 py-2.5 rounded-lg border border-border-main text-text-primary hover:bg-page-bg font-medium text-sm transition" + className="px-5 py-2 rounded-lg border border-border-main text-text-primary hover:bg-page-bg font-medium text-sm transition" > Cancel Edit diff --git a/client/src/pages/Providers.jsx b/client/src/pages/Providers.jsx index 537ac11..dce5f87 100644 --- a/client/src/pages/Providers.jsx +++ b/client/src/pages/Providers.jsx @@ -73,7 +73,7 @@ export default function Providers() { if (loading) { return (
-
+
); } @@ -81,7 +81,7 @@ export default function Providers() { return (
-

Provider Configuration

+

Provider Configuration

Edit the provider details stored on the active cURL profile.

{activeProfile && (

@@ -91,20 +91,20 @@ export default function Providers() {

{error && ( -
+
{error} - +
)} {success && ( -
+
{success} - +
)} - -
+ +
-
+
@@ -154,18 +154,18 @@ export default function Providers() { type="password" value={form.authKey} onChange={e => handleChange('authKey', e.target.value)} - className="w-full px-4 py-2.5 rounded-lg bg-surface-white border border-border-main text-text-primary font-mono placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue focus:border-transparent transition text-sm shadow-sm" + className="w-full px-4 py-2 rounded-lg bg-surface-white border border-border-main text-text-primary font-mono placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue focus:border-transparent transition text-sm " placeholder="Authorization key for your SMS provider" />

Used as the Authorization header in your SMS requests.

-
+
diff --git a/client/src/pages/Templates.jsx b/client/src/pages/Templates.jsx index f244255..a90ff9b 100644 --- a/client/src/pages/Templates.jsx +++ b/client/src/pages/Templates.jsx @@ -6,8 +6,8 @@ import TestSmsModal from '../components/TestSmsModal'; const STATUS_CONFIG = { generated: { label: 'Generated', bg: 'bg-page-bg', text: 'text-text-muted', border: 'border-border-main' }, - pending_whitelisting: { label: 'Pending Whitelisting', bg: 'bg-tags-bg', text: 'text-tags-text', border: 'border-tags-border' }, - whitelisted: { label: 'Published', bg: 'bg-badge-bg', text: 'text-badge-text', border: 'border-badge-border' }, + pending_whitelisting: { label: 'Pending Whitelisting', bg: 'bg-white', text: 'text-gray-700', border: 'border-gray-200' }, + whitelisted: { label: 'Published', bg: 'bg-white', text: 'text-gray-700', border: 'border-gray-200' }, }; export default function Templates() { @@ -103,7 +103,7 @@ export default function Templates() { if (loading) { return (
-
+
); } @@ -111,12 +111,12 @@ export default function Templates() { return (
-

Templates

+

Templates

Track whitelisting status and test your SMS templates.

{error && ( -
+
{error}
@@ -146,7 +146,7 @@ export default function Templates() {
{templates.length === 0 ? ( -
+
@@ -186,15 +186,15 @@ export default function Templates() { delete templateCardRefs.current[tmpl.eventSlug]; } }} - className={`rounded-lg bg-white border shadow-sm overflow-hidden transition-all duration-300 ${ + className={`rounded-lg bg-white border overflow-hidden transition-all duration-300 ${ highlightedEventSlug === tmpl.eventSlug - ? 'border-primary-blue ring-2 ring-indigo-200 animate-pulse' + ? 'border-primary-blue animate-pulse' : 'border-gray-200' }`} > -
+
-

+

{tmpl.eventLabel || tmpl.eventSlug.replace(/_/g, ' ')}

{tmpl.eventSlug}

@@ -204,16 +204,16 @@ export default function Templates() {
-
+
{boundProfile ? ( -
+
{boundProfile.name} {boundProfile.id}
) : ( -
+
{boundProfileMessage}
)} @@ -221,7 +221,7 @@ export default function Templates() {
-
+
{tmpl.selectedTemplate}
@@ -229,7 +229,7 @@ export default function Templates() { {tmpl.templateId && (
-

+

{tmpl.templateId}

@@ -240,7 +240,7 @@ export default function Templates() {
{Object.entries(tmpl.variableMap).map(([key, val]) => ( -
+
{key} {val} @@ -254,7 +254,7 @@ export default function Templates() { {!isBoundProfileMissing && tmpl.status === 'pending_whitelisting' && ( @@ -262,7 +262,7 @@ export default function Templates() { {!isBoundProfileMissing && tmpl.status === 'whitelisted' && (