Skip to content

Commit e542145

Browse files
committed
feat(blog): add search and filters
1 parent cf9ded7 commit e542145

File tree

2 files changed

+215
-48
lines changed

2 files changed

+215
-48
lines changed

src/app/blog/blog-client.tsx

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"use client"
2+
3+
import { useState, useEffect } from "react";
4+
import { Search } from "lucide-react";
5+
import { Input } from "@/components/ui/input";
6+
import { Button } from "@/components/ui/button";
7+
import { Badge } from "@/components/ui/badge";
8+
import {
9+
Select,
10+
SelectContent,
11+
SelectItem,
12+
SelectTrigger,
13+
SelectValue,
14+
} from "@/components/ui/select";
15+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
16+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
17+
import type { ArticleWithSlug } from "@/types";
18+
19+
interface BlogClientProps {
20+
articles: ArticleWithSlug[];
21+
years: string[];
22+
allTags: string[];
23+
}
24+
25+
export default function BlogClient({ articles, years, allTags }: BlogClientProps) {
26+
const [searchTerm, setSearchTerm] = useState("");
27+
const [selectedYear, setSelectedYear] = useState("");
28+
const [selectedTag, setSelectedTag] = useState("");
29+
const [filteredArticles, setFilteredArticles] = useState<ArticleWithSlug[]>(articles);
30+
const [view, setView] = useState("grid");
31+
32+
useEffect(() => {
33+
let filtered = articles;
34+
35+
if (searchTerm) {
36+
const term = searchTerm.toLowerCase();
37+
filtered = filtered.filter(
38+
(article) =>
39+
article.title.toLowerCase().includes(term) ||
40+
article.description.toLowerCase().includes(term)
41+
);
42+
}
43+
44+
if (selectedYear && selectedYear !== "All Years") {
45+
filtered = filtered.filter((article) => article.date.startsWith(selectedYear));
46+
}
47+
48+
if (selectedTag && selectedTag !== "All Tags") {
49+
filtered = filtered.filter((article) => Array.isArray(article.tags) && article.tags.includes(selectedTag));
50+
}
51+
52+
setFilteredArticles(filtered);
53+
}, [searchTerm, selectedYear, selectedTag, articles]);
54+
55+
const resetFilters = () => {
56+
setSearchTerm("");
57+
setSelectedYear("");
58+
setSelectedTag("");
59+
};
60+
61+
return (
62+
<div className="w-full">
63+
<header className="bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 sticky top-0 z-10">
64+
<div className="container mx-auto px-4 py-6">
65+
<h1 className="text-4xl font-bold text-slate-900 dark:text-white mb-2">Blog</h1>
66+
<p className="text-slate-600 dark:text-slate-400 max-w-2xl">
67+
My thoughts on engineering, AI, and modern development.
68+
</p>
69+
</div>
70+
</header>
71+
72+
<div className="container mx-auto px-4 py-8">
73+
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 p-4 mb-8">
74+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
75+
<div className="relative">
76+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400" size={18} />
77+
<Input
78+
placeholder="Search articles..."
79+
className="pl-10"
80+
value={searchTerm}
81+
onChange={(e) => setSearchTerm(e.target.value)}
82+
/>
83+
</div>
84+
85+
<Select value={selectedYear} onValueChange={setSelectedYear}>
86+
<SelectTrigger className="bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
87+
<SelectValue placeholder="Year" />
88+
</SelectTrigger>
89+
<SelectContent className="bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
90+
<SelectItem value="All Years">All Years</SelectItem>
91+
{years.map((year) => (
92+
<SelectItem key={year} value={year}>
93+
{year}
94+
</SelectItem>
95+
))}
96+
</SelectContent>
97+
</Select>
98+
99+
<Select value={selectedTag} onValueChange={setSelectedTag}>
100+
<SelectTrigger className="bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
101+
<SelectValue placeholder="Tag" />
102+
</SelectTrigger>
103+
<SelectContent className="bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
104+
<SelectItem value="All Tags">All Tags</SelectItem>
105+
{allTags.map((tag) => (
106+
<SelectItem key={tag} value={tag}>
107+
{tag}
108+
</SelectItem>
109+
))}
110+
</SelectContent>
111+
</Select>
112+
</div>
113+
114+
<div className="flex justify-between items-center mt-4">
115+
<Button variant="outline" size="sm" onClick={resetFilters} className="text-slate-600 dark:text-slate-400">
116+
Reset Filters
117+
</Button>
118+
<span className="text-sm text-slate-500 dark:text-slate-400">
119+
{filteredArticles.length} articles found
120+
</span>
121+
</div>
122+
</div>
123+
124+
<div className="space-y-8">
125+
<section>
126+
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white mb-4">All Articles</h2>
127+
128+
<div className="flex items-center justify-end gap-2 mb-4">
129+
<span className="text-sm text-slate-500 dark:text-slate-400">View:</span>
130+
<Tabs value={view} onValueChange={setView} className="w-full">
131+
<div className="flex justify-end">
132+
<TabsList className="grid w-[160px] grid-cols-2">
133+
<TabsTrigger value="grid">Grid</TabsTrigger>
134+
<TabsTrigger value="list">List</TabsTrigger>
135+
</TabsList>
136+
</div>
137+
138+
<TabsContent value="grid" className="mt-4">
139+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
140+
{filteredArticles.map((article) => (
141+
<Card key={article.slug} className="overflow-hidden hover:shadow-md transition-shadow duration-300 h-full flex flex-col bg-white dark:bg-slate-800">
142+
<CardHeader className="pb-2">
143+
<div className="flex justify-between items-start">
144+
<div className="flex items-center text-sm text-slate-500 dark:text-slate-400">
145+
{new Date(article.date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}
146+
</div>
147+
</div>
148+
<CardTitle className="text-xl font-bold hover:text-primary transition-colors">
149+
<a href={article.slug} className="flex items-center gap-1">
150+
{article.title}
151+
</a>
152+
</CardTitle>
153+
<CardDescription className="text-sm line-clamp-2 mt-1">
154+
{article.description}
155+
</CardDescription>
156+
</CardHeader>
157+
{article.tags && article.tags.length > 0 && (
158+
<CardContent className="pb-2 flex-grow">
159+
<div className="flex flex-wrap gap-1 mt-2">
160+
{article.tags.map((tag) => (
161+
<Badge key={tag} variant="secondary" className="text-xs">
162+
{tag}
163+
</Badge>
164+
))}
165+
</div>
166+
</CardContent>
167+
)}
168+
</Card>
169+
))}
170+
</div>
171+
</TabsContent>
172+
173+
<TabsContent value="list" className="mt-4">
174+
<div className="space-y-3">
175+
{filteredArticles.map((article) => (
176+
<div key={article.slug} className="flex flex-col gap-2 p-4 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 hover:shadow-sm transition-shadow">
177+
<h3 className="text-lg font-semibold mb-1">
178+
<a href={article.slug} className="hover:text-primary transition-colors">
179+
{article.title}
180+
</a>
181+
</h3>
182+
<p className="text-sm text-slate-600 dark:text-slate-400 mb-2 line-clamp-1">
183+
{article.description}
184+
</p>
185+
<div className="flex items-center justify-between">
186+
<div className="flex flex-wrap gap-1">
187+
{article.tags && article.tags.map((tag) => (
188+
<Badge key={tag} variant="secondary" className="text-xs">
189+
{tag}
190+
</Badge>
191+
))}
192+
</div>
193+
<span className="text-sm text-slate-500 dark:text-slate-400">
194+
{new Date(article.date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}
195+
</span>
196+
</div>
197+
</div>
198+
))}
199+
</div>
200+
</TabsContent>
201+
</Tabs>
202+
</div>
203+
</section>
204+
</div>
205+
</div>
206+
</div>
207+
);
208+
}
209+

src/app/blog/page.tsx

Lines changed: 6 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import { Metadata } from 'next'
2-
import { SimpleLayout } from '@/components/SimpleLayout'
3-
import { ContentCard } from '@/components/ContentCard'
2+
import BlogClient from './blog-client'
43
import { createMetadata } from '@/utils/createMetadata'
5-
import { Suspense } from 'react'
64
import { getAllContent } from '@/lib/content-handlers'
7-
import { ExtendedMetadata } from '@/types'
8-
import ArticleSearch from '@/components/ArticleSearch'
95

106
// Base metadata using createMetadata
117
const baseMetadata = createMetadata({
@@ -19,51 +15,13 @@ export const metadata: Metadata = {
1915
metadataBase: new URL('https://zackproser.com'),
2016
}
2117

22-
function ArticleGrid({ articles }: { articles: ExtendedMetadata[] }) {
23-
return (
24-
<div className="mx-auto mt-16 grid max-w-none grid-cols-1 gap-x-8 gap-y-16 lg:grid-cols-3">
25-
{articles.map((article, index) => {
26-
// Simply use the index as the key
27-
return (
28-
<ContentCard
29-
key={index}
30-
article={article}
31-
/>
32-
);
33-
})}
34-
</div>
35-
)
36-
}
3718

3819
export default async function ArticlesIndex() {
39-
// Use our helper function to get all blog post metadata
4020
const articles = await getAllContent('blog');
41-
42-
// Add debugging to log the articles being loaded
43-
// console.log(`Loaded ${articles.length} blog articles`); // Commented out verbose log
44-
45-
// Log the first few articles for debugging
46-
/* // Commented out verbose log
47-
articles.slice(0, 3).forEach((article, index) => {
48-
console.log(`Article ${index + 1}:`, {
49-
title: article.title,
50-
slug: article.slug,
51-
type: article.type
52-
});
53-
});
54-
*/
55-
56-
return (
57-
<SimpleLayout
58-
title="I write to learn, and publish to share"
59-
intro="All of my technical tutorials, musings and developer rants"
60-
>
61-
<div className="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
62-
<Suspense fallback={<div>Loading articles...</div>}>
63-
<ArticleGrid articles={articles} />
64-
</Suspense>
65-
</div>
66-
</SimpleLayout>
67-
)
21+
22+
const years = [...new Set(articles.map((a) => new Date(a.date).getFullYear().toString()))].sort((a, b) => parseInt(b) - parseInt(a));
23+
const allTags = [...new Set(articles.flatMap((a) => a.tags ?? []))].sort();
24+
25+
return <BlogClient articles={articles} years={years} allTags={allTags} />;
6826
}
6927

0 commit comments

Comments
 (0)