:root {
/* Apple-style Clean Minimal Palette */
–bg: #f5f5f7;
–panel: #ffffff;
–panel-2: #fbfbfc;
–text: #1d1d1f;
–muted: #86868b;
–accent: #0071e3;
–accent-hover: #0077ED;
–danger: #ff3b30;
–success: #34c759;
–warning: #f59e0b;
–border: #d2d2d7;
–radius: 16px;
–shadow-sm: 0 2px 8px rgba(0,0,0,0.04);
–shadow-lg: 0 10px 30px rgba(0,0,0,0.06), 0 1px 4px rgba(0,0,0,0.02);
–font-stack: -apple-system, BlinkMacSystemFont, “SF Pro Text”, “Segoe UI”, Roboto, Helvetica, Arial, sans-serif;
}
.Avm-note {
background: #ffffff;
border-left: 4px solid var(–accent);
padding: 16px 20px;
border-radius: 8px;
margin-bottom: 24px;
font-size: 0.95rem;
color: var(–text);
box-shadow: var(–shadow-sm);
}
/* — Buttons — */
.Avm-btn {
border: none;
border-radius: 10px;
padding: 12px 20px;
cursor: pointer;
font-weight: 600;
font-size: 0.95rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
font-family: inherit;
}
.Avm-btn:active { transform: scale(0.97); }
.Avm-btn-primary { background: var(–accent); color: white; }
.Avm-btn-primary:hover { background: var(–accent-hover); box-shadow: 0 4px 14px rgba(0, 113, 227, 0.3); }
.Avm-btn-secondary { background: #e8e8ed; color: var(–text); }
.Avm-btn-secondary:hover { background: #d2d2d7; }
.Avm-btn-danger { background: var(–danger); color: white; }
.Avm-btn-danger:hover { box-shadow: 0 4px 14px rgba(255, 59, 48, 0.3); }
.Avm-btn-success { background: var(–success); color: white; }
.Avm-btn-success:hover { box-shadow: 0 4px 14px rgba(52, 199, 89, 0.3); }
.Avm-btn:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; }
/* — Cards & Grids — */
.Avm-card {
background: var(–panel);
border-radius: var(–radius);
padding: 32px;
box-shadow: var(–shadow-lg);
margin-bottom: 32px;
border: 1px solid rgba(0,0,0,0.02);
}
.Avm-card h2 {
margin-top: 0;
font-size: 1.4rem;
font-weight: 600;
color: var(–text);
letter-spacing: -0.3px;
margin-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 16px;
}
.Avm-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 720px) { .Avm-grid-2 { grid-template-columns: 1fr; } }
.Avm-main-grid { display: grid; grid-template-columns: 1fr 450px; gap: 32px; align-items: start; }
@media (max-width: 1000px) {
.Avm-main-grid { grid-template-columns: 1fr; }
.Avm-json-output { position: static; }
}
/* — Forms & Inputs — */
.Avm-wrap label {
display: block;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(–muted);
margin-bottom: 8px;
margin-top: 16px;
}
.Avm-wrap input[type=”text”],
.Avm-wrap input[type=”number”],
.Avm-wrap textarea,
.Avm-wrap select {
width: 100%;
padding: 14px 16px;
border-radius: 12px;
border: 1px solid var(–border);
background: #ffffff;
color: var(–text);
font-size: 1rem;
font-family: inherit;
transition: all 0.2s ease;
}
.Avm-wrap input:focus,
.Avm-wrap textarea:focus,
.Avm-wrap select:focus {
outline: none;
border-color: var(–accent);
box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.15);
}
.Avm-wrap textarea { min-height: 80px; resize: vertical; }
.Avm-settings-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end; }
.Avm-settings-row > div { flex: 1; min-width: 220px; }
/* — Search, Tags & Stats — */
.Avm-search-wrap {
position: sticky;
top: 20px;
z-index: 10;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
padding: 24px;
border-radius: var(–radius);
box-shadow: var(–shadow-sm);
margin-bottom: 24px;
border: 1px solid rgba(0,0,0,0.06);
}
.Avm-filter-tags { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
.Avm-tag {
background: #f1f5f9;
border: 1px solid transparent;
border-radius: 20px;
padding: 6px 14px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
color: var(–text);
transition: all 0.2s ease;
}
.Avm-tag:hover { background: #e2e8f0; }
.Avm-tag.Avm-active { background: var(–text); color: #ffffff; }
.Avm-stats { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 16px; margin-bottom: 24px; }
.Avm-stat {
background: var(–panel-2);
border-radius: 12px;
padding: 16px;
min-width: 140px;
text-align: center;
border: 1px solid rgba(0,0,0,0.03);
}
.Avm-stat b { display: block; font-size: 1.8rem; font-weight: 700; color: var(–text); }
.Avm-stat span { font-size: 0.8rem; font-weight: 500; color: var(–muted); text-transform: uppercase; letter-spacing: 0.5px; }
/* — Blogger Posts Grid — */
.Avm-post-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
max-height: 500px;
overflow-y: auto;
padding-right: 8px;
padding-bottom: 10px;
}
.Avm-post-card {
background: #ffffff;
border: 1px solid var(–border);
border-radius: 14px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.Avm-post-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0,0,0,0.08);
border-color: var(–accent);
}
.Avm-post-card img {
width: 100%;
height: 120px;
object-fit: cover;
background: #f1f5f9;
display: block;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.Avm-post-card .Avm-pbody { padding: 14px; }
.Avm-post-card .Avm-ptitle {
font-size: 0.9rem;
font-weight: 600;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(–text);
}
.Avm-post-card .Avm-plabels {
font-size: 0.75rem;
color: var(–muted);
margin-top: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.Avm-post-card .Avm-add-icon {
position: absolute;
top: 8px;
right: 8px;
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.9);
backdrop-filter: blur(4px);
color: var(–text);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 1.2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: all 0.2s ease;
}
.Avm-post-card:hover .Avm-add-icon { background: var(–accent); color: white; }
/* — Collection Builder — */
.Avm-collection {
border: 1px solid var(–border);
border-radius: 14px;
margin-bottom: 20px;
overflow: hidden;
background: #ffffff;
box-shadow: var(–shadow-sm);
transition: all 0.3s ease;
}
.Avm-collection-header {
background: var(–panel-2);
border-bottom: 1px solid var(–border);
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
cursor: pointer;
transition: background 0.2s;
}
.Avm-collection-header:hover { background: #f5f5f7; }
.Avm-collection-header .Avm-title { font-weight: 600; font-size: 1.1rem; color: var(–text); }
.Avm-collection-header .Avm-meta { color: var(–muted); font-size: 0.85rem; margin-top: 4px; }
.Avm-collection-header .Avm-btn-danger { position: relative; z-index: 5; padding: 8px 14px; font-size: 0.85rem; }
.Avm-collection-body { padding: 24px; display: none; }
.Avm-collection.Avm-open .Avm-collection-body { display: block; }
.Avm-collection.Avm-open .Avm-toggle-icon { transform: rotate(180deg); }
.Avm-preview-img {
width: 100%;
height: 140px;
object-fit: cover;
border-radius: 10px;
background: #f1f5f9;
margin-top: 12px;
display: none;
border: 1px solid rgba(0,0,0,0.05);
}
.Avm-preview-img[src]:not([src=””]) { display: block; }
.Avm-game-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 20px;
margin-top: 20px;
}
.Avm-game-card {
background: var(–panel-2);
border: 1px solid var(–border);
border-radius: 12px;
padding: 16px;
position: relative;
}
.Avm-game-card img {
width: 100%;
height: 130px;
object-fit: cover;
border-radius: 8px;
background: #e2e8f0;
margin-bottom: 12px;
}
.Avm-game-card input { margin-top: 4px; padding: 10px 12px; font-size: 0.9rem; margin-bottom: 8px; }
.Avm-game-card .Avm-delete-game {
position: absolute;
top: -8px;
right: -8px;
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
background: var(–text);
color: white;
cursor: pointer;
font-size: 1rem;
z-index: 5;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
transition: background 0.2s;
}
.Avm-game-card .Avm-delete-game:hover { background: var(–danger); }
/* — JSON Output Panel — */
.Avm-json-output { position: sticky; top: 20px; }
.Avm-wrap pre {
background: #1e1e1e; /* VS Code style dark block */
color: #d4d4d4;
border-radius: 12px;
padding: 24px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 55vh;
font-size: 0.85rem;
font-family: “SF Mono”, Consolas, Menlo, monospace;
border: none;
box-shadow: inset 0 2px 10px rgba(0,0,0,0.2);
}
.Avm-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 20px; }
/* — Utilities — */
.Avm-toast {
position: fixed;
bottom: 30px;
right: 30px;
background: var(–text);
color: #ffffff;
padding: 16px 24px;
border-radius: 12px;
font-weight: 600;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
transform: translateY(120px);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 999;
}
.Avm-toast.Avm-show { transform: translateY(0); }
.Avm-empty { color: var(–muted); text-align: center; padding: 40px; font-weight: 500; }
.Avm-hint { font-size: 0.8rem; color: var(–muted); margin-top: 8px; }
.Avm-hidden-file { display: none; }
/* — Scrollbar — */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #d2d2d7; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #86868b; }
🎮 Blogger Game Collection Builder
Search your Blogger posts → click to add → generate window.ZG_COLLECTIONS JSON
1. Connect Your Blogger Blog
The feed URL used: —
For big blogs (1000+ posts): Use the “Search Blog” button below instead of loading all posts.
3. Your Collections
Generated Code
Single collection only (paste into existing ZG_COLLECTIONS)
Full script: window.ZG_COLLECTIONS = {…}
Paste this collection into your existing window.ZG_COLLECTIONS = { ... } object.
// ===== STATE =====
let data = JSON.parse(localStorage.getItem(‘zg_blogger_collections’) || ‘{}’);
let allPosts = [];
let activeLabel = ”;
// ===== UTILS =====
function uid() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); }
function sanitizeId(name) { return (name || ‘collection’).toLowerCase().replace(/[^a-z0-9]+/g, ‘_’).replace(/^_+|_+$/g, ”).substring(0, 40) || ‘col’; }
function stripMarkdownLink(raw) {
if (!raw) return ”;
const m = raw.trim().match(/^[.*?]((.*?))$/);
return m ? m[1] : raw.trim();
}
function toast(msg) { const t = document.getElementById(‘toast’); t.textContent = msg; t.classList.add(‘Avm-show’); setTimeout(() => t.classList.remove(‘Avm-show’), 2200); }
function save() { localStorage.setItem(‘zg_blogger_collections’, JSON.stringify(data)); render(); updateTargetCollection(); }
function escapeHtml(str) { return String(str ?? ”).replace(/[&”‘]/g, m => ({‘&’:’&’,”:’>’,'”‘:’"’,”‘”:’'’}[m])); }
function getOriginalImage(url) {
if (!url) return ”;
// Convert Blogger resized images to original size
return url.replace(//sd+//g, ‘/s0/’).replace(//wd+//g, ‘/s0/’);
}
function updateTargetCollection() {
const select = document.getElementById(‘targetCollection’);
if (!select) return;
const ids = Object.keys(data);
if (!ids.length) {
select.innerHTML = ‘– No collections yet –‘;
return;
}
let current = select.value;
if (!current || !ids.includes(current)) current = ids[0];
select.innerHTML = ids.map(id => `${escapeHtml(data[id].name || id)}`).join(”);
}
// ===== BLOGGER FEED =====
function normalizeBlogUrl(url) {
url = url.trim();
if (!url) return ”;
if (!url.startsWith(‘http’)) url = ‘https://’ + url;
url = url.replace(//$/, ”);
return url;
}
let currentFetch = { blogUrl: ”, max: 500, label: ”, start: 1, total: 0, btn: null };
function fetchPosts() {
const raw = document.getElementById(‘blogUrl’).value;
const blogUrl = normalizeBlogUrl(raw);
if (!blogUrl) { alert(‘Enter your Blogger URL first’); return; }
const max = Math.min(parseInt(document.getElementById(‘maxResults’).value) || 500, 500);
const label = document.getElementById(‘requiredLabel’).value.trim();
currentFetch = { blogUrl, max, label, start: 1, total: 0, btn: document.getElementById(‘fetchBtn’) };
allPosts = [];
currentFetch.btn.disabled = true;
document.getElementById(‘fetchStatus’).textContent = ‘Loading page 1…’;
document.getElementById(‘searchCard’).style.display = ‘none’;
fetchPage();
}
function fetchPage() {
const { blogUrl, max, label, start } = currentFetch;
let feed = `${blogUrl}/feeds/posts/default?alt=json-in-script&max-results=${max}&start-index=${start}`;
if (label) feed += `&category=${encodeURIComponent(label)}`;
document.getElementById(‘feedUrl’).textContent = feed;
const cbName = ‘bloggerFeed_’ + Date.now() + ‘_’ + start;
const script = document.createElement(‘script’);
window[cbName] = function(json) {
delete window[cbName];
if (script.parentNode) script.parentNode.removeChild(script);
const total = parseInt(json?.feed?.openSearch$totalResults?.$t || 0);
currentFetch.total = total;
const entries = json?.feed?.entry || [];
appendEntries(entries);
const loaded = allPosts.length;
const hasMore = loaded 0;
if (hasMore) {
currentFetch.start += max;
const pageNum = Math.ceil(currentFetch.start / max);
document.getElementById(‘fetchStatus’).textContent = `Loaded ${loaded} of ${total} posts… (page ${pageNum})`;
fetchPage();
} else {
finishFetch();
}
};
script.src = feed + ‘&callback=’ + cbName;
script.onerror = () => {
delete window[cbName];
currentFetch.btn.disabled = false;
document.getElementById(‘fetchStatus’).textContent = ‘Error loading feed. Check URL/labels.’;
alert(‘Could not load Blogger feed. Make sure the URL is correct and the blog is public.’);
};
document.body.appendChild(script);
}
function appendEntries(entries) {
entries.forEach(entry => {
const title = entry.title?.$t || ‘Untitled’;
const linkObj = (entry.link || []).find(l => l.rel === ‘alternate’);
const url = linkObj?.href || ”;
const labels = (entry.category || []).map(c => c.term).filter(Boolean);
const content = (entry.content?.$t || entry.summary?.$t || ”).replace(/]+>/g, ‘ ‘).replace(/s+/g, ‘ ‘).toLowerCase();
let image = ”;
// 1. Try first image in content (usually original size)
if (entry.content?.$t) {
const imgMatch = entry.content.$t.match(/]+src=[“‘]([^”‘]+)[“‘]/i);
if (imgMatch) image = getOriginalImage(imgMatch[1]);
}
// 2. Try summary
if (!image && entry.summary?.$t) {
const imgMatch = entry.summary.$t.match(/]+src=[“‘]([^”‘]+)[“‘]/i);
if (imgMatch) image = getOriginalImage(imgMatch[1]);
}
// 3. Fallback to media thumbnail
if (!image && entry.media$thumbnail?.url) image = entry.media$thumbnail.url;
allPosts.push({ title, url, image, labels, content });
});
}
function finishFetch() {
currentFetch.btn.disabled = false;
document.getElementById(‘fetchStatus’).textContent = `Loaded all ${allPosts.length} posts.`;
document.getElementById(‘searchCard’).style.display = ‘block’;
buildLabelFilters();
filterPosts();
toast(‘All posts loaded!’);
}
// Search directly on Blogger (best for 1000+ post blogs)
function searchBlogger() {
const raw = document.getElementById(‘blogUrl’).value;
const blogUrl = normalizeBlogUrl(raw);
if (!blogUrl) { alert(‘Enter your Blogger URL first’); return; }
const q = document.getElementById(‘searchInput’).value.trim();
if (!q) { alert(‘Type a search keyword first (e.g. Forza)’); return; }
const max = Math.min(parseInt(document.getElementById(‘maxResults’).value) || 500, 500);
let feed = `${blogUrl}/feeds/posts/default?q=${encodeURIComponent(q)}&alt=json-in-script&max-results=${max}`;
document.getElementById(‘feedUrl’).textContent = feed;
const btn = document.getElementById(‘searchBtn’);
btn.disabled = true;
document.getElementById(‘fetchStatus’).textContent = `Searching Blogger for “${q}”…`;
const cbName = ‘bloggerSearch_’ + Date.now();
const script = document.createElement(‘script’);
window[cbName] = function(json) {
delete window[cbName];
if (script.parentNode) script.parentNode.removeChild(script);
allPosts = [];
activeLabel = ”;
const entries = json?.feed?.entry || [];
appendEntries(entries);
btn.disabled = false;
document.getElementById(‘fetchStatus’).textContent = `Search found ${allPosts.length} posts for “${q}”.`;
document.getElementById(‘searchCard’).style.display = ‘block’;
buildLabelFilters();
filterPosts();
toast(`Found ${allPosts.length} results`);
};
script.src = feed + ‘&callback=’ + cbName;
script.onerror = () => {
delete window[cbName];
btn.disabled = false;
document.getElementById(‘fetchStatus’).textContent = ‘Search error. Check URL.’;
alert(‘Could not search Blogger. Make sure the URL is correct and the blog is public.’);
};
document.body.appendChild(script);
}
function clearSearch() {
document.getElementById(‘searchInput’).value = ”;
activeLabel = ”;
allPosts = [];
document.getElementById(‘postGrid’).innerHTML = ‘
‘;
document.getElementById(‘stats’).innerHTML = ”;
document.getElementById(‘labelFilters’).innerHTML = ”;
}
function buildLabelFilters() {
const counts = {};
allPosts.forEach(p => p.labels.forEach(l => counts[l] = (counts[l] || 0) + 1));
const sorted = Object.entries(counts).sort((a, b) => b[1] – a[1]).slice(0, 20);
const wrap = document.getElementById(‘labelFilters’);
if (!sorted.length) { wrap.innerHTML = ”; return; }
wrap.innerHTML = `All` +
sorted.map(([l, c]) => `${escapeHtml(l)} (${c})`).join(”);
}
function setLabel(l) { activeLabel = l; buildLabelFilters(); filterPosts(); }
function filterPosts() {
const q = document.getElementById(‘searchInput’).value.toLowerCase().trim();
const filtered = allPosts.filter(p => {
const matchesLabel = !activeLabel || p.labels.includes(activeLabel);
const matchesQuery = !q ||
p.title.toLowerCase().includes(q) ||
p.labels.some(l => l.toLowerCase().includes(q)) ||
p.content.includes(q);
return matchesLabel && matchesQuery;
});
document.getElementById(‘stats’).innerHTML = `
`;
const grid = document.getElementById(‘postGrid’);
if (!filtered.length) { grid.innerHTML = ‘
‘; return; }
grid.innerHTML = filtered.map((p, i) => `
`).join(”);
}
// ===== COLLECTIONS =====
function addCollection() {
const id = ‘col_’ + uid();
data[id] = { name: ‘New Collection’, description: ”, covers: [], games: [] };
save();
setTimeout(() => document.querySelectorAll(‘.Avm-collection’).forEach(el => { if (el.dataset.id === id) el.classList.add(‘Avm-open’); }), 10);
}
function deleteCollection(id) { if (confirm(‘Delete this collection and all its games?’)) { delete data[id]; save(); } }
function updateCollection(id, field, value) {
if (field === ‘covers’) data[id].covers = value ? [getOriginalImage(stripMarkdownLink(value))] : [];
else data[id][field] = value;
save();
}
function toggleCollection(el) { el.closest(‘.Avm-collection’).classList.toggle(‘Avm-open’); }
// ===== GAMES =====
function getFirstOpenCollectionId() {
// Prefer first collection
const ids = Object.keys(data);
if (!ids.length) { toast(‘Create a collection first’); return null; }
return ids[0];
}
function addGameFromPost(idx) {
// find post from current filtered view by re-filtering
const q = document.getElementById(‘searchInput’).value.toLowerCase().trim();
const filtered = allPosts.filter(p => {
const matchesLabel = !activeLabel || p.labels.includes(activeLabel);
const matchesQuery = !q || p.title.toLowerCase().includes(q) || p.labels.some(l => l.toLowerCase().includes(q));
return matchesLabel && matchesQuery;
});
const post = filtered[idx];
if (!post) return;
const collId = document.getElementById(‘targetCollection’)?.value || getFirstOpenCollectionId();
if (!collId) { toast(‘Create a collection first’); return; }
data[collId].games.push({
title: post.title,
url: post.url,
image: post.image
});
save();
toast(`Added “${post.title}” to ${data[collId].name}`);
}
function addGame(collId) { data[collId].games.push({ title: ”, url: ”, image: ” }); save(); }
function updateGame(collId, index, field, value) {
const val = field === ‘url’ || field === ‘image’ ? stripMarkdownLink(value) : value;
data[collId].games[index][field] = val;
save();
}
function deleteGame(collId, index) { data[collId].games.splice(index, 1); save(); }
// ===== RENDER =====
function render() {
const container = document.getElementById(‘collections’);
const empty = document.getElementById(’empty’);
container.innerHTML = ”;
const ids = Object.keys(data);
empty.style.display = ids.length ? ‘none’ : ‘block’;
ids.forEach(id => {
const coll = data[id];
const cover = coll.covers?.[0] || ”;
const el = document.createElement(‘div’);
el.className = ‘Avm-collection Avm-open’;
el.dataset.id = id;
el.innerHTML = `
Games in this collection
Tip: Click any post card above to add it here automatically.
`).join(”)}
`;
container.appendChild(el);
});
updateJSONPreview();
}
function updateJSONPreview() {
const mode = document.getElementById(‘outputMode’)?.value || ‘single’;
const singleWrap = document.getElementById(‘singleCollectionWrap’);
const exportSelect = document.getElementById(‘exportCollection’);
const hint = document.getElementById(‘outputHint’);
// Populate export collection dropdown
if (exportSelect) {
const ids = Object.keys(data);
let current = exportSelect.value;
if (!current || !ids.includes(current)) current = ids[0] || ”;
exportSelect.innerHTML = ids.map(id => `${escapeHtml(data[id].name || id)}`).join(”);
singleWrap.style.display = mode === ‘single’ ? ‘block’ : ‘none’;
}
const cleanCollection = (coll) => ({
name: coll.name || ”,
description: coll.description || ”,
covers: Array.isArray(coll.covers) ? coll.covers.filter(Boolean) : [],
games: (coll.games || []).map(g => ({
title: g.title || ”,
url: g.url || ”,
image: g.image || ”
}))
});
let output = ”;
if (mode === ‘single’) {
const exportId = exportSelect?.value || Object.keys(data)[0];
if (!exportId || !data[exportId]) {
document.getElementById(‘jsonPreview’).textContent = ‘// Create or select a collection first’;
if (hint) hint.textContent = ‘Create a collection to generate code.’;
return;
}
const safeKey = sanitizeId(data[exportId].name || exportId);
const json = JSON.stringify(cleanCollection(data[exportId]), null, 2);
output = `${safeKey}: ${unquoteKeys(json)},`;
if (hint) hint.textContent = `Paste this into your existing window.ZG_COLLECTIONS = { … } object.`;
} else {
const clean = {};
for (const [key, coll] of Object.entries(data)) {
const safeKey = sanitizeId(coll.name || key);
clean[safeKey] = cleanCollection(coll);
}
const json = JSON.stringify(clean, null, 2);
output = `window.ZG_COLLECTIONS = ${unquoteKeys(json)};`;
if (hint) hint.textContent = ‘Paste this full script into your Blogger page in HTML view.’;
}
document.getElementById(‘jsonPreview’).textContent = output;
}
function unquoteKeys(json) {
// Convert “key”: to key: for cleaner output matching user’s style
return json.replace(/”([a-zA-Z0-9_]+)”: /g, ‘$1: ‘);
}
// ===== IMPORT / EXPORT =====
function copyJSON() {
const text = document.getElementById(‘jsonPreview’).textContent;
navigator.clipboard.writeText(text).then(() => toast(‘Code copied!’));
}
function downloadJSON() {
const text = document.getElementById(‘jsonPreview’).textContent;
const mode = document.getElementById(‘outputMode’)?.value || ‘single’;
let blob, filename;
if (mode === ‘full’) {
blob = new Blob([`n${text}n`], { type: ‘text/html’ });
filename = ‘zg-collections.html’;
} else {
blob = new Blob([text], { type: ‘text/plain’ });
filename = ‘collection-snippet.txt’;
}
const a = document.createElement(‘a’);
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
toast(‘Downloaded!’);
}
function importJSON() { document.getElementById(‘fileInput’).click(); }
function handleFile(e) {
const file = e.target.files[0]; if (!file) return;
const reader = new FileReader();
reader.onload = ev => {
try {
let raw = ev.target.result;
const scriptMatch = raw.match(/]*>([sS]*?)/i);
if (scriptMatch) raw = scriptMatch[1];
const assignMatch = raw.match(/window.ZG_COLLECTIONSs*=s*({[sS]*});?/);
const jsonText = assignMatch ? assignMatch[1] : raw;
const parsed = JSON.parse(jsonText);
if (parsed && typeof parsed === ‘object’) {
// Convert all image URLs to original size on import
Object.values(parsed).forEach(coll => {
if (coll.covers) coll.covers = coll.covers.map(getOriginalImage);
if (coll.games) coll.games.forEach(g => { if (g.image) g.image = getOriginalImage(g.image); });
});
data = parsed; save(); toast(‘Imported!’);
}
else alert(‘Invalid structure’);
} catch (err) { alert(‘Could not parse: ‘ + err.message); }
};
reader.readAsText(file);
e.target.value = ”;
}
// Init
render();
updateTargetCollection();
updateJSONPreview();