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",
|
||||
"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",
|
||||
"key": "App.Forum.TopicManagement.NewTopic",
|
||||
|
|
@ -697,6 +709,30 @@
|
|||
"en": "Post Management",
|
||||
"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",
|
||||
"key": "App.Forum.PostManagement.NewPost",
|
||||
|
|
@ -844,7 +880,7 @@
|
|||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "DeleteConfirmation",
|
||||
"en": "Silmek istediğinize emin misiniz?",
|
||||
"en": "Are you sure you want to delete?",
|
||||
"tr": "Silmek istediğinize emin misiniz?"
|
||||
},
|
||||
{
|
||||
|
|
@ -2685,7 +2721,7 @@
|
|||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Sirket",
|
||||
"key": "Organization",
|
||||
"en": "Organization",
|
||||
"tr": "Kurum"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
|||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.ru7ltd9thg8"
|
||||
"revision": "0.kii9phg4rp8"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
|
|
|||
|
|
@ -225,11 +225,11 @@ const Login = () => {
|
|||
{isMultiTenant && (
|
||||
<>
|
||||
<label className="form-label mb-2" style={tenantStyle}>
|
||||
{translate('::Sirket')}
|
||||
{translate('::Organization')}
|
||||
</label>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
placeholder={translate('::Sirket')}
|
||||
placeholder={translate('::Organization')}
|
||||
value={tenantName}
|
||||
onChange={(e) => setTenantName(e.target.value)}
|
||||
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 { useStoreState } from '@/store/store'
|
||||
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 {
|
||||
categories: ForumCategory[]
|
||||
|
|
@ -47,6 +52,24 @@ export function CategoryManagement({
|
|||
tenantId: '',
|
||||
})
|
||||
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 = () => {
|
||||
setFormData({
|
||||
|
|
@ -149,121 +172,158 @@ export function CategoryManagement({
|
|||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
{editingCategory
|
||||
? translate('::App.Forum.CategoryManagement.EditCategory')
|
||||
: translate('::App.Forum.CategoryManagement.CreateCategory')}
|
||||
: translate('::App.Forum.CategoryManagement.AddCategory')}
|
||||
</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>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.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"
|
||||
required
|
||||
<FormItem
|
||||
label="Name"
|
||||
asterisk
|
||||
invalid={errors.name && touched.name}
|
||||
errorMessage={errors.name}
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.slug}
|
||||
onChange={(e) => setFormData({ ...formData, slug: 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
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label="Slug"
|
||||
asterisk
|
||||
invalid={errors.slug && touched.slug}
|
||||
errorMessage={errors.slug}
|
||||
>
|
||||
<Field
|
||||
name="slug"
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
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"
|
||||
required
|
||||
<FormItem
|
||||
label="Description"
|
||||
asterisk
|
||||
invalid={errors.slug && touched.slug}
|
||||
errorMessage={errors.slug}
|
||||
>
|
||||
<Field
|
||||
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>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Icon (Emoji)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.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"
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormItem
|
||||
label="Icon (Emoji)"
|
||||
asterisk
|
||||
invalid={errors.icon && touched.icon}
|
||||
errorMessage={errors.icon}
|
||||
>
|
||||
<Field
|
||||
name="icon"
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||
placeholder="💬"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Display Order
|
||||
</label>
|
||||
<input
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label="Display Order"
|
||||
asterisk
|
||||
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"
|
||||
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>
|
||||
<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>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetForm}
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50"
|
||||
>
|
||||
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 className="flex items-center space-x-4 pt-6">
|
||||
<FormItem label="Status">
|
||||
<Field as={Checkbox} name="isActive" />
|
||||
</FormItem>
|
||||
<FormItem label="Locked">
|
||||
<Field as={Checkbox} name="isLocked" />
|
||||
</FormItem>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Categories List */}
|
||||
<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">
|
||||
<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>
|
||||
|
||||
{loading ? (
|
||||
<div className="p-8 text-center">
|
||||
<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 className="divide-y divide-gray-200">
|
||||
|
|
@ -339,7 +399,7 @@ export function CategoryManagement({
|
|||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(category.id)}
|
||||
onClick={() => confirmDeleteCategory(category)}
|
||||
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
||||
title={translate('::App.Forum.CategoryManagement.DeleteCategory')}
|
||||
>
|
||||
|
|
@ -352,6 +412,34 @@ export function CategoryManagement({
|
|||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ import ReactQuill from 'react-quill'
|
|||
import 'react-quill/dist/quill.snow.css'
|
||||
import { useStoreState } from '@/store/store'
|
||||
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 {
|
||||
posts: ForumPost[]
|
||||
|
|
@ -44,6 +48,13 @@ export function PostManagement({
|
|||
isAcceptedAnswer: 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 = () => {
|
||||
setFormData({
|
||||
|
|
@ -130,6 +141,16 @@ export function PostManagement({
|
|||
}).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 (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -154,14 +175,45 @@ export function PostManagement({
|
|||
? translate('::App.Forum.PostManagement.EditPost')
|
||||
: translate('::App.Forum.PostManagement.AddPost')}
|
||||
</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Topic</label>
|
||||
<select
|
||||
value={formData.topicId}
|
||||
onChange={(e) => setFormData({ ...formData, topicId: 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
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
topicId: editingPost?.topicId || '',
|
||||
content: editingPost?.content || '',
|
||||
isAcceptedAnswer: editingPost?.isAcceptedAnswer || false,
|
||||
parentPostId: '',
|
||||
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>
|
||||
{topics.map((topic) => (
|
||||
|
|
@ -169,51 +221,63 @@ export function PostManagement({
|
|||
{topic.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
|
||||
<FormItem
|
||||
label="Content"
|
||||
asterisk
|
||||
invalid={!!errors.content && touched.content}
|
||||
errorMessage={errors.content}
|
||||
>
|
||||
<Field name="content">
|
||||
{({ field }: FieldProps) => (
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
value={field.value}
|
||||
onChange={(val) => setFieldValue('content', val)}
|
||||
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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isAcceptedAnswer}
|
||||
onChange={(e) => setFormData({ ...formData, isAcceptedAnswer: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Field type="checkbox" name="isAcceptedAnswer" className="mr-2" />
|
||||
Mark as Accepted Answer
|
||||
</label>
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
<div className="flex justify-end space-x-3 pt-2">
|
||||
<Button
|
||||
variant="plain"
|
||||
type="button"
|
||||
onClick={resetForm}
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50"
|
||||
onClick={() => {
|
||||
setShowCreateForm(false)
|
||||
setEditingPost(null)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
{translate('::Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
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"
|
||||
loading={isSubmitting}
|
||||
disabled={
|
||||
!values.topicId ||
|
||||
!values.content ||
|
||||
values.content.replace(/<[^>]+>/g, '').trim() === ''
|
||||
}
|
||||
>
|
||||
{submitting && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
<span>{editingPost ? 'Update' : 'Create'}</span>
|
||||
</button>
|
||||
{editingPost ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -300,7 +364,7 @@ export function PostManagement({
|
|||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(post.id)}
|
||||
onClick={() => confirmDeletePost(post)}
|
||||
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
||||
title={translate('::App.Forum.PostManagement.DeletePost')}
|
||||
>
|
||||
|
|
@ -313,6 +377,34 @@ export function PostManagement({
|
|||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ import {
|
|||
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum'
|
||||
import { useStoreState } from '@/store/store'
|
||||
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 {
|
||||
topics: ForumTopic[]
|
||||
|
|
@ -66,7 +70,8 @@ export function TopicManagement({
|
|||
tenantId: '',
|
||||
})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [topicToDelete, setTopicToDelete] = useState<ForumTopic | null>(null)
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
title: '',
|
||||
|
|
@ -81,6 +86,11 @@ export function TopicManagement({
|
|||
setEditingTopic(null)
|
||||
}
|
||||
|
||||
const confirmDeleteTopic = (topic: ForumTopic) => {
|
||||
setTopicToDelete(topic)
|
||||
setShowConfirm(true)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (submitting) return
|
||||
|
|
@ -182,6 +192,22 @@ export function TopicManagement({
|
|||
}).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 (
|
||||
<div className="space-y-6">
|
||||
<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">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
{editingTopic
|
||||
? translate('::App.Forum.TopicManagement.AddTopic')
|
||||
: translate('::App.Forum.TopicManagement.EditTopic')}
|
||||
? translate('::App.Forum.TopicManagement.EditTopic')
|
||||
: translate('::App.Forum.TopicManagement.AddTopic')}
|
||||
</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>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
||||
<select
|
||||
value={formData.categoryId}
|
||||
onChange={(e) => setFormData({ ...formData, categoryId: 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
|
||||
<Formik
|
||||
initialValues={
|
||||
editingTopic
|
||||
? {
|
||||
title: editingTopic.title,
|
||||
content: editingTopic.content,
|
||||
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>
|
||||
{categories.map((category) => (
|
||||
<option key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
{categories.map((cat) => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
|
||||
<textarea
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||
<FormItem
|
||||
label="Content"
|
||||
asterisk
|
||||
invalid={!!errors.content && touched.content}
|
||||
errorMessage={errors.content}
|
||||
>
|
||||
<Field
|
||||
as="textarea"
|
||||
name="content"
|
||||
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"
|
||||
required
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2"
|
||||
/>
|
||||
</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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isPinned}
|
||||
onChange={(e) => setFormData({ ...formData, isPinned: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Field type="checkbox" name="isPinned" className="mr-2" />
|
||||
Pinned
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isLocked}
|
||||
onChange={(e) => setFormData({ ...formData, isLocked: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Field type="checkbox" name="isLocked" className="mr-2" />
|
||||
Locked
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isSolved}
|
||||
onChange={(e) => setFormData({ ...formData, isSolved: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Field type="checkbox" name="isSolved" className="mr-2" />
|
||||
Solved
|
||||
</label>
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
<div className="flex justify-end space-x-3 pt-2">
|
||||
<Button
|
||||
variant="plain"
|
||||
type="button"
|
||||
onClick={resetForm}
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50"
|
||||
onClick={() => {
|
||||
setShowCreateForm(false)
|
||||
setEditingTopic(null)
|
||||
}}
|
||||
>
|
||||
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>{editingTopic ? 'Update' : 'Create'}</span>
|
||||
</button>
|
||||
{translate('::Cancel')}
|
||||
</Button>
|
||||
<Button variant="solid" type="submit" loading={isSubmitting}>
|
||||
{editingTopic ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Topics List */}
|
||||
<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">
|
||||
<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>
|
||||
|
||||
{loading ? (
|
||||
<div className="p-8 text-center">
|
||||
<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 className="divide-y divide-gray-200">
|
||||
|
|
@ -405,7 +463,7 @@ export function TopicManagement({
|
|||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(topic.id)}
|
||||
onClick={() => confirmDeleteTopic(topic)}
|
||||
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
||||
title={translate('::App.Forum.TopicManagement.DeleteTopic')}
|
||||
>
|
||||
|
|
@ -418,6 +476,34 @@ export function TopicManagement({
|
|||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { Formik, Form, Field, FieldProps } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import ReactQuill from 'react-quill'
|
||||
import 'react-quill/dist/quill.snow.css'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button, FormContainer, FormItem } from '@/components/ui'
|
||||
|
||||
interface CreatePostModalProps {
|
||||
onClose: () => void
|
||||
|
|
@ -11,17 +14,34 @@ interface CreatePostModalProps {
|
|||
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) {
|
||||
const { translate } = useLocalization()
|
||||
const [content, setContent] = useState('')
|
||||
const { tenant } = useStoreState((state) => state.auth)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const plainText = content.replace(/<[^>]+>/g, '').trim() // HTML etiketlerini temizle
|
||||
const handleSubmit = (
|
||||
values: { content: string },
|
||||
{ setSubmitting }: { setSubmitting: (isSubmitting: boolean) => void },
|
||||
) => {
|
||||
const plainText = values.content.replace(/<[^>]+>/g, '').trim()
|
||||
if (plainText) {
|
||||
onSubmit({ content, parentPostId, tenantId: tenant.tenantId })
|
||||
onSubmit({
|
||||
content: values.content,
|
||||
parentPostId,
|
||||
tenantId: tenant.tenantId,
|
||||
})
|
||||
}
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -38,37 +58,60 @@ export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostM
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{parentPostId ? 'Your Reply' : 'Message'}
|
||||
</label>
|
||||
<Formik
|
||||
initialValues={postInitialValues}
|
||||
validationSchema={postValidationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ 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
|
||||
theme="snow"
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
style={{ height: '400px', marginBottom: '50px' }}
|
||||
placeholder={parentPostId ? 'Write your reply...' : 'Write your message...'}
|
||||
value={field.value}
|
||||
onChange={(val) => setFieldValue('content', val)}
|
||||
style={{ height: '300px', marginBottom: '50px' }}
|
||||
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">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<Button variant="plain" type="button" onClick={onClose}>
|
||||
{translate('::Cancel')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors disabled:opacity-50"
|
||||
disabled={!content || content.replace(/<[^>]+>/g, '').trim() === ''}
|
||||
loading={isSubmitting}
|
||||
disabled={
|
||||
!values.content || values.content.replace(/<[^>]+>/g, '').trim() === ''
|
||||
}
|
||||
>
|
||||
{translate('::App.Forum.PostManagement.PostReply')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export function CreateTopicModal({ onClose, onSubmit }: CreateTopicModalProps) {
|
|||
<Form>
|
||||
<FormContainer className="space-y-4">
|
||||
<FormItem
|
||||
label={translate('::App.Forum.TopicManagement.Title')}
|
||||
label={translate('::App.Forum.TopicManagement.Baslik')}
|
||||
invalid={errors.title && touched.title}
|
||||
errorMessage={errors.title}
|
||||
>
|
||||
|
|
|
|||
Loading…
Reference in a new issue