Blog sistemindeki güncellemeler

This commit is contained in:
Sedat Öztürk 2025-06-20 00:42:16 +03:00
parent 2baff7538f
commit 2a1f06b2f4
15 changed files with 1072 additions and 382 deletions

View file

@ -46,6 +46,7 @@ namespace Kurs.Platform.Blog
public string Slug { get; set; } public string Slug { get; set; }
public string Content { get; set; } public string Content { get; set; }
public string Summary { get; set; } public string Summary { get; set; }
public string ReadTime { get; set; }
public string CoverImage { get; set; } public string CoverImage { get; set; }
public Guid CategoryId { get; set; } public Guid CategoryId { get; set; }
public List<string> Tags { get; set; } public List<string> Tags { get; set; }
@ -62,6 +63,7 @@ namespace Kurs.Platform.Blog
public string Title { get; set; } public string Title { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
public string Summary { get; set; } public string Summary { get; set; }
public string ReadTime { get; set; }
public string CoverImage { get; set; } public string CoverImage { get; set; }
public BlogCategoryDto Category { get; set; } public BlogCategoryDto Category { get; set; }

View file

@ -104,7 +104,7 @@ namespace Kurs.Platform.Blog
dto.Author = new AuthorDto dto.Author = new AuthorDto
{ {
Id = post.AuthorId, Id = post.AuthorId,
Name = post.CreatorId.HasValue ? "User" : "Unknown" // You should get actual user name Name = post.CreatorId.HasValue ? "User" : "Unknown"
}; };
postDtos.Add(dto); postDtos.Add(dto);
@ -164,6 +164,8 @@ namespace Kurs.Platform.Blog
input.Slug, input.Slug,
input.Content, input.Content,
input.Summary, input.Summary,
input.ReadTime,
input.CoverImage,
input.CategoryId, input.CategoryId,
_currentUser.Id.Value, _currentUser.Id.Value,
CurrentTenant.Id CurrentTenant.Id
@ -202,7 +204,7 @@ namespace Kurs.Platform.Blog
var post = await _postRepository.GetAsync(id); var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission // Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("Blog.Posts.Update")) if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Update"))
{ {
throw new Volo.Abp.Authorization.AbpAuthorizationException(); throw new Volo.Abp.Authorization.AbpAuthorizationException();
} }
@ -249,7 +251,7 @@ namespace Kurs.Platform.Blog
var post = await _postRepository.GetAsync(id); var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission // Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("Blog.Posts.Delete")) if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Delete"))
{ {
throw new Volo.Abp.Authorization.AbpAuthorizationException(); throw new Volo.Abp.Authorization.AbpAuthorizationException();
} }
@ -267,7 +269,7 @@ namespace Kurs.Platform.Blog
var post = await _postRepository.GetAsync(id); var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission // Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("Blog.Posts.Publish")) if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Publish"))
{ {
throw new Volo.Abp.Authorization.AbpAuthorizationException(); throw new Volo.Abp.Authorization.AbpAuthorizationException();
} }
@ -283,7 +285,7 @@ namespace Kurs.Platform.Blog
var post = await _postRepository.GetAsync(id); var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission // Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("Blog.Posts.Publish")) if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Publish"))
{ {
throw new Volo.Abp.Authorization.AbpAuthorizationException(); throw new Volo.Abp.Authorization.AbpAuthorizationException();
} }
@ -357,7 +359,7 @@ namespace Kurs.Platform.Blog
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category); return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
} }
[Authorize("Blog.Categories.Create")] [Authorize("App.Blog.Create")]
public async Task<BlogCategoryDto> CreateCategoryAsync(CreateUpdateBlogCategoryDto input) public async Task<BlogCategoryDto> CreateCategoryAsync(CreateUpdateBlogCategoryDto input)
{ {
var category = new BlogCategory( var category = new BlogCategory(
@ -377,7 +379,7 @@ namespace Kurs.Platform.Blog
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category); return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
} }
[Authorize("Blog.Categories.Update")] [Authorize("App.Blog.Update")]
public async Task<BlogCategoryDto> UpdateCategoryAsync(Guid id, CreateUpdateBlogCategoryDto input) public async Task<BlogCategoryDto> UpdateCategoryAsync(Guid id, CreateUpdateBlogCategoryDto input)
{ {
var category = await _categoryRepository.GetAsync(id); var category = await _categoryRepository.GetAsync(id);
@ -394,7 +396,7 @@ namespace Kurs.Platform.Blog
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category); return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
} }
[Authorize("Blog.Categories.Delete")] [Authorize("App.Blog.Delete")]
public async Task DeleteCategoryAsync(Guid id) public async Task DeleteCategoryAsync(Guid id)
{ {
// Check if category has posts // Check if category has posts

View file

@ -6,6 +6,7 @@ using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kurs.Languages.Entities; using Kurs.Languages.Entities;
using Kurs.Notifications.Entities; using Kurs.Notifications.Entities;
using Kurs.Platform.Blog;
using Kurs.Platform.Charts.Dto; using Kurs.Platform.Charts.Dto;
using Kurs.Platform.Entities; using Kurs.Platform.Entities;
using Kurs.Platform.Enums; using Kurs.Platform.Enums;
@ -48,6 +49,8 @@ public class PlatformDataSeeder : IDataSeedContributor, ITransientDependency
private readonly IRepository<SkillLevel, Guid> _skillLevelRepository; private readonly IRepository<SkillLevel, Guid> _skillLevelRepository;
private readonly IRepository<ContactTag, Guid> _contactTagRepository; private readonly IRepository<ContactTag, Guid> _contactTagRepository;
private readonly IRepository<ContactTitle, Guid> _contactTitleRepository; private readonly IRepository<ContactTitle, Guid> _contactTitleRepository;
private readonly IRepository<BlogCategory, Guid> _blogCategoryRepository;
private readonly IRepository<BlogPost, Guid> _blogPostsRepository;
public PlatformDataSeeder( public PlatformDataSeeder(
IRepository<Language, Guid> languages, IRepository<Language, Guid> languages,
@ -73,7 +76,9 @@ public class PlatformDataSeeder : IDataSeedContributor, ITransientDependency
IRepository<Skill, Guid> skillRepository, IRepository<Skill, Guid> skillRepository,
IRepository<SkillLevel, Guid> skillLevelRepository, IRepository<SkillLevel, Guid> skillLevelRepository,
IRepository<ContactTag, Guid> contactTagRepository, IRepository<ContactTag, Guid> contactTagRepository,
IRepository<ContactTitle, Guid> contactTitleRepository IRepository<ContactTitle, Guid> contactTitleRepository,
IRepository<BlogCategory, Guid> blogCategoryRepository,
IRepository<BlogPost, Guid> blogPostsRepository
) )
{ {
_languages = languages; _languages = languages;
@ -100,6 +105,8 @@ public class PlatformDataSeeder : IDataSeedContributor, ITransientDependency
_skillLevelRepository = skillLevelRepository; _skillLevelRepository = skillLevelRepository;
_contactTagRepository = contactTagRepository; _contactTagRepository = contactTagRepository;
_contactTitleRepository = contactTitleRepository; _contactTitleRepository = contactTitleRepository;
_blogCategoryRepository = blogCategoryRepository;
_blogPostsRepository = blogPostsRepository;
} }
private static IConfigurationRoot BuildConfiguration() private static IConfigurationRoot BuildConfiguration()
@ -543,5 +550,46 @@ public class PlatformDataSeeder : IDataSeedContributor, ITransientDependency
}); });
} }
} }
foreach (var item in items.BlogCategories)
{
var exists = await _blogCategoryRepository.AnyAsync(x => x.Name == item.Name);
if (!exists)
{
var newCategory = new BlogCategory(
item.Id,
item.Name,
item.Slug,
item.Description
)
{
DisplayOrder = item.DisplayOrder,
PostCount = 1
};
await _blogCategoryRepository.InsertAsync(newCategory);
}
}
foreach (var item in items.BlogPosts)
{
var exists = await _blogPostsRepository.AnyAsync(x => x.Title == item.Title);
if (!exists)
{
await _blogPostsRepository.InsertAsync(new BlogPost(
item.Id,
item.Title,
item.Slug,
item.Content,
item.Summary,
item.ReadTime,
item.CoverImage,
item.CategoryId,
item.AuthorId
));
}
}
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Kurs.Languages.Entities; using Kurs.Languages.Entities;
using Kurs.Platform.Charts.Dto; using Kurs.Platform.Charts.Dto;
using Kurs.Platform.Entities; using Kurs.Platform.Entities;
@ -32,6 +33,8 @@ public class SeederDto
public List<SkillLevelSeedDto> SkillLevels { get; set; } public List<SkillLevelSeedDto> SkillLevels { get; set; }
public List<ContactTagSeedDto> ContactTags { get; set; } public List<ContactTagSeedDto> ContactTags { get; set; }
public List<ContactTitleSeedDto> ContactTitles { get; set; } public List<ContactTitleSeedDto> ContactTitles { get; set; }
public List<BlogCategorySeedDto> BlogCategories { get; set; }
public List<BlogPostSeedDto> BlogPosts { get; set; }
} }
public class ChartsSeedDto public class ChartsSeedDto
@ -194,5 +197,27 @@ public class ContactTagSeedDto
public class ContactTitleSeedDto public class ContactTitleSeedDto
{ {
public string Title { get; set; } public string Title { get; set; }
public string Abbreviation { get; set; } public string Abbreviation { get; set; }
}
public class BlogCategorySeedDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Slug { get; set; }
public string Description { get; set; }
public int DisplayOrder { get; set; }
}
public class BlogPostSeedDto
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Slug { get; set; }
public string Content { get; set; }
public string ReadTime { get; set; }
public string Summary { get; set; }
public string CoverImage { get; set; }
public Guid CategoryId { get; set; }
public Guid AuthorId { get; set; }
} }

View file

@ -14,6 +14,7 @@ namespace Kurs.Platform.Blog
public string Content { get; set; } public string Content { get; set; }
public string Summary { get; set; } public string Summary { get; set; }
public string CoverImage { get; set; } public string CoverImage { get; set; }
public string ReadTime { get; set; }
public Guid CategoryId { get; set; } public Guid CategoryId { get; set; }
public virtual BlogCategory Category { get; set; } public virtual BlogCategory Category { get; set; }
@ -44,6 +45,8 @@ namespace Kurs.Platform.Blog
string slug, string slug,
string content, string content,
string summary, string summary,
string readTime,
string coverImage,
Guid categoryId, Guid categoryId,
Guid authorId, Guid authorId,
Guid? tenantId = null) : base(id) Guid? tenantId = null) : base(id)
@ -52,6 +55,8 @@ namespace Kurs.Platform.Blog
Slug = slug; Slug = slug;
Content = content; Content = content;
Summary = summary; Summary = summary;
ReadTime = readTime;
CoverImage = coverImage;
CategoryId = categoryId; CategoryId = categoryId;
AuthorId = authorId; AuthorId = authorId;
TenantId = tenantId; TenantId = tenantId;

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations namespace Kurs.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20250619131606_AddBlogForumEntities")] [Migration("20250619205823_AddBlogForumEntities")]
partial class AddBlogForumEntities partial class AddBlogForumEntities
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -894,6 +894,9 @@ namespace Kurs.Platform.Migrations
b.Property<DateTime?>("PublishedAt") b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
b.Property<string>("ReadTime")
.HasColumnType("nvarchar(max)");
b.Property<string>("Slug") b.Property<string>("Slug")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(256)

View file

@ -79,6 +79,7 @@ namespace Kurs.Platform.Migrations
Content = table.Column<string>(type: "nvarchar(max)", nullable: false), Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
Summary = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false), Summary = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
CoverImage = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true), CoverImage = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
ReadTime = table.Column<string>(type: "nvarchar(max)", nullable: true),
CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false), CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
AuthorId = table.Column<Guid>(type: "uniqueidentifier", nullable: false), AuthorId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ViewCount = table.Column<int>(type: "int", nullable: false), ViewCount = table.Column<int>(type: "int", nullable: false),

View file

@ -891,6 +891,9 @@ namespace Kurs.Platform.Migrations
b.Property<DateTime?>("PublishedAt") b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
b.Property<string>("ReadTime")
.HasColumnType("nvarchar(max)");
b.Property<string>("Slug") b.Property<string>("Slug")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(256)

View file

@ -130,7 +130,7 @@ const Blog = () => {
: "bg-gray-200 text-gray-700 hover:bg-gray-300" : "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`} }`}
> >
{category.name} ({category.postCount}) { t(category.name)} ({category.postCount})
</button> </button>
))} ))}
</div> </div>
@ -158,20 +158,20 @@ const Blog = () => {
<div className="aspect-w-16 aspect-h-9 relative"> <div className="aspect-w-16 aspect-h-9 relative">
<img <img
src={ src={
post.coverImage || "https://via.placeholder.com/400x225" post.coverImage
} }
alt={post.title} alt={post.title}
className="object-cover w-full h-48" 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"> <div className="absolute top-4 right-4 bg-blue-600 text-white px-3 py-1 rounded-full text-sm">
{post.category.name} {t(post.category.name)}
</div> </div>
</div> </div>
<div className="p-6 flex-1 flex flex-col"> <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"> <h2 className="text-xl font-bold text-gray-900 mb-3 hover:text-blue-600 transition-colors">
{post.title} {t(post.title)}
</h2> </h2>
<p className="text-gray-600 mb-4 flex-1">{post.summary}</p> <p className="text-gray-600 mb-4 flex-1">{t(post.summary)}</p>
{/* Tags */} {/* Tags */}
{post.tags.length > 0 && ( {post.tags.length > 0 && (
@ -203,11 +203,7 @@ const Blog = () => {
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<Clock size={16} className="mr-1" /> <Clock size={16} className="mr-1" />
{typeof post.content === "string" && {post.readTime}
post.content.length > 0
? Math.ceil(post.content.length / 1000)
: "-"}{" "}
dk
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,66 +1,110 @@
import React from 'react'; import React, { useState, useEffect } from "react";
import { Link, useParams } from 'react-router-dom'; // Link ve useParams'ı import et import { Link, useParams } from "react-router-dom";
import { useLanguage } from '../context/LanguageContext'; // useLanguage hook'unu import et import { useLanguage } from "../context/LanguageContext";
import { blogContent, BlogPostContent } from '../locales/blogContent'; // blogContent ve BlogPostContent interface'ini import et import { BlogPost, blogService } from "../services/api/blog.service";
import { format } from "date-fns";
import { tr } from "date-fns/locale";
interface PostData {
image?: string;
author?: {
id: string;
name: string;
avatar?: string;
};
}
const BlogDetail: React.FC = () => { const BlogDetail: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const { t, language } = useLanguage(); // useLanguage hook'unu kullan ve dil bilgisini al const { t } = useLanguage();
const [blogPost, setBlogPost] = useState<BlogPost | null>(null);
const [postData, setPostData] = useState<PostData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Basit slug oluşturma fonksiyonu (Blog.tsx'teki ile aynı olmalı) useEffect(() => {
const createSlug = (title: string) => { const fetchBlogPost = async () => {
return title setLoading(true);
.toLowerCase() setError(null);
.replace(/ /g, '-') try {
.replace(/[^\w-]+/g, ''); if (id) {
}; const response = await blogService.getPostBySlug(id);
setBlogPost(response);
setPostData({
image: response.coverImage,
author: response.author,
});
} else {
setError("Blog post ID is missing.");
}
} catch (error: any) {
setError(error.message || "Failed to fetch blog post.");
} finally {
setLoading(false);
}
};
// URL'deki slug'a göre blog yazısını bul fetchBlogPost();
// blogContent objesinden slug'a karşılık gelen blog yazısını ve mevcut dile göre içeriğini al }, [id]);
const postData = blogContent[id || '']; // id undefined olabilir, boş string ile kontrol et
const blogPost = postData ? postData[language as 'tr' | 'en'] : undefined; // Mevcut dile göre içeriği al
if (!blogPost) { if (loading) {
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="min-h-screen bg-gray-50 flex items-center justify-center">
<h1 className="text-2xl font-bold text-gray-900">{t('blog.notFound')}</h1> {/* Çeviri kullan */} <h1 className="text-2xl font-bold text-gray-900">Loading...</h1>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<h1 className="text-2xl font-bold text-gray-900">Error: {error}</h1>
</div>
);
}
if (!blogPost || !postData) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<h1 className="text-2xl font-bold text-gray-900">
{t("blog.notFound")}
</h1>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-gray-50 pt-32 pb-16"> {/* py-16 yerine pt-32 pb-16 kullanıldı */} <div className="min-h-screen bg-gray-50 pt-32 pb-16">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<Link to="/blog" className="text-blue-600 hover:underline mb-4 inline-block"> <Link
&larr; {t('blog.backToBlog')} {/* Geri dönüş butonu */} to="/blog"
className="text-blue-600 hover:underline mb-4 inline-block"
>
&larr; {t("blog.backToBlog")}
</Link> </Link>
{/* Blog yazısı görseli */}
{postData.image && ( {postData.image && (
<img <img
src={postData.image} // Görsel bilgisi blogContent'ten alınıyor src={postData.image}
alt={t(blogPost.title)} // Alt metni çevir alt={t(blogPost.title)}
className="w-full h-96 object-cover rounded-lg mb-8" className="w-full h-96 object-cover rounded-lg mb-8"
/> />
)} )}
<h1 className="text-4xl font-bold text-gray-900 mb-6">{t(blogPost.title)}</h1> {/* Çeviri kullan */} <h1 className="text-4xl font-bold text-gray-900 mb-6">
{/* Yazar, tarih, okuma süresi gibi bilgiler eklenebilir */} {t(blogPost.title)}
</h1>
<div className="flex items-center text-sm text-gray-500 space-x-4 mb-8"> <div className="flex items-center text-sm text-gray-500 space-x-4 mb-8">
<div className="flex items-center"> <div className="flex items-center">
{/* <User size={16} className="mr-1" /> */} {/* İkonlar eklenebilir */} <span>{postData.author?.name}</span>
<span>{postData.author}</span> {/* Yazar bilgisi blogContent'ten alınıyor */}
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
{/* <Calendar size={16} className="mr-1" /> */} {blogPost.publishedAt &&
{t(blogPost.date)} {/* Çeviri kullan */} format(new Date(blogPost.publishedAt), "dd MMM yyyy", {
</div> locale: tr,
<div className="flex items-center"> })}
{/* <Clock size={16} className="mr-1" /> */}
{blogPost.readTime}
</div> </div>
</div> </div>
<div className="prose max-w-none text-gray-800"> {/* Tailwind Typography eklentisi kuruluysa kullanılabilir */} <div className="prose max-w-none text-gray-800">
<p>{t(blogPost.content)}</p> {/* Tam içeriği çevirerek göster */} <p>{blogPost.content}</p>
{/* Daha uzun içerik burada paragraflar halinde yer alabilir */}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
import { apiClient } from './config'; import { apiClient } from "./config";
export interface BlogPost { export interface BlogPost {
id: string; id: string;
@ -6,6 +6,7 @@ export interface BlogPost {
slug: string; slug: string;
content?: string; content?: string;
summary: string; summary: string;
readTime: string;
coverImage?: string; coverImage?: string;
author: { author: {
id: string; id: string;
@ -22,6 +23,7 @@ export interface BlogPost {
likeCount: number; likeCount: number;
commentCount: number; commentCount: number;
isPublished: boolean; isPublished: boolean;
isLiked?: boolean;
publishedAt?: string; publishedAt?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@ -54,6 +56,7 @@ export interface BlogComment {
export interface CreateBlogPostRequest { export interface CreateBlogPostRequest {
title: string; title: string;
slug: string;
content: string; content: string;
summary: string; summary: string;
categoryId: string; categoryId: string;
@ -75,7 +78,7 @@ export interface BlogListParams {
tag?: string; tag?: string;
search?: string; search?: string;
authorId?: string; authorId?: string;
sortBy?: 'latest' | 'popular' | 'trending'; sortBy?: "latest" | "popular" | "trending";
} }
export interface PaginatedResponse<T> { export interface PaginatedResponse<T> {
@ -87,23 +90,45 @@ export interface PaginatedResponse<T> {
} }
class BlogService { class BlogService {
async getPosts(params: BlogListParams = {}): Promise<PaginatedResponse<BlogPost>> { async getPosts(
const response = await apiClient.get<PaginatedResponse<BlogPost>>('/api/app/blog/posts', { params }); params: BlogListParams = {}
): Promise<PaginatedResponse<BlogPost>> {
const response = await apiClient.get<PaginatedResponse<BlogPost>>(
"/api/app/blog/posts",
{ params }
);
return response.data; return response.data;
} }
async getPost(idOrSlug: string): Promise<BlogPost> { async getPostById(id: string): Promise<BlogPost> {
const response = await apiClient.get<BlogPost>(`/api/app/blog/posts/${idOrSlug}`); const response = await apiClient.get<BlogPost>(`/api/app/blog/posts/${id}`);
return response.data;
}
async getPostBySlug(slug: string): Promise<BlogPost> {
const response = await apiClient.get<BlogPost>(
`/api/app/blog/post-by-slug`,
{ params: { slug } }
);
return response.data; return response.data;
} }
async createPost(data: CreateBlogPostRequest): Promise<BlogPost> { async createPost(data: CreateBlogPostRequest): Promise<BlogPost> {
const response = await apiClient.post<BlogPost>('/api/app/blog/posts', data); const response = await apiClient.post<BlogPost>(
"/api/app/blog/posts",
data
);
return response.data; return response.data;
} }
async updatePost(id: string, data: Partial<CreateBlogPostRequest>): Promise<BlogPost> { async updatePost(
const response = await apiClient.put<BlogPost>(`/api/app/blog/posts/${id}`, data); id: string,
data: Partial<CreateBlogPostRequest>
): Promise<BlogPost> {
const response = await apiClient.put<BlogPost>(
`/api/app/blog/posts/${id}`,
data
);
return response.data; return response.data;
} }
@ -111,18 +136,59 @@ class BlogService {
await apiClient.delete(`/api/app/blog/posts/${id}`); await apiClient.delete(`/api/app/blog/posts/${id}`);
} }
async getCategories(): Promise<BlogCategory[]> { async publishPost(id: string): Promise<BlogPost> {
const response = await apiClient.get<BlogCategory[]>('/api/app/blog/categories'); const response = await apiClient.post<BlogPost>(
`/api/app/blog/posts/${id}/publish`
);
return response.data; return response.data;
} }
async unpublishPost(id: string): Promise<BlogPost> {
const response = await apiClient.post<BlogPost>(
`/api/app/blog/posts/${id}/unpublish`
);
return response.data;
}
async incrementViewCount(id: string): Promise<void> {
await apiClient.post(`/api/app/blog/posts/${id}/view`);
}
async likePost(id: string): Promise<void> {
await apiClient.post(`/api/app/blog/posts/${id}/like`);
}
async unlikePost(id: string): Promise<void> {
await apiClient.delete(`/api/app/blog/posts/${id}/like`);
}
async getCategories(): Promise<BlogCategory[]> {
const response = await apiClient.get<BlogCategory[]>(
"/api/app/blog/categories"
);
return response.data;
}
async getTags(count = 20): Promise<string[]> {
const response = await apiClient.get<string[]>(
`/api/app/blog/tags?count=${count}`
);
return response.data;
}
// Opsiyonel - Yorum API'si mevcutsa kullanılır
async getComments(postId: string): Promise<BlogComment[]> { async getComments(postId: string): Promise<BlogComment[]> {
const response = await apiClient.get<BlogComment[]>(`/api/app/blog/posts/${postId}/comments`); const response = await apiClient.get<BlogComment[]>(
`/api/app/blog/posts/${postId}/comments`
);
return response.data; return response.data;
} }
async createComment(data: CreateCommentRequest): Promise<BlogComment> { async createComment(data: CreateCommentRequest): Promise<BlogComment> {
const response = await apiClient.post<BlogComment>('/api/app/blog/comments', data); const response = await apiClient.post<BlogComment>(
"/api/app/blog/comments",
data
);
return response.data; return response.data;
} }
@ -130,25 +196,12 @@ class BlogService {
await apiClient.delete(`/api/app/blog/comments/${id}`); await apiClient.delete(`/api/app/blog/comments/${id}`);
} }
async likePost(postId: string): Promise<void> { async likeComment(id: string): Promise<void> {
await apiClient.post(`/api/app/blog/posts/${postId}/like`); await apiClient.post(`/api/app/blog/comments/${id}/like`);
} }
async unlikePost(postId: string): Promise<void> { async unlikeComment(id: string): Promise<void> {
await apiClient.delete(`/api/app/blog/posts/${postId}/like`); await apiClient.delete(`/api/app/blog/comments/${id}/like`);
}
async likeComment(commentId: string): Promise<void> {
await apiClient.post(`/api/app/blog/comments/${commentId}/like`);
}
async unlikeComment(commentId: string): Promise<void> {
await apiClient.delete(`/api/app/blog/comments/${commentId}/like`);
}
async getTags(): Promise<string[]> {
const response = await apiClient.get<string[]>('/api/app/blog/tags');
return response.data;
} }
} }

View file

@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.n85sh48g8go" "revision": "0.6c62okhp6do"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View file

@ -1,55 +1,55 @@
import apiService from "@/services/api.service"; import apiService from '@/services/api.service'
export interface BlogPost { export interface BlogPost {
id: string; id: string
title: string; title: string
slug: string; slug: string
content?: string; content?: string
summary: string; summary: string
coverImage?: string; coverImage?: string
author: { author: {
id: string; id: string
name: string; name: string
avatar?: string; avatar?: string
}; }
category: { category: {
id: string; id: string
name: string; name: string
slug: string; slug: string
}; }
tags: string[]; tags: string[]
viewCount: number; viewCount: number
likeCount: number; likeCount: number
commentCount: number; commentCount: number
isPublished: boolean; isPublished: boolean
publishedAt?: string; publishedAt?: string
createdAt: string; createdAt: string
updatedAt: string; updatedAt: string
} }
export interface BlogCategory { export interface BlogCategory {
id: string; id: string
name: string; name: string
slug: string; slug: string
description?: string; description?: string
postCount: number; postCount: number
} }
export interface BlogComment { export interface BlogComment {
id: string; id: string
postId: string; postId: string
content: string; content: string
author: { author: {
id: string; id: string
name: string; name: string
avatar?: string; avatar?: string
}; }
parentId?: string; parentId?: string
replies?: BlogComment[]; replies?: BlogComment[]
likeCount: number; likeCount: number
isLiked?: boolean; isLiked?: boolean
createdAt: string; createdAt: string
updatedAt: string; updatedAt: string
} }
export interface CreateUpdateBlogPostDto { export interface CreateUpdateBlogPostDto {
@ -72,27 +72,27 @@ export interface CreateUpdateBlogCategoryDto {
} }
export interface CreateCommentDto { export interface CreateCommentDto {
postId: string; postId: string
content: string; content: string
parentId?: string; parentId?: string
} }
export interface BlogListParams { export interface BlogListParams {
page?: number; page?: number
pageSize?: number; pageSize?: number
categoryId?: string; categoryId?: string
tag?: string; tag?: string
search?: string; search?: string
authorId?: string; authorId?: string
sortBy?: 'latest' | 'popular' | 'trending'; sortBy?: 'latest' | 'popular' | 'trending'
} }
export interface PaginatedResponse<T> { export interface PaginatedResponse<T> {
items: T[]; items: T[]
totalCount: number; totalCount: number
pageNumber: number; pageNumber: number
pageSize: number; pageSize: number
totalPages: number; totalPages: number
} }
class BlogService { class BlogService {
@ -100,161 +100,169 @@ class BlogService {
const response = await apiService.fetchData<PaginatedResponse<BlogPost>>({ const response = await apiService.fetchData<PaginatedResponse<BlogPost>>({
url: '/api/app/blog/posts', url: '/api/app/blog/posts',
method: 'GET', method: 'GET',
params params,
}); })
return response.data
}
async getPostBySlug(slug: string): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({
url : `/api/app/blog/posts/post-by-slug/${slug}`,
method: 'GET',
})
return response.data; return response.data;
} }
async getPost(idOrSlug: string): Promise<BlogPost> { async getPost(idOrSlug: string): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({ const response = await apiService.fetchData<BlogPost>({
url: `/api/app/blog/posts/${idOrSlug}`, url: `/api/app/blog/posts/${idOrSlug}`,
method: 'GET' method: 'GET',
}); })
return response.data; return response.data
} }
async createPost(data: CreateUpdateBlogPostDto): Promise<BlogPost> { async createPost(data: CreateUpdateBlogPostDto): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({ const response = await apiService.fetchData<BlogPost>({
url: '/api/app/blog/posts', url: '/api/app/blog/post',
method: 'POST', method: 'POST',
data: data as any data: data as any,
}); })
return response.data; return response.data
} }
async updatePost(id: string, data: CreateUpdateBlogPostDto): Promise<BlogPost> { async updatePost(id: string, data: CreateUpdateBlogPostDto): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({ const response = await apiService.fetchData<BlogPost>({
url: `/api/app/blog/posts/${id}`, url: `/api/app/blog/${id}/post`,
method: 'PUT', method: 'PUT',
data: data as any data: data as any,
}); })
return response.data; return response.data
} }
async deletePost(id: string): Promise<void> { async deletePost(id: string): Promise<void> {
await apiService.fetchData({ await apiService.fetchData({
url: `/api/app/blog/posts/${id}`, url: `/api/app/blog/${id}/post`,
method: 'DELETE' method: 'DELETE',
}); })
} }
async publishPost(id: string): Promise<BlogPost> { async publishPost(id: string): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({ const response = await apiService.fetchData<BlogPost>({
url: `/api/app/blog/posts/${id}/publish`, url: `/api/app/blog/${id}/publish-post`,
method: 'POST' method: 'POST',
}); })
return response.data; return response.data
} }
async unpublishPost(id: string): Promise<BlogPost> { async unpublishPost(id: string): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({ const response = await apiService.fetchData<BlogPost>({
url: `/api/app/blog/posts/${id}/unpublish`, url: `/api/app/blog/${id}/unpublish-post`,
method: 'POST' method: 'POST',
}); })
return response.data; return response.data
} }
async getCategories(): Promise<BlogCategory[]> { async getCategories(): Promise<BlogCategory[]> {
const response = await apiService.fetchData<BlogCategory[]>({ const response = await apiService.fetchData<BlogCategory[]>({
url: '/api/app/blog/categories', url: '/api/app/blog/categories',
method: 'GET' method: 'GET',
}); })
return response.data; return response.data
} }
async getComments(postId: string): Promise<BlogComment[]> { async getComments(postId: string): Promise<BlogComment[]> {
const response = await apiService.fetchData<BlogComment[]>({ const response = await apiService.fetchData<BlogComment[]>({
url: `/api/app/blog/posts/${postId}/comments`, url: `/api/app/blog/posts/${postId}/comments`,
method: 'GET' method: 'GET',
}); })
return response.data; return response.data
} }
async createComment(data: CreateCommentDto): Promise<BlogComment> { async createComment(data: CreateCommentDto): Promise<BlogComment> {
const response = await apiService.fetchData<BlogComment>({ const response = await apiService.fetchData<BlogComment>({
url: '/api/app/blog/comments', url: '/api/app/blog/comments',
method: 'POST', method: 'POST',
data: data as any data: data as any,
}); })
return response.data; return response.data
} }
async deleteComment(id: string): Promise<void> { async deleteComment(id: string): Promise<void> {
await apiService.fetchData({ await apiService.fetchData({
url: `/api/app/blog/comments/${id}`, url: `/api/app/blog/comments/${id}`,
method: 'DELETE' method: 'DELETE',
}); })
} }
async likePost(postId: string): Promise<void> { async likePost(postId: string): Promise<void> {
await apiService.fetchData({ await apiService.fetchData({
url: `/api/app/blog/posts/${postId}/like`, url: `/api/app/blog/${postId}/like-post`,
method: 'POST' method: 'POST',
}); })
} }
async unlikePost(postId: string): Promise<void> { async unlikePost(postId: string): Promise<void> {
await apiService.fetchData({ await apiService.fetchData({
url: `/api/app/blog/posts/${postId}/like`, url: `/api/app/blog/${postId}/unlike-post`,
method: 'DELETE' method: 'DELETE',
}); })
} }
async likeComment(commentId: string): Promise<void> { async likeComment(commentId: string): Promise<void> {
await apiService.fetchData({ await apiService.fetchData({
url: `/api/app/blog/comments/${commentId}/like`, url: `/api/app/blog/comments/${commentId}/like`,
method: 'POST' method: 'POST',
}); })
} }
async unlikeComment(commentId: string): Promise<void> { async unlikeComment(commentId: string): Promise<void> {
await apiService.fetchData({ await apiService.fetchData({
url: `/api/app/blog/comments/${commentId}/like`, url: `/api/app/blog/comments/${commentId}/like`,
method: 'DELETE' method: 'DELETE',
}); })
} }
async getTags(): Promise<string[]> { async getTags(): Promise<string[]> {
const response = await apiService.fetchData<string[]>({ const response = await apiService.fetchData<string[]>({
url: '/api/app/blog/tags', url: '/api/app/blog/tags',
method: 'GET' method: 'GET',
}); })
return response.data; return response.data
} }
// Category methods // Category methods
async getCategory(id: string): Promise<BlogCategory> { async getCategory(id: string): Promise<BlogCategory> {
const response = await apiService.fetchData<BlogCategory>({ const response = await apiService.fetchData<BlogCategory>({
url: `/api/app/blog/categories/${id}`, url: `/api/app/blog/${id}/category`,
method: 'GET' method: 'GET',
}); })
return response.data; return response.data
} }
async createCategory(data: CreateUpdateBlogCategoryDto): Promise<BlogCategory> { async createCategory(data: CreateUpdateBlogCategoryDto): Promise<BlogCategory> {
const response = await apiService.fetchData<BlogCategory>({ const response = await apiService.fetchData<BlogCategory>({
url: '/api/app/blog/categories', url: '/api/app/blog/category',
method: 'POST', method: 'POST',
data: data as any data: data as any,
}); })
return response.data; return response.data
} }
async updateCategory(id: string, data: CreateUpdateBlogCategoryDto): Promise<BlogCategory> { async updateCategory(id: string, data: CreateUpdateBlogCategoryDto): Promise<BlogCategory> {
const response = await apiService.fetchData<BlogCategory>({ const response = await apiService.fetchData<BlogCategory>({
url: `/api/app/blog/categories/${id}`, url: `/api/app/blog/${id}/category`,
method: 'PUT', method: 'PUT',
data: data as any data: data as any,
}); })
return response.data; return response.data
} }
async deleteCategory(id: string): Promise<void> { async deleteCategory(id: string): Promise<void> {
await apiService.fetchData({ await apiService.fetchData({
url: `/api/app/blog/categories/${id}`, url: `/api/app/blog/${id}/category`,
method: 'DELETE' method: 'DELETE',
}); })
} }
} }
export const blogService = new BlogService(); export const blogService = new BlogService()

View file

@ -19,7 +19,7 @@ import {
} from '@/services/blog.service' } from '@/services/blog.service'
import { format } from 'date-fns' import { format } from 'date-fns'
import { tr } from 'date-fns/locale' import { tr } from 'date-fns/locale'
import { Field, Form, Formik } from 'formik' import { Field, FieldProps, Form, Formik } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import toast from '@/components/ui/toast' import toast from '@/components/ui/toast'
import Notification from '@/components/ui/Notification' import Notification from '@/components/ui/Notification'
@ -30,6 +30,8 @@ import Th from '@/components/ui/Table/Th'
import THead from '@/components/ui/Table/THead' import THead from '@/components/ui/Table/THead'
import TBody from '@/components/ui/Table/TBody' import TBody from '@/components/ui/Table/TBody'
import Td from '@/components/ui/Table/Td' import Td from '@/components/ui/Table/Td'
import { SelectBoxOption } from '@/shared/types'
import { enumToList } from '@/utils/enumUtils'
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
title: Yup.string().required('Başlık gereklidir'), title: Yup.string().required('Başlık gereklidir'),
@ -54,6 +56,10 @@ const BlogManagement = () => {
const [categoryModalVisible, setCategoryModalVisible] = useState(false) const [categoryModalVisible, setCategoryModalVisible] = useState(false)
const [editingPost, setEditingPost] = useState<BlogPost | null>(null) const [editingPost, setEditingPost] = useState<BlogPost | null>(null)
const [editingCategory, setEditingCategory] = useState<BlogCategory | null>(null) const [editingCategory, setEditingCategory] = useState<BlogCategory | null>(null)
const categoryItems = categories?.map((cat) => ({
value: cat.id,
label: cat.name,
}))
useEffect(() => { useEffect(() => {
loadData() loadData()
@ -212,7 +218,7 @@ const BlogManagement = () => {
description: values.description, description: values.description,
icon: values.icon, icon: values.icon,
displayOrder: values.displayOrder, displayOrder: values.displayOrder,
isActive: values.isActive isActive: values.isActive,
} }
if (editingCategory) { if (editingCategory) {
@ -271,7 +277,7 @@ const BlogManagement = () => {
description: editingCategory.description || '', description: editingCategory.description || '',
icon: '', icon: '',
displayOrder: 0, displayOrder: 0,
isActive: true isActive: true,
} }
: { : {
name: '', name: '',
@ -279,7 +285,7 @@ const BlogManagement = () => {
description: '', description: '',
icon: '', icon: '',
displayOrder: 0, displayOrder: 0,
isActive: true isActive: true,
} }
return ( return (
@ -287,11 +293,11 @@ const BlogManagement = () => {
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3>Blog Yönetimi</h3> <h3>Blog Yönetimi</h3>
{activeTab === 'posts' ? ( {activeTab === 'posts' ? (
<Button variant="solid" size='xs' icon={<HiPlus />} onClick={handleCreate}> <Button variant="solid" size="xs" icon={<HiPlus />} onClick={handleCreate}>
Yeni Blog Yazısı Yeni Blog Yazısı
</Button> </Button>
) : ( ) : (
<Button variant="solid" size='xs' icon={<HiPlus />} onClick={handleCreateCategory}> <Button variant="solid" size="xs" icon={<HiPlus />} onClick={handleCreateCategory}>
Yeni Kategori Yeni Kategori
</Button> </Button>
)} )}
@ -304,13 +310,13 @@ const BlogManagement = () => {
className={`pb-2 px-1 ${activeTab === 'posts' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`} className={`pb-2 px-1 ${activeTab === 'posts' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('posts')} onClick={() => setActiveTab('posts')}
> >
Blog Yazıları <b>Blog Yazıları</b>
</button> </button>
<button <button
className={`pb-2 px-1 ${activeTab === 'categories' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`} className={`pb-2 px-1 ${activeTab === 'categories' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('categories')} onClick={() => setActiveTab('categories')}
> >
Kategoriler <b>Kategoriler</b>
</button> </button>
</div> </div>
</div> </div>
@ -413,7 +419,11 @@ const BlogManagement = () => {
<Td>{category.postCount}</Td> <Td>{category.postCount}</Td>
<Td> <Td>
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" icon={<HiPencil />} onClick={() => handleEditCategory(category)} /> <Button
size="sm"
icon={<HiPencil />}
onClick={() => handleEditCategory(category)}
/>
<Button <Button
size="sm" size="sm"
variant="solid" variant="solid"
@ -454,7 +464,13 @@ const BlogManagement = () => {
invalid={errors.title && touched.title} invalid={errors.title && touched.title}
errorMessage={errors.title} errorMessage={errors.title}
> >
<Field type="text" name="title" placeholder="Blog başlığı" component={Input} /> <Field
type="text"
name="title"
placeholder="Blog başlığı"
component={Input}
autoFocus={true}
/>
</FormItem> </FormItem>
<FormItem <FormItem
@ -477,15 +493,16 @@ const BlogManagement = () => {
errorMessage={errors.categoryId} errorMessage={errors.categoryId}
> >
<Field name="categoryId"> <Field name="categoryId">
{({ field, form }: any) => ( {({ field, form }: FieldProps<SelectBoxOption>) => (
<Select <Select
field={field} field={field}
form={form} form={form}
options={categories.map((cat) => ({ options={categoryItems}
value: cat.id, isClearable={true}
label: cat.name, value={categoryItems.filter(
}))} (option) => option.value === values.categoryId,
placeholder="Kategori seçiniz" )}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/> />
)} )}
</Field> </Field>
@ -575,6 +592,7 @@ const BlogManagement = () => {
errorMessage={errors.name} errorMessage={errors.name}
> >
<Field <Field
autoFocus={true}
type="text" type="text"
name="name" name="name"
placeholder="Kategori ismi" placeholder="Kategori ismi"
@ -587,12 +605,7 @@ const BlogManagement = () => {
invalid={errors.slug && touched.slug} invalid={errors.slug && touched.slug}
errorMessage={errors.slug} errorMessage={errors.slug}
> >
<Field <Field type="text" name="slug" placeholder="kategori-slug" component={Input} />
type="text"
name="slug"
placeholder="kategori-slug"
component={Input}
/>
</FormItem> </FormItem>
<FormItem <FormItem
@ -609,26 +622,12 @@ const BlogManagement = () => {
/> />
</FormItem> </FormItem>
<FormItem <FormItem label="İkon (Emoji)">
label="İkon (Emoji)" <Field type="text" name="icon" placeholder="📚" component={Input} />
>
<Field
type="text"
name="icon"
placeholder="📚"
component={Input}
/>
</FormItem> </FormItem>
<FormItem <FormItem label="Sıralama">
label="Sıralama" <Field type="number" name="displayOrder" placeholder="0" component={Input} />
>
<Field
type="number"
name="displayOrder"
placeholder="0"
component={Input}
/>
</FormItem> </FormItem>
<FormItem> <FormItem>
@ -646,17 +645,10 @@ const BlogManagement = () => {
<FormItem> <FormItem>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button variant="solid" type="submit" loading={isSubmitting}>
variant="solid"
type="submit"
loading={isSubmitting}
>
{editingCategory ? 'Güncelle' : 'Oluştur'} {editingCategory ? 'Güncelle' : 'Oluştur'}
</Button> </Button>
<Button <Button variant="plain" onClick={() => setCategoryModalVisible(false)}>
variant="plain"
onClick={() => setCategoryModalVisible(false)}
>
İptal İptal
</Button> </Button>
</div> </div>