541 lines
17 KiB
TypeScript
541 lines
17 KiB
TypeScript
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 Switcher from '@/components/ui/Switcher'
|
||
import { HiPlus, HiPencil, HiTrash, HiEye, HiLockClosed, HiLockOpen } from 'react-icons/hi'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import { format } from 'date-fns'
|
||
import { tr } from 'date-fns/locale'
|
||
import { Field, Form, Formik } from 'formik'
|
||
import * as Yup from 'yup'
|
||
import toast from '@/components/ui/toast'
|
||
import Notification from '@/components/ui/Notification'
|
||
import {
|
||
CreateUpdateForumCategoryDto,
|
||
ForumCategory,
|
||
forumService,
|
||
ForumTopic,
|
||
} from '@/services/forum.service'
|
||
import THead from '@/components/ui/Table/THead'
|
||
import Tr from '@/components/ui/Table/Tr'
|
||
import Th from '@/components/ui/Table/Th'
|
||
import TBody from '@/components/ui/Table/TBody'
|
||
import Td from '@/components/ui/Table/Td'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
import { Helmet } from 'react-helmet'
|
||
|
||
const categoryValidationSchema = Yup.object().shape({
|
||
name: Yup.string().required('İsim gereklidir'),
|
||
slug: Yup.string().required('Slug gereklidir'),
|
||
description: Yup.string().required('Açıklama gereklidir'),
|
||
})
|
||
|
||
const ForumManagement = () => {
|
||
const { translate } = useLocalization()
|
||
const navigate = useNavigate()
|
||
const [activeTab, setActiveTab] = useState<'topics' | 'categories'>('topics')
|
||
const [categories, setCategories] = useState<ForumCategory[]>([])
|
||
const [topics, setTopics] = useState<ForumTopic[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [categoryModalVisible, setCategoryModalVisible] = useState(false)
|
||
const [editingCategory, setEditingCategory] = useState<ForumCategory | null>(null)
|
||
|
||
useEffect(() => {
|
||
loadData()
|
||
}, [activeTab])
|
||
|
||
const loadData = async () => {
|
||
setLoading(true)
|
||
try {
|
||
if (activeTab === 'categories') {
|
||
const data = await forumService.getCategories()
|
||
setCategories(data)
|
||
} else {
|
||
const data = await forumService.getTopics({ pageSize: 100 })
|
||
setTopics(data.items)
|
||
}
|
||
} catch (error) {
|
||
toast.push(
|
||
<Notification title="Hata" type="danger">
|
||
Veriler yüklenirken hata oluştu
|
||
</Notification>,
|
||
{
|
||
placement: 'top-center',
|
||
},
|
||
)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleCreateCategory = () => {
|
||
setEditingCategory(null)
|
||
setCategoryModalVisible(true)
|
||
}
|
||
|
||
const handleEditCategory = (category: ForumCategory) => {
|
||
setEditingCategory(category)
|
||
setCategoryModalVisible(true)
|
||
}
|
||
|
||
const handleDeleteCategory = async (id: string) => {
|
||
try {
|
||
await forumService.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: CreateUpdateForumCategoryDto = {
|
||
name: values.name,
|
||
slug: values.slug,
|
||
description: values.description,
|
||
icon: values.icon,
|
||
displayOrder: values.displayOrder,
|
||
isActive: values.isActive,
|
||
isLocked: values.isLocked,
|
||
}
|
||
|
||
if (editingCategory) {
|
||
await forumService.updateCategory(editingCategory.id, data)
|
||
toast.push(
|
||
<Notification title="Başarılı" type="success">
|
||
Kategori güncellendi
|
||
</Notification>,
|
||
{
|
||
placement: 'top-center',
|
||
},
|
||
)
|
||
} else {
|
||
await forumService.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 handleToggleLock = async (topic: ForumTopic) => {
|
||
try {
|
||
if (topic.isLocked) {
|
||
await forumService.unlockTopic(topic.id)
|
||
toast.push(
|
||
<Notification title="Başarılı" type="success">
|
||
Konu kilidi açıldı
|
||
</Notification>,
|
||
{
|
||
placement: 'top-center',
|
||
},
|
||
)
|
||
} else {
|
||
await forumService.lockTopic(topic.id)
|
||
toast.push(
|
||
<Notification title="Başarılı" type="success">
|
||
Konu kilitlendi
|
||
</Notification>,
|
||
{
|
||
placement: 'top-center',
|
||
},
|
||
)
|
||
}
|
||
loadData()
|
||
} catch (error) {
|
||
toast.push(
|
||
<Notification title="Hata" type="danger">
|
||
İşlem başarısız
|
||
</Notification>,
|
||
{
|
||
placement: 'top-center',
|
||
},
|
||
)
|
||
}
|
||
}
|
||
|
||
const handleTogglePin = async (topic: ForumTopic) => {
|
||
try {
|
||
if (topic.isPinned) {
|
||
await forumService.unpinTopic(topic.id)
|
||
toast.push(
|
||
<Notification title="Başarılı" type="success">
|
||
Sabitleme kaldırıldı
|
||
</Notification>,
|
||
{
|
||
placement: 'top-center',
|
||
},
|
||
)
|
||
} else {
|
||
await forumService.pinTopic(topic.id)
|
||
toast.push(
|
||
<Notification title="Başarılı" type="success">
|
||
Konu sabitlendi
|
||
</Notification>,
|
||
{
|
||
placement: 'top-center',
|
||
},
|
||
)
|
||
}
|
||
loadData()
|
||
} catch (error) {
|
||
toast.push(
|
||
<Notification title="Hata" type="danger">
|
||
İşlem başarısız
|
||
</Notification>,
|
||
{
|
||
placement: 'top-center',
|
||
},
|
||
)
|
||
}
|
||
}
|
||
|
||
const initialCategoryValues = editingCategory
|
||
? {
|
||
name: editingCategory.name,
|
||
slug: editingCategory.slug,
|
||
description: editingCategory.description,
|
||
icon: editingCategory.icon || '',
|
||
displayOrder: editingCategory.displayOrder,
|
||
isActive: editingCategory.isActive,
|
||
isLocked: editingCategory.isLocked,
|
||
}
|
||
: {
|
||
name: '',
|
||
slug: '',
|
||
description: '',
|
||
icon: '',
|
||
displayOrder: 0,
|
||
isActive: true,
|
||
isLocked: false,
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Helmet
|
||
titleTemplate="%s | Kurs Platform"
|
||
title={translate('::' + 'Forum Management')}
|
||
defaultTitle="Kurs Platform"
|
||
></Helmet>
|
||
<Card>
|
||
<div className="mb-4">
|
||
<div className="flex gap-4 border-b">
|
||
<button
|
||
className={`pb-2 px-1 ${activeTab === 'topics' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
|
||
onClick={() => setActiveTab('topics')}
|
||
>
|
||
<b>Konular</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 === 'categories' ? (
|
||
<Table>
|
||
<THead>
|
||
<Tr>
|
||
<Th>İsim</Th>
|
||
<Th>Slug</Th>
|
||
<Th>Açıklama</Th>
|
||
<Th>Konu Sayısı</Th>
|
||
<Th>Mesaj Sayısı</Th>
|
||
<Th>Durum</Th>
|
||
<Th>Kilit</Th>
|
||
<Th>
|
||
<Button
|
||
variant="solid"
|
||
size="xs"
|
||
icon={<HiPlus />}
|
||
onClick={handleCreateCategory}
|
||
>
|
||
Yeni
|
||
</Button>
|
||
</Th>
|
||
</Tr>
|
||
</THead>
|
||
<TBody>
|
||
{loading ? (
|
||
<Tr>
|
||
<Td colSpan={8} className="text-center">
|
||
Yükleniyor...
|
||
</Td>
|
||
</Tr>
|
||
) : (
|
||
categories.map((category) => (
|
||
<Tr key={category.id}>
|
||
<Td>
|
||
<div className="flex items-center">
|
||
{category.icon && <span className="mr-2">{category.icon}</span>}
|
||
<span className="font-medium">{category.name}</span>
|
||
</div>
|
||
</Td>
|
||
<Td>{category.slug}</Td>
|
||
<Td>{category.description}</Td>
|
||
<Td>{category.topicCount}</Td>
|
||
<Td>{category.postCount}</Td>
|
||
<Td>
|
||
<Tag
|
||
className={
|
||
category.isActive
|
||
? 'bg-green-100 text-green-800'
|
||
: 'bg-red-100 text-red-800'
|
||
}
|
||
>
|
||
{category.isActive ? 'Aktif' : 'Pasif'}
|
||
</Tag>
|
||
</Td>
|
||
<Td>
|
||
{category.isLocked ? (
|
||
<HiLockClosed className="text-red-500" />
|
||
) : (
|
||
<HiLockOpen className="text-green-500" />
|
||
)}
|
||
</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>
|
||
) : (
|
||
<Table>
|
||
<THead>
|
||
<Tr>
|
||
<Th>Başlık</Th>
|
||
<Th>Kategori</Th>
|
||
<Th>Yazar</Th>
|
||
<Th>Görüntülenme</Th>
|
||
<Th>Cevap</Th>
|
||
<Th>Son Aktivite</Th>
|
||
<Th>İşlemler</Th>
|
||
</Tr>
|
||
</THead>
|
||
<TBody>
|
||
{loading ? (
|
||
<Tr>
|
||
<Td colSpan={7} className="text-center">
|
||
Yükleniyor...
|
||
</Td>
|
||
</Tr>
|
||
) : (
|
||
topics.map((topic) => (
|
||
<Tr key={topic.id}>
|
||
<Td>
|
||
<div>
|
||
<a
|
||
className="text-blue-600 hover:underline cursor-pointer font-medium"
|
||
onClick={() => navigate(`/forum/topic/${topic.slug || topic.id}`)}
|
||
>
|
||
{topic.title}
|
||
</a>
|
||
<div className="flex gap-2 mt-1">
|
||
{topic.isPinned && (
|
||
<Tag className="bg-yellow-100 text-yellow-800 text-xs">Sabit</Tag>
|
||
)}
|
||
{topic.isLocked && (
|
||
<Tag className="bg-red-100 text-red-800 text-xs">Kilitli</Tag>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Td>
|
||
<Td>{topic.category?.name}</Td>
|
||
<Td>{topic.author?.name}</Td>
|
||
<Td>{topic.viewCount}</Td>
|
||
<Td>{topic.replyCount}</Td>
|
||
<Td>
|
||
{topic.lastActivityAt
|
||
? format(new Date(topic.lastActivityAt), 'dd MMM yyyy HH:mm', {
|
||
locale: tr,
|
||
})
|
||
: '-'}
|
||
</Td>
|
||
<Td>
|
||
<div className="flex gap-2">
|
||
<Button
|
||
size="sm"
|
||
icon={<HiEye />}
|
||
onClick={() => navigate(`/forum/topic/${topic.slug || topic.id}`)}
|
||
/>
|
||
<Switcher
|
||
checked={topic.isPinned}
|
||
onChange={() => handleTogglePin(topic)}
|
||
checkedContent="📌"
|
||
unCheckedContent="📌"
|
||
/>
|
||
<Switcher
|
||
checked={topic.isLocked}
|
||
onChange={() => handleToggleLock(topic)}
|
||
checkedContent="🔒"
|
||
unCheckedContent="🔓"
|
||
/>
|
||
</div>
|
||
</Td>
|
||
</Tr>
|
||
))
|
||
)}
|
||
</TBody>
|
||
</Table>
|
||
)}
|
||
</Card>
|
||
|
||
<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
|
||
label="İsim"
|
||
invalid={errors.name && touched.name}
|
||
errorMessage={errors.name}
|
||
>
|
||
<Field type="text" name="name" placeholder="Kategori ismi" component={Input} />
|
||
</FormItem>
|
||
|
||
<FormItem
|
||
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>
|
||
<Field name="isActive">
|
||
{({ field, form }: any) => (
|
||
<Switcher
|
||
{...field}
|
||
onChange={(checked) => form.setFieldValue(field.name, checked)}
|
||
checkedContent="Aktif"
|
||
unCheckedContent="Pasif"
|
||
/>
|
||
)}
|
||
</Field>
|
||
</FormItem>
|
||
|
||
<FormItem>
|
||
<Field name="isLocked">
|
||
{({ field, form }: any) => (
|
||
<Switcher
|
||
{...field}
|
||
onChange={(checked) => form.setFieldValue(field.name, checked)}
|
||
checkedContent="Kilitli"
|
||
unCheckedContent="Açık"
|
||
/>
|
||
)}
|
||
</Field>
|
||
</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 ForumManagement
|