// 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 ? `${name} ${pack ? 'art pack preview' : 'game artwork'}` : `${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 ? `` : ''; return `
${media}
${pack ? 'PRE-MADE ART PACK' : 'PRE-MADE STAKE ENGINE SLOT'}

${name}

${item.description ? `

${e(item.description)}

` : ''}${includes}

${e(item.price)}

${pack && item.preview.type === 'video' ? '

Play the preview with sound.

' : ''}
`; } export function enhanceVideoPreviews(root) { for (const link of root.querySelectorAll('[data-video-preview]')) { if (link.dataset.enhanced) continue; const embedUrl = youtubeEmbedUrl(link.href); if (!embedUrl) continue; link.dataset.enhanced = 'true'; link.removeAttribute('target'); const name = link.closest('article').querySelector('h3').textContent; link.setAttribute('aria-label', 'Play video preview: ' + name); link.addEventListener('click', event => { if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; event.preventDefault(); const frame = element('iframe'); frame.src = embedUrl; frame.title = name + ' video preview'; frame.allow = 'autoplay; encrypted-media; picture-in-picture; fullscreen'; frame.allowFullscreen = true; frame.referrerPolicy = 'strict-origin-when-cross-origin'; frame.tabIndex = 0; link.closest('.listing-preview').replaceChildren(frame); frame.focus(); }); } } export function renderProduct(item, kind) { const template = document.createElement('template'); template.innerHTML = renderProductMarkup(item, kind); return template.content.firstElementChild; } export function renderCatalogue(items, root, kind) { const grid = root.querySelector('[data-catalogue-grid]'); grid.innerHTML = items.map(item => renderProductMarkup(item, kind)).join(''); root.querySelector('[data-catalogue-empty]').hidden = items.length > 0; const count = root.querySelector('[data-catalogue-count]'); count.hidden = items.length === 0; const noun = kind === 'packs' ? 'art pack' : 'slot'; count.textContent = items.length + ' ' + noun + (items.length === 1 ? '' : 's') + ' available'; enhanceVideoPreviews(root); return { listed: items.length, kind }; } if (typeof document !== 'undefined') { const requests = new Map(); for (const root of document.querySelectorAll('[data-catalogue]')) { if (root.hasAttribute('data-prerendered')) { enhanceVideoPreviews(root); continue; } const kind = root.dataset.catalogue; if (!requests.has(kind)) requests.set(kind, fetch(kind === 'packs' ? '/art-packs.json' : '/premade-catalogue.json', { cache: 'no-cache' }) .then(response => { if (!response.ok) throw new Error('Catalogue unavailable.'); return response.json(); }) .then(data => validateCatalogue(data, kind))); requests.get(kind).then(items => renderCatalogue(items, root, kind)).catch(() => { const status = root.querySelector('[data-catalogue-count]'); status.hidden = false; status.textContent = 'Online previews are unavailable right now. Contact us for the current lineup.'; }); } }