Bütün formlar Formik olarak değiştirildi.

This commit is contained in:
Sedat Öztürk 2025-06-27 22:46:34 +03:00
parent 6157b9b320
commit b6527e7b36
9 changed files with 637 additions and 1173 deletions

View file

@ -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"
},

View file

@ -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"), {

View file

@ -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}

View file

@ -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

View file

@ -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">
<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
/>
</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
/>
</div>
</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
/>
</div>
<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">
<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"
/>
</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"
placeholder="💬"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Display Order
</label>
<input
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>
</div>
<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"
/>
</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>
</form>
<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}
/>
</FormItem>
<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="💬"
/>
</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"
/>
</FormItem>
</div>
<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>
<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>
)
}

View file

@ -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,66 +175,109 @@ 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
>
<option value="">Select a topic</option>
{topics.map((topic) => (
<option key={topic.id} value={topic.id}>
{topic.title}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
<ReactQuill
theme="snow"
value={content}
onChange={setContent}
style={{ height: '400px', marginBottom: '50px' }}
placeholder={'Write your message...'}
/>
</div>
<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) => (
<option key={topic.id} value={topic.id}>
{topic.title}
</option>
))}
</Field>
</FormItem>
<div className="flex items-center">
<label className="flex items-center">
<input
type="checkbox"
checked={formData.isAcceptedAnswer}
onChange={(e) => setFormData({ ...formData, isAcceptedAnswer: e.target.checked })}
className="mr-2"
/>
Mark as Accepted Answer
</label>
</div>
<FormItem
label="Content"
asterisk
invalid={!!errors.content && touched.content}
errorMessage={errors.content}
>
<Field name="content">
{({ field }: FieldProps) => (
<ReactQuill
theme="snow"
value={field.value}
onChange={(val) => setFieldValue('content', val)}
style={{ height: '400px', marginBottom: '50px' }}
placeholder="Write your message..."
/>
)}
</Field>
</FormItem>
<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>{editingPost ? 'Update' : 'Create'}</span>
</button>
</div>
</form>
<FormItem label="Accepted Answer">
<label className="flex items-center">
<Field type="checkbox" name="isAcceptedAnswer" className="mr-2" />
Mark as Accepted Answer
</label>
</FormItem>
<div className="flex justify-end space-x-3 pt-2">
<Button
variant="plain"
type="button"
onClick={() => {
setShowCreateForm(false)
setEditingPost(null)
}}
>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
type="submit"
loading={isSubmitting}
disabled={
!values.topicId ||
!values.content ||
values.content.replace(/<[^>]+>/g, '').trim() === ''
}
>
{editingPost ? 'Update' : 'Create'}
</Button>
</div>
</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>
)
}

View file

@ -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
>
<option value="">Select a category</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
</div>
<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>
<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 })}
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
/>
</div>
<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((cat) => (
<option key={cat.id} value={cat.id}>
{cat.name}
</option>
))}
</Field>
</FormItem>
<div className="flex items-center space-x-6">
<label className="flex items-center">
<input
type="checkbox"
checked={formData.isPinned}
onChange={(e) => setFormData({ ...formData, isPinned: e.target.checked })}
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"
/>
Locked
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={formData.isSolved}
onChange={(e) => setFormData({ ...formData, isSolved: e.target.checked })}
className="mr-2"
/>
Solved
</label>
</div>
<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"
/>
</FormItem>
<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>{editingTopic ? 'Update' : 'Create'}</span>
</button>
</div>
</form>
<FormItem label="Options">
<div className="flex items-center space-x-6 pt-1">
<label className="flex items-center">
<Field type="checkbox" name="isPinned" className="mr-2" />
Pinned
</label>
<label className="flex items-center">
<Field type="checkbox" name="isLocked" className="mr-2" />
Locked
</label>
<label className="flex items-center">
<Field type="checkbox" name="isSolved" className="mr-2" />
Solved
</label>
</div>
</FormItem>
<div className="flex justify-end space-x-3 pt-2">
<Button
variant="plain"
type="button"
onClick={() => {
setShowCreateForm(false)
setEditingTopic(null)
}}
>
{translate('::Cancel')}
</Button>
<Button variant="solid" type="submit" loading={isSubmitting}>
{editingTopic ? 'Update' : 'Create'}
</Button>
</div>
</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>
)
}

View file

@ -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>
<ReactQuill
theme="snow"
value={content}
onChange={setContent}
style={{ height: '400px', marginBottom: '50px' }}
placeholder={parentPostId ? 'Write your reply...' : 'Write your message...'}
/>
</div>
<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={field.value}
onChange={(val) => setFieldValue('content', val)}
style={{ height: '300px', marginBottom: '50px' }}
placeholder={
parentPostId
? translate('::App.Forum.PostManagement.MessageEdit')
: translate('::App.Forum.PostManagement.MessageNew')
}
/>
)}
</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"
>
{translate('::Cancel')}
</button>
<button
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() === ''}
>
{translate('::App.Forum.PostManagement.PostReply')}
</button>
</div>
</form>
<div className="flex justify-end space-x-3 pt-4">
<Button variant="plain" type="button" onClick={onClose}>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
type="submit"
loading={isSubmitting}
disabled={
!values.content || values.content.replace(/<[^>]+>/g, '').trim() === ''
}
>
{translate('::App.Forum.PostManagement.PostReply')}
</Button>
</div>
</FormContainer>
</Form>
)}
</Formik>
</div>
</div>
)

View file

@ -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}
>