import express from 'express'; import { nanoid } from 'nanoid'; import db, { stmts, setFiddleTags, snapshotVersion } from './db.js'; const app = express(); app.use(express.json({ limit: '2mb' })); // 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' }); }); // Published fiddle (clean URL) app.get('/p/:slug', (req, res) => { const row = stmts.getPublishedFiddle.get(req.params.slug); if (!row || !row.published_html) return res.status(404).send('Not found'); res.type('html').send(row.published_html); }); 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', listed = 1, options = '{}', tags = [] } = req.body; try { stmts.insert.run({ id, title, html, css, css_type, js, js_type, listed: listed ? 1 : 0, options }); if (tags.length) setFiddleTags(id, tags); const fiddleTags = stmts.getTagsForFiddle.all(id); res.json({ id, title, html, css, css_type, js, js_type, listed, options, tags: fiddleTags }); } catch (e) { res.status(500).json({ error: e.message }); } }); // 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, f.screenshot, 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); }); // API: Update fiddle 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' }); // Snapshot current state as a version before overwriting snapshotVersion(req.params.id); 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, options = existing.options || '{}', screenshot, tags, } = req.body; stmts.update.run({ id: req.params.id, title, html, css, css_type, js, js_type, listed: listed ? 1 : 0, options }); if (screenshot !== undefined) stmts.updateScreenshot.run(screenshot, req.params.id); 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, options, tags: fiddleTags }); }); // API: List tags app.get('/api/tags', (_req, res) => { res.json({ tags: stmts.listTags.all() }); }); // ===================== Gist Import API ===================== app.post('/api/import/gist', async (req, res) => { const { url } = req.body; if (!url) return res.status(400).json({ error: 'url required' }); // Extract gist ID from URL const match = url.match(/gist\.github\.com\/(?:[^/]+\/)?([a-f0-9]+)/); if (!match) return res.status(400).json({ error: 'Invalid Gist URL' }); const gistId = match[1]; try { const resp = await fetch(`https://api.github.com/gists/${gistId}`, { headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'Fiddle-App' }, }); if (!resp.ok) throw new Error(`GitHub API returned ${resp.status}`); const gist = await resp.json(); let html = '', css = '', js = '', mode = 'html-css-js', css_type = 'css'; const files = Object.values(gist.files); for (const file of files) { const ext = file.filename.split('.').pop().toLowerCase(); const content = file.content || ''; if (ext === 'html' || ext === 'htm') { html = content; } else if (ext === 'css') { css = content; } else if (ext === 'scss') { css = content; css_type = 'scss'; } else if (ext === 'less') { css = content; css_type = 'less'; } else if (ext === 'js' || ext === 'mjs') { js = content; } else if (ext === 'ts') { js = content; mode = 'typescript'; } else if (ext === 'jsx') { js = content; mode = 'react'; } else if (ext === 'tsx') { js = content; mode = 'react-ts'; } else if (ext === 'vue') { js = content; mode = 'vue'; } else if (ext === 'svelte') { js = content; mode = 'svelte'; } else if (ext === 'md' || ext === 'markdown') { js = content; mode = 'markdown'; } else if (ext === 'py') { js = content; mode = 'python'; } } res.json({ html, css, js, mode, css_type, title: gist.description || 'Imported Gist' }); } catch (e) { res.status(502).json({ error: e.message }); } }); // ===================== Publish API ===================== app.post('/api/fiddles/:id/publish', (req, res) => { const fiddle = stmts.get.get(req.params.id); if (!fiddle) return res.status(404).json({ error: 'Not found' }); // Check if already published const existing = stmts.getPublishStatus.get(req.params.id); if (existing && existing.published_slug) { // Re-publish with updated HTML const { html } = req.body; if (html) stmts.publishFiddle.run({ id: req.params.id, slug: existing.published_slug, html }); return res.json({ slug: existing.published_slug, url: `/p/${existing.published_slug}` }); } const slug = nanoid(8); const { html } = req.body; if (!html) return res.status(400).json({ error: 'html body required' }); stmts.publishFiddle.run({ id: req.params.id, slug, html }); res.json({ slug, url: `/p/${slug}` }); }); app.delete('/api/fiddles/:id/publish', (req, res) => { const fiddle = stmts.get.get(req.params.id); if (!fiddle) return res.status(404).json({ error: 'Not found' }); stmts.unpublishFiddle.run(req.params.id); res.json({ ok: true }); }); app.get('/api/fiddles/:id/publish', (req, res) => { const fiddle = stmts.get.get(req.params.id); if (!fiddle) return res.status(404).json({ error: 'Not found' }); const status = stmts.getPublishStatus.get(req.params.id); res.json({ published: !!(status && status.published_slug), slug: status?.published_slug || null }); }); // ===================== Version History API ===================== app.get('/api/fiddles/:id/versions', (req, res) => { const fiddle = stmts.get.get(req.params.id); if (!fiddle) return res.status(404).json({ error: 'Not found' }); const versions = stmts.listVersions.all(req.params.id); res.json({ versions }); }); app.get('/api/fiddles/:id/versions/:ver', (req, res) => { const ver = parseInt(req.params.ver, 10); const version = stmts.getVersion.get(req.params.id, ver); if (!version) return res.status(404).json({ error: 'Version not found' }); res.json(version); }); app.post('/api/fiddles/:id/revert/:ver', (req, res) => { const existing = stmts.get.get(req.params.id); if (!existing) return res.status(404).json({ error: 'Not found' }); const ver = parseInt(req.params.ver, 10); const version = stmts.getVersion.get(req.params.id, ver); if (!version) return res.status(404).json({ error: 'Version not found' }); // Snapshot current state before reverting snapshotVersion(req.params.id); stmts.update.run({ id: req.params.id, title: existing.title, html: version.html, css: version.css, css_type: version.css_type, js: version.js, js_type: version.js_type, listed: existing.listed, options: version.options || existing.options || '{}', }); const updated = stmts.get.get(req.params.id); updated.tags = stmts.getTagsForFiddle.all(req.params.id); res.json(updated); }); // ===================== Collections API ===================== app.post('/api/collections', (req, res) => { const id = nanoid(10); const { name = 'Untitled Collection', description = '' } = req.body; try { stmts.insertCollection.run({ id, name, description }); res.json({ id, name, description, fiddle_count: 0 }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/collections', (_req, res) => { res.json({ collections: stmts.listCollections.all() }); }); app.get('/api/collections/:id', (req, res) => { const col = stmts.getCollection.get(req.params.id); if (!col) return res.status(404).json({ error: 'Not found' }); const fiddles = stmts.getCollectionFiddles.all(req.params.id); for (const f of fiddles) f.tags = stmts.getTagsForFiddle.all(f.id); res.json({ ...col, fiddles }); }); app.put('/api/collections/:id', (req, res) => { const col = stmts.getCollection.get(req.params.id); if (!col) return res.status(404).json({ error: 'Not found' }); const { name = col.name, description = col.description } = req.body; stmts.updateCollection.run({ id: req.params.id, name, description }); res.json({ id: req.params.id, name, description }); }); app.delete('/api/collections/:id', (req, res) => { const col = stmts.getCollection.get(req.params.id); if (!col) return res.status(404).json({ error: 'Not found' }); stmts.deleteCollection.run(req.params.id); res.json({ ok: true }); }); app.post('/api/collections/:id/fiddles', (req, res) => { const col = stmts.getCollection.get(req.params.id); if (!col) return res.status(404).json({ error: 'Collection not found' }); const { fiddle_id } = req.body; if (!fiddle_id) return res.status(400).json({ error: 'fiddle_id required' }); stmts.addFiddleToCollection.run({ collection_id: req.params.id, fiddle_id }); res.json({ ok: true }); }); app.delete('/api/collections/:id/fiddles/:fid', (req, res) => { stmts.removeFiddleFromCollection.run(req.params.id, req.params.fid); res.json({ ok: true }); }); // ===================== Presentation Slides API ===================== app.get('/api/fiddles/:id/slides', (req, res) => { const fiddle = stmts.get.get(req.params.id); if (!fiddle) return res.status(404).json({ error: 'Not found' }); const slides = stmts.listSlides.all(req.params.id); res.json({ slides }); }); app.post('/api/fiddles/:id/slides', (req, res) => { const fiddle = stmts.get.get(req.params.id); if (!fiddle) return res.status(404).json({ error: 'Not found' }); const id = nanoid(10); const { html = '', css = '', js = '', notes = '' } = req.body; const { max_order } = stmts.getMaxSlideOrder.get(req.params.id); stmts.insertSlide.run({ id, fiddle_id: req.params.id, slide_order: max_order + 1, html, css, js, notes }); res.json({ id, fiddle_id: req.params.id, slide_order: max_order + 1, html, css, js, notes }); }); app.put('/api/slides/:slideId', (req, res) => { const slide = stmts.getSlide.get(req.params.slideId); if (!slide) return res.status(404).json({ error: 'Slide not found' }); const { html = slide.html, css = slide.css, js = slide.js, notes = slide.notes, slide_order = slide.slide_order } = req.body; stmts.updateSlide.run({ id: req.params.slideId, html, css, js, notes, slide_order }); res.json({ ...slide, html, css, js, notes, slide_order }); }); app.delete('/api/slides/:slideId', (req, res) => { const slide = stmts.getSlide.get(req.params.slideId); if (!slide) return res.status(404).json({ error: 'Slide not found' }); stmts.deleteSlide.run(req.params.slideId); res.json({ ok: true }); }); const port = process.env.PORT || 3000; app.listen(port, () => console.log(`Fiddle server running on http://localhost:${port}`));