Add a global search feature to find articles and videos

Introduces a global search functionality with a dedicated search page and integrates a search icon/input into the header and mobile menu. The search queries articles by title, excerpt, and content, and videos by title and description. Updates are made to `App.tsx` to include the new route, and `header.tsx` to implement the search UI and logic.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 90e58e4e-8d41-41e7-bc45-74196314bd78
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/jdAEdU5
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic
2026-03-04 08:59:09 +00:00
parent 740595b231
commit 969c6912ed
6 changed files with 825 additions and 530 deletions
+528 -528
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -90,6 +90,50 @@ export async function registerRoutes(
res.json(articles);
});
app.get("/api/search", async (req, res) => {
const q = (req.query.q as string || "").trim().toLowerCase();
if (!q || q.length < 2) return res.json({ articles: [], videos: [] });
try {
const articles = await storage.getArticles();
const matchedArticles = articles.filter(a =>
a.title.toLowerCase().includes(q) ||
a.excerpt.toLowerCase().includes(q) ||
a.content.replace(/<[^>]+>/g, "").toLowerCase().includes(q) ||
a.category.toLowerCase().includes(q)
).slice(0, 10);
const cacheKey = `videos_1_100_`;
let videos: any[] = [];
const cached = getCached<any>(cacheKey, 30 * 60 * 1000);
if (cached) {
videos = cached.items || [];
} else {
try {
const data = await bunnyFetch(`/library/${LIBRARY_ID}/videos?page=1&itemsPerPage=100&orderBy=date`);
videos = (data.items || []).map((v: any) => {
const descTag = (v.metaTags || []).find((t: any) => t.property === "description");
return {
guid: v.guid,
title: (v.title || "").replace(/\.mp4$/i, ""),
description: descTag?.value || v.description || "",
thumbnail: `https://${CDN_HOST}/${v.guid}/${v.thumbnailFileName || "thumbnail.jpg"}`,
embedUrl: `https://player.mediadelivery.net/embed/${LIBRARY_ID}/${v.guid}`,
};
});
} catch {}
}
const matchedVideos = videos.filter((v: any) =>
v.title.toLowerCase().includes(q) ||
(v.description || "").toLowerCase().includes(q)
).slice(0, 10);
res.json({ articles: matchedArticles, videos: matchedVideos });
} catch (err: any) {
res.status(500).json({ message: err.message });
}
});
app.get("/api/articles/:slug", async (req, res) => {
const article = await storage.getArticleBySlug(req.params.slug);
if (!article) {