Files
avbeat-com/src/app/news/NewsPageClient.tsx

327 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useState, useMemo, useEffect } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import AppImage from "@/components/ui/AppImage";
import type { Article } from "@/lib/articles/types";
import AISuggestedArticles from "@/components/AISuggestedArticles";
const CATEGORIES = ["All", "Cloud", "AI", "IP Workflows", "Live Production", "Streaming", "Audio", "Cameras", "NAB 2026", "EAS", "Sports Broadcasting", "Ad Tech"];
// Map URL category slugs (?category=live-production) to a matching CATEGORIES label.
function categoryFromSlug(slug: string | null): string {
if (!slug) return "All";
const norm = slug.toLowerCase().replace(/[-_]+/g, " ").trim();
const hit = CATEGORIES.find((c) => c.toLowerCase() === norm);
return hit || "All";
}
const SORT_OPTIONS = [
{ value: "newest", label: "Newest First" },
{ value: "oldest", label: "Oldest First" },
{ value: "az", label: "A → Z" },
{ value: "za", label: "Z → A" },
];
function parseArticleDate(dateStr: string): Date {
return new Date(dateStr);
}
export default function NewsPage({ articles }: { articles: Article[] }) {
const allArticles = Array.isArray(articles) ? articles : [];
const searchParams = useSearchParams();
const [search, setSearch] = useState("");
const [activeCategory, setActiveCategory] = useState("All");
const [dateFrom, setDateFrom] = useState("");
const [dateTo, setDateTo] = useState("");
const [sortBy, setSortBy] = useState("newest");
// Hydrate filter state from URL (?category=live-production&search=foo)
useEffect(() => {
const cat = categoryFromSlug(searchParams?.get("category") ?? null);
if (cat !== "All") setActiveCategory(cat);
const q = searchParams?.get("search") ?? searchParams?.get("q");
if (q) setSearch(q);
}, [searchParams]);
const filtered = useMemo(() => {
let result = [...allArticles];
// Search filter
if (search.trim()) {
const q = search.toLowerCase();
result = result.filter((a) => {
const title = (a?.title ?? "").toLowerCase();
const excerpt = (a?.excerpt ?? "").toLowerCase();
const author = (a?.author ?? "").toLowerCase();
const tags = Array.isArray(a?.tags) ? a.tags : [];
return (
title.includes(q) ||
excerpt.includes(q) ||
author.includes(q) ||
tags.some((t) => (t ?? "").toLowerCase().includes(q))
);
});
}
// Category filter
if (activeCategory !== "All") {
const cat = activeCategory.toLowerCase();
result = result.filter((a) => {
const tags = Array.isArray(a?.tags) ? a.tags : [];
const category = (a?.category ?? "").toLowerCase();
return (
tags.some((t) => (t ?? "").toLowerCase() === cat) ||
category.includes(cat)
);
});
}
// Date range filter
if (dateFrom) {
const from = new Date(dateFrom);
result = result.filter((a) => parseArticleDate(a?.date ?? "") >= from);
}
if (dateTo) {
const to = new Date(dateTo);
to.setHours(23, 59, 59, 999);
result = result.filter((a) => parseArticleDate(a?.date ?? "") <= to);
}
// Sorting
result.sort((a, b) => {
const ta = (a?.title ?? "");
const tb = (b?.title ?? "");
if (sortBy === "newest") return parseArticleDate(b?.date ?? "").getTime() - parseArticleDate(a?.date ?? "").getTime();
if (sortBy === "oldest") return parseArticleDate(a?.date ?? "").getTime() - parseArticleDate(b?.date ?? "").getTime();
if (sortBy === "az") return ta.localeCompare(tb);
if (sortBy === "za") return tb.localeCompare(ta);
return 0;
});
return result;
}, [allArticles, search, activeCategory, dateFrom, dateTo, sortBy]);
const hasActiveFilters = search || activeCategory !== "All" || dateFrom || dateTo || sortBy !== "newest";
function clearFilters() {
setSearch("");
setActiveCategory("All");
setDateFrom("");
setDateTo("");
setSortBy("newest");
}
return (
<div className="min-h-screen bg-background">
<Header />
{/* DO NOT OVERRIDE — News page hero header */}
<div className="bg-[#111] border-b border-[#222] py-8">
<div className="max-w-container mx-auto px-4">
<div className="flex items-center gap-3 mb-2">
<span className="section-label">News</span>
<div className="flex-1 h-px bg-[#2a2a2a]" />
</div>
<h1 className="font-heading text-white text-3xl font-bold">Latest Broadcast News</h1>
<p className="text-[#777] font-body text-sm mt-2">Breaking news and industry updates from the world of broadcast engineering</p>
</div>
</div>
{/* Search & Filter Bar */}
<div className="bg-[#0d0d0d] border-b border-[#1e1e1e] py-4">
<div className="max-w-container mx-auto px-4 space-y-3">
{/* Row 1: Search + Sort */}
<div className="flex flex-col sm:flex-row gap-3">
{/* Search */}
<div className="relative flex-1">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#555]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" />
</svg>
<input
type="text"
placeholder="Search articles, topics, authors…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] text-[#e0e0e0] placeholder-[#555] text-sm font-body pl-9 pr-4 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors"
/>
{search && (
<button
onClick={() => setSearch("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[#555] hover:text-[#e0e0e0] transition-colors"
aria-label="Clear search"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* Date Range */}
<div className="flex items-center gap-2">
<input
type="date"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors [color-scheme:dark]"
title="From date"
/>
<span className="text-[#444] text-xs"></span>
<input
type="date"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors [color-scheme:dark]"
title="To date"
/>
</div>
{/* Sort */}
<div className="relative">
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="appearance-none bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body pl-3 pr-8 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors cursor-pointer"
>
{SORT_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<svg className="absolute right-2.5 top-1/2 -translate-y-1/2 w-3 h-3 text-[#555] pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
{/* Row 2: Category chips */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[#555] text-[10px] font-body uppercase tracking-wider shrink-0">Topics:</span>
<div className="flex gap-1.5 flex-wrap">
{CATEGORIES.map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-2.5 py-1 text-[11px] font-body rounded-sm border transition-colors ${
activeCategory === cat
? "bg-[#3b82f6] border-[#3b82f6] text-white"
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
}`}
>
{cat}
</button>
))}
</div>
{hasActiveFilters && (
<button
onClick={clearFilters}
className="ml-auto text-[11px] font-body text-[#555] hover:text-[#e0e0e0] underline transition-colors shrink-0"
>
Clear all
</button>
)}
</div>
</div>
</div>
<main className="max-w-container mx-auto px-4 py-8">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Article list */}
<div className="lg:col-span-8">
{/* Results count */}
<div className="flex items-center justify-between mb-4">
<p className="text-[#555] font-body text-xs">
{filtered.length === allArticles.length
? `${allArticles.length} articles`
: `${filtered.length} of ${allArticles.length} articles`}
</p>
</div>
{filtered.length === 0 ? (
<div className="py-16 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="text-[#555] font-body text-sm">No articles match your filters.</p>
<button onClick={clearFilters} className="mt-3 text-[#3b82f6] text-xs font-body hover:underline">Clear filters</button>
</div>
) : (
<div className="space-y-0">
{filtered.map((article) => {
if (!article?.slug) return null;
return (
<Link
key={article.slug}
href={`/news/${article.slug}`}
className="flex gap-4 py-5 border-b border-[#222] group hover:bg-[#111] transition-colors px-2 -mx-2">
<div className="flex-shrink-0 w-[160px] h-[105px] relative overflow-hidden rounded-sm">
<AppImage
src={article.image || "/assets/images/article-placeholder.jpg"}
alt={article.alt || article.title || "BroadcastBeat article"}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="160px"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-[#3b82f6] font-body text-[10px] font-bold uppercase tracking-wider">{article.category || "NEWS"}</span>
</div>
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2 group-hover:text-[#3b82f6] transition-colors line-clamp-2">
{article.title || "Untitled"}
</h2>
<p className="text-[#777] font-body text-sm leading-relaxed line-clamp-2">{article.excerpt || ""}</p>
<div className="flex items-center gap-2 mt-2">
<span className="text-[#555] font-body text-xs">By {article.author || "Staff Reporter"}</span>
<span className="text-[#444] text-[10px]">·</span>
<span className="text-[#555] font-body text-xs">{article.readTime || ""}</span>
</div>
</div>
</Link>
);
})}
</div>
)}
</div>
{/* Sidebar */}
<aside className="lg:col-span-4 space-y-6">
{/* AI Suggested Articles */}
<AISuggestedArticles variant="full" />
<div className="bg-[#111] border border-[#222] p-4">
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-3 pb-2 border-b border-[#222]">
Popular Tags
</h3>
<div className="flex flex-wrap gap-2">
{["IP Workflows", "Cloud", "AI", "Live Production", "Streaming", "NAB 2026", "Audio", "Cameras"].map((tag) => (
<button
key={tag}
onClick={() => setActiveCategory(tag)}
className={`px-2 py-1 border text-xs font-body rounded-sm transition-colors ${
activeCategory === tag
? "bg-[#3b82f6] border-[#3b82f6] text-white"
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
}`}
>
{tag}
</button>
))}
</div>
</div>
<div className="bg-[#0d1520] border border-[#1e3a5f] p-4">
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-2">Newsletter</h3>
<p className="text-[#777] text-xs font-body mb-3">Get broadcast news delivered to your inbox every week.</p>
<Link href="/home-page#newsletter" className="btn-subscribe text-xs py-2 px-4 inline-block">Subscribe Free</Link>
</div>
</aside>
</div>
</main>
<Footer />
</div>
);
}