// Template data access helpers and region normalization logic. const { parseJsonSafe, readFormArray } = require('./utils'); const animationPresets = require('../web/public/js/templates/animation-presets'); function sanitizeBackgroundColor(value) { const raw = String(value || '').trim(); if (/^#[0-9a-fA-F]{6}$/.test(raw) || /^#[0-9a-fA-F]{3}$/.test(raw)) { return raw; } return '#111111'; } function normalizeTemplateRegionType(value) { const rawType = String(value || 'text').trim(); return rawType || 'text'; } function normalizeTemplateRegionLockRatio(value) { const rawRatio = String(value || '').trim(); if (!/^\d+\s*:\s*\d+$/.test(rawRatio)) { return null; } return rawRatio.replace(/\s+/g, ''); } function normalizeTemplateRegionAnimation(value) { if (animationPresets && typeof animationPresets.normalize === 'function') { return animationPresets.normalize(value); } const allowed = new Set( Array.isArray(animationPresets && animationPresets.allValues) && animationPresets.allValues.length ? animationPresets.allValues : ['none', 'fadeIn', 'fadeInDown', 'fadeInLeft', 'fadeInRight', 'fadeInUp', 'zoomIn', 'zoomInDown', 'zoomInLeft', 'zoomInRight', 'zoomInUp', 'slideInDown', 'slideInLeft', 'slideInRight', 'slideInUp', 'fadeOut', 'fadeOutDown', 'fadeOutLeft', 'fadeOutRight', 'fadeOutUp', 'zoomOut', 'zoomOutDown', 'zoomOutLeft', 'zoomOutRight', 'zoomOutUp', 'slideOutDown', 'slideOutLeft', 'slideOutRight', 'slideOutUp', 'backInDown', 'backInLeft', 'backInRight', 'backInUp', 'backOutDown', 'backOutLeft', 'backOutRight', 'backOutUp', 'bounceIn', 'bounceInDown', 'bounceInLeft', 'bounceInRight', 'bounceInUp', 'bounceOut', 'bounceOutDown', 'bounceOutLeft', 'bounceOutRight', 'bounceOutUp', 'flip', 'flipInX', 'flipInY', 'flipOutX', 'flipOutY', 'lightSpeedInLeft', 'lightSpeedInRight', 'lightSpeedOutLeft', 'lightSpeedOutRight', 'rotateIn', 'rotateInDownLeft', 'rotateInDownRight', 'rotateInUpLeft', 'rotateInUpRight', 'rotateOut', 'rotateOutDownLeft', 'rotateOutDownRight', 'rotateOutUpLeft', 'rotateOutUpRight', 'hinge', 'jackInTheBox', 'rollIn', 'rollOut', 'flash', 'pulse', 'rubberBand', 'shakeX', 'shakeY', 'headShake', 'swing', 'tada', 'wobble', 'jello', 'heartBeat', 'bounce'] ); const raw = String(value || '').trim().toLowerCase(); return allowed.has(raw) ? raw : 'none'; } function normalizeTemplateRegionAnimationStep(value, fallbackPreset) { if (value && typeof value === 'object' && !Array.isArray(value)) { const normalized = {}; Object.keys(value).forEach((key) => { normalized[key] = value[key]; }); normalized.preset = normalizeTemplateRegionAnimation(value.preset !== undefined ? value.preset : fallbackPreset || 'none'); return normalized; } return { preset: normalizeTemplateRegionAnimation(typeof value === 'string' ? value : fallbackPreset || 'none') }; } function normalizeTemplateRegionAnimationConfig(value) { let raw = value; if (typeof raw === 'string') { const text = String(raw || '').trim(); if (!text) { raw = null; } else { try { raw = JSON.parse(text); } catch (_error) { raw = null; } } } if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { raw = {}; } return { intro: normalizeTemplateRegionAnimationStep(raw.intro, 'none'), outro: normalizeTemplateRegionAnimationStep(raw.outro !== undefined ? raw.outro : raw.out, 'none'), loop: normalizeTemplateRegionAnimationStep(raw.loop, 'none') }; } function normalizeTemplateRegionName(value) { return String(value || '').trim(); } function ensureUniqueTemplateRegionNames(regions) { const seen = new Map(); for (let i = 0; i < regions.length; i += 1) { const region = regions[i]; const regionName = normalizeTemplateRegionName(region.region_key || region.label); if (!regionName) { continue; } const normalized = regionName.toLowerCase(); if (seen.has(normalized)) { const error = new Error('Region names must be unique on this template.'); error.statusCode = 400; throw error; } seen.set(normalized, true); } } async function fetchTemplateById(pool, id) { const [templates] = await pool.query(` SELECT st.id, st.name, st.canvas_size_id, st.background_image_path, st.background_color, st.created_at, st.modified_at, cs.name AS canvas_size_name, cs.width AS canvas_size_width, cs.height AS canvas_size_height FROM c_templates st LEFT JOIN c_canvas_sizes cs ON cs.id = st.canvas_size_id WHERE st.id = ? `, [id]); if (!templates.length) { return null; } const template = templates[0]; const [regions] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, animation_json, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions WHERE template_id = ? ORDER BY z_index ASC, id ASC', [id]); template.regions = regions; return template; } async function fetchTemplatesData(pool) { const [templates] = await pool.query(` SELECT st.id, st.name, st.canvas_size_id, st.background_image_path, st.background_color, st.created_at, st.modified_at, cs.name AS canvas_size_name, cs.width AS canvas_size_width, cs.height AS canvas_size_height FROM c_templates st LEFT JOIN c_canvas_sizes cs ON cs.id = st.canvas_size_id ORDER BY st.id DESC `); const [templateRegions] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, animation_json, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions ORDER BY template_id ASC, z_index ASC, id ASC'); return { templates, templateRegions }; } function extractTemplateRegions(body) { const regionsJson = body.regions_json; if (regionsJson) { const parsed = parseJsonSafe(regionsJson); if (Array.isArray(parsed)) { return parsed.map((region) => { const regionType = normalizeTemplateRegionType(region.region_type); return { region_key: String(region.region_name || region.region_key || region.label || '').trim(), region_type: regionType, label: String(region.region_name || region.label || region.region_key || '').trim(), lock_ratio: normalizeTemplateRegionLockRatio(region.lock_ratio), animation_json: normalizeTemplateRegionAnimationConfig(region.animation_json), x: Number(region.x || 0), y: Number(region.y || 0), width: Number(region.width || 100), height: Number(region.height || 100), z_index: Number(region.z_index || 0) }; }).filter((region) => region.region_key && region.label); } } const keys = readFormArray(body, 'region_key[]'); const names = readFormArray(body, 'region_name[]'); const labels = readFormArray(body, 'region_label[]'); const types = readFormArray(body, 'region_type[]'); const ratios = readFormArray(body, 'region_lock_ratio[]'); const animationJsons = readFormArray(body, 'region_animation_json[]'); const xs = readFormArray(body, 'region_x[]'); const ys = readFormArray(body, 'region_y[]'); const widths = readFormArray(body, 'region_width[]'); const heights = readFormArray(body, 'region_height[]'); const zs = readFormArray(body, 'region_z[]'); const regions = []; for (let i = 0; i < keys.length; i += 1) { const name = String(names[i] || keys[i] || labels[i] || '').trim(); const rawType = String(types[i] || 'text').trim(); const regionType = normalizeTemplateRegionType(rawType); if (!name) { continue; } regions.push({ region_key: name, region_type: regionType, label: name, lock_ratio: normalizeTemplateRegionLockRatio(ratios[i]), animation_json: normalizeTemplateRegionAnimationConfig(animationJsons[i]), x: Number(xs[i] || 0), y: Number(ys[i] || 0), width: Number(widths[i] || 100), height: Number(heights[i] || 100), z_index: Number(zs[i] || 0) }); } return regions; } function extractGenericRegionContent(region, body, filesByField, existingContent) { const content = {}; const current = existingContent && existingContent[region.region_key] && typeof existingContent[region.region_key] === 'object' ? existingContent[region.region_key] : {}; const suffix = '_' + region.id; Object.keys(body || {}).forEach((key) => { if (!key.startsWith('region_') || !key.endsWith(suffix)) { return; } const field = key.slice('region_'.length, -suffix.length); if (!field || field === 'type' || field === 'key' || field === 'name' || field === 'label') { return; } content[field] = body[key]; }); Object.keys(filesByField || {}).forEach((fieldName) => { if (!fieldName.startsWith('region_') || !fieldName.endsWith(suffix)) { return; } const field = fieldName.slice('region_'.length, -suffix.length); if (!field) { return; } content[field] = `/media/uploads/${filesByField[fieldName].filename}`; }); Object.keys(current).forEach((key) => { if (content[key] === undefined) { content[key] = current[key]; } }); content.type = region.region_type; return content; } function getFilesByField(files) { const map = {}; (files || []).forEach((file) => { map[file.fieldname] = file; }); return map; } async function buildTemplatePayload(pool, req, existingTemplate) { const name = String(req.body.name || '').trim(); const canvasSizeId = req.body.canvas_size_id ? Number(req.body.canvas_size_id) : null; let canvasWidth = Math.max(1, Number((existingTemplate && existingTemplate.canvas_size_width) || 1920)); let canvasHeight = Math.max(1, Number((existingTemplate && existingTemplate.canvas_size_height) || 1080)); let regions = extractTemplateRegions(req.body); const filesByField = getFilesByField(req.files || []); const backgroundImage = filesByField.background_image; const removeBackgroundImage = Boolean(req.body.remove_background_image); const backgroundColor = sanitizeBackgroundColor(req.body.background_color || (existingTemplate && existingTemplate.background_color)); const backgroundImagePath = backgroundImage ? `/media/uploads/${backgroundImage.filename}` : removeBackgroundImage ? null : String(req.body.existing_background_image_path || (existingTemplate && existingTemplate.background_image_path) || '').trim() || null; if (!name) { const error = new Error('Template name is required.'); error.statusCode = 400; throw error; } let resolvedCanvasSizeId = canvasSizeId; if (resolvedCanvasSizeId) { const [canvasSizes] = await pool.query('SELECT id, name, width, height, created_at, modified_at, created_by, modified_by FROM c_canvas_sizes WHERE id = ?', [resolvedCanvasSizeId]); const canvasSize = canvasSizes[0]; if (!canvasSize) { const error = new Error('Canvas size not found.'); error.statusCode = 400; throw error; } canvasWidth = Number(canvasSize.width); canvasHeight = Number(canvasSize.height); } else { resolvedCanvasSizeId = null; } if (!regions.length) { if (existingTemplate) { const error = new Error('At least one region is required.'); error.statusCode = 400; throw error; } } ensureUniqueTemplateRegionNames(regions); return { name, canvasSizeId: resolvedCanvasSizeId, canvasSizeWidth: canvasWidth, canvasSizeHeight: canvasHeight, backgroundImagePath, backgroundColor, regions }; } module.exports = { fetchTemplateById, fetchTemplatesData, extractTemplateRegions, buildTemplatePayload, parseJsonSafe };