bookmarks restyling
This commit is contained in:
@@ -24,6 +24,8 @@ const SEARCH_POST_HIDE_THRESHOLD = 2;
|
||||
const SEARCH_POST_RETENTION_DAYS = 90;
|
||||
const MAX_POST_TEXT_LENGTH = 4000;
|
||||
const MIN_TEXT_HASH_LENGTH = 120;
|
||||
const MAX_BOOKMARK_LABEL_LENGTH = 120;
|
||||
const MAX_BOOKMARK_QUERY_LENGTH = 200;
|
||||
|
||||
const screenshotDir = path.join(__dirname, 'data', 'screenshots');
|
||||
if (!fs.existsSync(screenshotDir)) {
|
||||
@@ -316,6 +318,48 @@ function computePostTextHash(text) {
|
||||
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
||||
}
|
||||
|
||||
function normalizeBookmarkQuery(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let query = value.trim();
|
||||
if (!query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
query = query.replace(/\s+/g, ' ');
|
||||
if (query.length > MAX_BOOKMARK_QUERY_LENGTH) {
|
||||
query = query.slice(0, MAX_BOOKMARK_QUERY_LENGTH);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
function normalizeBookmarkLabel(value, fallback = '') {
|
||||
const base = typeof value === 'string' ? value.trim() : '';
|
||||
let label = base || fallback || '';
|
||||
label = label.replace(/\s+/g, ' ');
|
||||
if (label.length > MAX_BOOKMARK_LABEL_LENGTH) {
|
||||
label = label.slice(0, MAX_BOOKMARK_LABEL_LENGTH);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
function serializeBookmark(row) {
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
label: row.label,
|
||||
query: row.query,
|
||||
created_at: sqliteTimestampToUTC(row.created_at),
|
||||
updated_at: sqliteTimestampToUTC(row.updated_at),
|
||||
last_clicked_at: sqliteTimestampToUTC(row.last_clicked_at)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFacebookPostUrl(rawValue) {
|
||||
if (typeof rawValue !== 'string') {
|
||||
return null;
|
||||
@@ -655,6 +699,66 @@ db.exec(`
|
||||
ON search_seen_posts(last_seen_at);
|
||||
`);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS bookmarks (
|
||||
id TEXT PRIMARY KEY,
|
||||
label TEXT,
|
||||
query TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_clicked_at DATETIME,
|
||||
UNIQUE(query COLLATE NOCASE)
|
||||
);
|
||||
`);
|
||||
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_bookmarks_last_clicked_at
|
||||
ON bookmarks(last_clicked_at);
|
||||
`);
|
||||
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_bookmarks_created_at
|
||||
ON bookmarks(created_at);
|
||||
`);
|
||||
|
||||
const listBookmarksStmt = db.prepare(`
|
||||
SELECT id, label, query, created_at, updated_at, last_clicked_at
|
||||
FROM bookmarks
|
||||
ORDER BY
|
||||
(last_clicked_at IS NULL),
|
||||
datetime(COALESCE(last_clicked_at, created_at)) DESC,
|
||||
label COLLATE NOCASE
|
||||
`);
|
||||
|
||||
const getBookmarkByIdStmt = db.prepare(`
|
||||
SELECT id, label, query, created_at, updated_at, last_clicked_at
|
||||
FROM bookmarks
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
const findBookmarkByQueryStmt = db.prepare(`
|
||||
SELECT id
|
||||
FROM bookmarks
|
||||
WHERE LOWER(query) = LOWER(?)
|
||||
`);
|
||||
|
||||
const insertBookmarkStmt = db.prepare(`
|
||||
INSERT INTO bookmarks (id, label, query)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
|
||||
const deleteBookmarkStmt = db.prepare(`
|
||||
DELETE FROM bookmarks
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
const updateBookmarkLastClickedStmt = db.prepare(`
|
||||
UPDATE bookmarks
|
||||
SET last_clicked_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
ensureColumn('posts', 'checked_count', 'checked_count INTEGER DEFAULT 0');
|
||||
ensureColumn('posts', 'screenshot_path', 'screenshot_path TEXT');
|
||||
ensureColumn('posts', 'created_by_profile', 'created_by_profile INTEGER');
|
||||
@@ -1693,6 +1797,79 @@ function mapPostRow(post) {
|
||||
};
|
||||
}
|
||||
|
||||
app.get('/api/bookmarks', (req, res) => {
|
||||
try {
|
||||
const rows = listBookmarksStmt.all();
|
||||
res.json(rows.map(serializeBookmark));
|
||||
} catch (error) {
|
||||
console.error('Failed to load bookmarks:', error);
|
||||
res.status(500).json({ error: 'Bookmarks konnten nicht geladen werden' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/bookmarks', (req, res) => {
|
||||
try {
|
||||
const payload = req.body || {};
|
||||
const normalizedQuery = normalizeBookmarkQuery(payload.query);
|
||||
if (!normalizedQuery) {
|
||||
return res.status(400).json({ error: 'Ungültiger Suchbegriff' });
|
||||
}
|
||||
|
||||
if (findBookmarkByQueryStmt.get(normalizedQuery)) {
|
||||
return res.status(409).json({ error: 'Bookmark existiert bereits' });
|
||||
}
|
||||
|
||||
const normalizedLabel = normalizeBookmarkLabel(payload.label, normalizedQuery);
|
||||
const id = uuidv4();
|
||||
insertBookmarkStmt.run(id, normalizedLabel, normalizedQuery);
|
||||
const saved = getBookmarkByIdStmt.get(id);
|
||||
|
||||
res.status(201).json(serializeBookmark(saved));
|
||||
} catch (error) {
|
||||
console.error('Failed to create bookmark:', error);
|
||||
res.status(500).json({ error: 'Bookmark konnte nicht erstellt werden' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/bookmarks/:bookmarkId/click', (req, res) => {
|
||||
const { bookmarkId } = req.params;
|
||||
if (!bookmarkId) {
|
||||
return res.status(400).json({ error: 'Bookmark-ID fehlt' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = updateBookmarkLastClickedStmt.run(bookmarkId);
|
||||
if (!result.changes) {
|
||||
return res.status(404).json({ error: 'Bookmark nicht gefunden' });
|
||||
}
|
||||
|
||||
const updated = getBookmarkByIdStmt.get(bookmarkId);
|
||||
res.json(serializeBookmark(updated));
|
||||
} catch (error) {
|
||||
console.error('Failed to register bookmark click:', error);
|
||||
res.status(500).json({ error: 'Bookmark konnte nicht aktualisiert werden' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/bookmarks/:bookmarkId', (req, res) => {
|
||||
const { bookmarkId } = req.params;
|
||||
if (!bookmarkId) {
|
||||
return res.status(400).json({ error: 'Bookmark-ID fehlt' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = deleteBookmarkStmt.run(bookmarkId);
|
||||
if (!result.changes) {
|
||||
return res.status(404).json({ error: 'Bookmark nicht gefunden' });
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete bookmark:', error);
|
||||
res.status(500).json({ error: 'Bookmark konnte nicht gelöscht werden' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all posts
|
||||
app.get('/api/posts', (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user