first commit
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import type { Locale } from "@/i18n-config";
|
||||
import { blogPosts, BlogPost } from "@/data/blog";
|
||||
|
||||
const expo = [0.16, 1, 0.3, 1] as [number, number, number, number];
|
||||
|
||||
export default function BlogClient({
|
||||
lang,
|
||||
dict,
|
||||
initialPosts,
|
||||
}: {
|
||||
lang: Locale;
|
||||
dict: any;
|
||||
initialPosts?: BlogPost[];
|
||||
}) {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const postsList = initialPosts && initialPosts.length > 0 ? initialPosts : blogPosts;
|
||||
|
||||
const categories = Array.from(
|
||||
new Set(
|
||||
postsList.map((post) => {
|
||||
const content = post[lang] || post.en;
|
||||
return (content.category as string).split(" · ")[0]; // Main category e.g. YZ, Web3, Web
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const filteredPosts = postsList.filter((post) => {
|
||||
const content = post[lang] || post.en;
|
||||
const matchesCategory =
|
||||
!selectedCategory || (content.category as string).startsWith(selectedCategory);
|
||||
const matchesSearch =
|
||||
(content.title as string).toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(content.excerpt as string).toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(content.tags as string[]).some((t: string) => t.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-[#F4F0E8] text-[#0A0A0A] font-body min-h-screen">
|
||||
<Header lang={lang} dict={dict.nav} />
|
||||
|
||||
<main className="pt-32 pb-24 px-6 max-w-7xl mx-auto">
|
||||
{/* Header Block */}
|
||||
<div className="border-b border-[#C8C2B8] pb-12 mb-12">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-3 h-3 bg-[#FFE600]" />
|
||||
<span className="font-mono text-[10px] tracking-[0.3em] uppercase text-[#A0998E]">
|
||||
{dict.blog.badge}
|
||||
</span>
|
||||
</div>
|
||||
<motion.h1
|
||||
className="font-display font-black text-5xl sm:text-6xl lg:text-8xl uppercase leading-[0.85] tracking-tight"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: expo }}
|
||||
>
|
||||
{dict.blog.title}
|
||||
</motion.h1>
|
||||
<p className="text-[#6A6460] mt-6 text-lg max-w-2xl font-light">
|
||||
{dict.blog.desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Controls & Search */}
|
||||
<div className="flex flex-col md:flex-row gap-6 justify-between items-stretch md:items-center mb-12">
|
||||
{/* Categories */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedCategory(null)}
|
||||
className={`font-mono text-[10px] tracking-[0.15em] uppercase px-4 py-2 border transition-all cursor-pointer ${
|
||||
selectedCategory === null
|
||||
? "bg-[#0A0A0A] text-[#F4F0E8] border-[#0A0A0A]"
|
||||
: "border-[#C8C2B8] text-[#6A6460] hover:border-[#0A0A0A] hover:text-[#0A0A0A]"
|
||||
}`}
|
||||
>
|
||||
{lang === "tr" ? "HEPSİ" : "ALL"}
|
||||
</button>
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
className={`font-mono text-[10px] tracking-[0.15em] uppercase px-4 py-2 border transition-all cursor-pointer ${
|
||||
selectedCategory === cat
|
||||
? "bg-[#0A0A0A] text-[#F4F0E8] border-[#0A0A0A]"
|
||||
: "border-[#C8C2B8] text-[#6A6460] hover:border-[#0A0A0A] hover:text-[#0A0A0A]"
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search Box */}
|
||||
<div className="relative flex-grow md:max-w-xs">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={lang === "tr" ? "Arama yap..." : "Search..."}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full font-mono text-[11px] bg-[#EDE8E0] border border-[#C8C2B8] hover:border-[#0A0A0A] focus:border-[#0A0A0A] outline-none px-4 py-3 placeholder-[#A0998E] text-[#0A0A0A] transition-colors"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-[10px] font-mono text-[#A0998E] hover:text-[#0A0A0A] cursor-pointer"
|
||||
>
|
||||
[X]
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid List */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{filteredPosts.map((post, idx) => {
|
||||
const content = post[lang] || post.en;
|
||||
const accent = ["#FFE600", "#FF4500", "#00CC77"][idx % 3] || "#FFE600";
|
||||
return (
|
||||
<motion.div
|
||||
key={post.slug}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.4, ease: expo }}
|
||||
className="group flex flex-col bg-[#F4F0E8] border border-[#C8C2B8] hover:border-[#0A0A0A] p-8 relative overflow-hidden transition-all duration-300 h-full"
|
||||
whileHover={{ y: -6, backgroundColor: "#EDE8E0", boxShadow: "4px 4px 0px 0px #0A0A0A" }}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<span
|
||||
className="font-mono text-[9px] tracking-[0.2em] uppercase px-2.5 py-1 border border-[#C8C2B8] text-[#6A6460]"
|
||||
style={{ borderLeftColor: accent, borderLeftWidth: 3 }}
|
||||
>
|
||||
{content.category}
|
||||
</span>
|
||||
<span className="font-mono text-[9px] text-[#A0998E]">
|
||||
{post.date}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="font-display font-black text-xl uppercase text-[#0A0A0A] mb-4 leading-tight">
|
||||
{content.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-[#6A6460] text-[13px] leading-relaxed mb-8 flex-grow">
|
||||
{content.excerpt}
|
||||
</p>
|
||||
|
||||
<div className="pt-6 border-t border-[#C8C2B8] flex items-center justify-between mt-auto">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-display font-black text-[11px] uppercase text-[#0A0A0A]">{post.author}</span>
|
||||
<span className="font-mono text-[9px] text-[#A0998E] uppercase tracking-wider">{post.authorRole}</span>
|
||||
</div>
|
||||
<Link
|
||||
href={`/${lang}/blog/${post.slug}`}
|
||||
className="w-10 h-10 border border-[#C8C2B8] group-hover:border-[#0A0A0A] group-hover:bg-[#0A0A0A] group-hover:text-[#FFE600] text-[#0A0A0A] flex items-center justify-center transition-all"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Category Accent Stripe */}
|
||||
<div className="absolute top-0 left-0 right-0 h-1 transition-colors" style={{ backgroundColor: accent }} />
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
{filteredPosts.length === 0 && (
|
||||
<div className="col-span-full py-16 text-center border border-dashed border-[#C8C2B8]">
|
||||
<p className="font-mono text-xs uppercase text-[#A0998E]">
|
||||
{lang === "tr" ? "Sonuç bulunamadı." : "No entries match your parameters."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer lang={lang} dict={dict} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user