diff --git a/db.js b/db.js index fd7e44a..1875ff1 100644 --- a/db.js +++ b/db.js @@ -54,6 +54,47 @@ db.exec(` db.exec(`CREATE INDEX IF NOT EXISTS idx_fiddles_listed_updated ON fiddles(listed, updated_at DESC)`); +// Migration: add screenshot column +try { + db.exec(`ALTER TABLE fiddles ADD COLUMN screenshot TEXT`); +} catch (_) { /* column already exists */ } + +// Version history table +db.exec(` + CREATE TABLE IF NOT EXISTS fiddle_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fiddle_id TEXT NOT NULL REFERENCES fiddles(id) ON DELETE CASCADE, + version INTEGER NOT NULL, + html TEXT NOT NULL DEFAULT '', + css TEXT NOT NULL DEFAULT '', + css_type TEXT NOT NULL DEFAULT 'css', + js TEXT NOT NULL DEFAULT '', + js_type TEXT NOT NULL DEFAULT 'javascript', + options TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ) +`); +db.exec(`CREATE INDEX IF NOT EXISTS idx_versions_fiddle ON fiddle_versions(fiddle_id, version DESC)`); + +// Collections tables +db.exec(` + CREATE TABLE IF NOT EXISTS collections ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ) +`); +db.exec(` + CREATE TABLE IF NOT EXISTS collection_fiddles ( + collection_id TEXT NOT NULL REFERENCES collections(id) ON DELETE CASCADE, + fiddle_id TEXT NOT NULL REFERENCES fiddles(id) ON DELETE CASCADE, + position INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (collection_id, fiddle_id) + ) +`); + export const stmts = { insert: db.prepare(` INSERT INTO fiddles (id, title, html, css, css_type, js, js_type, listed, options) @@ -86,6 +127,58 @@ export const stmts = { HAVING count > 0 ORDER BY count DESC `), + + // Versions + insertVersion: db.prepare(` + INSERT INTO fiddle_versions (fiddle_id, version, html, css, css_type, js, js_type, options) + VALUES (@fiddle_id, @version, @html, @css, @css_type, @js, @js_type, @options) + `), + getMaxVersion: db.prepare('SELECT COALESCE(MAX(version), 0) as max_ver FROM fiddle_versions WHERE fiddle_id = ?'), + listVersions: db.prepare('SELECT id, version, created_at FROM fiddle_versions WHERE fiddle_id = ? ORDER BY version DESC'), + getVersion: db.prepare('SELECT * FROM fiddle_versions WHERE fiddle_id = ? AND version = ?'), + deleteOldVersions: db.prepare(` + DELETE FROM fiddle_versions WHERE fiddle_id = ? AND id NOT IN ( + SELECT id FROM fiddle_versions WHERE fiddle_id = ? ORDER BY version DESC LIMIT 50 + ) + `), + + // Screenshot + updateScreenshot: db.prepare('UPDATE fiddles SET screenshot = ? WHERE id = ?'), + + // Collections + insertCollection: db.prepare(` + INSERT INTO collections (id, name, description) VALUES (@id, @name, @description) + `), + listCollections: db.prepare(` + SELECT c.*, COUNT(cf.fiddle_id) as fiddle_count + FROM collections c + LEFT JOIN collection_fiddles cf ON cf.collection_id = c.id + GROUP BY c.id + ORDER BY c.updated_at DESC + `), + getCollection: db.prepare('SELECT * FROM collections WHERE id = ?'), + updateCollection: db.prepare(` + UPDATE collections SET name = @name, description = @description, updated_at = datetime('now') WHERE id = @id + `), + deleteCollection: db.prepare('DELETE FROM collections WHERE id = ?'), + addFiddleToCollection: db.prepare(` + INSERT OR IGNORE INTO collection_fiddles (collection_id, fiddle_id, position) + VALUES (@collection_id, @fiddle_id, (SELECT COALESCE(MAX(position), 0) + 1 FROM collection_fiddles WHERE collection_id = @collection_id)) + `), + removeFiddleFromCollection: db.prepare('DELETE FROM collection_fiddles WHERE collection_id = ? AND fiddle_id = ?'), + getCollectionFiddles: 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 + JOIN collection_fiddles cf ON cf.fiddle_id = f.id + WHERE cf.collection_id = ? + ORDER BY cf.position + `), + getCollectionsForFiddle: db.prepare(` + SELECT c.id, c.name FROM collections c + JOIN collection_fiddles cf ON cf.collection_id = c.id + WHERE cf.fiddle_id = ? + `), }; /** @@ -102,4 +195,26 @@ export function setFiddleTags(fiddleId, tagNames) { } } +/** + * Snapshot current fiddle state as a version before overwriting. + */ +export function snapshotVersion(fiddleId) { + const fiddle = stmts.get.get(fiddleId); + if (!fiddle) return; + const { max_ver } = stmts.getMaxVersion.get(fiddleId); + const version = max_ver + 1; + stmts.insertVersion.run({ + fiddle_id: fiddleId, + version, + html: fiddle.html, + css: fiddle.css, + css_type: fiddle.css_type, + js: fiddle.js, + js_type: fiddle.js_type, + options: fiddle.options || '{}', + }); + // Cap at 50 versions + stmts.deleteOldVersions.run(fiddleId, fiddleId); +} + export default db; diff --git a/public/browse.html b/public/browse.html index dd6a40b..bcfda7d 100644 --- a/public/browse.html +++ b/public/browse.html @@ -31,11 +31,26 @@ -
+