Blog sistemindeki güncellemeler
This commit is contained in:
parent
2baff7538f
commit
2a1f06b2f4
15 changed files with 1072 additions and 382 deletions
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
@ -196,3 +199,25 @@ 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; }
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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),
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
← {t('blog.backToBlog')} {/* Geri dönüş butonu */}
|
to="/blog"
|
||||||
|
className="text-blue-600 hover:underline mb-4 inline-block"
|
||||||
|
>
|
||||||
|
← {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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"), {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue