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

View file

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

View file

@ -6,6 +6,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using Kurs.Languages.Entities;
using Kurs.Notifications.Entities;
using Kurs.Platform.Blog;
using Kurs.Platform.Charts.Dto;
using Kurs.Platform.Entities;
using Kurs.Platform.Enums;
@ -48,6 +49,8 @@ public class PlatformDataSeeder : IDataSeedContributor, ITransientDependency
private readonly IRepository<SkillLevel, Guid> _skillLevelRepository;
private readonly IRepository<ContactTag, Guid> _contactTagRepository;
private readonly IRepository<ContactTitle, Guid> _contactTitleRepository;
private readonly IRepository<BlogCategory, Guid> _blogCategoryRepository;
private readonly IRepository<BlogPost, Guid> _blogPostsRepository;
public PlatformDataSeeder(
IRepository<Language, Guid> languages,
@ -73,7 +76,9 @@ public class PlatformDataSeeder : IDataSeedContributor, ITransientDependency
IRepository<Skill, Guid> skillRepository,
IRepository<SkillLevel, Guid> skillLevelRepository,
IRepository<ContactTag, Guid> contactTagRepository,
IRepository<ContactTitle, Guid> contactTitleRepository
IRepository<ContactTitle, Guid> contactTitleRepository,
IRepository<BlogCategory, Guid> blogCategoryRepository,
IRepository<BlogPost, Guid> blogPostsRepository
)
{
_languages = languages;
@ -100,6 +105,8 @@ public class PlatformDataSeeder : IDataSeedContributor, ITransientDependency
_skillLevelRepository = skillLevelRepository;
_contactTagRepository = contactTagRepository;
_contactTitleRepository = contactTitleRepository;
_blogCategoryRepository = blogCategoryRepository;
_blogPostsRepository = blogPostsRepository;
}
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.Platform.Charts.Dto;
using Kurs.Platform.Entities;
@ -32,6 +33,8 @@ public class SeederDto
public List<SkillLevelSeedDto> SkillLevels { get; set; }
public List<ContactTagSeedDto> ContactTags { get; set; }
public List<ContactTitleSeedDto> ContactTitles { get; set; }
public List<BlogCategorySeedDto> BlogCategories { get; set; }
public List<BlogPostSeedDto> BlogPosts { get; set; }
}
public class ChartsSeedDto
@ -196,3 +199,25 @@ public class ContactTitleSeedDto
public string Title { 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 Summary { get; set; }
public string CoverImage { get; set; }
public string ReadTime { get; set; }
public Guid CategoryId { get; set; }
public virtual BlogCategory Category { get; set; }
@ -44,6 +45,8 @@ namespace Kurs.Platform.Blog
string slug,
string content,
string summary,
string readTime,
string coverImage,
Guid categoryId,
Guid authorId,
Guid? tenantId = null) : base(id)
@ -52,6 +55,8 @@ namespace Kurs.Platform.Blog
Slug = slug;
Content = content;
Summary = summary;
ReadTime = readTime;
CoverImage = coverImage;
CategoryId = categoryId;
AuthorId = authorId;
TenantId = tenantId;

View file

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

View file

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

View file

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

View file

@ -130,7 +130,7 @@ const Blog = () => {
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
{category.name} ({category.postCount})
{ t(category.name)} ({category.postCount})
</button>
))}
</div>
@ -158,20 +158,20 @@ const Blog = () => {
<div className="aspect-w-16 aspect-h-9 relative">
<img
src={
post.coverImage || "https://via.placeholder.com/400x225"
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">
{post.category.name}
{t(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">
{post.title}
{t(post.title)}
</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 */}
{post.tags.length > 0 && (
@ -203,11 +203,7 @@ const Blog = () => {
</div>
<div className="flex items-center">
<Clock size={16} className="mr-1" />
{typeof post.content === "string" &&
post.content.length > 0
? Math.ceil(post.content.length / 1000)
: "-"}{" "}
dk
{post.readTime}
</div>
</div>
</div>

View file

@ -1,66 +1,110 @@
import React from 'react';
import { Link, useParams } from 'react-router-dom'; // Link ve useParams'ı import et
import { useLanguage } from '../context/LanguageContext'; // useLanguage hook'unu import et
import { blogContent, BlogPostContent } from '../locales/blogContent'; // blogContent ve BlogPostContent interface'ini import et
import React, { useState, useEffect } from "react";
import { Link, useParams } from "react-router-dom";
import { useLanguage } from "../context/LanguageContext";
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 { 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ı)
const createSlug = (title: string) => {
return title
.toLowerCase()
.replace(/ /g, '-')
.replace(/[^\w-]+/g, '');
useEffect(() => {
const fetchBlogPost = async () => {
setLoading(true);
setError(null);
try {
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
// blogContent objesinden slug'a karşılık gelen blog yazısını ve mevcut dile göre içeriğini al
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
fetchBlogPost();
}, [id]);
if (!blogPost) {
if (loading) {
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> {/* Ç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>
);
}
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">
<Link to="/blog" className="text-blue-600 hover:underline mb-4 inline-block">
&larr; {t('blog.backToBlog')} {/* Geri dönüş butonu */}
<Link
to="/blog"
className="text-blue-600 hover:underline mb-4 inline-block"
>
&larr; {t("blog.backToBlog")}
</Link>
{/* Blog yazısı görseli */}
{postData.image && (
<img
src={postData.image} // Görsel bilgisi blogContent'ten alınıyor
alt={t(blogPost.title)} // Alt metni çevir
src={postData.image}
alt={t(blogPost.title)}
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 */}
{/* Yazar, tarih, okuma süresi gibi bilgiler eklenebilir */}
<h1 className="text-4xl font-bold text-gray-900 mb-6">
{t(blogPost.title)}
</h1>
<div className="flex items-center text-sm text-gray-500 space-x-4 mb-8">
<div className="flex items-center">
{/* <User size={16} className="mr-1" /> */} {/* İkonlar eklenebilir */}
<span>{postData.author}</span> {/* Yazar bilgisi blogContent'ten alınıyor */}
<span>{postData.author?.name}</span>
</div>
<div className="flex items-center">
{/* <Calendar size={16} className="mr-1" /> */}
{t(blogPost.date)} {/* Çeviri kullan */}
</div>
<div className="flex items-center">
{/* <Clock size={16} className="mr-1" /> */}
{blogPost.readTime}
{blogPost.publishedAt &&
format(new Date(blogPost.publishedAt), "dd MMM yyyy", {
locale: tr,
})}
</div>
</div>
<div className="prose max-w-none text-gray-800"> {/* Tailwind Typography eklentisi kuruluysa kullanılabilir */}
<p>{t(blogPost.content)}</p> {/* Tam içeriği çevirerek göster */}
{/* Daha uzun içerik burada paragraflar halinde yer alabilir */}
<div className="prose max-w-none text-gray-800">
<p>{blogPost.content}</p>
</div>
</div>
</div>

View file

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

View file

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

View file

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