sozsoft-platform/ui/src/views/public/Blog.tsx
2026-06-04 15:08:52 +03:00

280 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { FaCalendarAlt, FaUser, FaTag, FaSearch } from 'react-icons/fa'
import { showDbDateAsIs } from '@/utils/dateUtils'
import { BlogCategory, BlogPost } from '@/proxy/blog/blog'
import { blogService } from '@/services/blog.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Helmet } from 'react-helmet'
import { Loading } from '@/components/shared'
import { APP_NAME } from '@/constants/app.constant'
import { Button } from '@/components/ui'
const Blog = () => {
const { translate } = useLocalization()
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)
useEffect(() => {
loadBlogData()
}, [currentPage, selectedCategory])
const loadBlogData = async () => {
try {
setLoading(true)
const postsData = await blogService.getPosts({
page: currentPage,
pageSize: 10,
categoryId: selectedCategory,
search: searchQuery,
})
if (
postsData.posts &&
postsData.posts.items &&
postsData.posts.totalCount &&
postsData.categories
) {
setPosts(postsData.posts.items.filter((a) => a.isPublished))
setTotalPages(postsData.posts.totalCount / 10)
setCategories(postsData.categories.filter((a) => a.isActive))
}
} catch (error) {
console.error('Blog verileri yüklenemedi:', error)
setPosts([])
} finally {
setLoading(false)
}
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
setCurrentPage(1)
loadBlogData()
}
const handleCategoryChange = (categoryId: string) => {
setSelectedCategory(categoryId)
setCurrentPage(1)
}
if (loading && posts.length === 0) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="text-center">
<Loading loading={loading} />
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.BlogManagement')}
defaultTitle={APP_NAME}
></Helmet>
{/* Hero Section */}
<div className="relative bg-blue-900 text-white py-12 dark:bg-gray-950">
<div
className="absolute inset-0 opacity-20 dark:opacity-35"
style={{
backgroundImage:
'url("https://images.pexels.com/photos/3183164/pexels-photo-3183164.jpeg?auto=compress&cs=tinysrgb&w=1920")',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
></div>
<div className="absolute inset-0 bg-blue-950/25 dark:bg-gray-950/45"></div>
<div className="container mx-auto pt-20 relative">
<h1 className="text-5xl font-bold ml-4 mt-3 mb-2 text-white">
{translate('::App.BlogManagement')}
</h1>
<p className="text-xl max-w-3xl ml-4">{translate('::Public.blog.subtitle')}</p>
</div>
</div>
{/* Blog Posts Grid */}
<div className="container mx-auto px-4 py-6">
{/* Search and Filter Section */}
<div className="shadow-sm border-b border-gray-200 dark:border-gray-800">
<div className="container mx-auto pb-4">
<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 bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400"
/>
<FaSearch 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 bg-blue-100 text-blue-800 text-sm font-medium rounded-lg transition-colors ${
selectedCategory === ''
? 'bg-blue-600 text-black'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700'
}`}
>
{translate('::App.Reports.Search')}
</Button>
{categories.map((category) => (
<Button
key={category.id}
onClick={() => handleCategoryChange(category.id)}
className={`px-4 py-2 bg-blue-100 text-blue-800 text-sm font-medium rounded-lg transition-colors ${
selectedCategory === category.id
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700'
}`}
>
{translate('::Public.' + category.name)} ({category.postCount})
</Button>
))}
</div>
</div>
</div>
</div>
{!Array.isArray(posts) || posts.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-600 text-lg dark:text-gray-300">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 dark:bg-gray-900 dark:shadow-gray-950/40">
<div className="aspect-w-16 aspect-h-9 relative">
<img
src={post.coverImage}
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 dark:bg-blue-900">
{translate('::Public.' + post.category.name)}
</div>
</div>
<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 dark:text-gray-100 dark:hover:text-blue-400">
{translate('::Public.' + post.title)}
</h2>
<p className="text-gray-600 mb-4 flex-1 dark:text-gray-300">
{translate('::Public.' + post.summary)}
</p>
{/* 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 dark:bg-gray-800 dark:text-gray-300"
>
<FaTag className="w-3 h-3 mr-1" />
{tag}
</span>
))}
</div>
)}
<div className="flex items-center text-sm text-gray-500 space-x-4 dark:text-gray-400">
<div className="flex items-center">
<FaUser size={16} className="mr-1" />
{post.author}
</div>
<div className="flex items-center">
<FaCalendarAlt size={16} className="mr-1" />
{showDbDateAsIs(post.publishedAt || post.creationTime)}
</div>
</div>
</div>
</article>
</Link>
))}
</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 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800"
>
Ö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 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800'
}`}
>
{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 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800"
>
Sonraki
</Button>
</nav>
</div>
)}
</div>
{/* Newsletter Section */}
<div className="bg-white py-16 dark:bg-gray-900">
<div className="container mx-auto px-4 text-center">
<h2 className="text-3xl font-bold text-gray-900 mb-4 dark:text-gray-100">
{translate('::Public.blog.subscribe')}
</h2>
<p className="text-gray-600 mb-8 max-w-2xl mx-auto dark:text-gray-300">
{translate('::Public.blog.subscribe.desc')}
</p>
<div className="max-w-md mx-auto flex gap-4">
<input
type="email"
placeholder={translate('::Abp.Account.EmailAddress')}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400"
/>
<Button className="bg-blue-600 text-black px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors dark:bg-blue-500 dark:hover:bg-blue-600">
{translate('::Public.common.subscribe')}
</Button>
</div>
</div>
</div>
</div>
)
}
export default Blog