erp-platform/ui/src/views/blog/BlogManagement.tsx

881 lines
30 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,
} 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, Tabs } from '@/components/ui'
import { Helmet } from 'react-helmet'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { ConfirmDialog } from '@/components/shared'
import { useStoreState } from '@/store/store'
import TabList from '@/components/ui/Tabs/TabList'
import TabNav from '@/components/ui/Tabs/TabNav'
import TabContent from '@/components/ui/Tabs/TabContent'
import { BlogCategory, BlogPost, CreateUpdateBlogCategoryDto, CreateUpdateBlogPostDto } from '@/proxy/blog/blog'
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 { texts } = useStoreState((state) => state.abpConfig)
const categoryItems = categories?.map((cat) => ({
value: cat.id,
label: texts?.Platform[cat.name] + ' (' + cat.name + ')',
}))
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
setLoading(true)
try {
const [postsData, categoriesData] = await Promise.all([
blogService.getPosts({ pageSize: 100 }),
blogService.getCategories(),
])
setCategories(categoriesData)
setPosts(postsData.items)
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
{translate('::Error:Loading')}
</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">
{translate('::KayitSilindi')}
</Notification>,
{
placement: 'top-center',
},
)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
{translate('::Error:Deleting')}
</Notification>,
{
placement: 'top-center',
},
)
}
}
const handleSubmit = async (values: any, { setSubmitting }: any) => {
try {
const data: CreateUpdateBlogPostDto = {
title: values.title,
slug: values.slug,
contentTr: values.contentTr,
contentEn: values.contentEn,
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">
{translate('::KayitGuncellendi')}
</Notification>,
{
placement: 'top-center',
},
)
} else {
await blogService.createPost(data)
toast.push(
<Notification title="Başarılı" type="success">
{translate('::KayitEklendi')}
</Notification>,
{
placement: 'top-center',
},
)
}
setModalVisible(false)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
{translate('::IslemBasarisiz')}
</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">
{translate('::YayinKaldirildi')}
</Notification>,
{
placement: 'top-center',
},
)
} else {
await blogService.publishPost(post.id)
toast.push(
<Notification title="Başarılı" type="success">
{translate('::Yayinlandi')}
</Notification>,
{
placement: 'top-center',
},
)
}
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
{translate('::IslemBasarisiz')}
</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">
{translate('::KayitSilindi')}
</Notification>,
{
placement: 'top-center',
},
)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
{translate('::IslemBasarisiz')}
</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">
{translate('::KayitGuncellendi')}
</Notification>,
{
placement: 'top-center',
},
)
} else {
await blogService.createCategory(data)
toast.push(
<Notification title="Başarılı" type="success">
{translate('::KayitEklendi')}
</Notification>,
{
placement: 'top-center',
},
)
}
setCategoryModalVisible(false)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
{translate('::IslemBasarisiz')}
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setSubmitting(false)
}
}
const initialValues = editingPost
? {
title: editingPost.title,
slug: editingPost.slug,
summary: editingPost.summary,
contentTr: editingPost.contentTr,
contentEn: editingPost.contentEn,
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,
}
const [confirmDeletePost, setConfirmDeletePost] = useState<BlogPost | null>(null)
const [confirmDeleteCategory, setConfirmDeleteCategory] = useState<BlogCategory | null>(null)
const askDeletePost = (post: BlogPost) => {
setConfirmDeletePost(post)
}
const askDeleteCategory = (category: BlogCategory) => {
setConfirmDeleteCategory(category)
}
return (
<>
<Helmet
titleTemplate="%s | Kurs Platform"
title={translate('::' + 'Blog Management')}
defaultTitle="Kurs Platform"
></Helmet>
<Card>
<div className="flex gap-2 border-b">
<button
className={`p-2 rounded-t-md transition ${
activeTab === 'posts'
? 'bg-blue-100 text-blue-600 font-bold'
: 'text-gray-600 hover:bg-gray-100 font-semibold'
}`}
onClick={() => setActiveTab('posts')}
>
{translate('::blog.posts.title')}
</button>
<button
className={`p-2 rounded-t-md transition ${
activeTab === 'categories'
? 'bg-blue-100 text-blue-600 font-bold'
: 'text-gray-600 hover:bg-gray-100 font-semibold'
}`}
onClick={() => setActiveTab('categories')}
>
{translate('::blog.posts.categories')}
</button>
</div>
{activeTab === 'posts' ? (
<Table compact>
<THead>
<Tr>
<Th>{translate('::blog.posts.post.title')}</Th>
<Th>{translate('::blog.posts.post.slug')}</Th>
<Th>{translate('::blog.posts.post.category')}</Th>
<Th>{translate('::blog.posts.post.author')}</Th>
<Th>{translate('::blog.posts.post.publishDate')}</Th>
<Th>{translate('::blog.posts.post.status')}</Th>
<Th>
{' '}
<Button variant="solid" size="xs" icon={<HiPlus />} onClick={handleCreate}>
{translate('::New')}
</Button>
</Th>
</Tr>
</THead>
<TBody>
{loading ? (
<Tr>
<Td colSpan={7} className="text-center">
{translate('::Loading')}
</Td>
</Tr>
) : (
posts.map((post) => (
<Tr key={post.id}>
<Td>{texts?.Platform[post.title]}</Td>
<Td>{post.slug}</Td>
<Td>{texts?.Platform[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
className="switcher-sm"
checked={post.isPublished}
onChange={() => handlePublish(post)}
/>
</Td>
<Td>
<div className="flex gap-2">
<Button size="xs" icon={<HiPencil />} onClick={() => handleEdit(post)} />
<Button
size="xs"
variant="solid"
color="red-600"
icon={<HiTrash />}
onClick={() => askDeletePost(post)}
/>
</div>
</Td>
</Tr>
))
)}
</TBody>
</Table>
) : (
<Table compact>
<THead>
<Tr>
<Th>{translate('::blog.posts.categories.name')}</Th>
<Th>{translate('::blog.posts.categories.slug')}</Th>
<Th>{translate('::blog.posts.categories.description')}</Th>
<Th>{translate('::blog.posts.categories.count')}</Th>
<Th>{translate('::blog.posts.categories.order')}</Th>
<Th>{translate('::blog.posts.categories.status')}</Th>
<Th>
<Button
variant="solid"
size="xs"
icon={<HiPlus />}
onClick={handleCreateCategory}
>
{translate('::New')}
</Button>
</Th>
</Tr>
</THead>
<TBody>
{loading ? (
<Tr>
<Td colSpan={5} className="text-center">
{translate('::Loading')}
</Td>
</Tr>
) : (
categories.map((category) => (
<Tr key={category.id}>
<Td>{texts?.Platform[category.name]}</Td>
<Td>{category.slug}</Td>
<Td>{texts?.Platform[category.description!!]}</Td>
<Td>{category.postCount}</Td>
<Td>{category.displayOrder}</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="xs"
icon={<HiPencil />}
onClick={() => handleEditCategory(category)}
/>
<Button
size="xs"
variant="solid"
color="red-600"
icon={<HiTrash />}
onClick={() => askDeleteCategory(category)}
/>
</div>
</Td>
</Tr>
))
)}
</TBody>
</Table>
)}
</Card>
{/* Post Modal */}
<Dialog
isOpen={modalVisible}
onClose={() => setModalVisible(false)}
onRequestClose={() => setModalVisible(false)}
width={1000}
>
<h5 className="mb-4">
{editingPost ? translate('::blog.posts.edittitle') : translate('::blog.posts.newtitle')}
</h5>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
enableReinitialize={true}
>
{({ values, touched, errors, isSubmitting, setFieldValue }) => (
<Form>
<FormContainer>
<FormItem
asterisk
label={translate('::blog.posts.post.title')}
invalid={errors.title && touched.title}
errorMessage={errors.title}
>
<Field name="title">
{({ field, form }: FieldProps<SelectBoxOption>) => {
const options = texts?.Platform
? Object.entries(texts.Platform).map(([key, value]) => ({
value: key,
label: value + ' (' + key + ')',
})).filter(a=> a.value.startsWith("blog"))
: []
return (
<Select
field={field}
form={form}
options={options}
isClearable={true}
value={options.find((opt) => opt.value === field.value)}
onChange={(option) => form.setFieldValue(field.name, option?.value || '')}
/>
)
}}
</Field>
</FormItem>
<FormItem
asterisk
label={translate('::blog.posts.post.slug')}
invalid={errors.title && touched.title}
errorMessage={errors.title}
>
<Field type="text" name="slug" placeholder="Slug" component={Input} />
</FormItem>
<FormItem
asterisk
label={translate('::blog.posts.post.summary')}
invalid={errors.summary && touched.summary}
errorMessage={errors.summary}
>
<Field name="summary">
{({ field, form }: FieldProps<SelectBoxOption>) => {
const options = texts?.Platform
? Object.entries(texts.Platform).map(([key, value]) => ({
value: key,
label: value + ' (' + key + ')',
})).filter(a=> a.value.startsWith("blog"))
: []
return (
<Select
field={field}
form={form}
options={options}
isClearable={true}
value={options.find((opt) => opt.value === field.value)}
onChange={(option) => form.setFieldValue(field.name, option?.value || '')}
/>
)
}}
</Field>
</FormItem>
<FormItem
asterisk
label={translate('::blog.posts.post.category')}
invalid={errors.categoryId && touched.categoryId}
errorMessage={errors.categoryId}
>
<Field name="categoryId">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={categoryItems.filter(a=> a.label.startsWith("blog"))}
isClearable={true}
value={categoryItems.filter((option) => option.value === values.categoryId)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem label={translate('::blog.posts.post.tags')}>
<Field
type="text"
name="tags"
placeholder="react, javascript, web"
component={Input}
/>
</FormItem>
<FormItem label={translate('::blog.posts.post.image')}>
<Field type="text" name="coverImage" component={Input} />
</FormItem>
<Tabs defaultValue="tr" variant="pill">
<TabList className="flex-wrap border-b mb-4 bg-slate-50 rounded-t">
<TabNav value="tr">Türkçe</TabNav>
<TabNav value="en">English</TabNav>
</TabList>
<TabContent value="tr">
<FormItem
label={translate('::blog.posts.post.content')}
asterisk
invalid={!!errors.contentTr}
errorMessage={errors.contentTr}
>
<ReactQuill
theme="snow"
value={values.contentTr}
onChange={(val: string) => setFieldValue('contentTr', val)}
style={{ height: '300px', marginBottom: '50px' }}
/>
</FormItem>
</TabContent>
<TabContent value="en">
<FormItem
label={translate('::blog.posts.post.content')}
asterisk
invalid={!!errors.contentEn}
errorMessage={errors.contentEn}
>
<ReactQuill
theme="snow"
value={values.contentEn}
onChange={(val: string) => setFieldValue('contentEn', val)}
style={{ height: '300px', marginBottom: '50px' }}
/>
</FormItem>
</TabContent>
</Tabs>
<FormItem
label={translate('::blog.posts.post.status')}
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
? translate('::blog.posts.post.update')
: translate('::blog.posts.post.create')}
</Button>
<Button variant="plain" onClick={() => setModalVisible(false)}>
{translate('::Cancel')}
</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
? translate('::blog.posts.categories.edittitle')
: translate('::blog.posts.categories.newtitle')}
</h5>
<Formik
initialValues={initialCategoryValues}
validationSchema={categoryValidationSchema}
onSubmit={handleSubmitCategory}
enableReinitialize
>
{({ values, touched, errors, isSubmitting }) => (
<Form>
<FormContainer>
<FormItem
asterisk
label={translate('::blog.posts.categories.name')}
invalid={errors.name && touched.name}
errorMessage={errors.name}
>
<Field name="name">
{({ field, form }: FieldProps<SelectBoxOption>) => {
const options = texts?.Platform
? Object.entries(texts.Platform).map(([key, value]) => ({
value: key,
label: value + ' (' + key + ')',
}))
: []
return (
<Select
field={field}
form={form}
options={options}
isClearable={true}
value={options.find((opt) => opt.value === field.value)}
onChange={(option) => form.setFieldValue(field.name, option?.value || '')}
/>
)
}}
</Field>
</FormItem>
<FormItem
asterisk
label={translate('::blog.posts.categories.slug')}
invalid={errors.slug && touched.slug}
errorMessage={errors.slug}
>
<Field type="text" name="slug" component={Input} />
</FormItem>
<FormItem
asterisk
label={translate('::blog.posts.categories.description')}
invalid={errors.description && touched.description}
errorMessage={errors.description}
>
<Field name="description">
{({ field, form }: FieldProps<SelectBoxOption>) => {
const options = texts?.Platform
? Object.entries(texts.Platform).map(([key, value]) => ({
value: key,
label: value + ' (' + key + ')',
}))
: []
return (
<Select
field={field}
form={form}
options={options}
isClearable={true}
value={options.find((opt) => opt.value === field.value)}
onChange={(option) => form.setFieldValue(field.name, option?.value || '')}
/>
)
}}
</Field>
</FormItem>
<FormItem label={translate('::blog.posts.categories.icon')}>
<Field type="text" name="icon" component={Input} />
</FormItem>
<FormItem label={translate('::blog.posts.categories.order')}>
<Field type="number" name="displayOrder" placeholder="0" component={Input} />
</FormItem>
<FormItem
label={translate('::blog.posts.categories.status')}
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
? translate('::blog.posts.categories.update')
: translate('::blog.posts.categories.create')}
</Button>
<Button variant="plain" onClick={() => setCategoryModalVisible(false)}>
{translate('::Cancel')}
</Button>
</div>
</FormItem>
</FormContainer>
</Form>
)}
</Formik>
</Dialog>
{/* Post Silme Onayı */}
<ConfirmDialog
isOpen={!!confirmDeletePost}
type="danger"
title={translate('::DeleteConfirmation')}
confirmText={translate('::Delete')}
cancelText={translate('::Cancel')}
confirmButtonColor="red-600"
onCancel={() => setConfirmDeletePost(null)}
onConfirm={async () => {
if (confirmDeletePost) {
await handleDelete(confirmDeletePost.id)
setConfirmDeletePost(null)
}
}}
>
<p>
<span className="font-semibold">{confirmDeletePost?.title}</span>{' '}
{translate('::DeleteConfirmation')}
</p>
</ConfirmDialog>
{/* Kategori Silme Onayı */}
<ConfirmDialog
isOpen={!!confirmDeleteCategory}
type="danger"
title={translate('::DeleteConfirmation')}
confirmText={translate('::Delete')}
cancelText={translate('::Cancel')}
confirmButtonColor="red-600"
onCancel={() => setConfirmDeleteCategory(null)}
onConfirm={async () => {
if (confirmDeleteCategory) {
await handleDeleteCategory(confirmDeleteCategory.id)
setConfirmDeleteCategory(null)
}
}}
>
<p>
<span className="font-semibold">{confirmDeleteCategory?.name}</span>{' '}
{translate('::DeleteConfirmation')}
</p>
</ConfirmDialog>
</>
)
}
export default BlogManagement