Bütün formlar Formik olarak değiştirildi.
This commit is contained in:
parent
6157b9b320
commit
b6527e7b36
9 changed files with 637 additions and 1173 deletions
|
|
@ -643,6 +643,18 @@
|
||||||
"en": "Topic Management",
|
"en": "Topic Management",
|
||||||
"tr": "Konu Yönetimi"
|
"tr": "Konu Yönetimi"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Forum.TopicManagement.Baslik",
|
||||||
|
"en": "Topic",
|
||||||
|
"tr": "Konu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Forum.TopicManagement.Content",
|
||||||
|
"en": "Content",
|
||||||
|
"tr": "İçerik"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Forum.TopicManagement.NewTopic",
|
"key": "App.Forum.TopicManagement.NewTopic",
|
||||||
|
|
@ -697,6 +709,30 @@
|
||||||
"en": "Post Management",
|
"en": "Post Management",
|
||||||
"tr": "Yazı Yönetimi"
|
"tr": "Yazı Yönetimi"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Forum.PostManagement.BaslikEdit",
|
||||||
|
"en": "Your Reply",
|
||||||
|
"tr": "Cevabınız"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Forum.PostManagement.BaslikNew",
|
||||||
|
"en": "Message",
|
||||||
|
"tr": "İçerik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Forum.PostManagement.MessageEdit",
|
||||||
|
"en": "Write your reply...",
|
||||||
|
"tr": "Cevabınızı yazın..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Forum.PostManagement.MessageNew",
|
||||||
|
"en": "Write your message...",
|
||||||
|
"tr": "Mesajınızı yazın..."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Forum.PostManagement.NewPost",
|
"key": "App.Forum.PostManagement.NewPost",
|
||||||
|
|
@ -844,7 +880,7 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "DeleteConfirmation",
|
"key": "DeleteConfirmation",
|
||||||
"en": "Silmek istediğinize emin misiniz?",
|
"en": "Are you sure you want to delete?",
|
||||||
"tr": "Silmek istediğinize emin misiniz?"
|
"tr": "Silmek istediğinize emin misiniz?"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -2685,7 +2721,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "Sirket",
|
"key": "Organization",
|
||||||
"en": "Organization",
|
"en": "Organization",
|
||||||
"tr": "Kurum"
|
"tr": "Kurum"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.ru7ltd9thg8"
|
"revision": "0.kii9phg4rp8"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|
|
||||||
|
|
@ -225,11 +225,11 @@ const Login = () => {
|
||||||
{isMultiTenant && (
|
{isMultiTenant && (
|
||||||
<>
|
<>
|
||||||
<label className="form-label mb-2" style={tenantStyle}>
|
<label className="form-label mb-2" style={tenantStyle}>
|
||||||
{translate('::Sirket')}
|
{translate('::Organization')}
|
||||||
</label>
|
</label>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder={translate('::Sirket')}
|
placeholder={translate('::Organization')}
|
||||||
value={tenantName}
|
value={tenantName}
|
||||||
onChange={(e) => setTenantName(e.target.value)}
|
onChange={(e) => setTenantName(e.target.value)}
|
||||||
style={tenantStyle}
|
style={tenantStyle}
|
||||||
|
|
|
||||||
|
|
@ -1,881 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -3,6 +3,11 @@ import { Plus, Edit2, Trash2, Lock, Unlock, Eye, EyeOff, Loader2 } from 'lucide-
|
||||||
import { ForumCategory } from '@/proxy/forum/forum'
|
import { ForumCategory } from '@/proxy/forum/forum'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Formik, Form, Field } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { FormContainer, FormItem } from '@/components/ui/Form'
|
||||||
|
import { Input, Checkbox, Button } from '@/components/ui'
|
||||||
|
import { ConfirmDialog } from '@/components/shared'
|
||||||
|
|
||||||
interface CategoryManagementProps {
|
interface CategoryManagementProps {
|
||||||
categories: ForumCategory[]
|
categories: ForumCategory[]
|
||||||
|
|
@ -47,6 +52,24 @@ export function CategoryManagement({
|
||||||
tenantId: '',
|
tenantId: '',
|
||||||
})
|
})
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
|
const [categoryToDelete, setCategoryToDelete] = useState<ForumCategory | null>(null)
|
||||||
|
|
||||||
|
// Validation şeması
|
||||||
|
const CategorySchema = Yup.object().shape({
|
||||||
|
name: Yup.string().required('Name is required'),
|
||||||
|
slug: Yup.string().required('Slug is required'),
|
||||||
|
description: Yup.string().required('Description is required'),
|
||||||
|
icon: Yup.string(),
|
||||||
|
displayOrder: Yup.number().required('Display order is required'),
|
||||||
|
isActive: Yup.boolean(),
|
||||||
|
isLocked: Yup.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const confirmDeleteCategory = (category: ForumCategory) => {
|
||||||
|
setCategoryToDelete(category)
|
||||||
|
setShowConfirm(true)
|
||||||
|
}
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|
@ -149,121 +172,158 @@ export function CategoryManagement({
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
{editingCategory
|
{editingCategory
|
||||||
? translate('::App.Forum.CategoryManagement.EditCategory')
|
? translate('::App.Forum.CategoryManagement.EditCategory')
|
||||||
: translate('::App.Forum.CategoryManagement.CreateCategory')}
|
: translate('::App.Forum.CategoryManagement.AddCategory')}
|
||||||
</h3>
|
</h3>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
name: editingCategory?.name ?? '',
|
||||||
|
slug: editingCategory?.slug ?? '',
|
||||||
|
description: editingCategory?.description ?? '',
|
||||||
|
icon: editingCategory?.icon ?? '',
|
||||||
|
displayOrder: editingCategory?.displayOrder ?? 0,
|
||||||
|
isActive: editingCategory?.isActive ?? true,
|
||||||
|
isLocked: editingCategory?.isLocked ?? false,
|
||||||
|
tenantId: tenant?.tenantId ?? '',
|
||||||
|
}}
|
||||||
|
validationSchema={CategorySchema}
|
||||||
|
onSubmit={async (values, { setSubmitting, resetForm }) => {
|
||||||
|
try {
|
||||||
|
setSubmitting(true)
|
||||||
|
if (editingCategory) {
|
||||||
|
await onUpdateCategory(editingCategory.id, values)
|
||||||
|
} else {
|
||||||
|
await onCreateCategory(values)
|
||||||
|
}
|
||||||
|
resetForm()
|
||||||
|
setShowCreateForm(false)
|
||||||
|
setEditingCategory(null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting form:', error)
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ errors, touched, isSubmitting, values }) => (
|
||||||
|
<Form>
|
||||||
|
<FormContainer className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<FormItem
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
label="Name"
|
||||||
<input
|
asterisk
|
||||||
type="text"
|
invalid={errors.name && touched.name}
|
||||||
value={formData.name}
|
errorMessage={errors.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
>
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
<Field
|
||||||
required
|
name="name"
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormItem>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Slug</label>
|
<FormItem
|
||||||
<input
|
label="Slug"
|
||||||
type="text"
|
asterisk
|
||||||
value={formData.slug}
|
invalid={errors.slug && touched.slug}
|
||||||
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
|
errorMessage={errors.slug}
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
>
|
||||||
required
|
<Field
|
||||||
|
name="slug"
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<FormItem
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
label="Description"
|
||||||
<textarea
|
asterisk
|
||||||
value={formData.description}
|
invalid={errors.slug && touched.slug}
|
||||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
errorMessage={errors.slug}
|
||||||
rows={3}
|
>
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
<Field
|
||||||
required
|
name="description"
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
textArea="true"
|
||||||
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormItem>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<FormItem
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Icon (Emoji)</label>
|
label="Icon (Emoji)"
|
||||||
<input
|
asterisk
|
||||||
type="text"
|
invalid={errors.icon && touched.icon}
|
||||||
value={formData.icon}
|
errorMessage={errors.icon}
|
||||||
onChange={(e) => setFormData({ ...formData, icon: e.target.value })}
|
>
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
<Field
|
||||||
|
name="icon"
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
placeholder="💬"
|
placeholder="💬"
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormItem>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<FormItem
|
||||||
Display Order
|
label="Display Order"
|
||||||
</label>
|
asterisk
|
||||||
<input
|
invalid={errors.displayOrder && touched.displayOrder}
|
||||||
|
errorMessage={errors.displayOrder}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
name="displayOrder"
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
placeholder="💬"
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.displayOrder}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...formData, displayOrder: parseInt(e.target.value) })
|
|
||||||
}
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormItem>
|
||||||
<div className="flex items-center space-x-4 pt-6">
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.isActive}
|
|
||||||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
Active
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.isLocked}
|
|
||||||
onChange={(e) => setFormData({ ...formData, isLocked: e.target.checked })}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
Locked
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3">
|
<div className="flex items-center space-x-4 pt-6">
|
||||||
<button
|
<FormItem label="Status">
|
||||||
type="button"
|
<Field as={Checkbox} name="isActive" />
|
||||||
onClick={resetForm}
|
</FormItem>
|
||||||
disabled={submitting}
|
<FormItem label="Locked">
|
||||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50"
|
<Field as={Checkbox} name="isLocked" />
|
||||||
>
|
</FormItem>
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={submitting}
|
|
||||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{submitting && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
||||||
<span>{editingCategory ? 'Update' : 'Create'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
<div className="flex justify-end space-x-3 mt-4">
|
||||||
|
<div className="flex justify-end space-x-3 pt-2">
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setShowCreateForm(false)
|
||||||
|
setEditingCategory(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate('::Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="solid" type="submit" loading={isSubmitting}>
|
||||||
|
{editingCategory ? 'Update' : 'Create'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormContainer>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Categories List */}
|
{/* Categories List */}
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-200">
|
<div className="px-6 py-4 border-b border-gray-200">
|
||||||
<h3 className="text-lg font-semibold text-gray-900">{translate('::App.Forum.CategoryManagement.Categories')} ({categories.length})</h3>
|
<h3 className="text-lg font-semibold text-gray-900">
|
||||||
|
{translate('::App.Forum.CategoryManagement.Categories')} ({categories.length})
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
|
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
|
||||||
<p className="text-gray-500">{translate('::App.Forum.CategoryManagement.Loadingcategories')}</p>
|
<p className="text-gray-500">
|
||||||
|
{translate('::App.Forum.CategoryManagement.Loadingcategories')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-200">
|
<div className="divide-y divide-gray-200">
|
||||||
|
|
@ -339,7 +399,7 @@ export function CategoryManagement({
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(category.id)}
|
onClick={() => confirmDeleteCategory(category)}
|
||||||
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
||||||
title={translate('::App.Forum.CategoryManagement.DeleteCategory')}
|
title={translate('::App.Forum.CategoryManagement.DeleteCategory')}
|
||||||
>
|
>
|
||||||
|
|
@ -352,6 +412,34 @@ export function CategoryManagement({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{categoryToDelete && (
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={showConfirm}
|
||||||
|
type="danger"
|
||||||
|
title={translate('::DeleteConfirmation')}
|
||||||
|
confirmText={translate('::Delete')}
|
||||||
|
cancelText={translate('::Cancel')}
|
||||||
|
confirmButtonColor="red-600"
|
||||||
|
onCancel={() => {
|
||||||
|
setShowConfirm(false)
|
||||||
|
setCategoryToDelete(null)
|
||||||
|
}}
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
if (categoryToDelete) {
|
||||||
|
await onDeleteCategory(categoryToDelete.id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting category:', error)
|
||||||
|
} finally {
|
||||||
|
setShowConfirm(false)
|
||||||
|
setCategoryToDelete(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</ConfirmDialog>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ import ReactQuill from 'react-quill'
|
||||||
import 'react-quill/dist/quill.snow.css'
|
import 'react-quill/dist/quill.snow.css'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Formik, Form, Field, FieldProps } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { FormContainer, FormItem, Button } from '@/components/ui'
|
||||||
|
import { ConfirmDialog } from '@/components/shared'
|
||||||
|
|
||||||
interface PostManagementProps {
|
interface PostManagementProps {
|
||||||
posts: ForumPost[]
|
posts: ForumPost[]
|
||||||
|
|
@ -44,6 +48,13 @@ export function PostManagement({
|
||||||
isAcceptedAnswer: false,
|
isAcceptedAnswer: false,
|
||||||
})
|
})
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
|
const [postToDelete, setPostToDelete] = useState<ForumPost | null>(null)
|
||||||
|
|
||||||
|
const confirmDeletePost = (post: ForumPost) => {
|
||||||
|
setPostToDelete(post)
|
||||||
|
setShowConfirm(true)
|
||||||
|
}
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|
@ -130,6 +141,16 @@ export function PostManagement({
|
||||||
}).format(date)
|
}).format(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const postValidationSchema = Yup.object().shape({
|
||||||
|
topicId: Yup.string().required('Topic is required'),
|
||||||
|
content: Yup.string()
|
||||||
|
.test('not-empty', 'Content is required', (value) => {
|
||||||
|
const plainText = value?.replace(/<[^>]+>/g, '').trim()
|
||||||
|
return !!plainText
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -154,14 +175,45 @@ export function PostManagement({
|
||||||
? translate('::App.Forum.PostManagement.EditPost')
|
? translate('::App.Forum.PostManagement.EditPost')
|
||||||
: translate('::App.Forum.PostManagement.AddPost')}
|
: translate('::App.Forum.PostManagement.AddPost')}
|
||||||
</h3>
|
</h3>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
<Formik
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Topic</label>
|
initialValues={{
|
||||||
<select
|
topicId: editingPost?.topicId || '',
|
||||||
value={formData.topicId}
|
content: editingPost?.content || '',
|
||||||
onChange={(e) => setFormData({ ...formData, topicId: e.target.value })}
|
isAcceptedAnswer: editingPost?.isAcceptedAnswer || false,
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
parentPostId: '',
|
||||||
required
|
tenantId: tenant.tenantId || '',
|
||||||
|
}}
|
||||||
|
validationSchema={postValidationSchema}
|
||||||
|
enableReinitialize
|
||||||
|
onSubmit={async (values, { setSubmitting, resetForm }) => {
|
||||||
|
try {
|
||||||
|
if (editingPost) {
|
||||||
|
await onUpdatePost(editingPost.id, values)
|
||||||
|
} else {
|
||||||
|
await onCreatePost(values)
|
||||||
|
}
|
||||||
|
resetForm()
|
||||||
|
setShowCreateForm(false)
|
||||||
|
setEditingPost(null)
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ values, setFieldValue, errors, touched, isSubmitting }) => (
|
||||||
|
<Form>
|
||||||
|
<FormContainer className="space-y-4">
|
||||||
|
<FormItem
|
||||||
|
label="Topic"
|
||||||
|
asterisk
|
||||||
|
invalid={!!errors.topicId && touched.topicId}
|
||||||
|
errorMessage={errors.topicId}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
as="select"
|
||||||
|
name="topicId"
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
>
|
>
|
||||||
<option value="">Select a topic</option>
|
<option value="">Select a topic</option>
|
||||||
{topics.map((topic) => (
|
{topics.map((topic) => (
|
||||||
|
|
@ -169,51 +221,63 @@ export function PostManagement({
|
||||||
{topic.title}
|
{topic.title}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</Field>
|
||||||
</div>
|
</FormItem>
|
||||||
|
|
||||||
<div>
|
<FormItem
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
|
label="Content"
|
||||||
|
asterisk
|
||||||
|
invalid={!!errors.content && touched.content}
|
||||||
|
errorMessage={errors.content}
|
||||||
|
>
|
||||||
|
<Field name="content">
|
||||||
|
{({ field }: FieldProps) => (
|
||||||
<ReactQuill
|
<ReactQuill
|
||||||
theme="snow"
|
theme="snow"
|
||||||
value={content}
|
value={field.value}
|
||||||
onChange={setContent}
|
onChange={(val) => setFieldValue('content', val)}
|
||||||
style={{ height: '400px', marginBottom: '50px' }}
|
style={{ height: '400px', marginBottom: '50px' }}
|
||||||
placeholder={'Write your message...'}
|
placeholder="Write your message..."
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<FormItem label="Accepted Answer">
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
<input
|
<Field type="checkbox" name="isAcceptedAnswer" className="mr-2" />
|
||||||
type="checkbox"
|
|
||||||
checked={formData.isAcceptedAnswer}
|
|
||||||
onChange={(e) => setFormData({ ...formData, isAcceptedAnswer: e.target.checked })}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
Mark as Accepted Answer
|
Mark as Accepted Answer
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</FormItem>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3">
|
<div className="flex justify-end space-x-3 pt-2">
|
||||||
<button
|
<Button
|
||||||
|
variant="plain"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={resetForm}
|
onClick={() => {
|
||||||
disabled={submitting}
|
setShowCreateForm(false)
|
||||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50"
|
setEditingPost(null)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant="solid"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
loading={isSubmitting}
|
||||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
disabled={
|
||||||
|
!values.topicId ||
|
||||||
|
!values.content ||
|
||||||
|
values.content.replace(/<[^>]+>/g, '').trim() === ''
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{submitting && <Loader2 className="w-4 h-4 animate-spin" />}
|
{editingPost ? 'Update' : 'Create'}
|
||||||
<span>{editingPost ? 'Update' : 'Create'}</span>
|
</Button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</FormContainer>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -300,7 +364,7 @@ export function PostManagement({
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(post.id)}
|
onClick={() => confirmDeletePost(post)}
|
||||||
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
||||||
title={translate('::App.Forum.PostManagement.DeletePost')}
|
title={translate('::App.Forum.PostManagement.DeletePost')}
|
||||||
>
|
>
|
||||||
|
|
@ -313,6 +377,34 @@ export function PostManagement({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{postToDelete && (
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={showConfirm}
|
||||||
|
type="danger"
|
||||||
|
title={translate('::DeleteConfirmation')}
|
||||||
|
confirmText={translate('::Delete')}
|
||||||
|
cancelText={translate('::Cancel')}
|
||||||
|
confirmButtonColor="red-600"
|
||||||
|
onCancel={() => {
|
||||||
|
setShowConfirm(false)
|
||||||
|
setPostToDelete(null)
|
||||||
|
}}
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
if (postToDelete) {
|
||||||
|
await onDeletePost(postToDelete.id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting post:', error)
|
||||||
|
} finally {
|
||||||
|
setShowConfirm(false)
|
||||||
|
setPostToDelete(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</ConfirmDialog>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ import {
|
||||||
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum'
|
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Formik, Form, Field } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { FormContainer, FormItem, Button } from '@/components/ui'
|
||||||
|
import { ConfirmDialog } from '@/components/shared'
|
||||||
|
|
||||||
interface TopicManagementProps {
|
interface TopicManagementProps {
|
||||||
topics: ForumTopic[]
|
topics: ForumTopic[]
|
||||||
|
|
@ -66,7 +70,8 @@ export function TopicManagement({
|
||||||
tenantId: '',
|
tenantId: '',
|
||||||
})
|
})
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
|
const [topicToDelete, setTopicToDelete] = useState<ForumTopic | null>(null)
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
title: '',
|
title: '',
|
||||||
|
|
@ -81,6 +86,11 @@ export function TopicManagement({
|
||||||
setEditingTopic(null)
|
setEditingTopic(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirmDeleteTopic = (topic: ForumTopic) => {
|
||||||
|
setTopicToDelete(topic)
|
||||||
|
setShowConfirm(true)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (submitting) return
|
if (submitting) return
|
||||||
|
|
@ -182,6 +192,22 @@ export function TopicManagement({
|
||||||
}).format(date)
|
}).format(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const topicInitialValues = {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
categoryId: '',
|
||||||
|
isPinned: false,
|
||||||
|
isLocked: false,
|
||||||
|
isSolved: false,
|
||||||
|
tenantId: tenant.tenantId || '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const topicValidationSchema = Yup.object().shape({
|
||||||
|
title: Yup.string().required(),
|
||||||
|
content: Yup.string().required(),
|
||||||
|
categoryId: Yup.string().required(),
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -203,111 +229,143 @@ export function TopicManagement({
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
{editingTopic
|
{editingTopic
|
||||||
? translate('::App.Forum.TopicManagement.AddTopic')
|
? translate('::App.Forum.TopicManagement.EditTopic')
|
||||||
: translate('::App.Forum.TopicManagement.EditTopic')}
|
: translate('::App.Forum.TopicManagement.AddTopic')}
|
||||||
</h3>
|
</h3>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Title</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.title}
|
|
||||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Formik
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
initialValues={
|
||||||
<select
|
editingTopic
|
||||||
value={formData.categoryId}
|
? {
|
||||||
onChange={(e) => setFormData({ ...formData, categoryId: e.target.value })}
|
title: editingTopic.title,
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
content: editingTopic.content,
|
||||||
required
|
categoryId: editingTopic.categoryId,
|
||||||
|
isPinned: editingTopic.isPinned,
|
||||||
|
isLocked: editingTopic.isLocked,
|
||||||
|
isSolved: editingTopic.isSolved,
|
||||||
|
tenantId: tenant.tenantId || '',
|
||||||
|
}
|
||||||
|
: topicInitialValues
|
||||||
|
}
|
||||||
|
validationSchema={topicValidationSchema}
|
||||||
|
enableReinitialize
|
||||||
|
onSubmit={async (values, { setSubmitting, resetForm }) => {
|
||||||
|
try {
|
||||||
|
if (editingTopic) {
|
||||||
|
await onUpdateTopic(editingTopic.id, values)
|
||||||
|
} else {
|
||||||
|
await onCreateTopic(values)
|
||||||
|
}
|
||||||
|
resetForm()
|
||||||
|
setShowCreateForm(false)
|
||||||
|
setEditingTopic(null)
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, errors, touched }) => (
|
||||||
|
<Form>
|
||||||
|
<FormContainer className="space-y-4">
|
||||||
|
<FormItem
|
||||||
|
label="Title"
|
||||||
|
asterisk
|
||||||
|
invalid={!!errors.title && touched.title}
|
||||||
|
errorMessage={errors.title}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
name="title"
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Category"
|
||||||
|
asterisk
|
||||||
|
invalid={!!errors.categoryId && touched.categoryId}
|
||||||
|
errorMessage={errors.categoryId}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
as="select"
|
||||||
|
name="categoryId"
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
>
|
>
|
||||||
<option value="">Select a category</option>
|
<option value="">Select a category</option>
|
||||||
{categories.map((category) => (
|
{categories.map((cat) => (
|
||||||
<option key={category.id} value={category.id}>
|
<option key={cat.id} value={cat.id}>
|
||||||
{category.name}
|
{cat.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</Field>
|
||||||
</div>
|
</FormItem>
|
||||||
|
|
||||||
<div>
|
<FormItem
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
|
label="Content"
|
||||||
<textarea
|
asterisk
|
||||||
value={formData.content}
|
invalid={!!errors.content && touched.content}
|
||||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
errorMessage={errors.content}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
as="textarea"
|
||||||
|
name="content"
|
||||||
rows={6}
|
rows={6}
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormItem>
|
||||||
|
|
||||||
<div className="flex items-center space-x-6">
|
<FormItem label="Options">
|
||||||
|
<div className="flex items-center space-x-6 pt-1">
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
<input
|
<Field type="checkbox" name="isPinned" className="mr-2" />
|
||||||
type="checkbox"
|
|
||||||
checked={formData.isPinned}
|
|
||||||
onChange={(e) => setFormData({ ...formData, isPinned: e.target.checked })}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
Pinned
|
Pinned
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
<input
|
<Field type="checkbox" name="isLocked" className="mr-2" />
|
||||||
type="checkbox"
|
|
||||||
checked={formData.isLocked}
|
|
||||||
onChange={(e) => setFormData({ ...formData, isLocked: e.target.checked })}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
Locked
|
Locked
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
<input
|
<Field type="checkbox" name="isSolved" className="mr-2" />
|
||||||
type="checkbox"
|
|
||||||
checked={formData.isSolved}
|
|
||||||
onChange={(e) => setFormData({ ...formData, isSolved: e.target.checked })}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
Solved
|
Solved
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3">
|
<div className="flex justify-end space-x-3 pt-2">
|
||||||
<button
|
<Button
|
||||||
|
variant="plain"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={resetForm}
|
onClick={() => {
|
||||||
disabled={submitting}
|
setShowCreateForm(false)
|
||||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50"
|
setEditingTopic(null)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button variant="solid" type="submit" loading={isSubmitting}>
|
||||||
type="submit"
|
{editingTopic ? 'Update' : 'Create'}
|
||||||
disabled={submitting}
|
</Button>
|
||||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{submitting && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
||||||
<span>{editingTopic ? 'Update' : 'Create'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</FormContainer>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Topics List */}
|
{/* Topics List */}
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-200">
|
<div className="px-6 py-4 border-b border-gray-200">
|
||||||
<h3 className="text-lg font-semibold text-gray-900">{translate('::App.Forum.TopicManagement.Topics')} ({topics.length})</h3>
|
<h3 className="text-lg font-semibold text-gray-900">
|
||||||
|
{translate('::App.Forum.TopicManagement.Topics')} ({topics.length})
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
|
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
|
||||||
<p className="text-gray-500">{translate('::App.Forum.TopicManagement.Loadingtopics')}</p>
|
<p className="text-gray-500">
|
||||||
|
{translate('::App.Forum.TopicManagement.Loadingtopics')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-200">
|
<div className="divide-y divide-gray-200">
|
||||||
|
|
@ -405,7 +463,7 @@ export function TopicManagement({
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(topic.id)}
|
onClick={() => confirmDeleteTopic(topic)}
|
||||||
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
||||||
title={translate('::App.Forum.TopicManagement.DeleteTopic')}
|
title={translate('::App.Forum.TopicManagement.DeleteTopic')}
|
||||||
>
|
>
|
||||||
|
|
@ -418,6 +476,34 @@ export function TopicManagement({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{topicToDelete && (
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={showConfirm}
|
||||||
|
type="danger"
|
||||||
|
title={translate('::DeleteConfirmation')}
|
||||||
|
confirmText={translate('::Delete')}
|
||||||
|
cancelText={translate('::Cancel')}
|
||||||
|
confirmButtonColor="red-600"
|
||||||
|
onCancel={() => {
|
||||||
|
setShowConfirm(false)
|
||||||
|
setTopicToDelete(null)
|
||||||
|
}}
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
if (topicToDelete) {
|
||||||
|
await onDeleteTopic(topicToDelete.id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting topic:', error)
|
||||||
|
} finally {
|
||||||
|
setShowConfirm(false)
|
||||||
|
setTopicToDelete(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</ConfirmDialog>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
import { X } from 'lucide-react'
|
import { X } from 'lucide-react'
|
||||||
|
import { Formik, Form, Field, FieldProps } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
import ReactQuill from 'react-quill'
|
import ReactQuill from 'react-quill'
|
||||||
import 'react-quill/dist/quill.snow.css'
|
import 'react-quill/dist/quill.snow.css'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Button, FormContainer, FormItem } from '@/components/ui'
|
||||||
|
|
||||||
interface CreatePostModalProps {
|
interface CreatePostModalProps {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
|
@ -11,17 +14,34 @@ interface CreatePostModalProps {
|
||||||
parentPostId: string
|
parentPostId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const postInitialValues = {
|
||||||
|
content: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const postValidationSchema = Yup.object().shape({
|
||||||
|
content: Yup.string().test('is-not-empty', 'İçerik zorunludur', (value) => {
|
||||||
|
const plainText = value?.replace(/<[^>]+>/g, '').trim()
|
||||||
|
return !!plainText
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostModalProps) {
|
export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostModalProps) {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const [content, setContent] = useState('')
|
|
||||||
const { tenant } = useStoreState((state) => state.auth)
|
const { tenant } = useStoreState((state) => state.auth)
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (
|
||||||
e.preventDefault()
|
values: { content: string },
|
||||||
const plainText = content.replace(/<[^>]+>/g, '').trim() // HTML etiketlerini temizle
|
{ setSubmitting }: { setSubmitting: (isSubmitting: boolean) => void },
|
||||||
|
) => {
|
||||||
|
const plainText = values.content.replace(/<[^>]+>/g, '').trim()
|
||||||
if (plainText) {
|
if (plainText) {
|
||||||
onSubmit({ content, parentPostId, tenantId: tenant.tenantId })
|
onSubmit({
|
||||||
|
content: values.content,
|
||||||
|
parentPostId,
|
||||||
|
tenantId: tenant.tenantId,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -38,37 +58,60 @@ export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostM
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
<Formik
|
||||||
<div>
|
initialValues={postInitialValues}
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
validationSchema={postValidationSchema}
|
||||||
{parentPostId ? 'Your Reply' : 'Message'}
|
onSubmit={handleSubmit}
|
||||||
</label>
|
>
|
||||||
|
{({ isSubmitting, errors, touched, values, setFieldValue }) => (
|
||||||
|
<Form>
|
||||||
|
<FormContainer className="p-6 space-y-4">
|
||||||
|
<FormItem
|
||||||
|
asterisk
|
||||||
|
label={
|
||||||
|
parentPostId
|
||||||
|
? translate('::App.Forum.PostManagement.BaslikEdit')
|
||||||
|
: translate('::App.Forum.PostManagement.BaslikNew')
|
||||||
|
}
|
||||||
|
invalid={!!errors.content && !!touched.content}
|
||||||
|
errorMessage={errors.content}
|
||||||
|
>
|
||||||
|
<Field name="content">
|
||||||
|
{({ field }: FieldProps) => (
|
||||||
<ReactQuill
|
<ReactQuill
|
||||||
theme="snow"
|
theme="snow"
|
||||||
value={content}
|
value={field.value}
|
||||||
onChange={setContent}
|
onChange={(val) => setFieldValue('content', val)}
|
||||||
style={{ height: '400px', marginBottom: '50px' }}
|
style={{ height: '300px', marginBottom: '50px' }}
|
||||||
placeholder={parentPostId ? 'Write your reply...' : 'Write your message...'}
|
placeholder={
|
||||||
|
parentPostId
|
||||||
|
? translate('::App.Forum.PostManagement.MessageEdit')
|
||||||
|
: translate('::App.Forum.PostManagement.MessageNew')
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 pt-4">
|
<div className="flex justify-end space-x-3 pt-4">
|
||||||
<button
|
<Button variant="plain" type="button" onClick={onClose}>
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant="solid"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors disabled:opacity-50"
|
loading={isSubmitting}
|
||||||
disabled={!content || content.replace(/<[^>]+>/g, '').trim() === ''}
|
disabled={
|
||||||
|
!values.content || values.content.replace(/<[^>]+>/g, '').trim() === ''
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{translate('::App.Forum.PostManagement.PostReply')}
|
{translate('::App.Forum.PostManagement.PostReply')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</FormContainer>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export function CreateTopicModal({ onClose, onSubmit }: CreateTopicModalProps) {
|
||||||
<Form>
|
<Form>
|
||||||
<FormContainer className="space-y-4">
|
<FormContainer className="space-y-4">
|
||||||
<FormItem
|
<FormItem
|
||||||
label={translate('::App.Forum.TopicManagement.Title')}
|
label={translate('::App.Forum.TopicManagement.Baslik')}
|
||||||
invalid={errors.title && touched.title}
|
invalid={errors.title && touched.title}
|
||||||
errorMessage={errors.title}
|
errorMessage={errors.title}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue