628 lines
24 KiB
JavaScript
628 lines
24 KiB
JavaScript
const fs = require('fs/promises');
|
|
const path = require('path');
|
|
const EventPage = require('../models/EventPage');
|
|
const { loadAgendaAndSpeakers } = require('./eventAgendaData');
|
|
const { buildSectionsHtml, escapeHtml } = require('./websiteHtml');
|
|
const { buildConferenceThemeStyleTag, buildConferenceFontLink } = require('./conferenceTheme');
|
|
const { WEBSITE_SECTION_TYPES, DEFAULT_MICROSITE_SECTIONS } = require('../config/constants');
|
|
|
|
const LAYOUT_TO_FOLDER = { conference: 'conference' };
|
|
|
|
function eventBuzzPublicBase() {
|
|
return (process.env.EVENTBUZZ_PUBLIC_URL || process.env.FRONTEND_URL || 'http://localhost:5173').replace(
|
|
/\/$/,
|
|
'',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Public dashboard form page (fallback when MICROSITE_REGISTRATION_URL_TEMPLATE is unset).
|
|
* @param {string} formId
|
|
* @returns {string}
|
|
*/
|
|
function publicFormUrl(formId, eventId) {
|
|
const fid = formId != null ? String(formId).trim() : '';
|
|
if (!fid) return '';
|
|
const eid = eventId != null ? String(eventId).trim() : '';
|
|
if (eid) return `${eventBuzzPublicBase()}/event/${eid}/form/${fid}`;
|
|
return `${eventBuzzPublicBase()}/form/${fid}`;
|
|
}
|
|
|
|
/**
|
|
* @param {object} event
|
|
* @returns {string}
|
|
*/
|
|
function registrationHref(event, website) {
|
|
const w = website || event.website || {};
|
|
const template = process.env.MICROSITE_REGISTRATION_URL_TEMPLATE || '';
|
|
const formId = w.registrationFormId != null ? String(w.registrationFormId).trim() : '';
|
|
const eid = event._id != null ? String(event._id).trim() : '';
|
|
if (template && formId && eid) {
|
|
return template.replace(/\{eventId\}/g, eid).replace(/\{formId\}/g, formId);
|
|
}
|
|
const legacy = w.registrationUrl != null ? String(w.registrationUrl).trim() : '';
|
|
if (legacy) return legacy;
|
|
if (eid) return `${eventBuzzPublicBase()}/event/${eid}/register`;
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Registration URL for an explicit form id (visual builder CTA). Uses same template vars as {@link registrationHref}.
|
|
* @param {object} event
|
|
* @param {object} [website]
|
|
* @param {string} [explicitFormId] Mongo id of Form
|
|
* @returns {string}
|
|
*/
|
|
function registrationUrlForForm(event, website, explicitFormId) {
|
|
const w = website || event.website || {};
|
|
const template = process.env.MICROSITE_REGISTRATION_URL_TEMPLATE || '';
|
|
const fid =
|
|
explicitFormId != null && String(explicitFormId).trim()
|
|
? String(explicitFormId).trim()
|
|
: w.registrationFormId != null
|
|
? String(w.registrationFormId).trim()
|
|
: '';
|
|
const eid = event._id != null ? String(event._id) : '';
|
|
if (template && fid && eid) {
|
|
return template.replace(/\{eventId\}/g, eid).replace(/\{formId\}/g, fid);
|
|
}
|
|
const legacy = w.registrationUrl != null ? String(w.registrationUrl).trim() : '';
|
|
if (legacy) return legacy;
|
|
if (fid && eid) return `${eventBuzzPublicBase()}/event/${eid}/register`;
|
|
if (fid) return publicFormUrl(fid, eid);
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @param {object} [website] website bag (event.website shape)
|
|
* @returns {string}
|
|
*/
|
|
function normalizeLayoutKey(website) {
|
|
const w = website || {};
|
|
const t = w.layoutTemplate || 'conference';
|
|
if (t === 'conference') return 'conference';
|
|
return 'conference';
|
|
}
|
|
|
|
/**
|
|
* @param {object} event
|
|
* @returns {string}
|
|
*/
|
|
function buildCountdownInlineScript(event) {
|
|
const start = event.startDate ? new Date(event.startDate).getTime() : NaN;
|
|
const nameJson = JSON.stringify(event.name || 'Event');
|
|
const valid = Number.isFinite(start);
|
|
const startExpr = valid ? String(start) : 'null';
|
|
return `<script>
|
|
(function(){
|
|
var phraseEl=document.getElementById("cd-phrase");
|
|
if(!phraseEl)return;
|
|
var target=${startExpr};
|
|
var eventName=${nameJson};
|
|
function toGo(n,unit){return n===1?"1 "+unit+" to go":n+" "+unit+"s to go";}
|
|
function tick(){
|
|
if(target==null||!Number.isFinite(target)){phraseEl.textContent="";return;}
|
|
var t=target-Date.now();
|
|
if(t<=0){phraseEl.textContent="We're live — welcome to "+eventName+"!";return;}
|
|
var sec=Math.floor(t/1e3),days=Math.floor(sec/86400);sec%=86400;
|
|
var hours=Math.floor(sec/3600);sec%=3600;var mins=Math.floor(sec/60);
|
|
if(days>=1)phraseEl.textContent=toGo(days,"day");
|
|
else if(hours>=1)phraseEl.textContent=toGo(hours,"hour");
|
|
else if(mins>=1)phraseEl.textContent=toGo(mins,"minute");
|
|
else phraseEl.textContent="Starting soon";
|
|
}
|
|
tick();setInterval(tick,1e3);
|
|
})();
|
|
</script>`;
|
|
}
|
|
|
|
const REPO_ROOT = path.join(__dirname, '..', '..', '..');
|
|
|
|
/**
|
|
* @param {object} event
|
|
* @returns {string}
|
|
*/
|
|
function formatHeroMeta(event) {
|
|
const loc = event.location || '';
|
|
const start = event.startDate ? new Date(event.startDate) : null;
|
|
const end = event.endDate ? new Date(event.endDate) : null;
|
|
let datePart = '';
|
|
if (start && !Number.isNaN(start.getTime())) {
|
|
const dopts = { month: 'short', day: 'numeric', year: 'numeric' };
|
|
if (end && !Number.isNaN(end.getTime()) && start.toDateString() !== end.toDateString()) {
|
|
datePart = `${start.toLocaleDateString('en-US', dopts)} \u2013 ${end.toLocaleDateString('en-US', dopts)}`;
|
|
} else {
|
|
datePart = start.toLocaleDateString('en-US', dopts);
|
|
}
|
|
}
|
|
return [datePart, loc].filter(Boolean).join(' \u00b7 ');
|
|
}
|
|
|
|
/**
|
|
* @param {object} event
|
|
* @returns {string}
|
|
*/
|
|
function heroBackgroundImage(event, website) {
|
|
if (event.bannerImage) return String(event.bannerImage);
|
|
const w = website || event.website || {};
|
|
if (Array.isArray(w.banners) && w.banners.length && w.banners[0]) return String(w.banners[0]);
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @param {string} layoutKey
|
|
* @returns {string}
|
|
*/
|
|
/**
|
|
* @param {string} type
|
|
* @param {object} event mongoose doc or plain
|
|
* @param {object} [website] website bag
|
|
* @returns {object}
|
|
*/
|
|
function defaultConfigFor(type, event, website) {
|
|
const w = website || event.website || {};
|
|
const name = event.name || 'Event';
|
|
const sub = event.subtitle || '';
|
|
const desc = event.description || '';
|
|
const reg = registrationHref(event, w) || '';
|
|
const regAnchor = reg || '#register';
|
|
switch (type) {
|
|
case 'header':
|
|
return {
|
|
logoText: name,
|
|
logoUrl: w.logo ? String(w.logo) : '',
|
|
menuItems: [
|
|
{ label: 'About', url: '#about' },
|
|
{ label: 'Speakers', url: '#speakers' },
|
|
{ label: 'Schedule', url: '#schedule' },
|
|
{ label: 'Register', url: regAnchor },
|
|
],
|
|
};
|
|
case 'banner':
|
|
return {
|
|
headline: name,
|
|
subtext: sub || desc.slice(0, 280),
|
|
ctaText: 'Register',
|
|
ctaUrl: regAnchor,
|
|
secondaryCtaText: 'View schedule',
|
|
secondaryCtaUrl: '#schedule',
|
|
backgroundClass: 'bg-primary',
|
|
backgroundImage: heroBackgroundImage(event, w),
|
|
heroMetaLine: formatHeroMeta(event),
|
|
};
|
|
case 'contact_form':
|
|
return { title: 'Contact us' };
|
|
case 'agenda':
|
|
return { title: 'Schedule' };
|
|
case 'register_links':
|
|
return {
|
|
title: 'Register',
|
|
primaryLabel: 'Register now',
|
|
primaryUrl: regAnchor,
|
|
};
|
|
case 'speakers':
|
|
return { title: 'Speakers', limit: 12 };
|
|
case 'footer':
|
|
return { copyrightText: `© ${new Date().getFullYear()} ${name}. All rights reserved.` };
|
|
case 'sponsors':
|
|
return {
|
|
title: 'Sponsors & partners',
|
|
sponsors: [],
|
|
partnerCtaLabel: 'Become a partner',
|
|
partnerCtaUrl: '#',
|
|
};
|
|
case 'cta':
|
|
return { headline: 'Join us', subtext: sub, buttonLabel: 'Register', buttonUrl: regAnchor };
|
|
case 'custom_html':
|
|
return {
|
|
html: desc
|
|
? `<p class="lead mb-0">${escapeHtml(desc)}</p>`
|
|
: `<p class="text-muted mb-0">Event details coming soon.</p>`,
|
|
};
|
|
case 'countdown': {
|
|
const start = event.startDate ? new Date(event.startDate) : null;
|
|
const line = start && !Number.isNaN(start.getTime())
|
|
? `Event starts · ${start.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' })}`
|
|
: `Event starts · ${name}`;
|
|
return { headingLine: line };
|
|
}
|
|
case 'gallery':
|
|
return {
|
|
title: 'Gallery',
|
|
images: [
|
|
{ url: 'https://images.unsplash.com/photo-1475721027785-f74eccf877e2?auto=format&fit=crop&w=800&q=80', alt: 'Conference audience (placeholder)' },
|
|
{ url: 'https://images.unsplash.com/photo-1505373877841-8d25f7d46678?auto=format&fit=crop&w=800&q=80', alt: 'Stage (placeholder)' },
|
|
],
|
|
};
|
|
case 'about':
|
|
return {
|
|
title: 'About the event',
|
|
lead: desc ? desc.slice(0, 400) : `Learn more about ${name}.`,
|
|
bulletsTitle: 'Why attend',
|
|
bullets: [
|
|
'Hands-on sessions and practical takeaways',
|
|
'Networking with peers and sponsors',
|
|
'Product tips from the EventBuzz team',
|
|
],
|
|
ctaLabel: 'Reserve your seat',
|
|
ctaUrl: regAnchor,
|
|
};
|
|
case 'venue':
|
|
return {
|
|
title: 'Venue',
|
|
venueName: event.location || 'Venue TBA',
|
|
intro: event.location ? `Join us at ${event.location}.` : 'Venue details will be announced soon.',
|
|
mapEmbedUrl: 'https://www.google.com/maps?q=0,0&z=15&hl=en&output=embed',
|
|
transportCards: [
|
|
{ icon: 'airplane', title: 'By plane', text: 'Check airport transfers and local transit.' },
|
|
{ icon: 'train-front', title: 'By train', text: 'Nearest station within walking distance.' },
|
|
{ icon: 'bus-front', title: 'By bus', text: 'Regional lines may stop nearby.' },
|
|
{ icon: 'car-front', title: 'Parking', text: 'Parking information will be shared before the event.' },
|
|
],
|
|
};
|
|
case 'faq':
|
|
return {
|
|
title: 'Frequently Asked Questions',
|
|
items: [
|
|
{ question: `When and where is ${name}?`, answer: formatHeroMeta(event) || 'Details coming soon.' },
|
|
{ question: 'What is included with my ticket?', answer: 'See the registration page for tier details (demo answer).' },
|
|
{ question: 'Will sessions be recorded?', answer: 'Recording policy will be communicated by the organizers.' },
|
|
],
|
|
contactEmail: 'events@example.com',
|
|
};
|
|
case 'tickets':
|
|
return {
|
|
title: 'Tickets',
|
|
intro: 'Choose the tier that fits you (demo pricing).',
|
|
tiers: [
|
|
{ name: 'Community', price: '199', blurb: 'Livestream + digital access (demo).', featured: false, buttonLabel: 'Choose', buttonUrl: regAnchor },
|
|
{ name: 'Professional', price: '449', blurb: 'Full access, workshops (demo).', featured: true, buttonLabel: 'Buy now', buttonUrl: regAnchor },
|
|
{ name: 'VIP', price: '799', blurb: 'Reserved seating + extras (demo).', featured: false, buttonLabel: 'Choose', buttonUrl: regAnchor },
|
|
],
|
|
includedTitle: "What's included (demo)",
|
|
includedBullets: ['Session recordings where available', 'Templates and resources', 'Community badge on the hub'],
|
|
};
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overlay Website Builder section config on publish defaults so live agenda/speakers
|
|
* and event-backed fields still fill gaps.
|
|
* @param {string} type
|
|
* @param {object|undefined|null} saved
|
|
* @param {object} event
|
|
* @returns {object}
|
|
*/
|
|
function mergeSectionConfig(type, saved, event, website) {
|
|
const base = defaultConfigFor(type, event, website);
|
|
if (!saved || typeof saved !== 'object' || Array.isArray(saved)) return base;
|
|
const out = { ...base, ...saved };
|
|
// Website Builder seeds empty strings; treat as “use event default” for key display fields.
|
|
if (type === 'header' && !String(out.logoText || '').trim()) {
|
|
out.logoText = base.logoText;
|
|
}
|
|
if (type === 'banner') {
|
|
if (!String(out.headline || '').trim()) out.headline = base.headline;
|
|
if (!String(out.subtext || '').trim()) out.subtext = base.subtext;
|
|
if (!String(out.secondaryCtaText || '').trim()) out.secondaryCtaText = base.secondaryCtaText;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Publish section list:
|
|
* - When Configure Microsite has a non-empty `micrositeSections` checklist, that order wins
|
|
* (so the panel is not ignored by an older Website Builder Home page).
|
|
* Config for each type is taken from the first matching Home section when present; otherwise defaults.
|
|
* - When the checklist is empty, use Website Builder Home sections if any; else synthetic from panel array.
|
|
* @param {object} event
|
|
* @param {object} website event.website-shaped bag
|
|
* @param {import('mongoose').Types.ObjectId|string|null|undefined} sourcePageId optional EventPage for sections
|
|
* @returns {Promise<{ title: string, sections: object[] }>}
|
|
*/
|
|
async function resolvePublishPage(event, website, sourcePageId) {
|
|
const allowed = new Set(WEBSITE_SECTION_TYPES);
|
|
let homePage = null;
|
|
if (sourcePageId) {
|
|
const p = await EventPage.findById(sourcePageId).lean();
|
|
const evMatch = p && p.event && event._id && String(p.event) === String(event._id);
|
|
if (evMatch && Array.isArray(p.sections) && p.sections.length > 0) {
|
|
homePage = p;
|
|
}
|
|
}
|
|
if (!homePage) {
|
|
const pages = await EventPage.find({ event: event._id })
|
|
.sort({ 'settings.menuOrder': 1, createdAt: 1 })
|
|
.lean();
|
|
homePage =
|
|
pages.find((p) => p.type === 'home' && Array.isArray(p.sections) && p.sections.length > 0) ||
|
|
pages.find((p) => Array.isArray(p.sections) && p.sections.length > 0);
|
|
}
|
|
|
|
const panelTypes = (website.micrositeSections || []).filter((t) => allowed.has(t));
|
|
|
|
const resolveTitle = () =>
|
|
(website && website.title) ||
|
|
(homePage && homePage.seo && homePage.seo.title) ||
|
|
(homePage && homePage.title) ||
|
|
event.name ||
|
|
'Event';
|
|
|
|
if (panelTypes.length > 0) {
|
|
const homeSections =
|
|
homePage && Array.isArray(homePage.sections) ? homePage.sections : [];
|
|
const usedHomeIdx = new Set();
|
|
const sections = panelTypes.map((type, i) => {
|
|
let saved = null;
|
|
for (let j = 0; j < homeSections.length; j++) {
|
|
const s = homeSections[j];
|
|
if (!s || typeof s.type !== 'string' || !allowed.has(s.type)) continue;
|
|
if (usedHomeIdx.has(j) || s.type !== type) continue;
|
|
usedHomeIdx.add(j);
|
|
saved = s.config;
|
|
break;
|
|
}
|
|
return {
|
|
id: `${type}-${i}`,
|
|
type,
|
|
order: i,
|
|
config: mergeSectionConfig(type, saved, event, website),
|
|
};
|
|
});
|
|
return { title: resolveTitle(), sections };
|
|
}
|
|
|
|
if (homePage && Array.isArray(homePage.sections) && homePage.sections.length > 0) {
|
|
const sections = homePage.sections
|
|
.filter((s) => s && typeof s.type === 'string' && allowed.has(s.type))
|
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
.map((s, i) => ({
|
|
id: s.id || `${s.type}-${i}`,
|
|
type: s.type,
|
|
order: s.order ?? i,
|
|
config: mergeSectionConfig(s.type, s.config, event, website),
|
|
}));
|
|
if (sections.length > 0) {
|
|
return { title: resolveTitle(), sections };
|
|
}
|
|
}
|
|
return buildSyntheticPage(website.micrositeSections || [], event, website);
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} micrositeSections
|
|
* @param {object} event
|
|
* @returns {{ title: string, sections: object[] }}
|
|
*/
|
|
function buildSyntheticPage(micrositeSections, event, website) {
|
|
const w = website || event.website || {};
|
|
const allowed = new Set(WEBSITE_SECTION_TYPES);
|
|
const configured = (micrositeSections || []).filter((t) => allowed.has(t));
|
|
const types = configured.length > 0 ? configured : DEFAULT_MICROSITE_SECTIONS;
|
|
const title = (w.title && String(w.title).trim()) || event.name || 'Event';
|
|
return {
|
|
title,
|
|
sections: types.map((type, i) => ({
|
|
id: `${type}-${i}`,
|
|
type,
|
|
order: i,
|
|
config: defaultConfigFor(type, event, w),
|
|
})),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compose the same HTML document the static publish step writes (conference shell + theme + main inject),
|
|
* without touching the filesystem.
|
|
*
|
|
* @param {object} event Event mongoose doc or lean
|
|
* @param {object} [opts]
|
|
* @param {string} [opts.registrationUrl]
|
|
* @param {boolean} [opts.previewOnly=false] When true, slug/folder is not required (preview in editor).
|
|
* @param {boolean} [opts.applyVisualMainOverride=true] When true, saved visual HTML replaces generated main markup (publish path).
|
|
* @param {string} [opts.publishFolderOverride] When set, must be the slug folder or `{slug}_draft` (validated).
|
|
* @returns {Promise<{ html: string, publishFolder: string|null, folderDir: string }>}
|
|
*/
|
|
async function buildMicrositeDocumentHtml(event, opts = {}) {
|
|
const previewOnly = !!opts.previewOnly;
|
|
const applyVisualMainOverride = opts.applyVisualMainOverride !== false;
|
|
const website = opts.website != null ? opts.website : event.website || {};
|
|
const sourcePageId = opts.sourcePageId;
|
|
const layoutKey = normalizeLayoutKey(website);
|
|
const folderDir = LAYOUT_TO_FOLDER[layoutKey];
|
|
if (!folderDir) {
|
|
throw new Error('Invalid layoutTemplate');
|
|
}
|
|
const shellPath = path.join(REPO_ROOT, 'microsites', folderDir, 'shell.html');
|
|
let shell;
|
|
try {
|
|
shell = await fs.readFile(shellPath, 'utf8');
|
|
} catch (e) {
|
|
throw new Error(`Microsite shell not found: ${shellPath}`);
|
|
}
|
|
|
|
const page = await resolvePublishPage(event, website, sourcePageId);
|
|
if (!page.sections || page.sections.length === 0) {
|
|
throw new Error(
|
|
'No sections to publish. Add sections on the Website Builder Home page, or select sections under Microsite settings.',
|
|
);
|
|
}
|
|
const { agendaItems, speakers } = await loadAgendaAndSpeakers(event._id, {
|
|
speakerVisibility: 'public',
|
|
});
|
|
let registrationUrl =
|
|
typeof opts.registrationUrl === 'string' && opts.registrationUrl.trim()
|
|
? opts.registrationUrl.trim()
|
|
: registrationHref(event, website);
|
|
if (!registrationUrl) registrationUrl = '';
|
|
|
|
let sectionsHtml = buildSectionsHtml(page, event, {
|
|
agendaItems,
|
|
speakers,
|
|
registrationUrl,
|
|
layoutTemplate: layoutKey,
|
|
});
|
|
|
|
if (applyVisualMainOverride && event._id) {
|
|
let raw = website.micrositeHtml != null ? String(website.micrositeHtml).trim() : '';
|
|
if (!raw && sourcePageId) {
|
|
const srcVisual = await EventPage.findById(sourcePageId).select('micrositeHtml').lean();
|
|
raw = srcVisual?.micrositeHtml != null ? String(srcVisual.micrositeHtml).trim() : '';
|
|
}
|
|
if (!raw) {
|
|
const homeVisual = await EventPage.findOne({ event: event._id, type: 'home' }).select('micrositeHtml').lean();
|
|
raw = homeVisual?.micrositeHtml != null ? String(homeVisual.micrositeHtml).trim() : '';
|
|
}
|
|
if (raw) sectionsHtml = raw;
|
|
}
|
|
|
|
const htmlTitle = (website.title && String(website.title).trim()) || page.title || event.name || 'Event';
|
|
const pageTitle = escapeHtml(htmlTitle);
|
|
const metaRaw =
|
|
(website.seoDescription && String(website.seoDescription).trim()) ||
|
|
(website.description && String(website.description).trim()) ||
|
|
(event.description && String(event.description).trim()) ||
|
|
'';
|
|
const metaDesc = escapeHtml(metaRaw.slice(0, 320));
|
|
const headInject = `<title>${pageTitle}</title>\n <meta name="description" content="${metaDesc}">`;
|
|
|
|
const apiBaseForMicrosite = process.env.MICROSITE_API_BASE_URL || '';
|
|
let html = shell
|
|
.replace('<!-- EB_INJECT_API_BASE -->', escapeHtml(apiBaseForMicrosite))
|
|
.replace('<!-- EB_INJECT_HEAD -->', headInject)
|
|
.replace('<!-- EB_INJECT_MAIN -->', sectionsHtml)
|
|
.replace('<!-- EB_INJECT_COUNTDOWN -->', buildCountdownInlineScript(event));
|
|
|
|
if (folderDir === 'conference') {
|
|
const fontLink = buildConferenceFontLink(website);
|
|
const themeTag = buildConferenceThemeStyleTag(website);
|
|
html = html
|
|
.replace('<!-- EB_INJECT_FONTS -->', fontLink)
|
|
.replace('<!-- EB_INJECT_THEME -->', themeTag);
|
|
}
|
|
|
|
const folder = (website.folder || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
if (!previewOnly && !folder) {
|
|
throw new Error('Website folder (slug) is required');
|
|
}
|
|
const draftName = `${folder}_draft`;
|
|
const allowedFolders = new Set([folder, draftName]);
|
|
let publishFolder;
|
|
if (previewOnly) {
|
|
publishFolder = null;
|
|
} else if (opts.publishFolderOverride != null && String(opts.publishFolderOverride).trim()) {
|
|
const ov = String(opts.publishFolderOverride).trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
if (!allowedFolders.has(ov)) {
|
|
throw new Error('Invalid publish folder target');
|
|
}
|
|
publishFolder = ov;
|
|
} else {
|
|
publishFolder = website.enabled ? folder : draftName;
|
|
}
|
|
return { html, publishFolder, folderDir };
|
|
}
|
|
|
|
/**
|
|
* @param {object} event Event mongoose doc (lean or hydrated)
|
|
* @param {object} opts
|
|
* @param {string} opts.staticRoot
|
|
* @param {string} [opts.registrationUrl]
|
|
* @param {string} [opts.publishFolderOverride] `folder` or `{folder}_draft`
|
|
* @returns {Promise<{ diskPath: string, html: string, publishFolder: string }>}
|
|
*/
|
|
async function buildMicrositeHtml(event, opts) {
|
|
const website = opts.website != null ? opts.website : event.website || {};
|
|
const folder = (website.folder || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
if (!folder) {
|
|
throw new Error('Website folder (slug) is required');
|
|
}
|
|
|
|
const { html, publishFolder, folderDir } = await buildMicrositeDocumentHtml(event, {
|
|
registrationUrl: opts.registrationUrl,
|
|
previewOnly: false,
|
|
applyVisualMainOverride: true,
|
|
publishFolderOverride: opts.publishFolderOverride,
|
|
website,
|
|
sourcePageId: opts.sourcePageId,
|
|
});
|
|
|
|
const outDir = path.join(opts.staticRoot, publishFolder);
|
|
await fs.mkdir(outDir, { recursive: true });
|
|
if (folderDir === 'conference') {
|
|
const layoutSrc = path.join(REPO_ROOT, 'microsites', 'conference', 'css', 'conference-layout.css');
|
|
await fs.copyFile(layoutSrc, path.join(outDir, 'conference-layout.css'));
|
|
const staticCssSrc = path.join(REPO_ROOT, 'microsites', 'conference', 'static', 'conference-static.css');
|
|
await fs.copyFile(staticCssSrc, path.join(outDir, 'conference-static.css'));
|
|
const contactJsSrc = path.join(REPO_ROOT, 'microsites', 'conference', 'static', 'eb-microsite-contact.js');
|
|
await fs.copyFile(contactJsSrc, path.join(outDir, 'eb-microsite-contact.js'));
|
|
}
|
|
const diskPath = path.join(outDir, 'index.html');
|
|
await fs.writeFile(diskPath, html, 'utf8');
|
|
|
|
return { diskPath, html, publishFolder };
|
|
}
|
|
|
|
/**
|
|
* CSS payload for the GrapesJS canvas so layouts match published conference microsites (theme vars + layout + static).
|
|
* @param {object} website event.website
|
|
* @returns {Promise<{ fontLinkTag: string, themeStyleTag: string, layoutCss: string, staticCss: string }>}
|
|
*/
|
|
/**
|
|
* Map EventMicrosite document (lean) to the same shape as event.website for layout/publish.
|
|
* @param {object} template
|
|
* @returns {object}
|
|
*/
|
|
function templateToWebsiteBag(template) {
|
|
if (!template || typeof template !== 'object') return {};
|
|
const t = template;
|
|
return {
|
|
enabled: t.enabled,
|
|
folder: t.publishFolder != null ? String(t.publishFolder).trim().toLowerCase().replace(/[^a-z0-9-]/g, '') : '',
|
|
logo: t.logo,
|
|
banners: Array.isArray(t.banners) ? t.banners : [],
|
|
favicon: t.favicon,
|
|
title: t.title,
|
|
description: t.description,
|
|
keywords: Array.isArray(t.keywords) ? t.keywords : [],
|
|
templateId: t.templateId,
|
|
layoutTemplate: t.layoutTemplate || 'conference',
|
|
micrositeSections: Array.isArray(t.micrositeSections) ? t.micrositeSections : [],
|
|
primaryColor: t.primaryColor,
|
|
accentColor: t.accentColor,
|
|
fontPair: t.fontPair,
|
|
registrationUrl: t.registrationUrl,
|
|
registrationFormId: t.registrationFormId,
|
|
seoDescription: t.seoDescription,
|
|
micrositeHtml: t.micrositeHtml,
|
|
};
|
|
}
|
|
|
|
async function getConferenceCanvasStylePayload(website) {
|
|
const fontLinkTag = buildConferenceFontLink(website || {});
|
|
const themeStyleTag = buildConferenceThemeStyleTag(website || {});
|
|
const layoutPath = path.join(REPO_ROOT, 'microsites', 'conference', 'css', 'conference-layout.css');
|
|
const staticPath = path.join(REPO_ROOT, 'microsites', 'conference', 'static', 'conference-static.css');
|
|
const [layoutCss, staticCss] = await Promise.all([
|
|
fs.readFile(layoutPath, 'utf8'),
|
|
fs.readFile(staticPath, 'utf8'),
|
|
]);
|
|
return { fontLinkTag, themeStyleTag, layoutCss, staticCss };
|
|
}
|
|
|
|
module.exports = {
|
|
buildMicrositeHtml,
|
|
buildMicrositeDocumentHtml,
|
|
getConferenceCanvasStylePayload,
|
|
templateToWebsiteBag,
|
|
buildSyntheticPage,
|
|
defaultConfigFor,
|
|
resolvePublishPage,
|
|
LAYOUT_TO_FOLDER,
|
|
registrationHref,
|
|
registrationUrlForForm,
|
|
publicFormUrl,
|
|
eventBuzzPublicBase,
|
|
};
|