Component Details

Filterable Component 02

Filterable Component 02

PostCard.tsx

Next.jsTailwind CSS
1"use client";
2
3import { useState, useCallback } from "react";
4import Link from "next/link";
5import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6import { Badge } from "@/components/ui/badge";
7import { Input } from "@/components/ui/input";
8import { Button } from "@/components/ui/button";
9import { Search } from "lucide-react";
10
11// Dummy data for blog posts
12const dummyPosts = [
13  {
14    id: 1,
15    slug: "getting-started-with-nextjs",
16    title: "Getting Started with Next.js 14",
17    excerpt: "Learn how to build modern web applications with Next.js 14 and its new features including Server Actions and partial prerendering.",
18    category: "Web Development",
19    author: "Jane Doe",
20    date: "2024-01-15",
21    readTime: 5,
22    content: "Next.js 14 introduces several new features that make building web applications faster and more efficient..."
23  },
24  {
25    id: 2,
26    slug: "tailwind-css-best-practices",
27    title: "Tailwind CSS Best Practices for 2024",
28    excerpt: "Discover the most effective ways to use Tailwind CSS in your projects, from component architecture to performance optimization.",
29    category: "CSS",
30    author: "John Smith",
31    date: "2024-01-10",
32    readTime: 8,
33    content: "Tailwind CSS has become the go-to utility-first CSS framework for many developers..."
34  },
35  {
36    id: 3,
37    slug: "react-performance-tips",
38    title: "Advanced React Performance Optimization",
39    excerpt: "Explore techniques to improve your React application performance including memoization, code splitting, and virtualization.",
40    category: "React",
41    author: "Alex Johnson",
42    date: "2024-01-05",
43    readTime: 10,
44    content: "React applications can suffer from performance issues as they grow in complexity..."
45  },
46  {
47    id: 4,
48    slug: "typescript-advanced-patterns",
49    title: "Advanced TypeScript Patterns for React",
50    excerpt: "Master advanced TypeScript patterns to build type-safe React applications with better developer experience.",
51    category: "TypeScript",
52    author: "Sarah Wilson",
53    date: "2023-12-20",
54    readTime: 12,
55    content: "TypeScript and React are a powerful combination for building robust applications..."
56  },
57  {
58    id: 5,
59    slug: "api-design-best-practices",
60    title: "Modern API Design Best Practices",
61    excerpt: "Learn how to design RESTful and GraphQL APIs that are scalable, maintainable, and developer-friendly.",
62    category: "Backend",
63    author: "Michael Chen",
64    date: "2023-12-10",
65    readTime: 7,
66    content: "API design is crucial for the success of any modern application..."
67  },
68  {
69    id: 6,
70    slug: "docker-for-frontend-developers",
71    title: "Docker for Frontend Developers",
72    excerpt: "A practical guide to using Docker in frontend development workflows for consistent environments.",
73    category: "DevOps",
74    author: "Emily Rodriguez",
75    date: "2023-11-28",
76    readTime: 9,
77    content: "Docker is not just for backend services and operations teams..."
78  }
79];
80
81// SearchBar Component
82interface SearchBarProps {
83  onSearch: (query: string) => void;
84}
85
86function SearchBar({ onSearch }: SearchBarProps) {
87  const [query, setQuery] = useState("");
88
89  const handleSubmit = (e: React.FormEvent) => {
90    e.preventDefault();
91    onSearch(query);
92  };
93
94  return (
95    <form onSubmit={handleSubmit} className="relative">
96      <div className="relative">
97        <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
98        <Input
99          type="text"
100          placeholder="Search articles..."
101          value={query}
102          onChange={(e) => {
103            setQuery(e.target.value);
104            if (!e.target.value.trim()) {
105              onSearch("");
106            }
107          }}
108          className="pl-10 pr-4 py-2 w-full"
109        />
110      </div>
111    </form>
112  );
113}
114
115// PostCard Component
116interface PostCardProps {
117  slug: string;
118  title: string;
119  excerpt: string;
120  category: string;
121  author: string;
122  date: string;
123  readTime: number;
124}
125
126export function PostCard({
127  slug,
128  title,
129  excerpt,
130  category,
131  author,
132  date,
133  readTime,
134}: PostCardProps) {
135  function formatDate(dateString: string) {
136    const date = new Date(dateString);
137    const months = [
138      "Jan",
139      "Feb",
140      "Mar",
141      "Apr",
142      "May",
143      "Jun",
144      "Jul",
145      "Aug",
146      "Sep",
147      "Oct",
148      "Nov",
149      "Dec",
150    ];
151    return `${
152      months[date.getMonth()]
153    } ${date.getDate()}, ${date.getFullYear()}`;
154  }
155
156  return (
157    <Link href={`/blog/${slug}`}>
158      <Card className="hover:shadow-lg transition h-full">
159        <CardHeader>
160          <div className="flex items-start justify-between gap-2 mb-2">
161            <Badge variant="outline">{category}</Badge>
162            <span className="text-sm text-muted-foreground">
163              {readTime} min read
164            </span>
165          </div>
166          <CardTitle className="line-clamp-2">{title}</CardTitle>
167        </CardHeader>
168        <CardContent>
169          <p className="text-sm text-muted-foreground mb-4 line-clamp-2">
170            {excerpt}
171          </p>
172          <div className="flex justify-between items-center text-xs text-muted-foreground">
173            <span>{author}</span>
174            <span>{formatDate(date)}</span>
175          </div>
176        </CardContent>
177      </Card>
178    </Link>
179  );
180}
181
182// Main BlogPage Component
183export default function BlogPage() {
184  const [searchResults, setSearchResults] = useState<typeof dummyPosts | null>(null);
185  const allPosts = dummyPosts.sort(
186    (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
187  );
188  const posts = searchResults !== null ? searchResults : allPosts;
189
190  const handleSearch = useCallback(
191    (query: string) => {
192      if (!query.trim()) {
193        setSearchResults(null);
194        return;
195      }
196
197      const results = allPosts.filter((post) => {
198        const lowerQuery = query.toLowerCase();
199        return (
200          post.title.toLowerCase().includes(lowerQuery) ||
201          post.excerpt.toLowerCase().includes(lowerQuery) ||
202          post.content.toLowerCase().includes(lowerQuery)
203        );
204      });
205      setSearchResults(results);
206    },
207    [allPosts]
208  );
209
210  return (
211    <div className="flex flex-col">
212      <main className="flex-1 max-w-4xl mx-auto px-4 sm:px-6 lg:px-0 py-12 w-full">
213        <h1 className="text-4xl font-bold mb-8">All Articles</h1>
214
215        <div className="mb-8">
216          <SearchBar onSearch={handleSearch} />
217        </div>
218
219        {posts.length === 0 ? (
220          <div className="text-center py-12">
221            <p className="text-muted-foreground text-lg">No articles found. Try a different search term.</p>
222          </div>
223        ) : (
224          <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
225            {posts.map((post) => (
226              <PostCard
227                key={post.id}
228                slug={post.slug}
229                title={post.title}
230                excerpt={post.excerpt}
231                category={post.category}
232                author={post.author}
233                date={post.date}
234                readTime={post.readTime}
235              />
236            ))}
237          </div>
238        )}
239      </main>
240    </div>
241  );
242}