init: init repo with existing code
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowLeft, Calendar, Clock, Twitter, Linkedin, Facebook, Link2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Footer from '@/components/Footer';
|
||||
import PageTransition from '@/components/PageTransition';
|
||||
import { getPostBySlug, getRelatedPosts } from '@/data/blogData';
|
||||
|
||||
export default function BlogArticle() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const navigate = useNavigate();
|
||||
const post = getPostBySlug(slug || '');
|
||||
|
||||
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">
|
||||
<Navbar />
|
||||
|
||||
{/* 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 }) => <a href={href} className="text-primary hover:underline">{children}</a>,
|
||||
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>
|
||||
)}
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user