erp-platform/ui/src/views/blog/BlogManagement.tsx
2025-06-20 15:38:59 +03:00

723 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import React, { useState, useEffect } from 'react'
import Card from '@/components/ui/Card'
import Button from '@/components/ui/Button'
import Table from '@/components/ui/Table'
import Tag from '@/components/ui/Tag'
import Dialog from '@/components/ui/Dialog'
import { FormContainer, FormItem } from '@/components/ui/Form'
import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
import Switcher from '@/components/ui/Switcher'
import { HiPlus, HiPencil, HiTrash, HiEye } from 'react-icons/hi'
import { useNavigate } from 'react-router-dom'
import {
blogService,
BlogPost,
BlogCategory,
CreateUpdateBlogPostDto,
CreateUpdateBlogCategoryDto,
} from '@/services/blog.service'
import { format } from 'date-fns'
import { tr } from 'date-fns/locale'
import { Field, FieldProps, Form, Formik } from 'formik'
import * as Yup from 'yup'
import toast from '@/components/ui/toast'
import Notification from '@/components/ui/Notification'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
import Tr from '@/components/ui/Table/Tr'
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 { CheckBox } from 'devextreme-react'
import { Checkbox } from '@/components/ui'
import { Helmet } from 'react-helmet'
import { useLocalization } from '@/utils/hooks/useLocalization'
const validationSchema = Yup.object().shape({
title: Yup.string().required(),
summary: Yup.string().required(),
categoryId: Yup.string().required(),
content: Yup.string(),
tags: Yup.string(),
coverImage: Yup.string(),
isPublished: Yup.bool(),
})
const categoryValidationSchema = Yup.object().shape({
name: Yup.string().required(),
slug: Yup.string().required(),
description: Yup.string(),
icon: Yup.string(),
displayOrder: Yup.number(),
isActive: Yup.bool(),
})
const BlogManagement = () => {
const { translate } = useLocalization()
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState<'posts' | 'categories'>('posts')
const [posts, setPosts] = useState<BlogPost[]>([])
const [categories, setCategories] = useState<BlogCategory[]>([])
const [loading, setLoading] = useState(false)
const [modalVisible, setModalVisible] = useState(false)
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()
}, [])
const loadData = async () => {
setLoading(true)
try {
const [postsData, categoriesData] = await Promise.all([
blogService.getPosts({ pageSize: 100 }),
blogService.getCategories(),
])
setPosts(postsData.items)
setCategories(categoriesData)
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
Veriler yüklenirken hata oluştu
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setLoading(false)
}
}
const handleCreate = () => {
setEditingPost(null)
setModalVisible(true)
}
const handleEdit = (post: BlogPost) => {
setEditingPost(post)
setModalVisible(true)
}
const handleDelete = async (id: string) => {
try {
await blogService.deletePost(id)
toast.push(
<Notification title="Başarılı" type="success">
Blog yazısı silindi
</Notification>,
{
placement: 'top-center',
},
)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
Silme işlemi başarısız
</Notification>,
{
placement: 'top-center',
},
)
}
}
const handleSubmit = async (values: any, { setSubmitting }: any) => {
try {
const data: CreateUpdateBlogPostDto = {
title: values.title,
slug: values.slug,
content: values.content,
summary: values.summary,
categoryId: values.categoryId,
tags: values.tags ? values.tags.split(',').map((t: string) => t.trim()) : [],
coverImage: values.coverImage,
isPublished: values.isPublished,
}
if (editingPost) {
await blogService.updatePost(editingPost.id, data)
toast.push(
<Notification title="Başarılı" type="success">
Blog yazısı güncellendi
</Notification>,
{
placement: 'top-center',
},
)
} else {
await blogService.createPost(data)
toast.push(
<Notification title="Başarılı" type="success">
Blog yazısı oluşturuldu
</Notification>,
{
placement: 'top-center',
},
)
}
setModalVisible(false)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setSubmitting(false)
}
}
const handlePublish = async (post: BlogPost) => {
try {
if (post.isPublished) {
await blogService.unpublishPost(post.id)
toast.push(
<Notification title="Başarılı" type="success">
Yayından kaldırıldı
</Notification>,
{
placement: 'top-center',
},
)
} else {
await blogService.publishPost(post.id)
toast.push(
<Notification title="Başarılı" type="success">
Yayınlandı
</Notification>,
{
placement: 'top-center',
},
)
}
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
{
placement: 'top-center',
},
)
}
}
// Category functions
const handleCreateCategory = () => {
setEditingCategory(null)
setCategoryModalVisible(true)
}
const handleEditCategory = (category: BlogCategory) => {
setEditingCategory(category)
console.log(category)
setCategoryModalVisible(true)
}
const handleDeleteCategory = async (id: string) => {
try {
await blogService.deleteCategory(id)
toast.push(
<Notification title="Başarılı" type="success">
Kategori silindi
</Notification>,
{
placement: 'top-center',
},
)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
Silme işlemi başarısız
</Notification>,
{
placement: 'top-center',
},
)
}
}
const handleSubmitCategory = async (values: any, { setSubmitting }: any) => {
try {
const data: CreateUpdateBlogCategoryDto = {
name: values.name,
slug: values.slug,
description: values.description,
icon: values.icon,
displayOrder: values.displayOrder,
isActive: values.isActive,
}
if (editingCategory) {
await blogService.updateCategory(editingCategory.id, data)
toast.push(
<Notification title="Başarılı" type="success">
Kategori güncellendi
</Notification>,
{
placement: 'top-center',
},
)
} else {
await blogService.createCategory(data)
toast.push(
<Notification title="Başarılı" type="success">
Kategori oluşturuldu
</Notification>,
{
placement: 'top-center',
},
)
}
setCategoryModalVisible(false)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setSubmitting(false)
}
}
const initialValues = editingPost
? {
title: editingPost.title,
slug: editingPost.slug,
summary: editingPost.summary,
content: editingPost.content,
categoryId: editingPost.category.id,
tags: editingPost.tags.join(', '),
coverImage: editingPost.coverImage || '',
isPublished: editingPost.isPublished,
}
: {
title: '',
slug: '',
summary: '',
content: '',
categoryId: '',
tags: '',
coverImage: '',
isPublished: false,
}
const initialCategoryValues = editingCategory
? {
name: editingCategory.name,
slug: editingCategory.slug,
description: editingCategory.description || '',
icon: editingCategory.icon,
displayOrder: editingCategory.displayOrder,
isActive: editingCategory.isActive,
}
: {
name: '',
slug: '',
description: '',
icon: '',
displayOrder: 0,
isActive: true,
}
return (
<>
<Helmet
titleTemplate="%s | Kurs Platform"
title={translate('::' + 'Blog Management')}
defaultTitle="Kurs Platform"
></Helmet>
<Card>
<div className="mb-4">
<div className="flex gap-4 border-b">
<button
className={`pb-2 px-1 ${activeTab === 'posts' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('posts')}
>
<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')}
>
<b>Kategoriler</b>
</button>
</div>
</div>
{activeTab === 'posts' ? (
<Table compact>
<THead>
<Tr>
<Th>Başlık</Th>
<Th>Slug</Th>
<Th>Kategori</Th>
<Th>Yazar</Th>
<Th>Yayın Tarihi</Th>
<Th>Durum</Th>
<Th>
{' '}
<Button variant="solid" size="xs" icon={<HiPlus />} onClick={handleCreate}>
Yeni
</Button>
</Th>
</Tr>
</THead>
<TBody>
{loading ? (
<Tr>
<Td colSpan={7} className="text-center">
Yükleniyor...
</Td>
</Tr>
) : (
posts.map((post) => (
<Tr key={post.id}>
<Td className="font-medium">{post.title}</Td>
<Td>{post.slug}</Td>
<Td>{post.category?.name}</Td>
<Td>{post.author?.name}</Td>
<Td>
{post.publishedAt
? format(new Date(post.publishedAt), 'dd MMM yyyy', { locale: tr })
: '-'}
</Td>
<Td>
<Switcher checked={post.isPublished} onChange={() => handlePublish(post)} />
</Td>
<Td>
<div className="flex gap-2">
<Button size="sm" icon={<HiPencil />} onClick={() => handleEdit(post)} />
<Button
size="sm"
variant="solid"
color="red-600"
icon={<HiTrash />}
onClick={() => handleDelete(post.id)}
/>
</div>
</Td>
</Tr>
))
)}
</TBody>
</Table>
) : (
<Table compact>
<THead>
<Tr>
<Th>İsim</Th>
<Th>Slug</Th>
<Th>ıklama</Th>
<Th>Yazı Sayısı</Th>
<Th>Durum</Th>
<Th>
<Button
variant="solid"
size="xs"
icon={<HiPlus />}
onClick={handleCreateCategory}
>
Yeni
</Button>
</Th>
</Tr>
</THead>
<TBody>
{loading ? (
<Tr>
<Td colSpan={5} className="text-center">
Yükleniyor...
</Td>
</Tr>
) : (
categories.map((category) => (
<Tr key={category.id}>
<Td className="font-medium">{category.name}</Td>
<Td>{category.slug}</Td>
<Td>{category.description}</Td>
<Td>{category.postCount}</Td>
<Td>
<Tag
className={
category.isActive
? 'bg-green-100 text-green-800'
: 'bg-orange-100 text-orange-800'
}
>
{category.isActive ? 'Aktif' : 'Pasif'}
</Tag>
</Td>
<Td>
<div className="flex gap-2">
<Button
size="sm"
icon={<HiPencil />}
onClick={() => handleEditCategory(category)}
/>
<Button
size="sm"
variant="solid"
color="red-600"
icon={<HiTrash />}
onClick={() => handleDeleteCategory(category.id)}
/>
</div>
</Td>
</Tr>
))
)}
</TBody>
</Table>
)}
</Card>
{/* Post Modal */}
<Dialog
isOpen={modalVisible}
onClose={() => setModalVisible(false)}
onRequestClose={() => setModalVisible(false)}
width={1000}
>
<h5 className="mb-4">{editingPost ? 'Blog Yazısını Düzenle' : 'Yeni Blog Yazısı'}</h5>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
enableReinitialize={true}
>
{({ values, touched, errors, isSubmitting, setFieldValue }) => (
<Form>
<FormContainer>
<FormItem
asterisk
label="Başlık"
invalid={errors.title && touched.title}
errorMessage={errors.title}
>
<Field
type="text"
name="title"
placeholder="Blog başlığı"
component={Input}
autoFocus={true}
/>
</FormItem>
<FormItem
asterisk
label="Slug"
invalid={errors.title && touched.title}
errorMessage={errors.title}
>
<Field type="text" name="slug" placeholder="Slug" component={Input} />
</FormItem>
<FormItem
asterisk
label="Özet"
invalid={errors.summary && touched.summary}
errorMessage={errors.summary}
>
<Field
name="summary"
placeholder="Kısa açıklama"
component={Input}
textArea={true}
rows={3}
/>
</FormItem>
<FormItem
asterisk
label="Kategori"
invalid={errors.categoryId && touched.categoryId}
errorMessage={errors.categoryId}
>
<Field name="categoryId">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={categoryItems}
isClearable={true}
value={categoryItems.filter((option) => option.value === values.categoryId)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem label="Etiketler - Virgül ile ayırarak yazınız">
<Field
type="text"
name="tags"
placeholder="örn: react, javascript, web"
component={Input}
/>
</FormItem>
<FormItem label="Kapak Görseli">
<Field
type="text"
name="coverImage"
placeholder="Görsel URL'si"
component={Input}
/>
</FormItem>
<FormItem
label="İçerik"
asterisk
invalid={!!errors.content}
errorMessage={errors.content}
>
<ReactQuill
theme="snow"
value={values.content}
onChange={(val: string) => setFieldValue('content', val)}
style={{ height: '300px', marginBottom: '50px' }}
/>
</FormItem>
<FormItem
label="Durum"
invalid={errors.isPublished && touched.isPublished}
errorMessage={errors.isPublished}
>
<Field name="isPublished" component={Switcher} />
</FormItem>
<FormItem>
<div className="flex gap-2">
<Button variant="solid" type="submit" loading={isSubmitting}>
{editingPost ? 'Güncelle' : 'Oluştur'}
</Button>
<Button variant="plain" onClick={() => setModalVisible(false)}>
İptal
</Button>
</div>
</FormItem>
</FormContainer>
</Form>
)}
</Formik>
</Dialog>
{/* Category Modal */}
<Dialog
isOpen={categoryModalVisible}
onClose={() => setCategoryModalVisible(false)}
onRequestClose={() => setCategoryModalVisible(false)}
width={600}
>
<h5 className="mb-4">{editingCategory ? 'Kategoriyi Düzenle' : 'Yeni Kategori'}</h5>
<Formik
initialValues={initialCategoryValues}
validationSchema={categoryValidationSchema}
onSubmit={handleSubmitCategory}
enableReinitialize
>
{({ values, touched, errors, isSubmitting }) => (
<Form>
<FormContainer>
<FormItem
asterisk
label="İsim"
invalid={errors.name && touched.name}
errorMessage={errors.name}
>
<Field
autoFocus={true}
type="text"
name="name"
placeholder="Kategori ismi"
component={Input}
/>
</FormItem>
<FormItem
asterisk
label="Slug"
invalid={errors.slug && touched.slug}
errorMessage={errors.slug}
>
<Field type="text" name="slug" placeholder="kategori-slug" component={Input} />
</FormItem>
<FormItem
label="Açıklama"
invalid={errors.description && touched.description}
errorMessage={errors.description}
>
<Field
name="description"
placeholder="Kategori açıklaması"
component={Input}
textArea={true}
rows={3}
/>
</FormItem>
<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>
<FormItem
label="Durum"
invalid={errors.isActive && touched.isActive}
errorMessage={errors.isActive}
>
<Field name="isActive" component={Checkbox} />
</FormItem>
<FormItem>
<div className="flex gap-2">
<Button variant="solid" type="submit" loading={isSubmitting}>
{editingCategory ? 'Güncelle' : 'Oluştur'}
</Button>
<Button variant="plain" onClick={() => setCategoryModalVisible(false)}>
İptal
</Button>
</div>
</FormItem>
</FormContainer>
</Form>
)}
</Formik>
</Dialog>
</>
)
}
export default BlogManagement