Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions cpsquad/app/blogs/BlogCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { motion } from "framer-motion";

const BlogCard = ({ blog, index, onReadMore }) => {
return (
<motion.article
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{
duration: 0.6,
delay: index * 0.1,
ease: "easeOut"
}}
className="group cursor-pointer"
onClick={() => onReadMore(blog)}
>
{/* Blog Image */}
<div className="relative aspect-video mb-4 overflow-hidden">
<div className="w-full h-full bg-[url('/images/blog-bg.jpg')] bg-cover bg-center bg-no-repeat transition-transform duration-500 group-hover:scale-105">
<div className="absolute inset-0 bg-black/40 group-hover:bg-black/30 transition-all duration-300"></div>
<div className="absolute bottom-4 left-4">
<span className="text-xs bg-white text-black px-2 py-1 font-medium uppercase tracking-wider">
{blog.category}
</span>
</div>
</div>
</div>

{/* Blog Content */}
<div className="mb-4">
<h3 className="text-xl md:text-2xl font-bold text-white mb-3 group-hover:text-gray-300 transition-colors duration-300">
{blog.title}
</h3>
<p className="text-gray-400 text-sm line-clamp-2 mb-3">
{blog.excerpt}
</p>
<div className="flex items-center gap-3 text-xs text-gray-500">
<span>{blog.date}</span>
<span>•</span>
<span>{blog.readTime}</span>
<span>•</span>
<span>{blog.author}</span>
</div>
</div>
</motion.article>
);
};

export default BlogCard;
139 changes: 139 additions & 0 deletions cpsquad/app/blogs/BlogPost.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use client";

import { motion } from "framer-motion";

const BlogPost = ({ blog, onBack }) => {
return (
<div className="font-sans bg-[#0a0a0a] text-white min-h-screen">
{/* Header */}
<div className="pt-20 pb-8 px-8">
<div className="max-w-4xl mx-auto">
<button
onClick={onBack}
className="flex items-center gap-2 text-green-400 hover:text-green-300 mb-8 transition-colors"
>
← Back to Blogs
</button>

<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<div className="mb-6">
<span className="text-sm bg-green-500 text-black px-3 py-1 rounded-full font-semibold">
{blog.category}
</span>
</div>

<h1 className="text-4xl md:text-6xl font-bold mb-6 tracking-tight">
{blog.title}
</h1>

<div className="flex items-center gap-4 text-gray-400 mb-8">
<span>By {blog.author}</span>
<span>•</span>
<span>{blog.date}</span>
<span>•</span>
<span>{blog.readTime}</span>
</div>

<div className="flex flex-wrap gap-2 mb-8">
{blog.tags.map((tag, index) => (
<span
key={index}
className="text-sm bg-gray-800 text-gray-300 px-3 py-1 rounded-full"
>
{tag}
</span>
))}
</div>
</motion.div>
</div>
</div>

{/* Content */}
<div className="px-8 pb-20">
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="blog-content"
dangerouslySetInnerHTML={{ __html: blog.content }}
/>
</div>
</div>

{/* Navigation */}
<div className="px-8 py-8 bg-[#111111]">
<div className="max-w-4xl mx-auto">
<button
onClick={onBack}
className="px-6 py-3 bg-green-500 hover:bg-green-600 text-black font-semibold rounded transition-colors"
>
← Back to All Blogs
</button>
</div>
</div>

<style jsx>{`
.blog-content {
color: #e5e5e5;
line-height: 1.8;
font-size: 1.125rem;
}
.blog-content h2 {
color: #ffffff;
font-size: 1.875rem;
font-weight: 700;
margin: 2rem 0 1rem 0;
border-bottom: 2px solid #22c55e;
padding-bottom: 0.5rem;
}
.blog-content h3 {
color: #22c55e;
font-size: 1.5rem;
font-weight: 600;
margin: 1.5rem 0 0.75rem 0;
}
.blog-content p {
margin-bottom: 1.5rem;
}
.blog-content ul, .blog-content ol {
margin: 1.5rem 0;
padding-left: 2rem;
}
.blog-content li {
margin-bottom: 0.5rem;
}
.blog-content strong {
color: #22c55e;
font-weight: 600;
}
.blog-content pre {
background: #1a1a1a;
border: 1px solid #374151;
border-radius: 0.5rem;
padding: 1rem;
margin: 1.5rem 0;
overflow-x: auto;
font-family: 'Fira Code', monospace;
}
.blog-content code {
background: #1a1a1a;
color: #22c55e;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-family: 'Fira Code', monospace;
}
.blog-content pre code {
background: transparent;
padding: 0;
}
`}</style>
</div>
);
};

export default BlogPost;
17 changes: 17 additions & 0 deletions cpsquad/app/blogs/FilterButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const FilterButton = ({ filter, currentFilter, onClick, children }) => {
const isActive = currentFilter === filter;
return (
<button
onClick={() => onClick(filter)}
className={`px-4 py-2 text-sm font-medium border transition-all duration-300 ${
isActive
? "bg-white text-black border-white"
: "bg-transparent text-gray-300 border-gray-600 hover:border-gray-400 hover:text-white"
}`}
>
{children}
</button>
);
};

export default FilterButton;
79 changes: 79 additions & 0 deletions cpsquad/app/blogs/FilterDropdown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use client";

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

const FilterDropdown = ({ currentFilter, categories, onFilterChange }) => {
const [isOpen, setIsOpen] = useState(false);

const handleFilterSelect = (filter) => {
onFilterChange(filter);
setIsOpen(false);
};

const getCurrentFilterLabel = () => {
return currentFilter.charAt(0).toUpperCase() + currentFilter.slice(1);
};

return (
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-between gap-3 px-4 py-2 border border-gray-600 text-gray-300 hover:border-gray-400 hover:text-white transition-all duration-300 min-w-[150px]"
>
<span className="text-sm font-medium">{getCurrentFilterLabel()}</span>
<svg
className={`w-4 h-4 transition-transform duration-200 ${
isOpen ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>

<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="absolute top-full left-0 right-0 mt-2 bg-[#1a1a1a] border border-gray-600 z-50"
>
{categories.map((category) => (
<button
key={category}
onClick={() => handleFilterSelect(category)}
className={`w-full px-4 py-3 text-left text-sm transition-colors duration-200 ${
currentFilter === category
? "bg-white text-black"
: "text-gray-300 hover:bg-gray-800 hover:text-white"
}`}
>
{category.charAt(0).toUpperCase() + category.slice(1)}
</button>
))}
</motion.div>
)}
</AnimatePresence>

{/* Backdrop to close dropdown */}
{isOpen && (
<div
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
/>
)}
</div>
);
};

export default FilterDropdown;
34 changes: 34 additions & 0 deletions cpsquad/app/blogs/Marquee.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { motion } from "framer-motion";

const Marquee = ({ children, speed = 50, direction = "left" }) => {
const duplicateCount = 3;

return (
<div className="overflow-hidden whitespace-nowrap flex">
<motion.div
className="flex"
animate={{
x: direction === "left" ? ["0%", "-50%"] : ["-50%", "0%"],
}}
transition={{
x: {
repeat: Infinity,
repeatType: "loop",
duration: speed,
ease: "linear",
},
}}
>
{[...Array(duplicateCount)].map((_, i) => (
<div key={i} className="flex-shrink-0 flex items-center">
{children}
</div>
))}
</motion.div>
</div>
);
};

export default Marquee;
Loading