Seed: blog template (17 files)

This commit is contained in:
2026-07-23 22:10:00 +00:00
parent b544975476
commit 8558117f79
+417
View File
@@ -0,0 +1,417 @@
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 { useSaas, useSaasMutation } from "@/hooks/use-tygarun";
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 seedComments = [
{ 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);
// tyga.run community (comments) + notifications (newsletter). Seed until live.
const { data: liveComments, refetch: refetchComments } = useSaas("community", "/comments", { query: { postId: slug } });
const { mutate: postComment, loading: posting } = useSaasMutation("community", "/comments");
const { mutate: subscribeNewsletter } = useSaasMutation("email", "/send/newsletter-confirm");
const comments = (liveComments && liveComments.length) ? liveComments : seedComments;
const handlePostComment = async (e) => {
e.preventDefault();
if (!commentText.trim()) return;
try {
await postComment({ postId: slug, body: commentText });
setCommentText("");
toast.success("Comment posted!");
refetchComments();
} catch {
toast.error("Could not post comment. Please try again.");
}
};
const handleArticleSubscribe = async (e) => {
e.preventDefault();
if (!nlEmail) return;
try {
await subscribeNewsletter({ email: nlEmail, list: "newsletter" });
setNlSubscribed(true);
toast.success("Subscribed! Check your inbox.");
} catch {
toast.error("Something went wrong. Please try again.");
}
};
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>&middot;</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">&ldquo;</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>&middot;</span>
<span>No spam</span>
<span>&middot;</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={handleArticleSubscribe} 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} &middot; {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">{comments.length} 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 || (comment.author || "?").slice(0, 2).toUpperCase()}</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 || (comment.createdAt ? new Date(comment.createdAt).toLocaleDateString() : "")}</span>
</div>
<p className="mt-1 text-sm text-stone-600 leading-relaxed">{comment.text || comment.body}</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 onClick={handlePostComment} disabled={posting} 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>
);
}