${name}
${item.description ? `${e(item.description)}
` : ''}${includes}${e(item.price)}
${pack && item.preview.type === 'video' ? 'Play the preview with sound.
' : ''}// Only records explicitly marked available are offered for sale.
export function safePreviewUrl(value) {
if (typeof value !== 'string' || !value.trim() || /[\\\u0000-\u0020]/.test(value)) throw new Error('A valid public preview URL is required.');
if (value.startsWith('/') && !value.startsWith('//')) return value;
const url = new URL(value);
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password) throw new Error('Use a public HTTP or HTTPS link.');
return url.href;
}
export function validateCatalogue(data, kind = 'slots') {
if (!['slots', 'packs'].includes(kind) || !data || !Array.isArray(data[kind])) throw new Error('Invalid catalogue.');
const ids = new Set();
return data[kind].filter(item => item && item.status === 'available').map(item => {
if (typeof item.id !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(item.id) || ids.has(item.id)) throw new Error('Available items need unique lowercase IDs.');
ids.add(item.id);
if (typeof item.name !== 'string' || !item.name.trim()) throw new Error('Every available item needs its real name.');
const types = kind === 'packs' ? ['video', 'image'] : ['demo', 'video'];
if (!item.preview || !types.includes(item.preview.type)) throw new Error(kind === 'packs' ? 'Art packs need an artwork or video preview.' : 'Slots need a demo or video preview.');
if (item.includes !== undefined && (!Array.isArray(item.includes) || item.includes.some(x => typeof x !== 'string' || !x.trim()))) throw new Error('Included assets must be a list of labels.');
return {
id: item.id, name: item.name.trim(), status: 'available',
description: typeof item.description === 'string' ? item.description.trim() : '',
price: typeof item.price === 'string' && item.price.trim() ? item.price.trim() : 'Price on request',
image: item.image ? safePreviewUrl(item.image) : null,
includes: (item.includes || []).map(x => x.trim()),
preview: { type: item.preview.type, url: safePreviewUrl(item.preview.url) }
};
});
}
export function productEnquiryUrl(item, kind = 'slots') {
const pack = kind === 'packs';
const subject = (pack ? 'Art pack enquiry — ' : 'Pre-made slot enquiry — ') + item.name;
const body = pack
? 'Hi, I’m interested in the ' + item.name + ' art pack.\n\nPlease confirm availability, pricing, and the usage licence.\n\nPlease share the included artwork, animation and sound files, their formats, and customization options.\n\nMy game or platform:\nAssets I need:'
: 'Hi, I’m interested in buying ' + item.name + '.\n\nPlease confirm availability, pricing, and purchase terms.\n\nCustomizations I would like:\nAdditional game modes:\nTarget delivery date:\n\nPlease confirm which changes fit within your two-week delivery scope.';
return 'mailto:support@gamixlabs.com?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body);
}
export const slotEnquiryUrl = item => productEnquiryUrl(item, 'slots');
function element(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
}
export function youtubeEmbedUrl(value) {
let url;
try { url = new URL(value); } catch { return null; }
if (url.protocol !== 'https:' || url.username || url.password) return null;
const id = url.hostname === 'youtu.be' ? url.pathname.slice(1) :
['youtube.com', 'www.youtube.com', 'm.youtube.com'].includes(url.hostname) && url.pathname === '/watch' ? url.searchParams.get('v') : null;
return typeof id === 'string' && /^[a-zA-Z0-9_-]{11}$/.test(id) ? 'https://www.youtube-nocookie.com/embed/' + id + '?autoplay=1&rel=0' : null;
}
// Shared by the static publisher and optional browser rendering.
export function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]);
}
export function renderProductMarkup(item, kind) {
const pack = kind === 'packs';
const e = escapeHtml;
const name = e(item.name);
const url = e(safePreviewUrl(item.preview.url));
const image = item.image ? e(safePreviewUrl(item.image)) : null;
const directVideo = item.preview.type === 'video' && /\.(?:mp4|webm|ogg)(?:[?#]|$)/i.test(item.preview.url);
const embed = item.preview.type === 'video' ? youtubeEmbedUrl(item.preview.url) : null;
const label = item.preview.type === 'demo' ? 'Play demo' : item.preview.type === 'image' ? 'View artwork' : 'Watch preview';
let media;
if (directVideo) {
media = ``;
} else {
media = image
? ``
: `${item.preview.type === 'demo' ? 'PLAYABLE DEMO' : 'VIDEO PREVIEW'}`;
// Real links work without JavaScript; enhancement swaps YouTube links for an inline player on click.
media += `${label}`;
}
const includes = item.includes.length ? `
${e(item.description)}
` : ''}${includes}${e(item.price)}
${pack && item.preview.type === 'video' ? 'Play the preview with sound.
' : ''}