Seed: blog template (17 files)
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Search as SearchIcon, X, FileText } from "lucide-react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
Technology: "bg-emerald-100 text-emerald-700",
|
||||
Design: "bg-amber-100 text-amber-700",
|
||||
Business: "bg-stone-200 text-stone-700",
|
||||
Lifestyle: "bg-teal-100 text-teal-700",
|
||||
};
|
||||
|
||||
const allArticles = [
|
||||
{ id: 1, title: "Understanding React Server Components in Production", excerpt: "A practical guide to adopting RSC in existing React applications without a full rewrite.", category: "Technology", author: "Sarah Chen", initials: "SC", date: "Mar 12, 2025", gradient: "from-amber-200 to-orange-300", slug: "react-server-components" },
|
||||
{ id: 2, title: "Building Design Systems That Scale", excerpt: "Lessons learned from building a design system used across 12 product teams.", category: "Design", author: "Marcus Rivera", initials: "MR", date: "Mar 10, 2025", gradient: "from-emerald-200 to-teal-300", slug: "design-systems-at-scale" },
|
||||
{ id: 3, title: "Growth Strategies for Early-Stage Startups", excerpt: "How three founders went from zero to product-market fit in under 18 months.", category: "Business", author: "Aisha Patel", initials: "AP", date: "Mar 8, 2025", gradient: "from-stone-200 to-stone-400", slug: "startup-growth-strategies" },
|
||||
{ id: 4, title: "TypeScript Best Practices for Large Codebases", excerpt: "Type-safe patterns and architectural decisions that pay off at scale.", category: "Technology", author: "James Liu", initials: "JL", date: "Mar 6, 2025", gradient: "from-amber-100 to-amber-300", slug: "typescript-best-practices" },
|
||||
{ id: 5, title: "The Remote Worker's Guide to Deep Focus", excerpt: "Strategies for maintaining productivity and avoiding burnout while working from home.", category: "Lifestyle", author: "Elena Torres", initials: "ET", date: "Mar 4, 2025", gradient: "from-emerald-100 to-emerald-300", slug: "remote-work-productivity" },
|
||||
{ id: 6, title: "Accessible UI Patterns Every Developer Should Know", excerpt: "Common accessibility mistakes and how to build inclusive interfaces by default.", category: "Design", author: "David Kim", initials: "DK", date: "Mar 2, 2025", gradient: "from-stone-100 to-amber-200", slug: "accessible-ui-patterns" },
|
||||
{ id: 7, title: "React Hooks: Advanced Patterns and Pitfalls", excerpt: "Going beyond useState and useEffect to build truly reusable custom hooks.", category: "Technology", author: "Sarah Chen", initials: "SC", date: "Feb 28, 2025", gradient: "from-emerald-200 to-emerald-400", slug: "react-hooks-advanced" },
|
||||
{ id: 8, title: "The Business Case for Design Tokens", excerpt: "How design tokens save engineering time and create consistency at scale.", category: "Business", author: "David Kim", initials: "DK", date: "Feb 25, 2025", gradient: "from-amber-100 to-stone-200", slug: "design-tokens-business" },
|
||||
];
|
||||
|
||||
const filterChips = ["All", "Technology", "Design", "Business", "Lifestyle"];
|
||||
|
||||
const suggestions = ["React hooks", "design systems", "TypeScript", "remote work", "accessibility"];
|
||||
|
||||
export default function SearchPage() {
|
||||
const [query, setQuery] = useState("react hooks");
|
||||
const [activeFilter, setActiveFilter] = useState("All");
|
||||
|
||||
const highlightMatch = (text: string, term: string) => {
|
||||
if (!term || term.length < 2) return text;
|
||||
const idx = text.toLowerCase().indexOf(term.toLowerCase());
|
||||
if (idx === -1) return text;
|
||||
const before = text.slice(0, idx);
|
||||
const match = text.slice(idx, idx + term.length);
|
||||
const after = text.slice(idx + term.length);
|
||||
return <>{before}<mark className="bg-primary/20 text-foreground rounded px-0.5">{match}</mark>{after}</>;
|
||||
};
|
||||
|
||||
const filtered = allArticles.filter((a) => {
|
||||
const matchesQuery = query.length === 0 || a.title.toLowerCase().includes(query.toLowerCase()) || a.excerpt.toLowerCase().includes(query.toLowerCase());
|
||||
const matchesFilter = activeFilter === "All" || a.category === activeFilter;
|
||||
return matchesQuery && matchesFilter;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50 text-stone-900 antialiased">
|
||||
<Header />
|
||||
|
||||
<div className="mx-auto max-w-4xl px-6 pt-32 pb-24">
|
||||
{/* Search Input */}
|
||||
<div className="mb-12 animate-fade-up">
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-5 top-1/2 -translate-y-1/2 h-5 w-5 text-stone-400" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search articles..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="pl-14 pr-12 h-16 text-lg rounded-2xl border-stone-200 bg-white shadow-sm focus-visible:ring-emerald-500"
|
||||
/>
|
||||
{query && (
|
||||
<button onClick={() => setQuery("")} className="absolute right-5 top-1/2 -translate-y-1/2 text-stone-400 hover:text-stone-600">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!query && (
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm text-stone-400">Popular:</span>
|
||||
{["React", "TypeScript", "AI", "Design Systems"].map((s) => (
|
||||
<Button key={s} variant="outline" size="sm" className="rounded-full text-xs border-stone-200 hover:border-emerald-300 hover:text-emerald-700" onClick={() => setQuery(s)}>
|
||||
{s}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Results heading */}
|
||||
{query && filtered.length > 0 && (
|
||||
<p className="text-lg font-medium mb-6">
|
||||
<span className="text-stone-400">{filtered.length} results for</span>{" "}
|
||||
<span className="text-stone-900">'{query}'</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Filter Chips */}
|
||||
<div className="flex flex-wrap gap-2 mb-8 stagger">
|
||||
{filterChips.map((chip) => (
|
||||
<Button
|
||||
key={chip}
|
||||
variant={activeFilter === chip ? "default" : "outline"}
|
||||
size="sm"
|
||||
className={`rounded-full active:scale-[0.98] \${
|
||||
activeFilter === chip
|
||||
? "bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||
: "border-stone-200 text-stone-600 hover:border-emerald-300 hover:text-emerald-700"
|
||||
}`}
|
||||
onClick={() => setActiveFilter(chip)}
|
||||
>
|
||||
{chip}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Results List */}
|
||||
{filtered.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{filtered.map((article) => (
|
||||
<Link to={`/article/\${article.slug}`} key={article.id} className="group block">
|
||||
<div className="flex gap-5 p-5 bg-white rounded-xl border border-stone-100 hover:shadow-md hover:border-stone-200 hover:-translate-y-0.5 transition-all duration-300">
|
||||
<div className={`hidden sm:flex w-28 h-20 flex-shrink-0 rounded-lg bg-gradient-to-br \${article.gradient} items-center justify-center relative overflow-hidden`}>
|
||||
<div className="absolute inset-0" style={{ backgroundImage: "radial-gradient(circle at 25% 25%, rgba(255,255,255,0.15) 1px, transparent 1px)", backgroundSize: "20px 20px" }} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge className={`border-0 text-[10px] px-2 py-0 hover:bg-primary hover:text-primary-foreground transition-colors \${categoryColors[article.category]}`}>
|
||||
{article.category}
|
||||
</Badge>
|
||||
</div>
|
||||
<h3 className="font-semibold text-stone-900 group-hover:text-emerald-600 transition-colors line-clamp-1">
|
||||
{highlightMatch(article.title, query)}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-stone-500 line-clamp-1">{highlightMatch(article.excerpt, query)}</p>
|
||||
<div className="mt-2 flex items-center gap-3 text-xs text-stone-400">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Avatar className="h-4 w-4">
|
||||
<AvatarFallback className="bg-emerald-100 text-emerald-700 text-[8px] font-semibold">{article.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>{article.author}</span>
|
||||
</div>
|
||||
<span>·</span>
|
||||
<span>{article.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* Empty State */
|
||||
<div className="text-center py-20">
|
||||
<div className="h-16 w-16 rounded-full bg-stone-100 flex items-center justify-center mx-auto mb-6">
|
||||
<FileText className="h-7 w-7 text-stone-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold tracking-tight mb-2">No articles found</h3>
|
||||
<p className="text-stone-500 mb-6 max-w-md mx-auto">
|
||||
We couldn't find any articles matching your search. Try different keywords or browse by category.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
<span className="text-sm text-stone-400 mr-2">Try searching for:</span>
|
||||
{suggestions.map((s) => (
|
||||
<Button
|
||||
key={s}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full text-xs border-stone-200"
|
||||
onClick={() => setQuery(s)}
|
||||
>
|
||||
{s}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user