2025-06-19 14:51:10 +00:00
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
|
|
|
|
import { Link } from "react-router-dom";
|
|
|
|
|
|
import { Calendar, Clock, User, Tag, Search } from "lucide-react";
|
|
|
|
|
|
import { useLanguage } from "../context/LanguageContext";
|
|
|
|
|
|
import {
|
|
|
|
|
|
blogService,
|
|
|
|
|
|
BlogPost,
|
|
|
|
|
|
BlogCategory,
|
2025-07-28 20:20:06 +00:00
|
|
|
|
} from "../services/blog.service";
|
2025-06-19 14:51:10 +00:00
|
|
|
|
import { format } from "date-fns";
|
|
|
|
|
|
import { tr } from "date-fns/locale";
|
2025-05-15 10:48:03 +00:00
|
|
|
|
|
2025-06-19 14:51:10 +00:00
|
|
|
|
const Blog = () => {
|
2025-05-15 10:48:03 +00:00
|
|
|
|
const { t } = useLanguage();
|
2025-06-19 14:51:10 +00:00
|
|
|
|
const [posts, setPosts] = useState<BlogPost[]>([]);
|
|
|
|
|
|
const [categories, setCategories] = useState<BlogCategory[]>([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [selectedCategory, setSelectedCategory] = useState<string>("");
|
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
|
|
const [totalPages, setTotalPages] = useState(1);
|
2025-05-15 10:48:03 +00:00
|
|
|
|
|
2025-06-19 14:51:10 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadBlogData();
|
|
|
|
|
|
}, [currentPage, selectedCategory]);
|
|
|
|
|
|
|
|
|
|
|
|
const loadBlogData = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const [postsData, categoriesData] = await Promise.all([
|
|
|
|
|
|
blogService.getPosts({
|
|
|
|
|
|
page: currentPage,
|
2025-06-20 11:11:42 +00:00
|
|
|
|
pageSize: 10,
|
2025-06-19 14:51:10 +00:00
|
|
|
|
categoryId: selectedCategory,
|
|
|
|
|
|
search: searchQuery,
|
|
|
|
|
|
}),
|
|
|
|
|
|
blogService.getCategories(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2025-06-20 11:11:42 +00:00
|
|
|
|
setPosts(postsData.items.filter(a=> a.isPublished));
|
2025-06-19 14:51:10 +00:00
|
|
|
|
setTotalPages(postsData.totalPages);
|
2025-06-20 11:11:42 +00:00
|
|
|
|
setCategories(categoriesData.filter(a=> a.isActive));
|
2025-06-19 14:51:10 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Blog verileri yüklenemedi:", error);
|
|
|
|
|
|
setPosts([]);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSearch = (e: React.FormEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setCurrentPage(1);
|
|
|
|
|
|
loadBlogData();
|
2025-05-15 10:48:03 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-06-19 14:51:10 +00:00
|
|
|
|
const handleCategoryChange = (categoryId: string) => {
|
|
|
|
|
|
setSelectedCategory(categoryId);
|
|
|
|
|
|
setCurrentPage(1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (loading && posts.length === 0) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
|
|
|
|
|
<p className="mt-4 text-gray-600">Blog yazıları yükleniyor...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-15 10:48:03 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gray-50">
|
|
|
|
|
|
{/* Hero Section */}
|
2025-06-20 14:55:55 +00:00
|
|
|
|
<div className="relative bg-blue-900 text-white py-12">
|
2025-06-19 14:51:10 +00:00
|
|
|
|
<div
|
|
|
|
|
|
className="absolute inset-0 opacity-20"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundImage:
|
2025-06-20 14:55:55 +00:00
|
|
|
|
'url("https://images.pexels.com/photos/3183164/pexels-photo-3183164.jpeg?auto=compress&cs=tinysrgb&w=1920")',
|
2025-06-19 14:51:10 +00:00
|
|
|
|
backgroundSize: "cover",
|
|
|
|
|
|
backgroundPosition: "center",
|
|
|
|
|
|
}}
|
|
|
|
|
|
></div>
|
2025-05-15 10:48:03 +00:00
|
|
|
|
<div className="container mx-auto pt-16 px-4 relative">
|
2025-06-19 14:51:10 +00:00
|
|
|
|
<h1 className="text-5xl font-bold mb-6">{t("blog.title")}</h1>
|
|
|
|
|
|
<p className="text-xl max-w-3xl">{t("blog.subtitle")}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Search and Filter Section */}
|
|
|
|
|
|
<div className="bg-white shadow-sm border-b">
|
|
|
|
|
|
<div className="container mx-auto px-4 py-6">
|
|
|
|
|
|
<div className="flex flex-col md:flex-row gap-4">
|
|
|
|
|
|
{/* Search */}
|
|
|
|
|
|
<form onSubmit={handleSearch} className="flex-1">
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
|
placeholder="Blog yazılarında ara..."
|
|
|
|
|
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Search className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Category Filter */}
|
|
|
|
|
|
<div className="flex gap-2 flex-wrap">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => handleCategoryChange("")}
|
|
|
|
|
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
|
|
|
|
|
selectedCategory === ""
|
|
|
|
|
|
? "bg-blue-600 text-white"
|
|
|
|
|
|
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
Tümü
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{categories.map((category) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={category.id}
|
|
|
|
|
|
onClick={() => handleCategoryChange(category.id)}
|
|
|
|
|
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
|
|
|
|
|
selectedCategory === category.id
|
|
|
|
|
|
? "bg-blue-600 text-white"
|
|
|
|
|
|
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
2025-06-19 21:42:16 +00:00
|
|
|
|
{ t(category.name)} ({category.postCount})
|
2025-06-19 14:51:10 +00:00
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-05-15 10:48:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Blog Posts Grid */}
|
|
|
|
|
|
<div className="container mx-auto px-4 py-16">
|
2025-06-19 14:51:10 +00:00
|
|
|
|
{!Array.isArray(posts) || posts.length === 0 ? (
|
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
|
<p className="text-gray-600 text-lg">
|
|
|
|
|
|
Henüz blog yazısı bulunmuyor.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
|
|
|
|
{posts.map((post) => (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
to={`/blog/${post.slug || post.id}`}
|
|
|
|
|
|
key={post.id}
|
|
|
|
|
|
className="block"
|
|
|
|
|
|
>
|
|
|
|
|
|
<article className="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-shadow h-full flex flex-col">
|
|
|
|
|
|
<div className="aspect-w-16 aspect-h-9 relative">
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={
|
2025-06-19 21:42:16 +00:00
|
|
|
|
post.coverImage
|
2025-06-19 14:51:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
alt={post.title}
|
|
|
|
|
|
className="object-cover w-full h-48"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="absolute top-4 right-4 bg-blue-600 text-white px-3 py-1 rounded-full text-sm">
|
2025-06-19 21:42:16 +00:00
|
|
|
|
{t(post.category.name)}
|
2025-06-19 14:51:10 +00:00
|
|
|
|
</div>
|
2025-05-15 10:48:03 +00:00
|
|
|
|
</div>
|
2025-06-19 14:51:10 +00:00
|
|
|
|
<div className="p-6 flex-1 flex flex-col">
|
|
|
|
|
|
<h2 className="text-xl font-bold text-gray-900 mb-3 hover:text-blue-600 transition-colors">
|
2025-06-19 21:42:16 +00:00
|
|
|
|
{t(post.title)}
|
2025-06-19 14:51:10 +00:00
|
|
|
|
</h2>
|
2025-06-19 21:42:16 +00:00
|
|
|
|
<p className="text-gray-600 mb-4 flex-1">{t(post.summary)}</p>
|
2025-06-19 14:51:10 +00:00
|
|
|
|
|
|
|
|
|
|
{/* Tags */}
|
|
|
|
|
|
{post.tags.length > 0 && (
|
|
|
|
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
|
|
|
|
|
{post.tags.slice(0, 3).map((tag, index) => (
|
|
|
|
|
|
<span
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
className="inline-flex items-center text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Tag className="w-3 h-3 mr-1" />
|
|
|
|
|
|
{tag}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center text-sm text-gray-500 space-x-4">
|
2025-05-15 10:48:03 +00:00
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
|
<User size={16} className="mr-1" />
|
2025-06-19 14:51:10 +00:00
|
|
|
|
{post.author.name}
|
2025-05-15 10:48:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
|
<Calendar size={16} className="mr-1" />
|
2025-06-19 14:51:10 +00:00
|
|
|
|
{format(
|
|
|
|
|
|
new Date(post.publishedAt || post.createdAt),
|
|
|
|
|
|
"dd MMM yyyy",
|
|
|
|
|
|
{ locale: tr }
|
|
|
|
|
|
)}
|
2025-05-15 10:48:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
|
<Clock size={16} className="mr-1" />
|
2025-06-19 21:42:16 +00:00
|
|
|
|
{post.readTime}
|
2025-05-15 10:48:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
))}
|
2025-06-19 14:51:10 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Pagination */}
|
|
|
|
|
|
{totalPages > 1 && (
|
|
|
|
|
|
<div className="mt-12 flex justify-center">
|
|
|
|
|
|
<nav className="flex gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
|
|
|
|
|
disabled={currentPage === 1}
|
|
|
|
|
|
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
|
>
|
|
|
|
|
|
Önceki
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
{[...Array(totalPages)].map((_, i) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={i + 1}
|
|
|
|
|
|
onClick={() => setCurrentPage(i + 1)}
|
|
|
|
|
|
className={`px-4 py-2 rounded-lg ${
|
|
|
|
|
|
currentPage === i + 1
|
|
|
|
|
|
? "bg-blue-600 text-white"
|
|
|
|
|
|
: "border border-gray-300 hover:bg-gray-50"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{i + 1}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setCurrentPage(Math.min(totalPages, currentPage + 1))
|
|
|
|
|
|
}
|
|
|
|
|
|
disabled={currentPage === totalPages}
|
|
|
|
|
|
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
|
>
|
|
|
|
|
|
Sonraki
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-05-15 10:48:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Newsletter Section */}
|
|
|
|
|
|
<div className="bg-white py-16">
|
|
|
|
|
|
<div className="container mx-auto px-4 text-center">
|
2025-06-19 14:51:10 +00:00
|
|
|
|
<h2 className="text-3xl font-bold text-gray-900 mb-4">
|
|
|
|
|
|
{t("blog.subscribe")}
|
|
|
|
|
|
</h2>
|
2025-05-15 10:48:03 +00:00
|
|
|
|
<p className="text-gray-600 mb-8 max-w-2xl mx-auto">
|
2025-06-19 14:51:10 +00:00
|
|
|
|
{t("blog.subscribe.desc")}
|
2025-05-15 10:48:03 +00:00
|
|
|
|
</p>
|
|
|
|
|
|
<div className="max-w-md mx-auto flex gap-4">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="email"
|
2025-06-19 14:51:10 +00:00
|
|
|
|
placeholder={t("common.email")}
|
2025-05-15 10:48:03 +00:00
|
|
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors">
|
2025-06-19 14:51:10 +00:00
|
|
|
|
{t("common.subscribe")}
|
2025-05-15 10:48:03 +00:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default Blog;
|