feat: full likes dashboard with connection status, stats, recent likes

- Controller passes baseline status, seen count, recent like posts,
  total count, and flash messages from query params
- View uses Indiekit UI components (section, summary, prose, button,
  notificationBanner) for consistent look
- Recent likes list with thumbnails, titles, channel names
- Connection badge (connected/disconnected), sync controls
- Overview stats: seen videos, like posts, baseline status, last sync
- CSS for likes dashboard components
- Updated en/de locale strings with flash messages and new labels

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-18 21:54:28 +01:00
parent ab5380bf19
commit 6bd7966409
5 changed files with 278 additions and 31 deletions

View File

@@ -16,7 +16,7 @@ import { syncLikes, getLastSyncStatus } from "../likes-sync.js";
export const likesController = {
/**
* GET /likes — show OAuth status & synced likes info
* GET /likes — dashboard showing connection status, sync info, recent likes
*/
async get(request, response, next) {
try {
@@ -26,7 +26,7 @@ export const likesController = {
if (!db) {
return response.render("youtube-likes", {
title: response.locals.__("youtube.likes.title"),
error: { message: "Database not available" },
error: "Database not available",
});
}
@@ -34,19 +34,60 @@ export const likesController = {
if (!oauth?.clientId) {
return response.render("youtube-likes", {
title: response.locals.__("youtube.likes.title"),
error: { message: response.locals.__("youtube.likes.error.noOAuth") },
error: response.locals.__("youtube.likes.error.noOAuth"),
});
}
const tokens = await loadTokens(db);
const isConnected = Boolean(tokens?.refreshToken);
const lastSync = await getLastSyncStatus(db);
const baseline = await db.collection("youtubeMeta").findOne({ key: "likes_baseline" });
const seenCount = await db.collection("youtubeLikesSeen").countDocuments();
// Fetch recent like posts for the overview
let recentLikes = [];
let totalLikePosts = 0;
const postsCollection = request.app.locals.application.collections?.get("posts");
if (postsCollection) {
recentLikes = await postsCollection
.find({
"properties.post-type": "like",
"properties.youtube-video-id": { $exists: true },
})
.sort({ "properties.published": -1 })
.limit(10)
.toArray();
totalLikePosts = await postsCollection.countDocuments({
"properties.post-type": "like",
"properties.youtube-video-id": { $exists: true },
});
}
// Flash messages from query params
const { error: qError, connected, disconnected, synced, skipped } = request.query;
let success = null;
let notice = null;
if (connected) success = response.locals.__("youtube.likes.flash.connected");
if (disconnected) notice = response.locals.__("youtube.likes.flash.disconnected");
if (synced !== undefined) {
const s = parseInt(synced, 10) || 0;
const sk = parseInt(skipped, 10) || 0;
success = response.locals.__("youtube.likes.flash.synced", { synced: s, skipped: sk });
}
response.render("youtube-likes", {
title: response.locals.__("youtube.likes.title"),
isConnected,
lastSync,
baseline,
seenCount,
recentLikes: recentLikes.map((l) => l.properties),
totalLikePosts,
mountPath: request.baseUrl,
error: qError || null,
success,
notice,
});
} catch (error) {
console.error("[YouTube] Likes page error:", error);
@@ -162,6 +203,14 @@ export const likesController = {
return response.json(result);
}
if (result.error) {
return response.redirect(`${request.baseUrl}/likes?error=${encodeURIComponent(result.error)}`);
}
if (result.baselined) {
return response.redirect(`${request.baseUrl}/likes?synced=0&skipped=${result.baselined}`);
}
response.redirect(`${request.baseUrl}/likes?synced=${result.synced}&skipped=${result.skipped}`);
} catch (error) {
console.error("[YouTube] Manual sync error:", error);