blog-list.tsx

1'use client'
2
3import { Content } from '@/lib/content'
4import { Link } from 'nextjs13-progress'
5import Layout from './Layout'
6import { useState, useMemo } from 'react'
7import { Select } from '@/components/ui/select'
8
9export function formatDate(
10	date: string,
11	includeTime: boolean = false,
12	locale: string = 'en-US',
13) {
14	const dateObj = new Date(date)
15	const options: Intl.DateTimeFormatOptions = {
16		year: 'numeric',
17		month: 'long',
18		day: 'numeric',
19	}
20	if (includeTime) options.hour = '2-digit'
21	options.minute = '2-digit'
22	return dateObj.toLocaleDateString(locale, options)
23}
24
25interface Props {
26	posts: Content[]
27	locale?: string
28}
29
30export function BlogList({ posts, locale = 'en-US' }: Props) {
31	const [searchQuery, setSearchQuery] = useState('')
32	const [selectedYear, setSelectedYear] = useState<string | null>(null)
33	const [selectedMonth, setSelectedMonth] = useState<string | null>(null)
34	const [selectedTags, setSelectedTags] = useState<string[]>([])
35
36	// Get unique years, months, and tags from posts
37	const filters = useMemo(() => {
38		const years = new Set<string>()
39		const months = new Set<string>()
40		const tags = new Set<string>()
41
42		posts.forEach((post) => {
43			const date = new Date(post.metadata.date)
44			years.add(date.getFullYear().toString())
45			months.add(date.toLocaleString(locale, { month: 'long' }))
46			post.metadata.tags.forEach((tag: string) => tags.add(tag))
47		})
48
49		return {
50			years: Array.from(years).sort().reverse(),
51			months: Array.from(months),
52			tags: Array.from(tags).sort(),
53		}
54	}, [posts, locale])
55
56	// Filter posts based on search and filters
57	const filteredPosts = useMemo(() => {
58		return posts.filter((post) => {
59			const date = new Date(post.metadata.date)
60			const matchesSearch =
61				searchQuery === '' ||
62				post.metadata.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
63				post.metadata.description
64					?.toLowerCase()
65					.includes(searchQuery.toLowerCase())
66
67			const matchesYear =
68				selectedYear === null || date.getFullYear().toString() === selectedYear
69
70			const matchesMonth =
71				selectedMonth === null ||
72				date.toLocaleString(locale, { month: 'long' }) === selectedMonth
73
74			const matchesTags =
75				selectedTags.length === 0 ||
76				selectedTags.every((tag) => post.metadata.tags.includes(tag))
77
78			return matchesSearch && matchesYear && matchesMonth && matchesTags
79		})
80	}, [posts, searchQuery, selectedYear, selectedMonth, selectedTags, locale])
81
82	return (
83		<Layout>
84			<section className="py-20">
85				<div className="container mx-auto px-4">
86					<h1 className="mb-12 bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-center text-4xl font-bold text-transparent">
87						Blog Posts
88					</h1>
89
90					{/* Search and Filters */}
91					<div className="mx-auto mb-8 max-w-2xl">
92						<div className="flex gap-4">
93							<input
94								type="text"
95								placeholder="Search posts..."
96								value={searchQuery}
97								onChange={(e) => setSearchQuery(e.target.value)}
98								className="flex-1 rounded-lg border border-gray-800 bg-gray-900/50 px-4 py-2 text-gray-100 focus:border-purple-500 focus:outline-none"
99							/>
100
101							<Select
102								value={selectedYear}
103								onValueChange={(value) => setSelectedYear(value)}
104								options={[
105									{ value: null, label: 'All Years' },
106									...filters.years.map((year) => ({
107										value: year,
108										label: year,
109									})),
110								]}
111							/>
112
113							<Select
114								value={selectedMonth}
115								onValueChange={(value) => setSelectedMonth(value)}
116								options={[
117									{ value: null, label: 'All Months' },
118									...filters.months.map((month) => ({
119										value: month,
120										label: month,
121									})),
122								]}
123							/>
124						</div>
125
126						<div className="mt-4 flex flex-wrap gap-2">
127							{filters.tags.map((tag) => (
128								<button
129									key={tag}
130									onClick={() =>
131										setSelectedTags((prev) =>
132											prev.includes(tag) ?
133												prev.filter((t) => t !== tag)
134											:	[...prev, tag],
135										)
136									}
137									className={`rounded-full px-3 py-1 text-sm transition-colors ${
138										selectedTags.includes(tag) ?
139											'bg-purple-500 text-white'
140										:	'bg-purple-500/10 text-purple-400 hover:bg-purple-500/20'
141									}`}
142								>
143									#{tag}
144								</button>
145							))}
146						</div>
147					</div>
148
149					{/* Posts List */}
150					<div className="mx-auto max-w-2xl space-y-8">
151						{filteredPosts.length === 0 ?
152							<p className="text-center text-gray-400">No posts found</p>
153						:	filteredPosts.map((post) => {
154								const date = new Date(post.metadata.date)
155								const url = `/blog/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${post.slug}`
156
157								return (
158									<article
159										key={post.slug}
160										className="group relative rounded-xl border border-gray-800 bg-gray-900/50 p-6 backdrop-blur-sm transition-all duration-300 hover:border-purple-500/50 hover:bg-gray-800/75"
161									>
162										<div className="absolute inset-x-0 -bottom-px h-px bg-gradient-to-r from-transparent via-purple-500 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
163
164										<Link href={url}>
165											<h2 className="mb-2 text-2xl font-bold transition-colors group-hover:text-purple-400">
166												{post.metadata.title}
167											</h2>
168										</Link>
169
170										<time className="flex items-center gap-2 text-sm text-gray-400">
171											<svg className="h-4 w-4" /* Add calendar icon SVG */ />
172											{formatDate(post.metadata.date, true, locale)}
173										</time>
174
175										{post.metadata.description && (
176											<p className="mt-4 line-clamp-3 text-gray-300">
177												{post.metadata.description}
178											</p>
179										)}
180
181										<div className="mt-4 flex flex-wrap gap-2">
182											{post.metadata.tags.map((tag: string) => (
183												<span
184													key={tag}
185													className="rounded-full bg-purple-500/10 px-3 py-1 text-sm text-purple-400"
186												>
187													#{tag}
188												</span>
189											))}
190										</div>
191									</article>
192								)
193							})
194						}
195					</div>
196				</div>
197			</section>
198		</Layout>
199	)
200}
201