feat: related content and featured cross-content sidebar widgets

- Add excludeId arg to videos and articles GraphQL resolvers
- Add excludeId + featured params to getVideos/getArticles services
- Video page: fetch related videos by tag + featured article in parallel
- Article page: fetch related articles by category + featured video in parallel
- Implement sidebar widgets with thumbnails, metadata, hover interactions
- Add videos.related and magazine.related i18n keys
- Seed dummy articles (spotlight, interview, psychology) and videos with
  overlapping tags for testing related content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 18:35:04 +01:00
parent 6d1726ee97
commit 91951667e3
8 changed files with 251 additions and 85 deletions

View File

@@ -1,7 +1,7 @@
import { builder } from "../builder";
import { ArticleType, ArticleListType, AdminArticleListType } from "../types/index";
import { articles, users } from "../../db/schema/index";
import { eq, and, lte, desc, asc, ilike, or, count, arrayContains, isNotNull, type SQL } from "drizzle-orm";
import { eq, and, lte, desc, asc, ilike, or, count, arrayContains, isNotNull, ne, type SQL } from "drizzle-orm";
import { requireAdmin } from "../../lib/acl";
import type { DB } from "../../db/connection";
@@ -35,6 +35,7 @@ builder.queryField("articles", (t) =>
offset: t.arg.int(),
sortBy: t.arg.string(),
tag: t.arg.string(),
excludeId: t.arg.string(),
},
resolve: async (_root, args, ctx) => {
const pageSize = args.limit ?? 24;
@@ -46,6 +47,7 @@ builder.queryField("articles", (t) =>
}
if (args.category) conditions.push(eq(articles.category, args.category));
if (args.tag) conditions.push(arrayContains(articles.tags, [args.tag]));
if (args.excludeId) conditions.push(ne(articles.id, args.excludeId));
if (args.search) {
conditions.push(
or(