11'use client' ;
22
3- import React , { useState , useCallback , Suspense } from 'react' ;
3+ import React , { useState , useCallback , useMemo , Suspense } from 'react' ;
44import { BlogPost , getAllBlogPosts } from '@/lib/data/blog' ;
55import BlogCard from './BlogCard' ;
66import { Search , Loader2 } from 'lucide-react' ;
77import { Input } from '@/components/ui/input' ;
88import AuthLoadingState from '@/components/auth/AuthLoadingState' ;
9+ import {
10+ DropdownMenu ,
11+ DropdownMenuTrigger ,
12+ DropdownMenuContent ,
13+ DropdownMenuRadioGroup ,
14+ DropdownMenuRadioItem ,
15+ } from '@/components/ui/dropdown-menu' ;
16+ import { cn } from '@/lib/utils' ;
917
1018interface BlogGridProps {
1119 posts : BlogPost [ ] ;
@@ -19,13 +27,59 @@ const BlogGrid: React.FC<BlogGridProps> = ({
1927 maxPosts,
2028} ) => {
2129 const [ visiblePosts , setVisiblePosts ] = useState ( maxPosts || 12 ) ;
22- const [ selectedCategory , setSelectedCategory ] = useState ( 'Latest' ) ;
30+ const [ selectedCategories , setSelectedCategories ] = useState < string [ ] > ( [ ] ) ;
31+ const [ sortOrder , setSortOrder ] = useState < 'Latest' | 'Oldest' | '' > ( '' ) ;
32+ const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
2333 const [ isLoading , setIsLoading ] = useState ( false ) ;
2434 const [ isNavigating , setIsNavigating ] = useState ( false ) ;
2535
26- // Get posts to display (no filtering)
27- // const displayPosts = posts.slice(0, visiblePosts);
28- const hasMorePosts = visiblePosts < posts . length ;
36+ // Filter and sort posts
37+ const filteredPosts = useMemo ( ( ) => {
38+ let filtered = posts ;
39+
40+ // Filter by categories
41+ if ( selectedCategories . length > 0 ) {
42+ filtered = filtered . filter ( post =>
43+ selectedCategories . includes ( post . category )
44+ ) ;
45+ }
46+
47+ // Filter by search query
48+ if ( searchQuery . trim ( ) ) {
49+ const query = searchQuery . toLowerCase ( ) ;
50+ filtered = filtered . filter (
51+ post =>
52+ post . title . toLowerCase ( ) . includes ( query ) ||
53+ post . excerpt . toLowerCase ( ) . includes ( query ) ||
54+ post . tags . some ( tag => tag . toLowerCase ( ) . includes ( query ) )
55+ ) ;
56+ }
57+
58+ // Sort posts
59+ if ( sortOrder ) {
60+ filtered = [ ...filtered ] . sort ( ( a , b ) => {
61+ const dateA = new Date ( a . publishedAt ) . getTime ( ) ;
62+ const dateB = new Date ( b . publishedAt ) . getTime ( ) ;
63+ return sortOrder === 'Latest' ? dateB - dateA : dateA - dateB ;
64+ } ) ;
65+ }
66+
67+ return filtered ;
68+ } , [ posts , selectedCategories , searchQuery , sortOrder ] ) ;
69+
70+ // Get posts to display
71+ const displayPosts = filteredPosts . slice ( 0 , visiblePosts ) ;
72+ const hasMorePosts = visiblePosts < filteredPosts . length ;
73+ // Blog categories
74+ const categories : string [ ] = [
75+ 'Category 1' ,
76+ 'Category 2' ,
77+ 'Category 3' ,
78+ 'Category 4' ,
79+ 'Category 5' ,
80+ ] ;
81+ // Sort options for dropdown
82+ const sortOptions : Array < 'Latest' | 'Oldest' > = [ 'Latest' , 'Oldest' ] ;
2983
3084 // Load more handler
3185 const handleLoadMore = useCallback ( ( ) => {
@@ -41,8 +95,17 @@ const BlogGrid: React.FC<BlogGridProps> = ({
4195 } , [ isLoading , hasMorePosts ] ) ;
4296
4397 const handleCategoryChange = ( category : string ) => {
44- setSelectedCategory ( category ) ;
45- // UI only - no filtering functionality
98+ setSelectedCategories ( prev =>
99+ prev . includes ( category )
100+ ? prev . filter ( c => c !== category )
101+ : [ ...prev , category ]
102+ ) ;
103+ setVisiblePosts ( maxPosts || 12 ) ; // Reset visible posts when filtering
104+ } ;
105+
106+ const handleSearchChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
107+ setSearchQuery ( e . target . value ) ;
108+ setVisiblePosts ( maxPosts || 12 ) ; // Reset visible posts when searching
46109 } ;
47110
48111 const handleCardClick = useCallback ( ( slug : string ) => {
@@ -63,57 +126,175 @@ const BlogGrid: React.FC<BlogGridProps> = ({
63126 { /* Header Navigation */ }
64127 < div >
65128 < div className = 'mx-auto max-w-6xl px-6 py-8' >
66- < div className = 'flex flex-col gap-4 md:flex-row md:items-center md:justify-between' >
129+ < div className = 'flex gap-3 md:flex-row md:items-center md:justify-between lg:gap-16 ' >
67130 { /* Category Buttons */ }
68131 < div className = 'flex items-center gap-3' >
69- < button
70- onClick = { ( ) => handleCategoryChange ( 'Latest' ) }
71- className = { `flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
72- selectedCategory === 'Latest'
73- ? 'border border-white/20 text-white'
74- : 'border border-white/10 text-white hover:bg-[#2A2A2A]'
75- } `}
76- >
77- < svg
78- className = 'h-4 w-4'
79- fill = 'currentColor'
80- viewBox = '0 0 20 20'
132+ { /* SORT */ }
133+ < DropdownMenu >
134+ < DropdownMenuTrigger asChild >
135+ < button
136+ className = { cn (
137+ 'flex items-center gap-2 rounded-lg border px-2 py-2 text-sm font-medium transition-colors sm:px-4 sm:text-base' ,
138+ sortOrder
139+ ? 'border-[#A7F950]/30 bg-[#0F1A0B] text-[#A7F950]'
140+ : 'border-white/24 text-white hover:bg-[#2A2A2A]'
141+ ) }
142+ >
143+ < svg
144+ width = '20'
145+ height = '20'
146+ className = { sortOrder ? 'text-[#A7F950]' : 'text-white' }
147+ viewBox = '0 0 20 20'
148+ fill = 'none'
149+ xmlns = 'http://www.w3.org/2000/svg'
150+ >
151+ < path
152+ d = 'M9.16667 8.33333L15 8.33337M9.16667 11.6667H13.3333M9.16667 15H11.6667M9.16667 5H17.5M4.16667 17.5L4.16667 12.5M4.16667 17.5C3.69985 17.5 2.82769 16.0458 2.5 15.6771M4.16667 17.5C4.63348 17.5 5.50565 16.0458 5.83333 15.6771M4.16667 2.5V7.5M4.16667 2.5C4.63349 2.5 5.50565 3.95418 5.83333 4.32292M4.16667 2.5C3.69985 2.5 2.82769 3.95418 2.5 4.32292'
153+ stroke = 'currentColor'
154+ strokeWidth = '1.4'
155+ strokeLinecap = 'round'
156+ strokeLinejoin = 'round'
157+ />
158+ </ svg >
159+ < span className = 'hidden md:block' >
160+ { sortOrder || 'Sort' }
161+ </ span >
162+ </ button >
163+ </ DropdownMenuTrigger >
164+ < DropdownMenuContent
165+ align = 'start'
166+ className = 'bg-background mt-2.5 w-[250px] rounded-[16px] border border-white/24 p-3 lg:w-[320px]'
81167 >
82- < path
83- fillRule = 'evenodd'
84- d = 'M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z'
85- clipRule = 'evenodd'
86- />
87- </ svg >
88- Latest
89- </ button >
90- < button
91- onClick = { ( ) => handleCategoryChange ( 'Category' ) }
92- className = { `flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
93- selectedCategory === 'Category'
94- ? 'border border-white/20 text-white'
95- : 'border border-white/10 text-white hover:bg-[#2A2A2A]'
96- } `}
97- >
98- Category
99- </ button >
168+ < DropdownMenuRadioGroup
169+ value = { sortOrder }
170+ onValueChange = { value =>
171+ setSortOrder ( value as 'Latest' | 'Oldest' )
172+ }
173+ >
174+ { sortOptions . map ( ( opt : 'Latest' | 'Oldest' ) => (
175+ < DropdownMenuRadioItem
176+ key = { opt }
177+ value = { opt }
178+ className = 'cursor-pointer rounded-md px-3 py-2 text-base font-medium text-white transition-colors duration-200 hover:!bg-transparent hover:!text-white data-[state=checked]:text-white [&>span:first-child]:hidden'
179+ >
180+ < span
181+ className = { cn (
182+ 'mr-3 inline-flex h-5 w-5 items-center justify-center rounded-full border-2' ,
183+ opt === sortOrder
184+ ? 'border-[#A7F950]'
185+ : 'border-[#B5B5B5]'
186+ ) }
187+ >
188+ { opt === sortOrder && (
189+ < span className = 'h-2.5 w-2.5 rounded-full bg-[#A7F950]' />
190+ ) }
191+ </ span >
192+ < span className = '' > { opt } </ span >
193+ </ DropdownMenuRadioItem >
194+ ) ) }
195+ </ DropdownMenuRadioGroup >
196+ </ DropdownMenuContent >
197+ </ DropdownMenu >
198+
199+ { /* CATEGORY */ }
200+ < DropdownMenu >
201+ < DropdownMenuTrigger asChild >
202+ < button
203+ className = { cn (
204+ 'flex items-center gap-2 rounded-lg border border-white/24 px-2 py-2 text-sm font-medium text-white transition-colors sm:px-4 sm:text-base' ,
205+ selectedCategories . length > 0 &&
206+ 'border-[#A7F950]/30 bg-[#0F1A0B] text-[#A7F950]'
207+ ) }
208+ >
209+ < svg
210+ width = '20'
211+ height = '20'
212+ className = 'md:hidden'
213+ viewBox = '0 0 20 20'
214+ fill = 'none'
215+ xmlns = 'http://www.w3.org/2000/svg'
216+ >
217+ < path
218+ d = 'M5 10H15M2.5 5H17.5M7.5 15H12.5'
219+ stroke = 'currentColor'
220+ strokeWidth = '1.4'
221+ strokeLinecap = 'round'
222+ strokeLinejoin = 'round'
223+ />
224+ </ svg >
225+ < span className = 'hidden md:block' > Category</ span >
226+ </ button >
227+ </ DropdownMenuTrigger >
228+ < DropdownMenuContent
229+ align = 'start'
230+ className = 'bg-background mt-2.5 w-[220px] rounded-[16px] border border-white/24 p-3 sm:w-[250px] lg:w-[320px]'
231+ >
232+ < div className = 'space-y-1' >
233+ { categories . map ( ( cat : string ) => (
234+ < button
235+ key = { cat }
236+ onClick = { ( ) => handleCategoryChange ( cat ) }
237+ className = 'flex w-full cursor-pointer items-center rounded-md px-3 py-2 text-base font-medium text-white transition-colors duration-200 hover:bg-transparent hover:text-white'
238+ >
239+ < span
240+ className = { cn (
241+ 'mr-3 inline-flex h-5 w-5 items-center justify-center rounded-full border-2' ,
242+ selectedCategories . includes ( cat )
243+ ? 'border-[#A7F950]'
244+ : 'border-[#B5B5B5]'
245+ ) }
246+ >
247+ { selectedCategories . includes ( cat ) && (
248+ < span className = 'h-2.5 w-2.5 rounded-full bg-[#A7F950]' />
249+ ) }
250+ </ span >
251+ < span className = '' > { cat } </ span >
252+ </ button >
253+ ) ) }
254+ </ div >
255+ </ DropdownMenuContent >
256+ </ DropdownMenu >
100257 </ div >
101258
102259 { /* Search Bar */ }
103- < div className = 'relative w-full md:w-auto md: min-w-[300px]' >
104- < Search className = 'absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-white ' />
260+ < div className = 'relative w-full md:min-w-[300px]' >
261+ < Search className = 'absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-[#787878] ' />
105262 < Input
106263 type = 'text'
107264 placeholder = 'Search article'
108- className = 'w-full rounded-lg border border-white/10 bg-[#101010] py-2 pr-4 pl-10 text-white placeholder:text-[#B5B5B5] focus:border-white/20 focus:ring-white/20'
109- readOnly
265+ className = 'w-full rounded-lg border border-white/10 bg-[#101010] py-2 pr-4 pl-10 text-sm text-white placeholder:text-[#B5B5B5] focus:outline-none focus-visible:border-[#A7F950] focus-visible:ring-[3px] focus-visible:ring-[#A7F950]/30 md:h-[42px] md:text-base'
266+ value = { searchQuery }
267+ onChange = { handleSearchChange }
110268 />
111269 </ div >
112270 </ div >
113271 </ div >
114272 </ div >
115273
116274 < div className = 'mx-auto max-w-6xl px-6 py-12' >
275+ { displayPosts . length > 0 ? (
276+ < div className = 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3' >
277+ { displayPosts . map ( post => (
278+ < div key = { post . id } className = 'w-full' >
279+ < BlogCard post = { post } onCardClick = { handleCardClick } />
280+ </ div >
281+ ) ) }
282+ </ div >
283+ ) : (
284+ < div className = 'flex flex-col items-center justify-center py-12 text-center' >
285+ < div className = 'mb-4 text-[#B5B5B5]' >
286+ < Search className = 'mx-auto h-12 w-12' />
287+ </ div >
288+ < h3 className = 'mb-2 text-xl font-medium text-white' >
289+ No articles found
290+ </ h3 >
291+ < p className = 'text-[#B5B5B5]' >
292+ { searchQuery
293+ ? `No articles match "${ searchQuery } "`
294+ : 'No articles in this category' }
295+ </ p >
296+ </ div >
297+ ) }
117298 < Suspense
118299 fallback = { < AuthLoadingState message = 'Loading blog posts...' /> }
119300 >
@@ -146,7 +327,7 @@ const BlogGrid: React.FC<BlogGridProps> = ({
146327 ) }
147328
148329 { /* No more posts message */ }
149- { ! hasMorePosts && posts . length > 0 && ! isLoading && (
330+ { ! hasMorePosts && filteredPosts . length > 0 && ! isLoading && (
150331 < div className = 'mt-12 text-center text-[#B5B5B5]' >
151332 < p > You've reached the end of the blog posts!</ p >
152333 </ div >
0 commit comments