413 lines
16 KiB
TypeScript
413 lines
16 KiB
TypeScript
import PageTransition from "@/components/home/PageTransition";
|
|
import { Button } from "@/components/ui/button";
|
|
import { getRelatedPosts } from "@/data/blogData";
|
|
import { useBlogById } from "@/hooks/queries/useBlogs";
|
|
import { motion } from "framer-motion";
|
|
import {
|
|
ArrowLeft,
|
|
Calendar,
|
|
Clock,
|
|
Facebook,
|
|
Link2,
|
|
Linkedin,
|
|
Twitter,
|
|
} from "lucide-react";
|
|
import ReactMarkdown from "react-markdown";
|
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
|
import remarkGfm from "remark-gfm";
|
|
import { toast } from "sonner";
|
|
export default function BlogArticle() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const { data } = useBlogById(id);
|
|
const post = data?.data.data;
|
|
const navigate = useNavigate();
|
|
// const post = getPostBySlug(id || '');
|
|
|
|
if (!post) {
|
|
return (
|
|
<PageTransition>
|
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
<div className="text-center">
|
|
<h1 className="text-4xl font-bold mb-4">Article Not Found</h1>
|
|
<p className="text-muted-foreground mb-8">
|
|
The article you're looking for doesn't exist.
|
|
</p>
|
|
<Button onClick={() => navigate("/blog")}>Back to Blog</Button>
|
|
</div>
|
|
</div>
|
|
</PageTransition>
|
|
);
|
|
}
|
|
|
|
const relatedPosts = getRelatedPosts(post);
|
|
const shareUrl = window.location.href;
|
|
|
|
const handleShare = (platform: string) => {
|
|
const urls: Record<string, string> = {
|
|
twitter: `https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(post.title)}`,
|
|
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`,
|
|
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`,
|
|
};
|
|
|
|
if (platform === "copy") {
|
|
navigator.clipboard.writeText(shareUrl);
|
|
toast.success("Link copied to clipboard!");
|
|
} else {
|
|
window.open(urls[platform], "_blank", "width=600,height=400");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<PageTransition>
|
|
<div className="min-h-screen bg-background overflow-x-hidden">
|
|
{/* Hero */}
|
|
<section className="pt-32 pb-16 relative overflow-hidden">
|
|
<div className="absolute inset-0">
|
|
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/20 rounded-full blur-3xl" />
|
|
</div>
|
|
|
|
<div className="container mx-auto px-4 relative z-10 max-w-4xl">
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<Link to="/blog">
|
|
<Button variant="ghost" className="mb-8 group">
|
|
<ArrowLeft className="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" />
|
|
Back to Blog
|
|
</Button>
|
|
</Link>
|
|
</motion.div>
|
|
|
|
{/* Category */}
|
|
<motion.span
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
className="inline-block px-4 py-1.5 rounded-full bg-primary/20 text-primary text-sm font-medium mb-6"
|
|
>
|
|
{post.category}
|
|
</motion.span>
|
|
|
|
{/* Title */}
|
|
<motion.h1
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.1 }}
|
|
className="text-4xl md:text-5xl font-bold mb-6 leading-tight"
|
|
>
|
|
{post.title}
|
|
</motion.h1>
|
|
|
|
{/* Meta */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.2 }}
|
|
className="flex flex-wrap items-center gap-6 text-muted-foreground mb-8"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<img
|
|
src={post.author.avatar}
|
|
alt={post.author.name}
|
|
className="w-10 h-10 rounded-full object-cover border-2 border-primary/50"
|
|
/>
|
|
<div>
|
|
<p className="font-medium text-foreground">
|
|
{post.author.name}
|
|
</p>
|
|
<p className="text-sm">{post.author.role}</p>
|
|
</div>
|
|
</div>
|
|
<span className="flex items-center gap-2">
|
|
<Calendar className="w-4 h-4" />
|
|
{new Date(post.date).toLocaleDateString("en-US", {
|
|
month: "long",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
})}
|
|
</span>
|
|
<span className="flex items-center gap-2">
|
|
<Clock className="w-4 h-4" />
|
|
{post.readTime}
|
|
</span>
|
|
</motion.div>
|
|
|
|
{/* Featured Image */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.3 }}
|
|
className="rounded-2xl overflow-hidden mb-12"
|
|
>
|
|
<img
|
|
src={post.image}
|
|
alt={post.title}
|
|
className="w-full h-[400px] object-cover"
|
|
/>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Content */}
|
|
<section className="pb-16">
|
|
<div className="container mx-auto px-4 max-w-4xl">
|
|
<div className="grid lg:grid-cols-[1fr_200px] gap-12">
|
|
{/* Article Content */}
|
|
<motion.article
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.4 }}
|
|
className="max-w-none prose-content"
|
|
>
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
components={{
|
|
h1: ({ children }) => (
|
|
<h1 className="text-3xl font-bold text-foreground mt-8 mb-4">
|
|
{children}
|
|
</h1>
|
|
),
|
|
h2: ({ children }) => (
|
|
<h2 className="text-2xl font-bold text-foreground mt-8 mb-4">
|
|
{children}
|
|
</h2>
|
|
),
|
|
h3: ({ children }) => (
|
|
<h3 className="text-xl font-bold text-foreground mt-6 mb-3">
|
|
{children}
|
|
</h3>
|
|
),
|
|
h4: ({ children }) => (
|
|
<h4 className="text-lg font-bold text-foreground mt-4 mb-2">
|
|
{children}
|
|
</h4>
|
|
),
|
|
p: ({ children }) => (
|
|
<p className="text-muted-foreground leading-relaxed mb-4">
|
|
{children}
|
|
</p>
|
|
),
|
|
ul: ({ children }) => (
|
|
<ul className="list-disc list-inside text-muted-foreground space-y-2 mb-4 ml-4">
|
|
{children}
|
|
</ul>
|
|
),
|
|
ol: ({ children }) => (
|
|
<ol className="list-decimal list-inside text-muted-foreground space-y-2 mb-4 ml-4">
|
|
{children}
|
|
</ol>
|
|
),
|
|
li: ({ children }) => (
|
|
<li className="text-muted-foreground">{children}</li>
|
|
),
|
|
a: ({ href, children }) => (
|
|
<Link to={href} className="text-primary hover:underline">
|
|
{children}
|
|
</Link>
|
|
),
|
|
strong: ({ children }) => (
|
|
<strong className="font-bold text-foreground">
|
|
{children}
|
|
</strong>
|
|
),
|
|
em: ({ children }) => (
|
|
<em className="italic">{children}</em>
|
|
),
|
|
blockquote: ({ children }) => (
|
|
<blockquote className="border-l-4 border-primary pl-4 italic text-muted-foreground my-4">
|
|
{children}
|
|
</blockquote>
|
|
),
|
|
code: ({ className, children }) => {
|
|
const isInline = !className;
|
|
if (isInline) {
|
|
return (
|
|
<code className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
|
|
{children}
|
|
</code>
|
|
);
|
|
}
|
|
return (
|
|
<code className="block bg-muted border border-border rounded-lg p-4 overflow-x-auto text-sm font-mono my-4">
|
|
{children}
|
|
</code>
|
|
);
|
|
},
|
|
pre: ({ children }) => (
|
|
<pre className="bg-muted border border-border rounded-lg p-4 overflow-x-auto my-4">
|
|
{children}
|
|
</pre>
|
|
),
|
|
hr: () => <hr className="border-border my-8" />,
|
|
}}
|
|
>
|
|
{post.content}
|
|
</ReactMarkdown>
|
|
</motion.article>
|
|
|
|
{/* Sidebar */}
|
|
<aside className="hidden lg:block">
|
|
<div className="sticky top-32 space-y-8">
|
|
{/* Share */}
|
|
<div className="glass rounded-2xl p-6">
|
|
<h4 className="font-bold mb-4 text-sm uppercase tracking-wide text-muted-foreground">
|
|
Share
|
|
</h4>
|
|
<div className="flex flex-col gap-3">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleShare("twitter")}
|
|
className="justify-start gap-2 glass border-primary/30"
|
|
>
|
|
<Twitter className="w-4 h-4" /> Twitter
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleShare("linkedin")}
|
|
className="justify-start gap-2 glass border-primary/30"
|
|
>
|
|
<Linkedin className="w-4 h-4" /> LinkedIn
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleShare("facebook")}
|
|
className="justify-start gap-2 glass border-primary/30"
|
|
>
|
|
<Facebook className="w-4 h-4" /> Facebook
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleShare("copy")}
|
|
className="justify-start gap-2 glass border-primary/30"
|
|
>
|
|
<Link2 className="w-4 h-4" /> Copy Link
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
<div className="glass rounded-2xl p-6">
|
|
<h4 className="font-bold mb-4 text-sm uppercase tracking-wide text-muted-foreground">
|
|
Tags
|
|
</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{post.tags.map((tag) => (
|
|
<span
|
|
key={tag}
|
|
className="px-3 py-1 rounded-full bg-muted text-muted-foreground text-xs"
|
|
>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Author Bio */}
|
|
<section className="py-16 bg-secondary/30">
|
|
<div className="container mx-auto px-4 max-w-4xl">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
className="glass rounded-3xl p-8 flex flex-col md:flex-row gap-6 items-center md:items-start"
|
|
>
|
|
<img
|
|
src={post.author.avatar}
|
|
alt={post.author.name}
|
|
className="w-24 h-24 rounded-full object-cover border-4 border-primary/50"
|
|
/>
|
|
<div className="text-center md:text-left">
|
|
<h3 className="text-2xl font-bold mb-2">{post.author.name}</h3>
|
|
<p className="text-primary font-medium mb-4">
|
|
{post.author.role}
|
|
</p>
|
|
<p className="text-muted-foreground mb-4">{post.author.bio}</p>
|
|
<div className="flex gap-3 justify-center md:justify-start">
|
|
{post.author.twitter && (
|
|
<a
|
|
href={`https://twitter.com/${post.author.twitter}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="p-2 rounded-full glass hover:neon-glow transition-all"
|
|
>
|
|
<Twitter className="w-5 h-5" />
|
|
</a>
|
|
)}
|
|
{post.author.linkedin && (
|
|
<a
|
|
href={`https://linkedin.com/in/${post.author.linkedin}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="p-2 rounded-full glass hover:neon-glow transition-all"
|
|
>
|
|
<Linkedin className="w-5 h-5" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Related Articles */}
|
|
{relatedPosts.length > 0 && (
|
|
<section className="py-24">
|
|
<div className="container mx-auto px-4">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
className="text-center mb-12"
|
|
>
|
|
<h2 className="text-3xl font-bold">Related Articles</h2>
|
|
</motion.div>
|
|
|
|
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
|
{relatedPosts.map((relatedPost, index) => (
|
|
<motion.article
|
|
key={relatedPost.id}
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: index * 0.1 }}
|
|
className="group glass rounded-2xl overflow-hidden hover:neon-glow transition-all duration-500"
|
|
>
|
|
<Link to={`/blog/${relatedPost.slug}`}>
|
|
<div className="relative h-40 overflow-hidden">
|
|
<img
|
|
src={relatedPost.image}
|
|
alt={relatedPost.title}
|
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
|
/>
|
|
</div>
|
|
<div className="p-5">
|
|
<h3 className="font-bold mb-2 group-hover:text-primary transition-colors line-clamp-2">
|
|
{relatedPost.title}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground flex items-center gap-2">
|
|
<Clock className="w-3 h-3" /> {relatedPost.readTime}
|
|
</p>
|
|
</div>
|
|
</Link>
|
|
</motion.article>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</PageTransition>
|
|
);
|
|
}
|