Initial: blog template via tAI
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Bash",
|
||||
"Glob",
|
||||
"Grep"
|
||||
]
|
||||
},
|
||||
"model": "claude-sonnet-4-20250514"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# ByteSize
|
||||
Tech blog with article pages, categories, author profiles, and search
|
||||
Stack: React 18 / TypeScript / Vite / Tailwind CSS / shadcn/ui
|
||||
Pages: Index, Article, Category, Author, Search
|
||||
Palette: emerald/stone warm editorial tones
|
||||
|
||||
## Files
|
||||
- src/pages/Index.tsx (15KB)
|
||||
- src/pages/Article.tsx (22KB)
|
||||
- src/pages/Category.tsx (10KB)
|
||||
- src/pages/Author.tsx (8KB)
|
||||
- src/pages/Search.tsx (10KB)
|
||||
- src/components/Header.tsx (4KB)
|
||||
- src/components/Footer.tsx (5KB)
|
||||
- src/App.tsx (1KB)
|
||||
|
||||
## Imports
|
||||
UI: @/components/ui/{button,card,badge,input,textarea,tabs,accordion,dialog,sheet}
|
||||
Icons: lucide-react
|
||||
Toast: import { toast } from "sonner"
|
||||
Theme: ThemeToggle in headers (next-themes)
|
||||
Router: react-router-dom BrowserRouter
|
||||
|
||||
## Rules
|
||||
- MODIFY existing files — never rebuild from scratch
|
||||
- Pages import Header + Footer from @/components/
|
||||
- Tailwind only — no inline styles
|
||||
- Use existing shadcn components — don't create custom ones
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import IndexPage from "@/pages/Index";
|
||||
import ArticlePage from "@/pages/Article";
|
||||
import CategoryPage from "@/pages/Category";
|
||||
import AuthorPage from "@/pages/Author";
|
||||
import SearchPage from "@/pages/Search";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<IndexPage />} />
|
||||
<Route path="/article/:slug" element={<ArticlePage />} />
|
||||
<Route path="/category/:name" element={<CategoryPage />} />
|
||||
<Route path="/author/:slug" element={<AuthorPage />} />
|
||||
<Route path="/search" element={<SearchPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Twitter, Github, Linkedin, Rss, Mail } from "lucide-react";
|
||||
|
||||
const categoryLinks = [
|
||||
{ label: "Technology", to: "/category/technology" },
|
||||
{ label: "Design", to: "/category/design" },
|
||||
{ label: "Business", to: "/category/business" },
|
||||
{ label: "Lifestyle", to: "/category/lifestyle" },
|
||||
];
|
||||
|
||||
const quickLinks = [
|
||||
{ label: "About", to: "/" },
|
||||
{ label: "Contact", to: "/" },
|
||||
{ label: "Privacy Policy", to: "/" },
|
||||
{ label: "Terms of Service", to: "/" },
|
||||
{ label: "RSS Feed", to: "/" },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [subscribed, setSubscribed] = useState(false);
|
||||
|
||||
return (
|
||||
<footer className="bg-stone-900 text-white">
|
||||
<div className="mx-auto max-w-7xl px-6 py-24">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-10">
|
||||
{/* Brand */}
|
||||
<div className="md:col-span-1">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="h-8 w-8 rounded-lg bg-emerald-500 flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">B</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold">ByteSize</span>
|
||||
</div>
|
||||
<p className="text-sm text-stone-400 leading-relaxed mb-6">
|
||||
ByteSize is a modern publication exploring technology, design, business, and lifestyle through thoughtful long-form writing.
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<a href="#" className="h-8 w-8 rounded-full bg-stone-800 flex items-center justify-center text-stone-400 hover:text-white hover:bg-stone-700 transition-colors">
|
||||
<Twitter className="h-4 w-4" />
|
||||
</a>
|
||||
<a href="#" className="h-8 w-8 rounded-full bg-stone-800 flex items-center justify-center text-stone-400 hover:text-white hover:bg-stone-700 transition-colors">
|
||||
<Github className="h-4 w-4" />
|
||||
</a>
|
||||
<a href="#" className="h-8 w-8 rounded-full bg-stone-800 flex items-center justify-center text-stone-400 hover:text-white hover:bg-stone-700 transition-colors">
|
||||
<Linkedin className="h-4 w-4" />
|
||||
</a>
|
||||
<a href="#" className="h-8 w-8 rounded-full bg-stone-800 flex items-center justify-center text-stone-400 hover:text-white hover:bg-stone-700 transition-colors">
|
||||
<Rss className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-4 text-stone-300 uppercase tracking-wider">Categories</h4>
|
||||
<ul className="space-y-3">
|
||||
{categoryLinks.map((link) => (
|
||||
<li key={link.label}>
|
||||
<Link to={link.to} className="text-sm text-stone-400 hover:text-white transition-colors">{link.label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-4 text-stone-300 uppercase tracking-wider">Quick Links</h4>
|
||||
<ul className="space-y-3">
|
||||
{quickLinks.map((link) => (
|
||||
<li key={link.label}>
|
||||
<Link to={link.to} className="text-sm text-stone-400 hover:text-white transition-colors">{link.label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Newsletter */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-4 text-stone-300 uppercase tracking-wider">Newsletter</h4>
|
||||
<p className="text-sm text-stone-400 mb-4">
|
||||
Get the best articles delivered to your inbox weekly.
|
||||
</p>
|
||||
{subscribed ? (
|
||||
<p className="text-sm text-emerald-400 font-medium">Subscribed! Check your inbox.</p>
|
||||
) : (
|
||||
<form onSubmit={(e) => { e.preventDefault(); if (email) { setSubscribed(true); toast.success("Subscribed! Check your inbox."); } }} className="flex gap-2">
|
||||
<Input
|
||||
type="email"
|
||||
required
|
||||
placeholder="your@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="bg-stone-800 border-stone-700 text-white placeholder:text-stone-500 focus-visible:ring-emerald-500 text-sm"
|
||||
/>
|
||||
<Button type="submit" className="bg-emerald-600 hover:bg-emerald-700 active:scale-[0.98] text-white flex-shrink-0">
|
||||
<Mail className="h-4 w-4" />
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-10 bg-stone-800" />
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<p className="text-xs text-stone-500">
|
||||
© 2025 ByteSize. All rights reserved.
|
||||
</p>
|
||||
<p className="text-xs text-stone-500">
|
||||
Built with care for readers who love great writing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Menu, Search } from "lucide-react";
|
||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "Technology", to: "/category/technology" },
|
||||
{ label: "Design", to: "/category/design" },
|
||||
{ label: "Business", to: "/category/business" },
|
||||
{ label: "Lifestyle", to: "/category/lifestyle" },
|
||||
];
|
||||
|
||||
export default function Header() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 20);
|
||||
window.addEventListener("scroll", onScroll);
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className={\`fixed top-0 inset-x-0 z-50 transition-all duration-300 \${
|
||||
scrolled
|
||||
? "bg-white/95 backdrop-blur-lg border-b border-stone-100 shadow-sm"
|
||||
: "bg-white/80 backdrop-blur-sm"
|
||||
}\`}>
|
||||
<div className="mx-auto max-w-7xl flex items-center justify-between px-6 py-3.5">
|
||||
{/* Logo */}
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-emerald-600 flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">B</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold tracking-tight text-stone-900">ByteSize</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<nav className="hidden md:flex items-center gap-8">
|
||||
{navLinks.map((l) => (
|
||||
<Link
|
||||
key={l.to}
|
||||
to={l.to}
|
||||
className="text-sm text-stone-500 hover:text-stone-900 transition-colors font-medium"
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Right Actions */}
|
||||
<div className="flex items-center gap-1">
|
||||
<Button asChild variant="ghost" size="icon" className="h-9 w-9 text-stone-500">
|
||||
<Link to="/search"><Search className="h-4 w-4" /></Link>
|
||||
</Button>
|
||||
<ThemeToggle />
|
||||
<Button asChild className="hidden sm:flex bg-emerald-600 hover:bg-emerald-700 active:scale-[0.98] text-white rounded-full px-5 text-sm ml-2">
|
||||
<Link to="/">Subscribe</Link>
|
||||
</Button>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
||||
<SheetTrigger asChild className="md:hidden">
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9 ml-1">
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right" className="w-72 bg-white">
|
||||
<div className="mt-8 flex flex-col gap-1">
|
||||
{navLinks.map((l) => (
|
||||
<Link
|
||||
key={l.to}
|
||||
to={l.to}
|
||||
className="px-4 py-3 text-base font-medium hover:bg-stone-50 rounded-lg transition-colors"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
<div className="border-t border-stone-100 mt-4 pt-4 space-y-1">
|
||||
<Link to="/search" className="flex items-center gap-3 px-4 py-3 text-sm text-stone-600 hover:bg-stone-50 rounded-lg" onClick={() => setMobileOpen(false)}>
|
||||
<Search className="h-4 w-4" /> Search
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-4 px-4 space-y-3">
|
||||
<Button className="w-full bg-emerald-600 hover:bg-emerald-700 active:scale-[0.98] text-white rounded-full">
|
||||
Subscribe
|
||||
</Button>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,385 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
ArrowRight, Clock, Twitter, Facebook, Linkedin, Link2,
|
||||
ChevronRight, MessageSquare, ThumbsUp, Bookmark, Share2, Globe, Mail
|
||||
} 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 relatedArticles = [
|
||||
{ id: 1, title: "TypeScript Best Practices for Large Codebases", category: "Technology", author: "James Liu", date: "Mar 6, 2025", gradient: "from-amber-100 to-amber-300", slug: "typescript-best-practices" },
|
||||
{ id: 2, title: "Accessible UI Patterns Every Developer Should Know", category: "Design", author: "David Kim", date: "Mar 2, 2025", gradient: "from-emerald-100 to-emerald-300", slug: "accessible-ui-patterns" },
|
||||
{ id: 3, title: "State Management in 2025: What Actually Works", category: "Technology", author: "Sarah Chen", date: "Feb 28, 2025", gradient: "from-stone-200 to-stone-400", slug: "state-management-2025" },
|
||||
];
|
||||
|
||||
const comments = [
|
||||
{ id: 1, author: "Alex Thompson", date: "2 days ago", text: "Excellent article! The section on edge computing really resonated with the challenges we face at our company. Would love to see a follow-up on specific migration strategies.", initials: "AT" },
|
||||
{ id: 2, author: "Maya Johnson", date: "1 day ago", text: "I think the shift toward server components is the most interesting trend here. It fundamentally changes how we think about component architecture.", initials: "MJ" },
|
||||
{ id: 3, author: "Ryan Park", date: "5 hours ago", text: "Great read. One thing I would add is the role of WebAssembly in this evolution. It opens up possibilities for languages beyond JavaScript on the web.", initials: "RP" },
|
||||
];
|
||||
|
||||
const tocHeadings = [
|
||||
{ id: "introduction", label: "Introduction" },
|
||||
{ id: "getting-started", label: "Getting Started" },
|
||||
{ id: "best-practices", label: "Best Practices" },
|
||||
{ id: "conclusion", label: "Conclusion" },
|
||||
];
|
||||
|
||||
export default function ArticlePage() {
|
||||
const { slug } = useParams();
|
||||
const [commentText, setCommentText] = useState("");
|
||||
const [nlEmail, setNlEmail] = useState("");
|
||||
const [nlSubscribed, setNlSubscribed] = useState(false);
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const totalHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
||||
const progress = totalHeight > 0 ? (window.scrollY / totalHeight) * 100 : 0;
|
||||
setScrollProgress(progress);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-stone-900 antialiased">
|
||||
{/* Reading Progress Bar */}
|
||||
<div className="fixed top-0 left-0 z-[60] h-1 bg-primary transition-all duration-150" style={{ width: scrollProgress + "%" }} />
|
||||
<Header />
|
||||
|
||||
<article className="pt-24">
|
||||
<div className="mx-auto max-w-4xl px-6 pt-8">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-2 text-sm text-stone-400 mb-8">
|
||||
<Link to="/" className="hover:text-emerald-600 transition-colors">Home</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<Link to="/category/technology" className="hover:text-emerald-600 transition-colors">Technology</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="text-stone-600">Article</span>
|
||||
</nav>
|
||||
|
||||
{/* Article Header */}
|
||||
<div className="animate-fade-up">
|
||||
<Badge className="mb-4 border-0 bg-emerald-100 text-emerald-700 hover:bg-primary hover:text-primary-foreground transition-colors">Technology</Badge>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight leading-tight">
|
||||
The Future of Web Development: What Comes After the JavaScript Era
|
||||
</h1>
|
||||
<p className="mt-4 text-xl text-stone-500">Exploring the emerging paradigms that may reshape how we build for the web.</p>
|
||||
</div>
|
||||
|
||||
{/* Author row */}
|
||||
<div className="mt-8 flex items-center gap-4">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarFallback className="bg-emerald-600 text-white font-semibold">SC</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<Link to="/author/sarah-chen" className="font-medium hover:text-emerald-600 transition-colors">Sarah Chen</Link>
|
||||
<p className="text-sm text-stone-400 flex items-center gap-2">
|
||||
<span>Mar 15, 2025</span>
|
||||
<span>·</span>
|
||||
<span className="flex items-center gap-1"><Clock className="h-3 w-3" /> 5 min read</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9 text-stone-400 hover:text-emerald-600">
|
||||
<Bookmark className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9 text-stone-400 hover:text-emerald-600">
|
||||
<Share2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Image */}
|
||||
<div className="mx-auto max-w-5xl px-6 mt-10">
|
||||
<div className="relative aspect-video bg-gradient-to-br from-emerald-400 via-teal-500 to-emerald-700 rounded-2xl overflow-hidden animate-fade-in">
|
||||
<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 className="absolute top-1/4 left-1/3 w-64 h-64 rounded-full bg-white/10 blur-3xl" />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-48 h-48 rounded-full bg-white/10 blur-3xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article Body with TOC */}
|
||||
<div className="mx-auto max-w-5xl px-6 mt-12">
|
||||
<div className="flex gap-10">
|
||||
{/* Table of Contents - Desktop Only */}
|
||||
<nav className="hidden lg:block sticky top-24 w-48 flex-shrink-0 self-start">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-4">On this page</h4>
|
||||
<ul className="space-y-3 border-l border-stone-200">
|
||||
{tocHeadings.map((heading) => (
|
||||
<li key={heading.id}>
|
||||
<a
|
||||
href={\`#\${heading.id}\`}
|
||||
className="block pl-4 text-sm text-stone-500 hover:text-emerald-600 transition-colors -ml-px border-l-2 border-transparent hover:border-emerald-500"
|
||||
onClick={(e) => { e.preventDefault(); document.getElementById(heading.id)?.scrollIntoView({ behavior: "smooth" }); }}
|
||||
>
|
||||
{heading.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* Article Content */}
|
||||
<div className="flex-1 max-w-3xl">
|
||||
<div className="prose prose-lg prose-stone max-w-none">
|
||||
<p id="introduction" className="text-lg leading-relaxed text-stone-600 first-letter:text-5xl first-letter:font-bold first-letter:font-serif first-letter:text-stone-900 first-letter:float-left first-letter:mr-3 first-letter:mt-1 first-letter:leading-none">
|
||||
The web development landscape is shifting. After two decades of JavaScript dominance, new paradigms are emerging that challenge our fundamental assumptions about how web applications should be built, deployed, and experienced by users.
|
||||
</p>
|
||||
|
||||
<h2 id="getting-started" className="text-2xl font-bold font-serif text-stone-900 mt-10 mb-4">The Rise of Edge Computing</h2>
|
||||
<p className="text-stone-600 leading-relaxed">
|
||||
Edge computing has transformed the way we think about server architecture. Instead of relying on centralized data centers, applications now run closer to the user, reducing latency and improving reliability across geographically distributed systems.
|
||||
</p>
|
||||
<p className="text-stone-600 leading-relaxed mt-4">
|
||||
This shift has profound implications for how we structure our applications. Traditional client-server architectures are giving way to more distributed models where the boundary between frontend and backend becomes increasingly blurred.
|
||||
</p>
|
||||
|
||||
<blockquote className="border-l-4 border-emerald-500 pl-6 py-2 my-10 italic text-stone-600 text-xl font-serif leading-relaxed bg-emerald-50/50 rounded-r-lg pr-6">
|
||||
<p className="relative">
|
||||
<span className="absolute -left-8 -top-2 text-4xl text-emerald-300 font-serif">“</span>
|
||||
The best code is the code that never has to run on the client. The future of web development is about moving computation to where it makes the most sense.
|
||||
</p>
|
||||
</blockquote>
|
||||
|
||||
<h3 id="best-practices" className="text-xl font-bold font-serif text-stone-900 mt-8 mb-4">Practical Implications for Developers</h3>
|
||||
<p className="text-stone-600 leading-relaxed">
|
||||
For developers, this means rethinking many of the patterns we have relied on. State management, data fetching, and rendering strategies all need to be reconsidered in light of these new architectural possibilities.
|
||||
</p>
|
||||
|
||||
{/* Code Block */}
|
||||
<div className="my-8 rounded-lg bg-slate-900 p-4 overflow-x-auto shadow-lg">
|
||||
<div className="flex items-center gap-1.5 mb-3">
|
||||
<div className="h-3 w-3 rounded-full bg-red-500/80" />
|
||||
<div className="h-3 w-3 rounded-full bg-yellow-500/80" />
|
||||
<div className="h-3 w-3 rounded-full bg-green-500/80" />
|
||||
<span className="ml-2 text-xs text-slate-500 font-mono">component.tsx</span>
|
||||
</div>
|
||||
<pre className="text-sm text-slate-100 font-mono leading-relaxed">
|
||||
{String.raw\`// Example: Modern component pattern
|
||||
interface ComponentProps {
|
||||
variant?: "default" | "outline" | "ghost";
|
||||
size?: "sm" | "md" | "lg";
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Component({
|
||||
variant = "default",
|
||||
size = "md",
|
||||
children,
|
||||
}: ComponentProps) {
|
||||
return (
|
||||
<div className={cn(variants[variant], sizes[size])}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}\`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h2 id="conclusion" className="text-2xl font-bold font-serif text-stone-900 mt-10 mb-4">What This Means for the Future</h2>
|
||||
<p className="text-stone-600 leading-relaxed">
|
||||
Looking ahead, we can expect to see continued convergence between traditionally separate concerns. The distinction between build-time and run-time, between static and dynamic, will continue to dissolve.
|
||||
</p>
|
||||
<p className="text-stone-600 leading-relaxed mt-4">
|
||||
The developers who thrive will be those who embrace these changes rather than resist them, building mental models that are flexible enough to accommodate the rapid pace of innovation in our field.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Inline Newsletter CTA */}
|
||||
<Card className="mt-12 mb-4 border-0 shadow-sm bg-gradient-to-r from-emerald-50 to-teal-50 animate-fade-up">
|
||||
<CardContent className="p-8 text-center">
|
||||
<Mail className="h-8 w-8 text-emerald-600 mx-auto mb-3" />
|
||||
<p className="text-sm font-semibold text-emerald-700 mb-2">Join 12,000+ readers</p>
|
||||
<h3 className="text-xl font-bold tracking-tight mb-2">Enjoyed this? Subscribe for more</h3>
|
||||
<p className="text-sm text-stone-500 mb-2 max-w-md mx-auto">Get articles like this delivered to your inbox every week.</p>
|
||||
<div className="flex items-center justify-center gap-4 text-xs text-stone-400 mb-4">
|
||||
<span>Weekly insights</span>
|
||||
<span>·</span>
|
||||
<span>No spam</span>
|
||||
<span>·</span>
|
||||
<span>Unsubscribe anytime</span>
|
||||
</div>
|
||||
{nlSubscribed ? (
|
||||
<p className="text-sm text-emerald-700 font-medium">You are subscribed! Check your inbox.</p>
|
||||
) : (
|
||||
<form onSubmit={(e) => { e.preventDefault(); if (nlEmail) { setNlSubscribed(true); toast.success("Subscribed! Check your inbox."); } }} className="flex gap-2 max-w-sm mx-auto">
|
||||
<Input
|
||||
type="email"
|
||||
required
|
||||
placeholder="your@email.com"
|
||||
value={nlEmail}
|
||||
onChange={(e) => setNlEmail(e.target.value)}
|
||||
className="border-stone-200 focus-visible:ring-emerald-500"
|
||||
/>
|
||||
<Button type="submit" className="bg-emerald-600 hover:bg-emerald-700 text-white rounded-full px-6 active:scale-[0.98] flex-shrink-0">
|
||||
Subscribe
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
<p className="mt-3 text-[11px] text-stone-400 flex items-center justify-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
Your email is safe with us
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Share Buttons */}
|
||||
<Separator className="my-10" />
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<span className="text-sm font-medium text-stone-500">Share this article</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="rounded-full gap-2 text-stone-600 active:scale-[0.98]">
|
||||
<Twitter className="h-4 w-4" /> Twitter
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="rounded-full gap-2 text-stone-600 active:scale-[0.98]">
|
||||
<Facebook className="h-4 w-4" /> Facebook
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="rounded-full gap-2 text-stone-600 active:scale-[0.98]">
|
||||
<Linkedin className="h-4 w-4" /> LinkedIn
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="rounded-full gap-2 text-stone-600 active:scale-[0.98]">
|
||||
<Link2 className="h-4 w-4" /> Copy Link
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Author Bio */}
|
||||
<Card className="mt-10 border-0 shadow-md bg-gradient-to-br from-stone-50 to-emerald-50/30">
|
||||
<CardContent className="p-8">
|
||||
<div className="flex gap-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="h-16 w-16 rounded-full bg-gradient-to-br from-emerald-400 to-teal-600 p-0.5">
|
||||
<div className="h-full w-full rounded-full bg-white flex items-center justify-center">
|
||||
<span className="text-xl font-bold text-emerald-600">SC</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs uppercase tracking-wider text-stone-400 font-medium mb-1">Written by</p>
|
||||
<Link to="/author/sarah-chen" className="text-lg font-bold hover:text-emerald-600 transition-colors">Sarah Chen</Link>
|
||||
<p className="text-sm text-emerald-600 font-medium">Senior Software Engineer & Technical Writer</p>
|
||||
<p className="mt-3 text-sm text-stone-500 leading-relaxed">
|
||||
Sarah is a senior software engineer and technical writer specializing in frontend architecture. She has been building for the web for over 10 years and loves exploring the intersection of developer experience and user experience.
|
||||
</p>
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-stone-400 hover:text-blue-500">
|
||||
<Twitter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-stone-400 hover:text-blue-700">
|
||||
<Linkedin className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-stone-400 hover:text-stone-900">
|
||||
<Globe className="h-4 w-4" />
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-5 mx-1" />
|
||||
<Link to="/author/sarah-chen" className="text-sm font-medium text-emerald-600 hover:text-emerald-700 flex items-center gap-1">
|
||||
More from this author <ArrowRight className="h-3 w-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Related Articles */}
|
||||
<div className="mt-16">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight mb-12 animate-fade-up">Related Articles</h2>
|
||||
<div className="grid md:grid-cols-3 gap-6 stagger">
|
||||
{relatedArticles.map((article) => (
|
||||
<Link to={\`/article/\${article.slug}\`} key={article.id} className="group">
|
||||
<Card className="overflow-hidden border-0 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||
<div className={\`relative aspect-video bg-gradient-to-br \${article.gradient}\`}>
|
||||
<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 className="absolute top-1/3 left-1/4 w-24 h-24 rounded-full bg-white/10 blur-2xl" />
|
||||
</div>
|
||||
<CardContent className="p-5">
|
||||
<Badge className={\`mb-2 border-0 text-xs hover:bg-primary hover:text-primary-foreground transition-colors \${categoryColors[article.category] || "bg-emerald-100 text-emerald-700"}\`}>
|
||||
{article.category}
|
||||
</Badge>
|
||||
<h3 className="font-semibold text-sm text-stone-900 group-hover:text-emerald-600 transition-colors line-clamp-2">
|
||||
{article.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-xs text-stone-400">{article.author} · {article.date}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comments Section */}
|
||||
<div className="mt-16 mb-16">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight mb-2 flex items-center gap-2 animate-fade-up">
|
||||
<MessageSquare className="h-5 w-5" /> Comments
|
||||
</h2>
|
||||
<p className="text-sm text-stone-400 mb-8">3 comments</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
{comments.map((comment) => (
|
||||
<div key={comment.id} className="flex gap-4">
|
||||
<Avatar className="h-10 w-10 flex-shrink-0">
|
||||
<AvatarFallback className="bg-stone-200 text-stone-600 text-sm font-semibold">{comment.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">{comment.author}</span>
|
||||
<span className="text-xs text-stone-400">{comment.date}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-stone-600 leading-relaxed">{comment.text}</p>
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
<button className="flex items-center gap-1 text-xs text-stone-400 hover:text-emerald-600 transition-colors">
|
||||
<ThumbsUp className="h-3 w-3" /> Like
|
||||
</button>
|
||||
<button className="text-xs text-stone-400 hover:text-emerald-600 transition-colors">
|
||||
Reply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Separator className="my-8" />
|
||||
|
||||
{/* Comment Form */}
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Leave a Comment</h3>
|
||||
<Textarea
|
||||
placeholder="Share your thoughts..."
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
className="min-h-[120px] border-stone-200 focus-visible:ring-emerald-500"
|
||||
/>
|
||||
<Button className="mt-4 bg-emerald-600 hover:bg-emerald-700 active:scale-[0.98] text-white rounded-full px-8">
|
||||
Post Comment
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>{/* end Article Content */}
|
||||
</div>{/* end flex */}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
ArrowRight, Twitter, Github, Linkedin, Globe, Clock,
|
||||
FileText, Users, CalendarDays, ChevronLeft, ChevronRight
|
||||
} 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 authorArticles = [
|
||||
{ id: 1, title: "The Future of Web Development: What Comes After the JavaScript Era", category: "Technology", date: "Mar 15, 2025", gradient: "from-emerald-400 to-teal-500", slug: "future-of-web-development" },
|
||||
{ id: 2, title: "Understanding React Server Components in Production", category: "Technology", date: "Mar 12, 2025", gradient: "from-amber-200 to-orange-300", slug: "react-server-components" },
|
||||
{ id: 3, title: "Edge Functions: A Practical Introduction", category: "Technology", date: "Mar 1, 2025", gradient: "from-stone-200 to-stone-400", slug: "edge-functions-intro" },
|
||||
{ id: 4, title: "Design Principles Every Developer Should Know", category: "Design", date: "Feb 20, 2025", gradient: "from-amber-100 to-amber-300", slug: "design-for-developers" },
|
||||
{ id: 5, title: "Why Rust is the Future of WebAssembly", category: "Technology", date: "Feb 10, 2025", gradient: "from-emerald-100 to-emerald-300", slug: "rust-for-web" },
|
||||
{ id: 6, title: "Navigating Your Career as a Senior Developer", category: "Lifestyle", date: "Jan 28, 2025", gradient: "from-stone-100 to-amber-200", slug: "dev-career-growth" },
|
||||
];
|
||||
|
||||
export default function AuthorPage() {
|
||||
const { slug } = useParams();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50 text-stone-900 antialiased">
|
||||
<Header />
|
||||
|
||||
{/* Author Hero */}
|
||||
<section className="py-24 pb-0">
|
||||
<div className="mx-auto max-w-4xl px-6 pt-12 pb-12 text-center animate-fade-up">
|
||||
<Avatar className="h-28 w-28 mx-auto mb-6 ring-4 ring-emerald-100">
|
||||
<AvatarFallback className="bg-emerald-600 text-white text-3xl font-bold">SC</AvatarFallback>
|
||||
</Avatar>
|
||||
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight">Sarah Chen</h1>
|
||||
<p className="mt-4 text-lg text-stone-500 max-w-xl mx-auto leading-relaxed">
|
||||
Senior software engineer and technical writer specializing in frontend architecture. Building for the web for over 10 years, exploring the intersection of developer experience and user experience. Previously at Stripe, Vercel, and Mozilla.
|
||||
</p>
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="mt-6 flex items-center justify-center gap-2">
|
||||
<Button variant="outline" size="icon" className="h-10 w-10 rounded-full border-stone-200 text-stone-500 hover:text-emerald-600 hover:border-emerald-300">
|
||||
<Twitter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" className="h-10 w-10 rounded-full border-stone-200 text-stone-500 hover:text-emerald-600 hover:border-emerald-300">
|
||||
<Github className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" className="h-10 w-10 rounded-full border-stone-200 text-stone-500 hover:text-emerald-600 hover:border-emerald-300">
|
||||
<Linkedin className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" className="h-10 w-10 rounded-full border-stone-200 text-stone-500 hover:text-emerald-600 hover:border-emerald-300">
|
||||
<Globe className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mt-10 flex items-center justify-center gap-8">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center gap-2 text-emerald-600 mb-1">
|
||||
<FileText className="h-5 w-5" />
|
||||
<span className="text-2xl font-bold">42</span>
|
||||
</div>
|
||||
<p className="text-sm text-stone-400">Articles</p>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-12" />
|
||||
<div className="text-center">
|
||||
<div className="flex items-center gap-2 text-emerald-600 mb-1">
|
||||
<Users className="h-5 w-5" />
|
||||
<span className="text-2xl font-bold">12K</span>
|
||||
</div>
|
||||
<p className="text-sm text-stone-400">Readers</p>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-12" />
|
||||
<div className="text-center">
|
||||
<div className="flex items-center gap-2 text-emerald-600 mb-1">
|
||||
<CalendarDays className="h-5 w-5" />
|
||||
<span className="text-2xl font-bold">2024</span>
|
||||
</div>
|
||||
<p className="text-sm text-stone-400">Member since</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator className="max-w-7xl mx-auto" />
|
||||
|
||||
{/* Author Articles */}
|
||||
<section className="py-24">
|
||||
<div className="mx-auto max-w-7xl px-6">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight mb-12 animate-fade-up">Articles by Sarah Chen</h2>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 stagger">
|
||||
{authorArticles.map((article) => (
|
||||
<Link to={\`/article/\${article.slug}\`} key={article.id} className="group">
|
||||
<Card className="overflow-hidden border-0 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 bg-white">
|
||||
<div className={\`relative aspect-video bg-gradient-to-br \${article.gradient}\`}>
|
||||
<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 className="absolute top-1/3 left-1/4 w-24 h-24 rounded-full bg-white/10 blur-2xl" />
|
||||
</div>
|
||||
<CardContent className="p-5">
|
||||
<Badge className={\`mb-2 border-0 text-xs hover:bg-primary hover:text-primary-foreground transition-colors \${categoryColors[article.category] || "bg-emerald-100 text-emerald-700"}\`}>
|
||||
{article.category}
|
||||
</Badge>
|
||||
<h3 className="font-semibold text-stone-900 group-hover:text-emerald-600 transition-colors leading-snug line-clamp-2">
|
||||
{article.title}
|
||||
</h3>
|
||||
<div className="mt-3 flex items-center text-xs text-stone-400">
|
||||
<span>{article.date}</span>
|
||||
<span className="mx-1.5">·</span>
|
||||
<span className="flex items-center gap-1"><Clock className="h-3 w-3" /> 5 min read</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="mt-12 flex items-center justify-center gap-2">
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 rounded-full">
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
{[1, 2, 3].map((page) => (
|
||||
<Button
|
||||
key={page}
|
||||
variant={page === 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={\`h-9 w-9 rounded-full \${page === 1 ? "bg-emerald-600 hover:bg-emerald-700" : ""}\`}
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
))}
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 rounded-full">
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import {
|
||||
ArrowRight, ChevronLeft, ChevronRight, Clock
|
||||
} 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 categoryInfo: Record<string, { description: string; count: number }> = {
|
||||
technology: { description: "Deep dives into software development, web technologies, AI, and the tools shaping the future of tech.", count: 42 },
|
||||
design: { description: "Exploring visual design, UX principles, design systems, and the art of crafting beautiful interfaces.", count: 28 },
|
||||
business: { description: "Insights on startups, product strategy, growth, and the business side of technology.", count: 19 },
|
||||
lifestyle: { description: "Productivity tips, remote work culture, career advice, and life as a tech professional.", count: 15 },
|
||||
};
|
||||
|
||||
const articles = [
|
||||
{ id: 1, title: "Understanding React Server Components in Production", excerpt: "A practical guide to adopting RSC in existing React applications.", 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: "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-emerald-200 to-teal-300", slug: "typescript-best-practices" },
|
||||
{ id: 3, title: "Edge Functions: A Practical Introduction", excerpt: "How edge computing changes the way we deploy and run web applications.", category: "Technology", author: "Sarah Chen", initials: "SC", date: "Mar 1, 2025", gradient: "from-stone-200 to-stone-400", slug: "edge-functions-intro" },
|
||||
{ id: 4, title: "The State of WebAssembly in 2025", excerpt: "Where Wasm stands today and where it is heading for web developers.", category: "Technology", author: "Aisha Patel", initials: "AP", date: "Feb 25, 2025", gradient: "from-amber-100 to-amber-300", slug: "webassembly-2025" },
|
||||
{ id: 5, title: "Building Real-Time Apps with Server-Sent Events", excerpt: "A simpler alternative to WebSockets for many real-time use cases.", category: "Technology", author: "Marcus Rivera", initials: "MR", date: "Feb 20, 2025", gradient: "from-emerald-100 to-emerald-300", slug: "server-sent-events" },
|
||||
{ id: 6, title: "Modern CSS Layout Patterns You Should Know", excerpt: "Container queries, subgrid, and other CSS features transforming layout.", category: "Technology", author: "David Kim", initials: "DK", date: "Feb 15, 2025", gradient: "from-stone-100 to-amber-200", slug: "modern-css-layout" },
|
||||
];
|
||||
|
||||
const allCategories = ["Technology", "Design", "Business", "Lifestyle"];
|
||||
|
||||
export default function CategoryPage() {
|
||||
const { name } = useParams();
|
||||
const categoryName = name ? name.charAt(0).toUpperCase() + name.slice(1) : "Technology";
|
||||
const info = categoryInfo[name || "technology"] || categoryInfo.technology;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50 text-stone-900 antialiased">
|
||||
<Header />
|
||||
|
||||
{/* Category Hero */}
|
||||
<section className="py-24 pb-0">
|
||||
<div className="mx-auto max-w-7xl px-6 pt-8 pb-12 border-b border-stone-200">
|
||||
<nav className="flex items-center gap-2 text-sm text-stone-400 mb-6">
|
||||
<Link to="/" className="hover:text-emerald-600 transition-colors">Home</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="text-stone-600">Categories</span>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="text-stone-600">{categoryName}</span>
|
||||
</nav>
|
||||
<div className="animate-fade-up">
|
||||
<Badge className={\`mb-4 border-0 hover:bg-primary hover:text-primary-foreground transition-colors \${categoryColors[categoryName] || "bg-emerald-100 text-emerald-700"}\`}>
|
||||
{categoryName}
|
||||
</Badge>
|
||||
<div className="flex items-start justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight">{categoryName}</h1>
|
||||
<p className="mt-3 text-stone-500 max-w-2xl">{info.description}</p>
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
<Badge variant="secondary" className="bg-emerald-100 text-emerald-700 text-xs">{info.count} articles</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm text-stone-400 mt-2">
|
||||
<span className="font-medium text-stone-900 cursor-pointer hover:text-emerald-600 transition-colors">Latest</span>
|
||||
<span className="mx-1">|</span>
|
||||
<span className="cursor-pointer hover:text-emerald-600 transition-colors">Popular</span>
|
||||
<span className="mx-1">|</span>
|
||||
<span className="cursor-pointer hover:text-emerald-600 transition-colors">Oldest</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Articles + Sidebar */}
|
||||
<section className="py-24">
|
||||
<div className="mx-auto max-w-7xl px-6">
|
||||
<div className="grid lg:grid-cols-4 gap-10">
|
||||
{/* Articles Grid */}
|
||||
<div className="lg:col-span-3">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 stagger">
|
||||
{articles.map((article) => (
|
||||
<Link to={\`/article/\${article.slug}\`} key={article.id} className="group">
|
||||
<Card className="overflow-hidden border-0 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 bg-white">
|
||||
<div className={\`relative aspect-video bg-gradient-to-br \${article.gradient}\`}>
|
||||
<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 className="absolute top-1/3 left-1/4 w-24 h-24 rounded-full bg-white/10 blur-2xl" />
|
||||
</div>
|
||||
<CardContent className="p-5">
|
||||
<h3 className="font-semibold text-stone-900 group-hover:text-emerald-600 transition-colors leading-snug line-clamp-2">
|
||||
{article.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-stone-500 line-clamp-2">{article.excerpt}</p>
|
||||
<div className="mt-3 flex items-center text-xs text-stone-400">
|
||||
<span>{article.date}</span>
|
||||
<span className="mx-1.5">·</span>
|
||||
<span className="flex items-center gap-1"><Clock className="h-3 w-3" /> 5 min read</span>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<div className="h-5 w-5 rounded-full bg-gradient-to-br from-emerald-400 to-teal-600 flex items-center justify-center">
|
||||
<span className="text-[8px] text-white font-bold">{article.initials}</span>
|
||||
</div>
|
||||
<span className="text-xs text-stone-500">{article.author}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="mt-12 flex items-center justify-center gap-2">
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 rounded-full">
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
{[1, 2, 3, 4, 5].map((page) => (
|
||||
<Button
|
||||
key={page}
|
||||
variant={page === 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={\`h-9 w-9 rounded-full \${page === 1 ? "bg-emerald-600 hover:bg-emerald-700" : ""}\`}
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
))}
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 rounded-full">
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="border-0 shadow-sm bg-white sticky top-28">
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold text-sm uppercase tracking-wider text-stone-500 mb-4">All Categories</h3>
|
||||
<ul className="space-y-2">
|
||||
{allCategories.map((cat) => (
|
||||
<li key={cat}>
|
||||
<Link
|
||||
to={\`/category/\${cat.toLowerCase()}\`}
|
||||
className={\`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors \${
|
||||
cat === categoryName
|
||||
? "bg-emerald-50 text-emerald-700 font-medium"
|
||||
: "text-stone-600 hover:bg-stone-50"
|
||||
}\`}
|
||||
>
|
||||
<span className={\`h-2 w-2 rounded-full \${
|
||||
cat === categoryName ? "bg-emerald-500" : "bg-stone-300"
|
||||
}\`} />
|
||||
{cat}
|
||||
<Badge variant="secondary" className="ml-auto bg-stone-100 text-stone-500 text-xs">
|
||||
{categoryInfo[cat.toLowerCase()]?.count || 0}
|
||||
</Badge>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
ArrowRight, Clock, BookOpen, TrendingUp, Mail, Tag
|
||||
} from "lucide-react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
const featuredArticle = {
|
||||
title: "The Future of Web Development: What Comes After the JavaScript Era",
|
||||
excerpt: "Exploring the emerging paradigms, languages, and frameworks that may reshape how we build for the web in the next decade.",
|
||||
category: "Technology",
|
||||
author: "Sarah Chen",
|
||||
date: "Mar 15, 2025",
|
||||
readTime: "8 min read",
|
||||
gradient: "from-emerald-400 via-teal-500 to-emerald-700",
|
||||
slug: "future-of-web-development",
|
||||
};
|
||||
|
||||
const latestArticles = [
|
||||
{ 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", 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", 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", 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", 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", 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", date: "Mar 2, 2025", gradient: "from-stone-100 to-amber-200", slug: "accessible-ui-patterns" },
|
||||
];
|
||||
|
||||
const categories = [
|
||||
{ name: "Technology", count: 42 },
|
||||
{ name: "Design", count: 28 },
|
||||
{ name: "Business", count: 19 },
|
||||
{ name: "Lifestyle", count: 15 },
|
||||
];
|
||||
|
||||
const tags = [
|
||||
"React", "TypeScript", "CSS", "Node.js", "Figma", "Tailwind",
|
||||
"Next.js", "AI", "UX", "Startup", "Remote Work", "Productivity",
|
||||
];
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
export default function IndexPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [subscribed, setSubscribed] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50 text-stone-900 antialiased">
|
||||
<Header />
|
||||
|
||||
{/* ── FEATURED HERO ── */}
|
||||
<section className="py-24 pb-0">
|
||||
<div className="mx-auto max-w-7xl px-6 pt-8">
|
||||
<Link to={\`/article/\${featuredArticle.slug}\`} className="group block animate-fade-in">
|
||||
<div className="relative overflow-hidden rounded-3xl hover:shadow-2xl transition-all duration-500">
|
||||
<div className={\`aspect-[21/9] bg-gradient-to-br \${featuredArticle.gradient}\`}>
|
||||
<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 className="absolute top-1/4 left-1/4 w-64 h-64 rounded-full bg-white/10 blur-3xl" />
|
||||
<div className="absolute bottom-1/3 right-1/4 w-48 h-48 rounded-full bg-white/10 blur-3xl" />
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-black/10" />
|
||||
<Badge className={\`absolute top-6 left-6 border-0 z-10 \${categoryColors[featuredArticle.category] || "bg-emerald-100 text-emerald-700"}\`}>
|
||||
{featuredArticle.category}
|
||||
</Badge>
|
||||
<div className="absolute bottom-0 left-0 right-0 p-8 sm:p-12">
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white max-w-3xl leading-tight">
|
||||
{featuredArticle.title}
|
||||
</h1>
|
||||
<p className="mt-4 text-lg text-white/80 max-w-2xl line-clamp-2">
|
||||
{featuredArticle.excerpt}
|
||||
</p>
|
||||
<div className="mt-6 flex items-center gap-4">
|
||||
<Avatar className="h-10 w-10 border-2 border-white/30">
|
||||
<AvatarFallback className="bg-emerald-600 text-white text-sm font-semibold">
|
||||
{featuredArticle.author.split(" ").map(n => n[0]).join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-white">
|
||||
<p className="text-sm font-medium">{featuredArticle.author}</p>
|
||||
<p className="text-xs text-white/60">{featuredArticle.date} · {featuredArticle.readTime}</p>
|
||||
</div>
|
||||
<Button className="ml-auto bg-white text-stone-900 hover:bg-white/90 active:scale-[0.98] rounded-full px-6 hidden sm:flex">
|
||||
Read Article <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── LATEST ARTICLES + SIDEBAR ── */}
|
||||
<section className="py-24">
|
||||
<div className="mx-auto max-w-7xl px-6">
|
||||
<div className="flex items-end justify-between mb-12 animate-fade-up">
|
||||
<div>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">Latest Articles</h2>
|
||||
<p className="mt-2 text-stone-500">Fresh perspectives on technology, design, and culture</p>
|
||||
</div>
|
||||
<Link to="/category/technology" className="hidden sm:flex items-center text-sm font-medium text-emerald-600 hover:text-emerald-700">
|
||||
View All <ArrowRight className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-4 gap-10">
|
||||
{/* Articles Grid */}
|
||||
<div className="lg:col-span-3">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 stagger">
|
||||
{latestArticles.map((article) => (
|
||||
<Link to={\`/article/\${article.slug}\`} key={article.id} className="group">
|
||||
<Card className="overflow-hidden border-0 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 bg-white">
|
||||
<div className={\`relative aspect-video bg-gradient-to-br \${article.gradient}\`}>
|
||||
<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 className="absolute top-1/3 left-1/4 w-24 h-24 rounded-full bg-white/10 blur-2xl" />
|
||||
<Badge className={\`absolute top-3 left-3 border-0 text-xs \${categoryColors[article.category] || "bg-emerald-100 text-emerald-700"}\`}>
|
||||
{article.category}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardContent className="p-5">
|
||||
<h3 className="font-semibold text-stone-900 group-hover:text-emerald-600 transition-colors leading-snug line-clamp-2">
|
||||
{article.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-stone-500 line-clamp-2">{article.excerpt}</p>
|
||||
<div className="mt-4 flex items-center text-xs text-stone-400">
|
||||
<span>{article.date}</span>
|
||||
<Separator orientation="vertical" className="mx-2 h-3" />
|
||||
<span className="flex items-center gap-1"><Clock className="h-3 w-3" /> 5 min read</span>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-5 w-5 rounded-full bg-gradient-to-br from-emerald-400 to-teal-600 flex items-center justify-center">
|
||||
<span className="text-[8px] text-white font-bold">{article.author.split(" ").map(n => n[0]).join("")}</span>
|
||||
</div>
|
||||
<span className="text-xs text-stone-500">{article.author}</span>
|
||||
</div>
|
||||
<span className="inline-flex items-center text-sm font-medium text-emerald-600 group-hover:text-emerald-700">
|
||||
Read More <ArrowRight className="ml-1 h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-10 text-center">
|
||||
<Button variant="outline" className="rounded-full border-stone-300 px-8 active:scale-[0.98]">
|
||||
Load More Articles
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="lg:col-span-1 space-y-8">
|
||||
{/* Categories */}
|
||||
<Card className="border-0 shadow-sm bg-white">
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold text-sm uppercase tracking-wider text-stone-500 mb-4 flex items-center gap-2">
|
||||
<BookOpen className="h-4 w-4" /> Categories
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{categories.map((cat) => (
|
||||
<li key={cat.name}>
|
||||
<Link to={\`/category/\${cat.name.toLowerCase()}\`} className="flex items-center justify-between text-sm text-stone-600 hover:text-emerald-600 transition-colors">
|
||||
<span>{cat.name}</span>
|
||||
<Badge variant="secondary" className="bg-stone-100 text-stone-500 text-xs">{cat.count}</Badge>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tags Cloud */}
|
||||
<Card className="border-0 shadow-sm bg-white">
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold text-sm uppercase tracking-wider text-stone-500 mb-4 flex items-center gap-2">
|
||||
<Tag className="h-4 w-4" /> Popular Tags
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2 stagger">
|
||||
{tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="bg-stone-100 text-stone-600 hover:bg-primary hover:text-primary-foreground transition-colors cursor-pointer text-xs px-3 py-1">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Newsletter */}
|
||||
<Card className="border-0 shadow-sm bg-gradient-to-br from-emerald-600 to-teal-700 text-white">
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold mb-2 flex items-center gap-2">
|
||||
<Mail className="h-4 w-4" /> Stay in the Loop
|
||||
</h3>
|
||||
<p className="text-sm text-emerald-100 mb-3">Get the best articles delivered to your inbox every week. No spam, ever.</p>
|
||||
<p className="text-xs font-semibold text-white/90 mb-3">Join 12,000+ readers</p>
|
||||
{subscribed ? (
|
||||
<p className="text-sm text-emerald-100 font-medium py-2">Thanks for subscribing! Check your inbox.</p>
|
||||
) : (
|
||||
<form onSubmit={(e) => { e.preventDefault(); if (email) { setSubscribed(true); toast.success("Subscribed! Check your inbox."); } }} className="space-y-2">
|
||||
<Input
|
||||
type="email"
|
||||
required
|
||||
placeholder="your@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="bg-white/20 border-white/30 text-white placeholder:text-emerald-200 focus-visible:ring-white"
|
||||
/>
|
||||
<Button type="submit" className="w-full bg-white text-emerald-700 hover:bg-white/90 active:scale-[0.98] font-semibold">
|
||||
Subscribe
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
<ul className="mt-3 space-y-1 text-[11px] text-emerald-200">
|
||||
<li className="flex items-center gap-1.5">• Weekly insights</li>
|
||||
<li className="flex items-center gap-1.5">• No spam</li>
|
||||
<li className="flex items-center gap-1.5">• Unsubscribe anytime</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Trending */}
|
||||
<Card className="border-0 shadow-sm bg-white">
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-semibold text-sm uppercase tracking-wider text-stone-500 mb-4 flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4" /> Trending
|
||||
</h3>
|
||||
<ul className="space-y-4">
|
||||
{latestArticles.slice(0, 3).map((article, i) => (
|
||||
<li key={article.id}>
|
||||
<Link to={\`/article/\${article.slug}\`} className="flex gap-3 group">
|
||||
<span className="text-2xl font-bold text-stone-200">0{i + 1}</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-800 group-hover:text-emerald-600 transition-colors line-clamp-2">{article.title}</p>
|
||||
<p className="text-xs text-stone-400 mt-1">{article.date}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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