Skip to content

Commit de0f2d7

Browse files
committed
2 parents 6c0cfde + 720f1ab commit de0f2d7

1 file changed

Lines changed: 224 additions & 43 deletions

File tree

components/landing-page/blog/BlogGrid.tsx

Lines changed: 224 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
'use client';
22

3-
import React, { useState, useCallback, Suspense } from 'react';
3+
import React, { useState, useCallback, useMemo, Suspense } from 'react';
44
import { BlogPost, getAllBlogPosts } from '@/lib/data/blog';
55
import BlogCard from './BlogCard';
66
import { Search, Loader2 } from 'lucide-react';
77
import { Input } from '@/components/ui/input';
88
import 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

1018
interface 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

Comments
 (0)