feat: board thumbnails, inline rename, UX overhaul + zoom/counter fixes
- Auto-generate board thumbnail from canvas content on save (webp, 400px) - Collection cards show stacked/fanned board thumbnails with random offsets - Three-dot menu on all cards with Rename, Share, Delete actions - Inline rename for collections (header + card) and boards - Collection cards show public/private status and member count - Share button on collection cards for quick access - Add updateCollection/updateBoard API functions - Add thumbnail + object_count columns to boards table with migration - Fix zoom drift: use element-relative coords (offsetX/Y) not getScenePoint - Fix thumbnail generation breaking viewport: use finally block for restore - Fix thumbnail bounds: use scene-space obj coords not screen-space getBoundingRect - Fix object counter: live count from canvas instead of stale DB images table - Fix three-dot menu clipping by removing overflow:hidden from card containers - Status bar shows "objects" instead of "images", updates on add/delete
This commit is contained in:
+37
-6
@@ -57,6 +57,8 @@ db.exec(`
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
canvas_state TEXT DEFAULT '{}',
|
||||
thumbnail TEXT DEFAULT NULL,
|
||||
object_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_by TEXT NOT NULL REFERENCES users(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
@@ -83,6 +85,18 @@ db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_images_board ON images(board_id);
|
||||
`);
|
||||
|
||||
// Migrations — add columns to existing tables
|
||||
try {
|
||||
db.prepare("SELECT thumbnail FROM boards LIMIT 0").get();
|
||||
} catch {
|
||||
db.exec("ALTER TABLE boards ADD COLUMN thumbnail TEXT DEFAULT NULL");
|
||||
}
|
||||
try {
|
||||
db.prepare("SELECT object_count FROM boards LIMIT 0").get();
|
||||
} catch {
|
||||
db.exec("ALTER TABLE boards ADD COLUMN object_count INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// User helpers
|
||||
// ---------------------
|
||||
@@ -128,7 +142,10 @@ function getUserCount() {
|
||||
function getCollections(userId, search, limit = 50, offset = 0) {
|
||||
let query = `
|
||||
SELECT DISTINCT c.*, cm.role as member_role,
|
||||
(SELECT COUNT(*) FROM boards WHERE collection_id = c.id) as board_count
|
||||
(SELECT COUNT(*) FROM boards WHERE collection_id = c.id) as board_count,
|
||||
(SELECT COUNT(*) FROM collection_members WHERE collection_id = c.id) as member_count,
|
||||
(SELECT b.thumbnail FROM boards b WHERE b.collection_id = c.id AND b.thumbnail IS NOT NULL ORDER BY b.updated_at DESC LIMIT 1) as preview_thumbnail,
|
||||
(SELECT GROUP_CONCAT(sub.thumbnail, '|||') FROM (SELECT thumbnail FROM boards WHERE collection_id = c.id AND thumbnail IS NOT NULL ORDER BY updated_at DESC LIMIT 4) sub) as preview_thumbnails
|
||||
FROM collections c
|
||||
LEFT JOIN collection_members cm ON c.id = cm.collection_id AND cm.user_id = ?
|
||||
WHERE (cm.user_id IS NOT NULL OR c.is_public = 1)
|
||||
@@ -230,8 +247,9 @@ function checkCollectionAccess(collectionId, userId) {
|
||||
// ---------------------
|
||||
function getCollectionBoards(collectionId, search, limit = 50, offset = 0) {
|
||||
let query = `
|
||||
SELECT b.*,
|
||||
(SELECT COUNT(*) FROM images WHERE board_id = b.id) as image_count
|
||||
SELECT b.id, b.collection_id, b.name, b.description, b.thumbnail,
|
||||
b.created_by, b.created_at, b.updated_at,
|
||||
b.object_count as image_count
|
||||
FROM boards b
|
||||
WHERE b.collection_id = ?
|
||||
`;
|
||||
@@ -281,9 +299,22 @@ function deleteBoard(boardId) {
|
||||
db.prepare('DELETE FROM boards WHERE id = ?').run(boardId);
|
||||
}
|
||||
|
||||
function saveBoardCanvas(boardId, canvasState) {
|
||||
db.prepare("UPDATE boards SET canvas_state = ?, updated_at = datetime('now') WHERE id = ?")
|
||||
.run(typeof canvasState === 'string' ? canvasState : JSON.stringify(canvasState), boardId);
|
||||
function saveBoardCanvas(boardId, canvasState, thumbnail) {
|
||||
const stateStr = typeof canvasState === 'string' ? canvasState : JSON.stringify(canvasState);
|
||||
// Count objects from canvas state JSON
|
||||
let objectCount = 0;
|
||||
try {
|
||||
const parsed = typeof canvasState === 'string' ? JSON.parse(canvasState) : canvasState;
|
||||
if (parsed && Array.isArray(parsed.objects)) objectCount = parsed.objects.length;
|
||||
} catch {}
|
||||
|
||||
if (thumbnail) {
|
||||
db.prepare("UPDATE boards SET canvas_state = ?, thumbnail = ?, object_count = ?, updated_at = datetime('now') WHERE id = ?")
|
||||
.run(stateStr, thumbnail, objectCount, boardId);
|
||||
} else {
|
||||
db.prepare("UPDATE boards SET canvas_state = ?, object_count = ?, updated_at = datetime('now') WHERE id = ?")
|
||||
.run(stateStr, objectCount, boardId);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
|
||||
@@ -169,12 +169,12 @@ router.post('/:boardId/save', (req, res) => {
|
||||
const result = resolveBoard(req, res, 'editor');
|
||||
if (!result) return;
|
||||
|
||||
const { canvas_state } = req.body;
|
||||
const { canvas_state, thumbnail } = req.body;
|
||||
if (canvas_state === undefined) {
|
||||
return res.status(400).json({ error: 'canvas_state is required' });
|
||||
}
|
||||
|
||||
saveBoardCanvas(result.board.id, canvas_state);
|
||||
saveBoardCanvas(result.board.id, canvas_state, thumbnail || null);
|
||||
|
||||
return res.json({ message: 'Canvas saved' });
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user