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