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:
101
server.js
101
server.js
@@ -1,32 +1,94 @@
|
||||
import express from 'express';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { stmts } from './db.js';
|
||||
import db, { stmts, setFiddleTags } from './db.js';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
|
||||
// HTML routes must be defined before static middleware (which would serve index.html for /)
|
||||
app.get('/', (_req, res) => {
|
||||
res.sendFile('browse.html', { root: 'public' });
|
||||
});
|
||||
|
||||
app.get('/new', (_req, res) => {
|
||||
res.sendFile('index.html', { root: 'public' });
|
||||
});
|
||||
|
||||
app.get('/embed/:id', (_req, res) => {
|
||||
res.sendFile('embed.html', { root: 'public' });
|
||||
});
|
||||
|
||||
app.get('/f/:id', (_req, res) => {
|
||||
res.sendFile('index.html', { root: 'public' });
|
||||
});
|
||||
|
||||
app.use(express.static('public', { index: false }));
|
||||
|
||||
// API: Create fiddle
|
||||
app.post('/api/fiddles', (req, res) => {
|
||||
const id = nanoid(10);
|
||||
const { title = 'Untitled', html = '', css = '', css_type = 'css', js = '', js_type = 'javascript' } = req.body;
|
||||
const { title = 'Untitled', html = '', css = '', css_type = 'css', js = '', js_type = 'javascript', listed = 1, tags = [] } = req.body;
|
||||
try {
|
||||
stmts.insert.run({ id, title, html, css, css_type, js, js_type });
|
||||
res.json({ id, title, html, css, css_type, js, js_type });
|
||||
stmts.insert.run({ id, title, html, css, css_type, js, js_type, listed: listed ? 1 : 0 });
|
||||
if (tags.length) setFiddleTags(id, tags);
|
||||
const fiddleTags = stmts.getTagsForFiddle.all(id);
|
||||
res.json({ id, title, html, css, css_type, js, js_type, listed, tags: fiddleTags });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API: List recent fiddles
|
||||
app.get('/api/fiddles', (_req, res) => {
|
||||
res.json(stmts.list.all());
|
||||
// API: List/search fiddles
|
||||
app.get('/api/fiddles', (req, res) => {
|
||||
const { q = '', js_type = '', tag = '', page = '1', limit = '20', sort = 'updated' } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page, 10) || 1);
|
||||
const limitNum = Math.min(100, Math.max(1, parseInt(limit, 10) || 20));
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
let where = 'WHERE f.listed = 1';
|
||||
const params = {};
|
||||
|
||||
if (q) {
|
||||
where += ' AND f.title LIKE @q';
|
||||
params.q = `%${q}%`;
|
||||
}
|
||||
if (js_type) {
|
||||
where += ' AND f.js_type = @js_type';
|
||||
params.js_type = js_type;
|
||||
}
|
||||
if (tag) {
|
||||
where += ' AND EXISTS (SELECT 1 FROM fiddle_tags ft2 JOIN tags t2 ON t2.id = ft2.tag_id WHERE ft2.fiddle_id = f.id AND t2.name = @tag COLLATE NOCASE)';
|
||||
params.tag = tag;
|
||||
}
|
||||
|
||||
const orderBy = sort === 'created' ? 'f.created_at DESC' : 'f.updated_at DESC';
|
||||
|
||||
try {
|
||||
const countRow = db.prepare(`SELECT COUNT(*) as total FROM fiddles f ${where}`).get(params);
|
||||
const fiddles = db.prepare(`
|
||||
SELECT f.id, f.title, f.css_type, f.js_type, f.created_at, f.updated_at,
|
||||
SUBSTR(f.html, 1, 200) as html_preview, SUBSTR(f.js, 1, 200) as js_preview
|
||||
FROM fiddles f ${where}
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT @limit OFFSET @offset
|
||||
`).all({ ...params, limit: limitNum, offset });
|
||||
|
||||
// Attach tags to each fiddle
|
||||
for (const f of fiddles) {
|
||||
f.tags = stmts.getTagsForFiddle.all(f.id);
|
||||
}
|
||||
|
||||
res.json({ fiddles, total: countRow.total, page: pageNum, limit: limitNum });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API: Get fiddle
|
||||
app.get('/api/fiddles/:id', (req, res) => {
|
||||
const fiddle = stmts.get.get(req.params.id);
|
||||
if (!fiddle) return res.status(404).json({ error: 'Not found' });
|
||||
fiddle.tags = stmts.getTagsForFiddle.all(fiddle.id);
|
||||
res.json(fiddle);
|
||||
});
|
||||
|
||||
@@ -34,14 +96,25 @@ app.get('/api/fiddles/:id', (req, res) => {
|
||||
app.put('/api/fiddles/:id', (req, res) => {
|
||||
const existing = stmts.get.get(req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'Not found' });
|
||||
const { title = existing.title, html = existing.html, css = existing.css, css_type = existing.css_type, js = existing.js, js_type = existing.js_type || 'javascript' } = req.body;
|
||||
stmts.update.run({ id: req.params.id, title, html, css, css_type, js, js_type });
|
||||
res.json({ id: req.params.id, title, html, css, css_type, js, js_type });
|
||||
const {
|
||||
title = existing.title,
|
||||
html = existing.html,
|
||||
css = existing.css,
|
||||
css_type = existing.css_type,
|
||||
js = existing.js,
|
||||
js_type = existing.js_type || 'javascript',
|
||||
listed = existing.listed,
|
||||
tags,
|
||||
} = req.body;
|
||||
stmts.update.run({ id: req.params.id, title, html, css, css_type, js, js_type, listed: listed ? 1 : 0 });
|
||||
if (Array.isArray(tags)) setFiddleTags(req.params.id, tags);
|
||||
const fiddleTags = stmts.getTagsForFiddle.all(req.params.id);
|
||||
res.json({ id: req.params.id, title, html, css, css_type, js, js_type, listed, tags: fiddleTags });
|
||||
});
|
||||
|
||||
// SPA route: serve index.html for /f/:id
|
||||
app.get('/f/:id', (_req, res) => {
|
||||
res.sendFile('index.html', { root: 'public' });
|
||||
// API: List tags
|
||||
app.get('/api/tags', (_req, res) => {
|
||||
res.json({ tags: stmts.listTags.all() });
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
Reference in New Issue
Block a user