- public/assets/images/logo.png + public/brand/logo*.png replaced with the 1320x310 AV Beat logo
- public/assets/logos/avbeat.png added as the canonical source
- tailwind.config.js + src/styles/tailwind.css + src/styles/redesign-tokens.css palette swapped (primary #0F0E0E, secondary #1D1A1A, accent #E67E22 orange, foreground #F0EBE6 warm white)
- Layout / robots / sitemap / home-page / about / contact / press-kit / team / global-error metadata switched to AV Beat (Where AV Meets IT)
- Bulk replaced 'Broadcast Beat'/'BroadcastBeat.com'/'broadcastbeat.com' across user-facing news/forum/marketplace/advertise/search/newsletter pages
- src/lib/supabase/{admin,server,client} now default to schema 'avb' (was 'bb') — overridable via NEXT_PUBLIC_SUPABASE_SCHEMA
- package.json renamed to 'avbeat'
- Single d60701 reference in about/team swapped to E67E22
Design-only replica — no article content migrated.
601 lines
24 KiB
TypeScript
601 lines
24 KiB
TypeScript
"use client";
|
|
import React, { useState, useEffect, useCallback, useId } from "react";
|
|
|
|
import Link from "next/link";
|
|
import Header from "@/components/Header";
|
|
import Footer from "@/components/Footer";
|
|
import SidebarAdStack from "@/components/SidebarAdStack";
|
|
|
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
interface ArticleBlock {
|
|
title?: string;
|
|
excerpt?: string;
|
|
url?: string;
|
|
category?: string;
|
|
featured?: boolean;
|
|
}
|
|
|
|
interface Newsletter {
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
layout: string;
|
|
subject_prefix?: string;
|
|
header_text?: string;
|
|
accent_color?: string;
|
|
article_blocks: ArticleBlock[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
// ─── Topic options ─────────────────────────────────────────────────────────────
|
|
const TOPICS = [
|
|
"All Topics",
|
|
"Broadcast",
|
|
"Live Production",
|
|
"Post Production",
|
|
"AI & Automation",
|
|
"Products",
|
|
"Technology",
|
|
"Events",
|
|
"Industry News",
|
|
"NAB Show",
|
|
"IBC",
|
|
];
|
|
|
|
const CATEGORIES = [
|
|
"Broadcast",
|
|
"Post Production",
|
|
"Animation",
|
|
"AI & Automation",
|
|
"Live Production",
|
|
"NAB Show",
|
|
];
|
|
|
|
// ─── Newsletter Card ───────────────────────────────────────────────────────────
|
|
function NewsletterCard({ newsletter }: { newsletter: Newsletter }) {
|
|
const date = new Date(newsletter.created_at);
|
|
const formattedDate = date.toLocaleDateString("en-US", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
const blocks = Array.isArray(newsletter.article_blocks)
|
|
? newsletter.article_blocks.filter((b) => b.title)
|
|
: [];
|
|
const accent = newsletter.accent_color || "#E67E22";
|
|
|
|
const layoutLabel: Record<string, string> = {
|
|
featured: "Weekly Digest",
|
|
minimal: "Breaking News",
|
|
"two-column": "Product Spotlight",
|
|
standard: "Industry Report",
|
|
};
|
|
|
|
return (
|
|
<article className="bg-[#111] border border-[#222] hover:border-[#333] transition-colors rounded-sm overflow-hidden">
|
|
{/* Accent bar */}
|
|
<div className="h-0.5 w-full" style={{ backgroundColor: accent }} />
|
|
<div className="p-5">
|
|
{/* Meta */}
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<span
|
|
className="font-body text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-sm"
|
|
style={{ color: accent, backgroundColor: `${accent}18` }}>
|
|
{layoutLabel[newsletter.layout] || newsletter.layout}
|
|
</span>
|
|
<span className="text-[#444] text-[10px]">·</span>
|
|
<time className="text-[#555] font-body text-[11px]" dateTime={newsletter.created_at}>
|
|
{formattedDate}
|
|
</time>
|
|
</div>
|
|
|
|
{/* Title */}
|
|
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2">
|
|
{newsletter.subject_prefix ? (
|
|
<span className="text-[#777] font-normal">{newsletter.subject_prefix} </span>
|
|
) : null}
|
|
{newsletter.name}
|
|
</h2>
|
|
|
|
{/* Description */}
|
|
{newsletter.header_text && (
|
|
<p className="font-body text-[#777] text-sm leading-relaxed mb-4 line-clamp-2">
|
|
{newsletter.header_text}
|
|
</p>
|
|
)}
|
|
|
|
{/* Article blocks preview */}
|
|
{blocks.length > 0 && (
|
|
<ul className="space-y-1.5 mb-4">
|
|
{blocks.slice(0, 3).map((block, i) => (
|
|
<li key={i} className="flex items-start gap-2">
|
|
<span className="mt-1.5 w-1 h-1 rounded-full bg-[#444] flex-shrink-0" />
|
|
<span className="font-body text-[#888] text-xs leading-relaxed line-clamp-1">
|
|
{block.category && (
|
|
<span className="text-[#E67E22] font-bold mr-1">{block.category}:</span>
|
|
)}
|
|
{block.title}
|
|
</span>
|
|
</li>
|
|
))}
|
|
{blocks.length > 3 && (
|
|
<li className="font-body text-[#555] text-xs pl-3">
|
|
+{blocks.length - 3} more stories
|
|
</li>
|
|
)}
|
|
</ul>
|
|
)}
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-between pt-3 border-t border-[#1a1a1a]">
|
|
<span className="font-body text-[#555] text-xs">
|
|
{blocks.length} {blocks.length === 1 ? "story" : "stories"}
|
|
</span>
|
|
<span className="font-body text-[#E67E22] text-xs font-medium">
|
|
Issue #{newsletter.id.slice(0, 6).toUpperCase()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
// ─── Signup CTA ────────────────────────────────────────────────────────────────
|
|
function SignupCTA() {
|
|
const [email, setEmail] = useState("");
|
|
const [selected, setSelected] = useState<string[]>(["Broadcast"]);
|
|
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
|
const [message, setMessage] = useState("");
|
|
const formId = useId();
|
|
|
|
const toggleCategory = (cat: string) => {
|
|
setSelected((prev) =>
|
|
prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat]
|
|
);
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
setStatus("error");
|
|
setMessage("Please enter a valid email address.");
|
|
return;
|
|
}
|
|
setStatus("loading");
|
|
setMessage("");
|
|
try {
|
|
const res = await fetch("/api/newsletter/subscribe", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, topics: selected }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to subscribe");
|
|
setStatus("success");
|
|
} catch (err: unknown) {
|
|
setStatus("error");
|
|
setMessage(err instanceof Error ? err.message : "Something went wrong. Please try again.");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<section
|
|
className="bg-[#0d1520] border border-[#1e3a5f] rounded-sm p-6 md:p-8"
|
|
aria-labelledby={`${formId}-heading`}>
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="flex-1 h-px bg-[#1e3a5f]" />
|
|
<span className="section-label" id={`${formId}-heading`}>Subscribe</span>
|
|
<div className="flex-1 h-px bg-[#1e3a5f]" />
|
|
</div>
|
|
<h3 className="font-heading text-[#e0e0e0] text-xl font-bold text-center mb-1">
|
|
Never Miss an Issue
|
|
</h3>
|
|
<p className="font-body text-[#777] text-sm text-center mb-6">
|
|
Get the latest broadcast industry news delivered to your inbox.
|
|
</p>
|
|
|
|
{status === "success" ? (
|
|
<div className="text-center py-4" role="status" aria-live="polite">
|
|
<div className="w-10 h-10 rounded-full bg-[#E67E22]/20 flex items-center justify-center mx-auto mb-3">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
<path d="M4 10l4 4 8-8" stroke="#E67E22" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
</div>
|
|
<p className="font-heading text-[#e0e0e0] font-bold mb-1">You're subscribed!</p>
|
|
<p className="font-body text-[#777] text-sm">Check your inbox for a confirmation email.</p>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleSubmit} noValidate>
|
|
{/* Topic chips */}
|
|
<fieldset className="mb-5">
|
|
<legend className="font-body text-xs font-bold text-[#888] uppercase tracking-wider mb-3 block text-center">
|
|
Topics you care about
|
|
</legend>
|
|
<div className="flex flex-wrap justify-center gap-2">
|
|
{CATEGORIES.map((cat) => {
|
|
const checked = selected.includes(cat);
|
|
return (
|
|
<label
|
|
key={cat}
|
|
className={`newsletter-chip ${checked ? "newsletter-chip-active" : ""}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={checked}
|
|
onChange={() => toggleCategory(cat)}
|
|
className="sr-only"
|
|
aria-label={`Subscribe to ${cat} news`}
|
|
/>
|
|
<span
|
|
aria-hidden="true"
|
|
className={`mr-1.5 inline-block w-3 h-3 rounded-sm border transition-colors ${checked ? "bg-[#E67E22] border-[#E67E22]" : "border-[#444]"}`}>
|
|
{checked && (
|
|
<svg viewBox="0 0 12 12" fill="none" className="w-full h-full" aria-hidden="true">
|
|
<path d="M2 6l3 3 5-5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
)}
|
|
</span>
|
|
{cat}
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</fieldset>
|
|
|
|
{/* Email + button */}
|
|
<div className="flex flex-col sm:flex-row gap-2 max-w-md mx-auto">
|
|
<div className="flex-1">
|
|
<label htmlFor={`${formId}-email`} className="sr-only">Email address</label>
|
|
<input
|
|
id={`${formId}-email`}
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => { setEmail(e.target.value); setMessage(""); setStatus("idle"); }}
|
|
placeholder="Enter your email address"
|
|
autoComplete="email"
|
|
aria-required="true"
|
|
aria-invalid={status === "error"}
|
|
disabled={status === "loading"}
|
|
className={`search-input w-full py-2.5 px-4 text-sm ${status === "error" ? "border-red-500" : ""} ${status === "loading" ? "opacity-60 cursor-not-allowed" : ""}`}
|
|
/>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={status === "loading"}
|
|
className={`btn-subscribe py-2.5 px-6 text-sm whitespace-nowrap focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] ${status === "loading" ? "opacity-70 cursor-not-allowed" : ""}`}>
|
|
{status === "loading" ? "Subscribing..." : "Subscribe Free"}
|
|
</button>
|
|
</div>
|
|
|
|
{status === "error" && message && (
|
|
<p className="font-body text-red-400 text-xs mt-2 text-center" role="alert">
|
|
{message}
|
|
</p>
|
|
)}
|
|
|
|
<p className="font-body text-[#555] text-xs mt-3 text-center">
|
|
No spam. Unsubscribe anytime.{" "}
|
|
<Link href="/privacy" className="text-[#E67E22] hover:underline">
|
|
Privacy Policy
|
|
</Link>
|
|
</p>
|
|
</form>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
// ─── Main Page ─────────────────────────────────────────────────────────────────
|
|
export default function NewsletterArchivePage() {
|
|
const [newsletters, setNewsletters] = useState<Newsletter[]>([]);
|
|
const [years, setYears] = useState<number[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
|
|
// Filters
|
|
const [selectedTopic, setSelectedTopic] = useState("All Topics");
|
|
const [selectedYear, setSelectedYear] = useState("");
|
|
const [selectedMonth, setSelectedMonth] = useState("");
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
const MONTHS = [
|
|
{ value: "", label: "All Months" },
|
|
{ value: "1", label: "January" },
|
|
{ value: "2", label: "February" },
|
|
{ value: "3", label: "March" },
|
|
{ value: "4", label: "April" },
|
|
{ value: "5", label: "May" },
|
|
{ value: "6", label: "June" },
|
|
{ value: "7", label: "July" },
|
|
{ value: "8", label: "August" },
|
|
{ value: "9", label: "September" },
|
|
{ value: "10", label: "October" },
|
|
{ value: "11", label: "November" },
|
|
{ value: "12", label: "December" },
|
|
];
|
|
|
|
const fetchArchive = useCallback(async () => {
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (selectedTopic && selectedTopic !== "All Topics") params.set("topic", selectedTopic);
|
|
if (selectedYear) params.set("year", selectedYear);
|
|
if (selectedYear && selectedMonth) params.set("month", selectedMonth);
|
|
|
|
const res = await fetch(`/api/newsletter/archive?${params.toString()}`);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to load archive");
|
|
setNewsletters(data.newsletters || []);
|
|
setYears(data.years || []);
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : "Failed to load newsletter archive.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [selectedTopic, selectedYear, selectedMonth]);
|
|
|
|
useEffect(() => {
|
|
fetchArchive();
|
|
}, [fetchArchive]);
|
|
|
|
// Client-side text search on top of server results
|
|
const filtered = newsletters.filter((n) => {
|
|
if (!searchQuery.trim()) return true;
|
|
const q = searchQuery.toLowerCase();
|
|
return (
|
|
n.name?.toLowerCase().includes(q) ||
|
|
n.description?.toLowerCase().includes(q) ||
|
|
n.header_text?.toLowerCase().includes(q) ||
|
|
(Array.isArray(n.article_blocks) &&
|
|
n.article_blocks.some(
|
|
(b) =>
|
|
b.title?.toLowerCase().includes(q) ||
|
|
b.category?.toLowerCase().includes(q)
|
|
))
|
|
);
|
|
});
|
|
|
|
const handleClearFilters = () => {
|
|
setSelectedTopic("All Topics");
|
|
setSelectedYear("");
|
|
setSelectedMonth("");
|
|
setSearchQuery("");
|
|
};
|
|
|
|
const hasActiveFilters =
|
|
selectedTopic !== "All Topics" || selectedYear || selectedMonth || searchQuery;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<Header />
|
|
|
|
{/* Hero */}
|
|
<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">Newsletter</span>
|
|
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
|
</div>
|
|
<h1 className="font-heading text-white text-3xl font-bold">Newsletter Archive</h1>
|
|
<p className="text-[#777] font-body text-sm mt-2">
|
|
Browse past issues of the AV Beat newsletter — searchable by date and topic.
|
|
</p>
|
|
</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">
|
|
|
|
{/* ── Left: Archive list ─────────────────────────────────────────── */}
|
|
<div className="lg:col-span-8">
|
|
|
|
{/* Search bar */}
|
|
<div className="mb-5">
|
|
<label htmlFor="archive-search" className="sr-only">Search newsletters</label>
|
|
<div className="relative">
|
|
<svg
|
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-[#555]"
|
|
width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
|
aria-hidden="true">
|
|
<circle cx="11" cy="11" r="8" />
|
|
<path d="m21 21-4.35-4.35" />
|
|
</svg>
|
|
<input
|
|
id="archive-search"
|
|
type="search"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Search newsletters by title, topic, or content…"
|
|
className="search-input w-full py-2.5 pl-9 pr-4 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Topic filter chips */}
|
|
<div className="flex flex-wrap gap-2 mb-5" role="group" aria-label="Filter by topic">
|
|
{TOPICS.map((topic) => (
|
|
<button
|
|
key={topic}
|
|
onClick={() => setSelectedTopic(topic)}
|
|
className={`font-body text-xs px-3 py-1.5 rounded-sm border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] ${
|
|
selectedTopic === topic
|
|
? "bg-[#E67E22] border-[#E67E22] text-white"
|
|
: "bg-transparent border-[#333] text-[#888] hover:border-[#555] hover:text-[#ccc]"
|
|
}`}
|
|
aria-pressed={selectedTopic === topic}>
|
|
{topic}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Date filters */}
|
|
<div className="flex flex-wrap gap-3 mb-6">
|
|
<div>
|
|
<label htmlFor="filter-year" className="sr-only">Filter by year</label>
|
|
<select
|
|
id="filter-year"
|
|
value={selectedYear}
|
|
onChange={(e) => { setSelectedYear(e.target.value); setSelectedMonth(""); }}
|
|
className="search-input py-2 px-3 text-sm min-w-[120px]">
|
|
<option value="">All Years</option>
|
|
{years.map((y) => (
|
|
<option key={y} value={String(y)}>{y}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
{selectedYear && (
|
|
<div>
|
|
<label htmlFor="filter-month" className="sr-only">Filter by month</label>
|
|
<select
|
|
id="filter-month"
|
|
value={selectedMonth}
|
|
onChange={(e) => setSelectedMonth(e.target.value)}
|
|
className="search-input py-2 px-3 text-sm min-w-[140px]">
|
|
{MONTHS.map((m) => (
|
|
<option key={m.value} value={m.value}>{m.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
{hasActiveFilters && (
|
|
<button
|
|
onClick={handleClearFilters}
|
|
className="font-body text-xs text-[#E67E22] hover:text-[#E67E22] transition-colors underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]">
|
|
Clear filters
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Results count */}
|
|
{!loading && (
|
|
<p className="font-body text-[#555] text-xs mb-4" aria-live="polite">
|
|
{filtered.length === 0
|
|
? "No newsletters found"
|
|
: `${filtered.length} ${filtered.length === 1 ? "issue" : "issues"} found`}
|
|
{hasActiveFilters && " (filtered)"}
|
|
</p>
|
|
)}
|
|
|
|
{/* Loading */}
|
|
{loading && (
|
|
<div className="space-y-4" aria-busy="true" aria-label="Loading newsletters">
|
|
{[1, 2, 3].map((i) => (
|
|
<div key={i} className="bg-[#111] border border-[#222] rounded-sm p-5 animate-pulse">
|
|
<div className="h-3 bg-[#1e1e1e] rounded w-24 mb-3" />
|
|
<div className="h-5 bg-[#1e1e1e] rounded w-3/4 mb-2" />
|
|
<div className="h-3 bg-[#1e1e1e] rounded w-full mb-1" />
|
|
<div className="h-3 bg-[#1e1e1e] rounded w-2/3" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Error */}
|
|
{!loading && error && (
|
|
<div className="bg-[#1a0a0a] border border-red-900 rounded-sm p-5 text-center" role="alert">
|
|
<p className="font-body text-red-400 text-sm mb-3">{error}</p>
|
|
<button
|
|
onClick={fetchArchive}
|
|
className="font-body text-xs text-[#E67E22] hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]">
|
|
Try again
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty state */}
|
|
{!loading && !error && filtered.length === 0 && (
|
|
<div className="text-center py-16 border border-[#222] rounded-sm">
|
|
<svg
|
|
className="mx-auto mb-4 text-[#333]"
|
|
width="48" height="48" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
|
|
aria-hidden="true">
|
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
|
|
<polyline points="22,6 12,13 2,6" />
|
|
</svg>
|
|
<p className="font-heading text-[#555] text-base font-bold mb-1">No newsletters found</p>
|
|
<p className="font-body text-[#444] text-sm mb-4">
|
|
Try adjusting your filters or search query.
|
|
</p>
|
|
<button
|
|
onClick={handleClearFilters}
|
|
className="font-body text-xs text-[#E67E22] hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]">
|
|
Clear all filters
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Newsletter grid */}
|
|
{!loading && !error && filtered.length > 0 && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
{filtered.map((newsletter) => (
|
|
<NewsletterCard key={newsletter.id} newsletter={newsletter} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* ── Right: Sidebar ─────────────────────────────────────────────── */}
|
|
<aside className="lg:col-span-4 space-y-6">
|
|
{/* 300x600 + rotating 300x250 stack */}
|
|
<SidebarAdStack />
|
|
|
|
{/* Signup CTA */}
|
|
<SignupCTA />
|
|
|
|
{/* Browse by topic */}
|
|
<div className="bg-[#111] border border-[#222] rounded-sm p-5">
|
|
<h3 className="font-heading text-[#e0e0e0] text-sm font-bold uppercase tracking-wider mb-4">
|
|
Browse by Topic
|
|
</h3>
|
|
<ul className="space-y-1">
|
|
{TOPICS.filter((t) => t !== "All Topics").map((topic) => (
|
|
<li key={topic}>
|
|
<button
|
|
onClick={() => setSelectedTopic(topic)}
|
|
className={`w-full text-left font-body text-sm py-1.5 px-2 rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] ${
|
|
selectedTopic === topic
|
|
? "text-[#E67E22] bg-[#E67E22]/10"
|
|
: "text-[#888] hover:text-[#ccc] hover:bg-[#1a1a1a]"
|
|
}`}>
|
|
{topic}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
{/* Browse by year */}
|
|
{years.length > 0 && (
|
|
<div className="bg-[#111] border border-[#222] rounded-sm p-5">
|
|
<h3 className="font-heading text-[#e0e0e0] text-sm font-bold uppercase tracking-wider mb-4">
|
|
Browse by Year
|
|
</h3>
|
|
<ul className="space-y-1">
|
|
{years.map((year) => (
|
|
<li key={year}>
|
|
<button
|
|
onClick={() => { setSelectedYear(String(year)); setSelectedMonth(""); }}
|
|
className={`w-full text-left font-body text-sm py-1.5 px-2 rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] ${
|
|
selectedYear === String(year)
|
|
? "text-[#E67E22] bg-[#E67E22]/10"
|
|
: "text-[#888] hover:text-[#ccc] hover:bg-[#1a1a1a]"
|
|
}`}>
|
|
{year}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</aside>
|
|
</div>
|
|
</main>
|
|
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|