init: init repo with existing code
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowLeft, Calendar, Clock, User, Search } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Footer from '@/components/Footer';
|
||||
import PageTransition from '@/components/PageTransition';
|
||||
import { blogPosts } from '@/data/blogData';
|
||||
|
||||
const categories = ['All', 'AI & Machine Learning', 'Cloud Solutions', 'Web Development', 'Mobile Development'];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.1 },
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 30 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.5, ease: 'easeOut' as const },
|
||||
},
|
||||
};
|
||||
|
||||
export default function Blog() {
|
||||
const [activeCategory, setActiveCategory] = useState('All');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const filteredPosts = blogPosts.filter(post => {
|
||||
const matchesCategory = activeCategory === 'All' || post.category === activeCategory;
|
||||
const matchesSearch = post.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
post.excerpt.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
|
||||
return (
|
||||
<PageTransition>
|
||||
<div className="min-h-screen bg-background overflow-x-hidden">
|
||||
<Navbar />
|
||||
|
||||
{/* Hero Section */}
|
||||
<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 className="absolute bottom-0 right-1/4 w-64 h-64 bg-neon-purple/20 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Link to="/">
|
||||
<Button variant="ghost" className="mb-8 group">
|
||||
<ArrowLeft className="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" />
|
||||
Back to Home
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="text-center mb-12"
|
||||
>
|
||||
<h1 className="text-5xl md:text-6xl font-bold mb-6">
|
||||
Our <span className="text-primary neon-text-glow">Blog</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
|
||||
Insights, tutorials, and thought leadership from our team of experts.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Search */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="max-w-md mx-auto mb-8"
|
||||
>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search articles..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-12 py-6 rounded-full glass border-primary/30 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="flex flex-wrap justify-center gap-3 mb-16"
|
||||
>
|
||||
{categories.map((category) => (
|
||||
<Button
|
||||
key={category}
|
||||
variant={activeCategory === category ? 'default' : 'outline'}
|
||||
onClick={() => setActiveCategory(category)}
|
||||
className={`rounded-full px-6 transition-all duration-300 ${
|
||||
activeCategory === category
|
||||
? 'neon-glow bg-primary text-primary-foreground'
|
||||
: 'glass border-primary/30 hover:border-primary'
|
||||
}`}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Blog Posts Grid */}
|
||||
<section className="pb-24">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||
>
|
||||
{filteredPosts.map((post) => (
|
||||
<motion.article
|
||||
key={post.id}
|
||||
variants={itemVariants}
|
||||
className="group glass rounded-2xl overflow-hidden hover:neon-glow transition-all duration-500"
|
||||
>
|
||||
<Link to={`/blog/${post.slug}`}>
|
||||
<div className="relative h-48 overflow-hidden">
|
||||
<img
|
||||
src={post.image}
|
||||
alt={post.title}
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background/80 to-transparent" />
|
||||
<span className="absolute bottom-4 left-4 px-3 py-1 rounded-full bg-primary/20 text-primary text-xs font-medium backdrop-blur-sm">
|
||||
{post.category}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<h3 className="text-xl font-bold mb-3 group-hover:text-primary transition-colors line-clamp-2">
|
||||
{post.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm mb-4 line-clamp-2">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<User className="w-3 h-3" />
|
||||
{post.author.name}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{new Date(post.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{post.readTime}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.article>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{filteredPosts.length === 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-center py-20"
|
||||
>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
No articles found matching your criteria.
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import Navbar from '@/components/Navbar';
|
||||
import HeroSection from '@/components/HeroSection';
|
||||
import ServicesSection from '@/components/ServicesSection';
|
||||
import ProjectsSection from '@/components/ProjectsSection';
|
||||
import TeamSection from '@/components/TeamSection';
|
||||
import AboutSection from '@/components/AboutSection';
|
||||
import TestimonialsSection from '@/components/TestimonialsSection';
|
||||
import BlogSection from '@/components/BlogSection';
|
||||
import FAQSection from '@/components/FAQSection';
|
||||
import ContactSection from '@/components/ContactSection';
|
||||
import Footer from '@/components/Footer';
|
||||
import PageTransition from '@/components/PageTransition';
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<PageTransition>
|
||||
<div className="min-h-screen bg-background overflow-x-hidden">
|
||||
<Navbar />
|
||||
<HeroSection />
|
||||
<ServicesSection />
|
||||
<ProjectsSection />
|
||||
<TeamSection />
|
||||
<AboutSection />
|
||||
<TestimonialsSection />
|
||||
<BlogSection />
|
||||
<FAQSection />
|
||||
<ContactSection />
|
||||
<Footer />
|
||||
</div>
|
||||
</PageTransition>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const NotFound = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
console.error("404 Error: User attempted to access non-existent route:", location.pathname);
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-muted">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-4 text-4xl font-bold">404</h1>
|
||||
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p>
|
||||
<a href="/" className="text-primary underline hover:text-primary/90">
|
||||
Return to Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
@@ -0,0 +1,372 @@
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowLeft, ExternalLink, Github, Calendar, Clock, Users, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { getProjectBySlug, getRelatedProjects } from '@/data/projectData';
|
||||
|
||||
export default function ProjectDetails() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const navigate = useNavigate();
|
||||
const project = getProjectBySlug(slug || '');
|
||||
|
||||
if (!project) {
|
||||
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">Project Not Found</h1>
|
||||
<p className="text-muted-foreground mb-8">The project you're looking for doesn't exist.</p>
|
||||
<Button onClick={() => navigate('/projects')}>Back to Projects</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
const relatedProjects = getRelatedProjects(project);
|
||||
const categoryLabels: Record<string, string> = {
|
||||
web: 'Web Development',
|
||||
mobile: 'Mobile App',
|
||||
ai: 'AI & Machine Learning',
|
||||
cloud: 'Cloud Solutions',
|
||||
};
|
||||
|
||||
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 className="absolute bottom-0 right-1/4 w-64 h-64 bg-neon-purple/20 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
{/* Breadcrumb */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="flex items-center gap-2 text-sm text-muted-foreground mb-8"
|
||||
>
|
||||
<Link to="/" className="hover:text-primary transition-colors">Home</Link>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
<Link to="/projects" className="hover:text-primary transition-colors">Projects</Link>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
<span className="text-foreground">{project.title}</span>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
{/* Content */}
|
||||
<div>
|
||||
<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"
|
||||
>
|
||||
{categoryLabels[project.category]}
|
||||
</motion.span>
|
||||
|
||||
<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"
|
||||
>
|
||||
{project.title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="text-lg text-muted-foreground mb-8"
|
||||
>
|
||||
{project.description}
|
||||
</motion.p>
|
||||
|
||||
{/* Meta */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"
|
||||
>
|
||||
<div className="glass rounded-xl p-4 text-center">
|
||||
<Calendar className="w-5 h-5 mx-auto mb-2 text-primary" />
|
||||
<p className="text-sm text-muted-foreground">Year</p>
|
||||
<p className="font-bold">{project.year}</p>
|
||||
</div>
|
||||
<div className="glass rounded-xl p-4 text-center">
|
||||
<Clock className="w-5 h-5 mx-auto mb-2 text-primary" />
|
||||
<p className="text-sm text-muted-foreground">Duration</p>
|
||||
<p className="font-bold">{project.duration}</p>
|
||||
</div>
|
||||
<div className="glass rounded-xl p-4 text-center">
|
||||
<Users className="w-5 h-5 mx-auto mb-2 text-primary" />
|
||||
<p className="text-sm text-muted-foreground">Team</p>
|
||||
<p className="font-bold">{project.team}</p>
|
||||
</div>
|
||||
<div className="glass rounded-xl p-4 text-center">
|
||||
<p className="text-sm text-muted-foreground mb-1">Client</p>
|
||||
<p className="font-bold text-sm">{project.client}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Actions */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
className="flex gap-4"
|
||||
>
|
||||
<Button className="rounded-full neon-glow">
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
View Live
|
||||
</Button>
|
||||
<Button variant="outline" className="rounded-full glass border-primary/30">
|
||||
<Github className="w-4 h-4 mr-2" />
|
||||
View Code
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="rounded-3xl overflow-hidden neon-glow"
|
||||
>
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
className="w-full h-[400px] object-cover"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Results */}
|
||||
<section className="py-16 bg-secondary/30">
|
||||
<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">Key Results</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 max-w-4xl mx-auto">
|
||||
{project.results.map((result, index) => (
|
||||
<motion.div
|
||||
key={result.label}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="glass-strong rounded-2xl p-6 text-center"
|
||||
>
|
||||
<p className="text-3xl md:text-4xl font-bold text-primary neon-text-glow mb-2">
|
||||
{result.value}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">{result.label}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Details */}
|
||||
<section className="py-24">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid lg:grid-cols-2 gap-16 max-w-6xl mx-auto">
|
||||
{/* Full Description */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<h3 className="text-2xl font-bold mb-6">About the Project</h3>
|
||||
<div className="prose-content">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
p: ({ children }) => <p className="text-muted-foreground leading-relaxed mb-4">{children}</p>,
|
||||
strong: ({ children }) => <strong className="font-bold text-foreground">{children}</strong>,
|
||||
ul: ({ children }) => <ul className="list-disc list-inside text-muted-foreground space-y-2 mb-4 ml-4">{children}</ul>,
|
||||
li: ({ children }) => <li className="text-muted-foreground">{children}</li>,
|
||||
}}
|
||||
>
|
||||
{project.fullDescription}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Features & Tech */}
|
||||
<div className="space-y-12">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<h3 className="text-2xl font-bold mb-6">Key Features</h3>
|
||||
<ul className="space-y-3">
|
||||
{project.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start gap-3">
|
||||
<span className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0" />
|
||||
<span className="text-muted-foreground">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<h3 className="text-2xl font-bold mb-6">Technologies Used</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{project.technologies.map((tech) => (
|
||||
<span
|
||||
key={tech}
|
||||
className="px-4 py-2 rounded-full glass text-sm font-medium border border-primary/30"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<h3 className="text-2xl font-bold mb-6">Challenges Solved</h3>
|
||||
<ul className="space-y-3">
|
||||
{project.challenges.map((challenge, index) => (
|
||||
<li key={index} className="flex items-start gap-3">
|
||||
<span className="w-2 h-2 rounded-full bg-neon-purple mt-2 flex-shrink-0" />
|
||||
<span className="text-muted-foreground">{challenge}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Gallery */}
|
||||
<section className="py-16 bg-secondary/30">
|
||||
<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">Project Gallery</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
||||
{project.gallery.map((image, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="rounded-2xl overflow-hidden hover:neon-glow transition-all duration-500"
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={`${project.title} screenshot ${index + 1}`}
|
||||
className="w-full h-48 object-cover hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Projects */}
|
||||
{relatedProjects.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 Projects</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||
{relatedProjects.map((relatedProject, index) => (
|
||||
<motion.article
|
||||
key={relatedProject.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={`/projects/${relatedProject.slug}`}>
|
||||
<div className="relative h-40 overflow-hidden">
|
||||
<img
|
||||
src={relatedProject.image}
|
||||
alt={relatedProject.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">
|
||||
{relatedProject.title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{relatedProject.description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mt-12"
|
||||
>
|
||||
<Link to="/projects">
|
||||
<Button variant="outline" className="rounded-full px-8 glass border-primary/30 hover:neon-glow">
|
||||
View All Projects
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { ArrowLeft, Globe, Smartphone, Brain, Cloud, ExternalLink, Github } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Footer from '@/components/Footer';
|
||||
import PageTransition from '@/components/PageTransition';
|
||||
import { projects } from '@/data/projectData';
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', name: 'All Projects', icon: null },
|
||||
{ id: 'web', name: 'Web', icon: Globe },
|
||||
{ id: 'mobile', name: 'Mobile', icon: Smartphone },
|
||||
{ id: 'ai', name: 'AI', icon: Brain },
|
||||
{ id: 'cloud', name: 'Cloud', icon: Cloud },
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 30 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.5, ease: 'easeOut' as const },
|
||||
},
|
||||
};
|
||||
|
||||
export default function Projects() {
|
||||
const [activeCategory, setActiveCategory] = useState('all');
|
||||
|
||||
const filteredProjects = activeCategory === 'all'
|
||||
? projects
|
||||
: projects.filter(project => project.category === activeCategory);
|
||||
|
||||
return (
|
||||
<PageTransition>
|
||||
<div className="min-h-screen bg-background overflow-x-hidden">
|
||||
<Navbar />
|
||||
|
||||
{/* Hero Section */}
|
||||
<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 className="absolute bottom-0 right-1/4 w-64 h-64 bg-neon-purple/20 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
{/* Back Button */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Link to="/">
|
||||
<Button variant="ghost" className="mb-8 group">
|
||||
<ArrowLeft className="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" />
|
||||
Back to Home
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="text-center mb-12"
|
||||
>
|
||||
<h1 className="text-5xl md:text-6xl font-bold mb-6">
|
||||
Our <span className="text-primary neon-text-glow">Projects</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
|
||||
Explore our portfolio of innovative solutions that have transformed businesses across industries.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="flex flex-wrap justify-center gap-3 mb-16"
|
||||
>
|
||||
{categories.map((category) => {
|
||||
const Icon = category.icon;
|
||||
return (
|
||||
<Button
|
||||
key={category.id}
|
||||
variant={activeCategory === category.id ? 'default' : 'outline'}
|
||||
onClick={() => setActiveCategory(category.id)}
|
||||
className={`rounded-full px-6 transition-all duration-300 ${
|
||||
activeCategory === category.id
|
||||
? 'neon-glow bg-primary text-primary-foreground'
|
||||
: 'glass border-primary/30 hover:border-primary'
|
||||
}`}
|
||||
>
|
||||
{Icon && <Icon className="w-4 h-4 mr-2" />}
|
||||
{category.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Projects Grid */}
|
||||
<section className="pb-24">
|
||||
<div className="container mx-auto px-4">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeCategory}
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
className="grid md:grid-cols-2 gap-8"
|
||||
>
|
||||
{filteredProjects.map((project) => (
|
||||
<motion.article
|
||||
key={project.id}
|
||||
variants={itemVariants}
|
||||
layout
|
||||
className="group glass rounded-3xl overflow-hidden hover:neon-glow transition-all duration-500"
|
||||
>
|
||||
{/* Image */}
|
||||
<div className="relative h-64 overflow-hidden">
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/50 to-transparent opacity-60" />
|
||||
|
||||
{/* Category Badge */}
|
||||
<div className="absolute top-4 left-4">
|
||||
<span className="px-3 py-1 rounded-full bg-primary/20 text-primary text-sm font-medium backdrop-blur-sm border border-primary/30">
|
||||
{categories.find(c => c.id === project.category)?.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div className="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<a
|
||||
href={project.liveUrl}
|
||||
className="p-2 rounded-full glass hover:bg-primary/20 transition-colors"
|
||||
aria-label="View live project"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
<a
|
||||
href={project.githubUrl}
|
||||
className="p-2 rounded-full glass hover:bg-primary/20 transition-colors"
|
||||
aria-label="View on GitHub"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-8">
|
||||
{/* Meta */}
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground mb-4">
|
||||
<span>{project.client}</span>
|
||||
<span className="w-1 h-1 rounded-full bg-muted-foreground" />
|
||||
<span>{project.year}</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-2xl font-bold mb-4 group-hover:text-primary transition-colors">
|
||||
{project.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-muted-foreground mb-6 leading-relaxed">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
{/* Technologies */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.technologies.map((tech) => (
|
||||
<span
|
||||
key={tech}
|
||||
className="px-3 py-1 rounded-full bg-muted text-muted-foreground text-xs font-medium"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.article>
|
||||
))}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Empty State */}
|
||||
{filteredProjects.length === 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-center py-20"
|
||||
>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
No projects found in this category.
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user