Some checks failed
Client Admin / build-deploy (push) Failing after 8s
Client Client / build-deploy (push) Failing after 3s
Client Registration / build-deploy (push) Failing after 20s
Client Tech / build-deploy (push) Failing after 1s
Client Home / build-deploy (push) Successful in 14s
159 lines
5.7 KiB
JavaScript
159 lines
5.7 KiB
JavaScript
import { createContext, useContext, useState, useEffect } from 'react';
|
|
import { useAuth } from '../auth/AuthProvider';
|
|
import { getWizardConfig } from '../services/wizardApi';
|
|
import { getAvailableChannels } from '../services/initiativeApi';
|
|
|
|
// ── Context ──
|
|
const WizardConfigContext = createContext(null);
|
|
export const useWizardConfig = () => useContext(WizardConfigContext);
|
|
|
|
// ── Fallbacks (used if API call fails — wizard is never blocked) ──
|
|
const fallbackObjectives = [
|
|
{ name: 'awareness', objectiveId: 1 },
|
|
{ name: 'traffic', objectiveId: 2 },
|
|
{ name: 'leads', objectiveId: 3 },
|
|
{ name: 'conversions', objectiveId: 4 },
|
|
{ name: 'sales', objectiveId: 5 },
|
|
];
|
|
|
|
const fallbackCategories = [
|
|
{ name: 'restaurant', categoryId: 1, icon: '🍽️' },
|
|
{ name: 'retail', categoryId: 2, icon: '🛍️' },
|
|
{ name: 'b2b services', categoryId: 3, icon: '💼' },
|
|
{ name: 'local services', categoryId: 4, icon: '🏠' },
|
|
{ name: 'health & wellness', categoryId: 5, icon: '🏥' },
|
|
{ name: 'general', categoryId: 6, icon: '📦' },
|
|
];
|
|
|
|
// ── Display label helper ──
|
|
// DB stores lowercase names like "b2b services" — format for display
|
|
export function toDisplayLabel(name) {
|
|
if (!name) return '';
|
|
return name
|
|
.split(' ')
|
|
.map(w => {
|
|
if (w === '&') return '&';
|
|
if (w.toLowerCase() === 'b2b') return 'B2B';
|
|
return w.charAt(0).toUpperCase() + w.slice(1);
|
|
})
|
|
.join(' ');
|
|
}
|
|
|
|
// ── Client-side enrichment maps ──
|
|
// DB stores core data (name, color); client adds icons + descriptions
|
|
// New objectives added via admin still appear, just without enrichment.
|
|
export const objectiveDescriptions = {
|
|
awareness: 'Reach new audiences and build recognition',
|
|
traffic: 'Drive more visitors to your site',
|
|
leads: 'Get contact form submissions and signups',
|
|
conversions: 'Drive specific actions on your site',
|
|
sales: 'Drive purchases and revenue',
|
|
};
|
|
|
|
export const objectiveIcons = {
|
|
awareness: '📣',
|
|
traffic: '🌐',
|
|
leads: '📋',
|
|
conversions: '🎯',
|
|
sales: '💰',
|
|
};
|
|
|
|
// Labels for display contexts (ReviewStep, etc.)
|
|
export const objectiveLabels = Object.fromEntries(
|
|
['awareness', 'traffic', 'leads', 'conversions', 'sales']
|
|
.map(k => [k, toDisplayLabel(k)])
|
|
);
|
|
|
|
export const categoryLabels = Object.fromEntries(
|
|
fallbackCategories.map(c => [c.name, toDisplayLabel(c.name)])
|
|
);
|
|
|
|
// ── Fallback channels (used if API call fails) ──
|
|
const fallbackChannels = [
|
|
{ channelType: 'google_ads', displayName: 'Google Ads', description: 'Search, Display, Shopping & Performance Max', minMonthlyBudget: 300, color: '#4285F4', isStub: false },
|
|
{ channelType: 'meta', displayName: 'Meta Ads', description: 'Facebook, Instagram, Messenger & Threads', minMonthlyBudget: 250, color: '#0668E1', isStub: true },
|
|
{ channelType: 'tiktok', displayName: 'TikTok Ads', description: 'In-feed video ads across TikTok', minMonthlyBudget: 200, color: '#000000', isStub: true },
|
|
];
|
|
|
|
// ── Channel lookup helpers ──
|
|
// Use these in AllocationStep, ReviewStep, etc. instead of hardcoded maps.
|
|
|
|
export function getChannelLabel(channelType, channels) {
|
|
if (!channelType) return 'Unknown';
|
|
const ch = channels.find(c => c.channelType === channelType);
|
|
return ch?.displayName || toDisplayLabel(channelType.replace(/_/g, ' '));
|
|
}
|
|
|
|
export function getChannelColor(channelType, channels) {
|
|
if (!channelType) return '#4F46E5';
|
|
const ch = channels.find(c => c.channelType === channelType);
|
|
return ch?.color || '#4F46E5';
|
|
}
|
|
|
|
// ── Provider ──
|
|
export default function WizardConfigProvider({ children }) {
|
|
const { sessionToken } = useAuth();
|
|
const [categories, setCategories] = useState([]);
|
|
const [objectives, setObjectives] = useState([]);
|
|
const [channels, setChannels] = useState([]);
|
|
const [allocationRules, setAllocationRules] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
async function loadConfig() {
|
|
setLoading(true);
|
|
// Fetch wizard config (categories + objectives) and channels in parallel
|
|
const [configRes, channelsRes] = await Promise.all([
|
|
getWizardConfig(sessionToken),
|
|
getAvailableChannels(sessionToken),
|
|
]);
|
|
if (cancelled) return;
|
|
|
|
if (configRes.ok && configRes.data) {
|
|
const d = configRes.data;
|
|
setCategories(Array.isArray(d.categories) ? d.categories : fallbackCategories);
|
|
setObjectives(Array.isArray(d.objectives) ? d.objectives : fallbackObjectives);
|
|
} else {
|
|
console.warn('Wizard config load failed, using fallbacks:', configRes.error);
|
|
setCategories(fallbackCategories);
|
|
setObjectives(fallbackObjectives);
|
|
}
|
|
|
|
if (channelsRes.ok && channelsRes.data) {
|
|
setChannels(channelsRes.data.channels || fallbackChannels);
|
|
setAllocationRules(channelsRes.data.allocation || null);
|
|
} else {
|
|
console.warn('Channels load failed, using fallbacks:', channelsRes.error);
|
|
setChannels(fallbackChannels);
|
|
}
|
|
|
|
setLoading(false);
|
|
}
|
|
|
|
loadConfig();
|
|
return () => { cancelled = true; };
|
|
}, [sessionToken]);
|
|
|
|
const value = {
|
|
categories,
|
|
objectives,
|
|
channels,
|
|
allocationRules,
|
|
loading,
|
|
// Helpers exposed so consumers don't need separate imports
|
|
toDisplayLabel,
|
|
objectiveDescriptions,
|
|
objectiveIcons,
|
|
getChannelLabel: (type) => getChannelLabel(type, channels),
|
|
getChannelColor: (type) => getChannelColor(type, channels),
|
|
};
|
|
|
|
return (
|
|
<WizardConfigContext.Provider value={value}>
|
|
{children}
|
|
</WizardConfigContext.Provider>
|
|
);
|
|
}
|