Add browse dashboard, tags, visibility control, export, QR sharing, and embed mode

- Browse dashboard at / with search, framework filter, tag pills, and pagination
- Tags system with autocomplete datalist and per-fiddle tag management
- Listed/unlisted toggle for visibility control (unlisted still accessible via direct URL)
- Export standalone HTML with inlined CSS/JS and framework CDN tags
- QR code modal for sharing fiddle URLs
- Embed mode at /embed/:id for minimal preview-only rendering
- Extract shared loadScript() utility from 4 files into utils.js
- Database schema: listed column, tags and fiddle_tags tables with index
This commit is contained in:
root
2026-02-26 14:19:52 -06:00
parent 7f51af17a3
commit e41c3e7dc4
18 changed files with 921 additions and 78 deletions

View File

@@ -8,14 +8,17 @@ import { renderPreview } from './preview.js';
import { initConsole, clearConsole } from './console-panel.js';
import { compileCss } from './preprocessors.js';
import { compileJs } from './js-preprocessors.js';
import { createFiddle, loadFiddle, updateFiddle } from './api.js';
import { createFiddle, loadFiddle, updateFiddle, listTags } from './api.js';
import { getPref, setPref } from './preferences.js';
import { initEmmet } from './emmet.js';
import { initKeybindings } from './keybindings.js';
import { initResizer, clearInlineSizes } from './resizer.js';
import { exportHtml } from './export.js';
import { showQrModal } from './qr.js';
let currentId = null;
let debounceTimer = null;
let currentTags = [];
const $ = (sel) => document.querySelector(sel);
@@ -89,12 +92,14 @@ async function save() {
const title = $('#title-input').value || 'Untitled';
const css_type = getCssType();
const js_type = MODE_TO_JS_TYPE[getCurrentMode()] || 'javascript';
const listed = $('#listed-checkbox').checked ? 1 : 0;
const tags = currentTags.slice();
try {
if (currentId) {
await updateFiddle(currentId, { title, html, css, css_type, js, js_type });
await updateFiddle(currentId, { title, html, css, css_type, js, js_type, listed, tags });
showToast(`Saved! Share: ${location.origin}/f/${currentId}`);
} else {
const result = await createFiddle({ title, html, css, css_type, js, js_type });
const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, tags });
currentId = result.id;
history.pushState(null, '', `/f/${currentId}`);
showToast(`Saved! Share: ${location.origin}/f/${currentId}`);
@@ -109,8 +114,10 @@ async function fork() {
const title = ($('#title-input').value || 'Untitled') + ' (fork)';
const css_type = getCssType();
const js_type = MODE_TO_JS_TYPE[getCurrentMode()] || 'javascript';
const listed = $('#listed-checkbox').checked ? 1 : 0;
const tags = currentTags.slice();
try {
const result = await createFiddle({ title, html, css, css_type, js, js_type });
const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, tags });
currentId = result.id;
$('#title-input').value = title;
history.pushState(null, '', `/f/${currentId}`);
@@ -136,6 +143,11 @@ async function loadFromUrl() {
// Restore CSS type
setCssType(fiddle.css_type || 'css');
// Restore listed/tags
$('#listed-checkbox').checked = fiddle.listed !== 0;
currentTags = (fiddle.tags || []).map(t => t.name);
renderTags();
setEditorValues(fiddle);
setTimeout(run, 100);
} catch (e) {
@@ -221,6 +233,43 @@ async function init() {
}
});
// Tags input
const tagsInput = $('#tags-input');
tagsInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
const val = tagsInput.value.trim().replace(/,/g, '');
if (val && !currentTags.includes(val)) {
currentTags.push(val);
renderTags();
}
tagsInput.value = '';
}
});
// Load tag suggestions
loadTagSuggestions();
// Export & QR buttons
$('#btn-export').addEventListener('click', async () => {
const mode = getCurrentMode();
const { html, css, js } = getEditorValues();
const cssType = getCssType();
const title = $('#title-input').value || 'Untitled';
try {
const compiledCss = await compileCss(css, cssType);
const result = await compileJs(js, mode);
exportHtml({ title, html, css: compiledCss, js: result.js, mode, extraCss: result.extraCss, isModule: result.isModule });
} catch (e) {
showToast(`Export failed: ${e.message}`);
}
});
$('#btn-qr').addEventListener('click', () => {
const url = currentId ? `${location.origin}/f/${currentId}` : location.href;
showQrModal(url);
});
// Load fiddle from URL if present
loadFromUrl();
@@ -231,4 +280,32 @@ async function init() {
});
}
function renderTags() {
const container = $('#tags-display');
container.innerHTML = '';
for (const tag of currentTags) {
const pill = document.createElement('span');
pill.className = 'tag-pill';
pill.innerHTML = `${tag}<button class="tag-remove">&times;</button>`;
pill.querySelector('.tag-remove').addEventListener('click', () => {
currentTags = currentTags.filter(t => t !== tag);
renderTags();
});
container.appendChild(pill);
}
}
async function loadTagSuggestions() {
try {
const { tags } = await listTags();
const datalist = $('#tags-datalist');
datalist.innerHTML = '';
for (const t of tags) {
const opt = document.createElement('option');
opt.value = t.name;
datalist.appendChild(opt);
}
} catch (_) { /* ignore */ }
}
init();