@@ -4,33 +4,40 @@ import { useState, useEffect } from "react";
4
4
import { Search } from "lucide-react" ;
5
5
import { Input } from "@/components/ui/input" ;
6
6
import { Button } from "@/components/ui/button" ;
7
- import { Badge } from "@/components/ui/badge" ;
8
7
import {
9
8
Select ,
10
9
SelectContent ,
11
10
SelectItem ,
12
11
SelectTrigger ,
13
12
SelectValue ,
14
13
} 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" ;
14
+ import { ContentCard } from "@/components/ContentCard" ;
15
+ import { track } from "@vercel/analytics" ;
16
+ import debounce from "lodash.debounce" ;
17
17
import type { ArticleWithSlug } from "@/types" ;
18
18
19
19
interface BlogClientProps {
20
20
articles : ArticleWithSlug [ ] ;
21
21
years : string [ ] ;
22
- allTags : string [ ] ;
23
22
}
24
23
25
- export default function BlogClient ( { articles, years, allTags } : BlogClientProps ) {
24
+ export default function BlogClient ( { articles, years } : BlogClientProps ) {
26
25
const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
27
26
const [ selectedYear , setSelectedYear ] = useState ( "" ) ;
28
- const [ selectedTag , setSelectedTag ] = useState ( "" ) ;
29
27
const [ filteredArticles , setFilteredArticles ] = useState < ArticleWithSlug [ ] > ( articles ) ;
30
- const [ view , setView ] = useState ( "grid" ) ;
28
+
29
+ // Debounced tracking function for search analytics
30
+ const debouncedTrack = debounce ( ( query : string ) => {
31
+ if ( query . trim ( ) ) {
32
+ track ( 'blog-search' , { term : query } ) ;
33
+ }
34
+ } , 1200 ) ;
31
35
32
36
useEffect ( ( ) => {
33
- let filtered = articles ;
37
+ let filtered = [ ...articles ] ;
38
+
39
+ // Sort articles by date in chronological order (newest first)
40
+ filtered . sort ( ( a , b ) => new Date ( b . date ) . getTime ( ) - new Date ( a . date ) . getTime ( ) ) ;
34
41
35
42
if ( searchTerm ) {
36
43
const term = searchTerm . toLowerCase ( ) ;
@@ -45,40 +52,40 @@ export default function BlogClient({ articles, years, allTags }: BlogClientProps
45
52
filtered = filtered . filter ( ( article ) => article . date . startsWith ( selectedYear ) ) ;
46
53
}
47
54
48
- if ( selectedTag && selectedTag !== "All Tags" ) {
49
- filtered = filtered . filter ( ( article ) => Array . isArray ( article . tags ) && article . tags . includes ( selectedTag ) ) ;
50
- }
51
-
52
55
setFilteredArticles ( filtered ) ;
53
- } , [ searchTerm , selectedYear , selectedTag , articles ] ) ;
56
+ } , [ searchTerm , selectedYear , articles ] ) ;
57
+
58
+ const handleSearchChange = ( value : string ) => {
59
+ setSearchTerm ( value ) ;
60
+ debouncedTrack ( value ) ;
61
+ } ;
54
62
55
63
const resetFilters = ( ) => {
56
64
setSearchTerm ( "" ) ;
57
65
setSelectedYear ( "" ) ;
58
- setSelectedTag ( "" ) ;
59
66
} ;
60
67
61
68
return (
62
69
< 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.
70
+ < div className = "container mx-auto px-4 py-16" >
71
+ < div className = "text-center mb-12" >
72
+ < h1 className = "text-5xl font-bold text-slate-900 dark:text-white mb-4" >
73
+ I write to learn, and publish to share
74
+ </ h1 >
75
+ < p className = "text-xl text-slate-600 dark:text-slate-400 max-w-3xl mx-auto" >
76
+ All of my technical tutorials, musings and developer rants
68
77
</ p >
69
78
</ div >
70
- </ header >
71
79
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" >
80
+ < div className = "bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 p-4 mb-8 max-w-4xl mx-auto" >
81
+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
75
82
< div className = "relative" >
76
83
< Search className = "absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400" size = { 18 } />
77
84
< Input
78
85
placeholder = "Search articles..."
79
86
className = "pl-10"
80
87
value = { searchTerm }
81
- onChange = { ( e ) => setSearchTerm ( e . target . value ) }
88
+ onChange = { ( e ) => handleSearchChange ( e . target . value ) }
82
89
/>
83
90
</ div >
84
91
@@ -95,20 +102,6 @@ export default function BlogClient({ articles, years, allTags }: BlogClientProps
95
102
) ) }
96
103
</ SelectContent >
97
104
</ 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
105
</ div >
113
106
114
107
< div className = "flex justify-between items-center mt-4" >
@@ -123,82 +116,14 @@ export default function BlogClient({ articles, years, allTags }: BlogClientProps
123
116
124
117
< div className = "space-y-8" >
125
118
< 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 >
119
+ < h2 className = "text-2xl font-semibold text-slate-900 dark:text-white mb-6 text-center" > All Articles</ h2 >
120
+
121
+ < div className = "md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40" >
122
+ < div className = "mx-auto grid max-w-2xl grid-cols-1 gap-x-8 gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-3" >
123
+ { filteredArticles . map ( ( article ) => (
124
+ < ContentCard key = { article . slug } article = { article } />
125
+ ) ) }
126
+ </ div >
202
127
</ div >
203
128
</ section >
204
129
</ div >
0 commit comments