200 lines
6.5 KiB
JavaScript
200 lines
6.5 KiB
JavaScript
import { listFiddles, listTags, listCollections, getCollection } from './api.js';
|
|
|
|
const $ = (sel) => document.querySelector(sel);
|
|
let debounceTimer = null;
|
|
let currentPage = 1;
|
|
let activeTag = '';
|
|
let currentView = 'fiddles';
|
|
|
|
const JS_TYPE_LABELS = {
|
|
javascript: 'JS',
|
|
typescript: 'TS',
|
|
react: 'React',
|
|
'react-ts': 'React+TS',
|
|
vue: 'Vue',
|
|
svelte: 'Svelte',
|
|
markdown: 'MD',
|
|
wasm: 'WASM',
|
|
};
|
|
|
|
function relativeTime(dateStr) {
|
|
const now = Date.now();
|
|
const then = new Date(dateStr + 'Z').getTime();
|
|
const diff = Math.floor((now - then) / 1000);
|
|
if (diff < 60) return 'just now';
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
if (diff < 2592000) return `${Math.floor(diff / 86400)}d ago`;
|
|
return new Date(dateStr).toLocaleDateString();
|
|
}
|
|
|
|
function renderCards(fiddles, gridSelector = '#fiddle-grid') {
|
|
const grid = $(gridSelector);
|
|
if (!fiddles.length) {
|
|
grid.innerHTML = '<div class="empty-state">No fiddles found. <a href="/new" style="color: var(--accent)">Create one!</a></div>';
|
|
return;
|
|
}
|
|
grid.innerHTML = fiddles.map(f => {
|
|
const preview = f.js_preview || f.html_preview || '';
|
|
const badge = JS_TYPE_LABELS[f.js_type] || 'JS';
|
|
const tags = (f.tags || []).map(t => `<span class="card-tag">${esc(t.name)}</span>`).join('');
|
|
const thumb = f.screenshot
|
|
? `<img class="card-thumbnail" src="${f.screenshot}" alt="" loading="lazy">`
|
|
: (preview ? `<div class="card-preview" style="padding:12px 16px">${esc(preview)}</div>` : '');
|
|
return `
|
|
<a href="/f/${f.id}" class="fiddle-card">
|
|
${thumb}
|
|
<div class="card-body">
|
|
<div class="card-title">${esc(f.title)}</div>
|
|
<div class="card-meta">
|
|
<span class="card-badge">${badge}</span>
|
|
<span>${relativeTime(f.updated_at)}</span>
|
|
</div>
|
|
${tags ? `<div class="card-tags">${tags}</div>` : ''}
|
|
</div>
|
|
</a>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderPagination(total, page, limit) {
|
|
const pages = Math.ceil(total / limit);
|
|
if (pages <= 1) { $('#pagination').innerHTML = ''; return; }
|
|
let html = '';
|
|
if (page > 1) html += `<button class="page-btn" data-page="${page - 1}">« Prev</button>`;
|
|
for (let i = 1; i <= pages; i++) {
|
|
if (pages > 7 && i > 2 && i < pages - 1 && Math.abs(i - page) > 1) {
|
|
if (i === 3 || i === pages - 2) html += '<span style="color:var(--text-dim)">...</span>';
|
|
continue;
|
|
}
|
|
html += `<button class="page-btn${i === page ? ' active' : ''}" data-page="${i}">${i}</button>`;
|
|
}
|
|
if (page < pages) html += `<button class="page-btn" data-page="${page + 1}">Next »</button>`;
|
|
const el = $('#pagination');
|
|
el.innerHTML = html;
|
|
el.querySelectorAll('.page-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
currentPage = parseInt(btn.dataset.page, 10);
|
|
fetchAndRender();
|
|
});
|
|
});
|
|
}
|
|
|
|
async function renderTagsBar() {
|
|
try {
|
|
const { tags } = await listTags();
|
|
const bar = $('#tags-bar');
|
|
bar.innerHTML = tags.map(t =>
|
|
`<span class="tag-filter${activeTag === t.name ? ' active' : ''}" data-tag="${esc(t.name)}">${esc(t.name)} (${t.count})</span>`
|
|
).join('');
|
|
bar.querySelectorAll('.tag-filter').forEach(el => {
|
|
el.addEventListener('click', () => {
|
|
activeTag = activeTag === el.dataset.tag ? '' : el.dataset.tag;
|
|
currentPage = 1;
|
|
fetchAndRender();
|
|
renderTagsBar();
|
|
});
|
|
});
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
|
|
async function fetchAndRender() {
|
|
const q = $('#search-input').value.trim();
|
|
const js_type = $('#filter-framework').value;
|
|
const sort = $('#filter-sort').value;
|
|
try {
|
|
const { fiddles, total, page, limit } = await listFiddles({ q, js_type, tag: activeTag, page: currentPage, sort });
|
|
renderCards(fiddles);
|
|
renderPagination(total, page, limit);
|
|
} catch (e) {
|
|
$('#fiddle-grid').innerHTML = `<div class="empty-state">Error loading fiddles: ${esc(e.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
function esc(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Event listeners
|
|
$('#search-input').addEventListener('input', () => {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(() => {
|
|
currentPage = 1;
|
|
fetchAndRender();
|
|
}, 300);
|
|
});
|
|
|
|
$('#filter-framework').addEventListener('change', () => {
|
|
currentPage = 1;
|
|
fetchAndRender();
|
|
});
|
|
|
|
$('#filter-sort').addEventListener('change', () => {
|
|
currentPage = 1;
|
|
fetchAndRender();
|
|
});
|
|
|
|
// Browse tabs
|
|
document.querySelectorAll('.browse-tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
currentView = tab.dataset.view;
|
|
document.querySelectorAll('.browse-tab').forEach(t => t.classList.toggle('active', t === tab));
|
|
$('#fiddles-view').style.display = currentView === 'fiddles' ? '' : 'none';
|
|
$('#collections-view').style.display = currentView === 'collections' ? '' : 'none';
|
|
if (currentView === 'collections') renderCollections();
|
|
});
|
|
});
|
|
|
|
async function renderCollections() {
|
|
const grid = $('#collections-grid');
|
|
const detail = $('#collection-detail');
|
|
grid.style.display = '';
|
|
detail.style.display = 'none';
|
|
|
|
try {
|
|
const { collections } = await listCollections();
|
|
if (!collections.length) {
|
|
grid.innerHTML = '<div class="empty-state">No collections yet</div>';
|
|
return;
|
|
}
|
|
grid.innerHTML = collections.map(c => `
|
|
<div class="collection-card" data-id="${c.id}">
|
|
<div class="collection-card-name">${esc(c.name)}</div>
|
|
<div class="collection-card-desc">${esc(c.description || '')}</div>
|
|
<div class="collection-card-count">${c.fiddle_count} fiddle${c.fiddle_count !== 1 ? 's' : ''}</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
grid.querySelectorAll('.collection-card').forEach(card => {
|
|
card.addEventListener('click', () => showCollectionDetail(card.dataset.id));
|
|
});
|
|
} catch (e) {
|
|
grid.innerHTML = `<div class="empty-state">Error: ${esc(e.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
async function showCollectionDetail(id) {
|
|
const grid = $('#collections-grid');
|
|
const detail = $('#collection-detail');
|
|
grid.style.display = 'none';
|
|
detail.style.display = '';
|
|
|
|
try {
|
|
const col = await getCollection(id);
|
|
$('#collection-header').textContent = col.name;
|
|
$('#collection-desc').textContent = col.description || '';
|
|
renderCards(col.fiddles || [], '#collection-fiddles');
|
|
} catch (e) {
|
|
$('#collection-header').textContent = 'Error';
|
|
$('#collection-desc').textContent = e.message;
|
|
}
|
|
}
|
|
|
|
$('#collection-back').addEventListener('click', () => renderCollections());
|
|
|
|
// Init
|
|
renderTagsBar();
|
|
fetchAndRender();
|