diff --git a/.env b/.env index e982ef0..6056929 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ VITE_TOKEN_NAME = 'admin_token' VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token' -VITE_BASE_URL = 'https://api.danakcorp.com' -# VITE_BASE_URL = 'http://192.168.0.216:4000' \ No newline at end of file +# VITE_BASE_URL = 'https://api.danakcorp.com' +VITE_BASE_URL = 'http://192.168.1.108:4000' \ No newline at end of file diff --git a/src/config/Pages.ts b/src/config/Pages.ts index 146f501..568fd2c 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -79,6 +79,7 @@ export const Pages = { list: "/blog/list", create: "/blog/create", category: "/blog/category", + detail: "/blog/detail/", }, messages: { list: "/messages/list", diff --git a/src/langs/fa.json b/src/langs/fa.json index 9849a5a..e0807b3 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -333,7 +333,7 @@ "category": "دسته بندی", "new_blog": "افزودن بلاگ", "blog_title": "عنوان بلاگ", - "blog_Summary": "خلاصه بلاگ", + "blog_summary": "خلاصه بلاگ", "content": "محتوا", "blog_status": "وضعیت بلاگ", "featured_image": "تصویر شاخص", @@ -367,7 +367,17 @@ "discount_code": "کد تخفیف", "direct_discount": "تخفیف مستقیم", "fixed_amount": "مبلغ ثابت", - "percent": "درصد" + "percent": "درصد", + "enter_your_summary": "خلاصه بلاگ را وارد کنید", + "enter_your_content": "محتوای بلاگ را وارد کنید", + "blog_content": "محتوای بلاگ", + "enter_tags": "برچسب ها را وارد کنید", + "enter_your_slug": "اسلاگ بلاگ را وارد کنید", + "slug": "اسلاگ", + "meta_title": "عنوان متا", + "enter_your_meta_title": "عنوان متا بلاگ را وارد کنید", + "meta_description": "توضیحات متا", + "enter_your_meta_description": "توضیحات متا بلاگ را وارد کنید" }, "receip": { "receips": "صورتحساب ها", diff --git a/src/pages/blog/BlogList.tsx b/src/pages/blog/BlogList.tsx index 60e46c1..71c648c 100644 --- a/src/pages/blog/BlogList.tsx +++ b/src/pages/blog/BlogList.tsx @@ -1,16 +1,21 @@ -import { FC } from 'react' +import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '../../components/Button' -import { Add, Eye } from 'iconsax-react' +import { Add, Eye, Trash } from 'iconsax-react' import Td from '../../components/Td' import { Link } from 'react-router-dom' import { Pages } from '../../config/Pages' -import SwitchComponent from '../../components/Switch' -import DatePickerComponent from '../../components/DatePicker' -import Select from '../../components/Select' import Input from '../../components/Input' +import { useGetBlogs, useDeleteBlog } from './hooks/useBlogData' +import { BlogItemType } from './types/BlogTypes' +import moment from 'moment-jalaali' const BlogList: FC = () => { + const { t } = useTranslation('global') + const [search, setSearch] = useState('') + const getBlogs = useGetBlogs(search); + const deleteBlog = useDeleteBlog() + return (
@@ -37,7 +42,7 @@ const BlogList: FC = () => {
-
+ {/*
+
+
+
-

{t('blog.blog_Summary')}

+

{t('blog.blog_summary')}

-
+
+ { + formik.touched.previewContent && formik.errors.previewContent && ( +

{formik.errors.previewContent}

+ ) + } +

{t('blog.content')}

-
+
+ { + formik.touched.content && formik.errors.content && ( +

{formik.errors.content}

+ ) + } + +
+ +
+ +
+ +
+ +
@@ -63,46 +189,91 @@ const CreateBlog: FC = () => { {t('blog.blog_status')}

-
- {t('ads.deactive')} -
{ }} + active={formik.values.isActive} + onChange={(value) => formik.setFieldValue('isActive', value)} + /> +
+
+
+

+ پین +

+
+ formik.setFieldValue('isPinned', value)} />

{t('blog.category')}

-
-
- -
-
- لورم ایپسوم -
-
-
-
- -
-
- لورم ایپسوم -
-
-
-
- -
-
- لورم ایپسوم -
+ { + getBlogCategories.data?.data?.categories?.map((item: BlogCategoryType) => { + return ( +
+ formik.setFieldValue('categoryId', e.target.checked ? item.id : '')} + /> +
{item.title}
+
+ ) + }) + } +
+
+

{t('blog.featured_image')}

+
+ setFile(files[0])} + />
-

{t('blog.tags')}

- + { + formik.setFieldValue('tagInput', e.target.value); + formik.setFieldError('tags', undefined); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const newTag = formik.values.tagInput?.trim(); + if (newTag) { + if (formik.values.tags.includes(newTag)) { + formik.setFieldError('tags', t('blog.duplicate_tag_error')); + } else { + formik.setFieldValue('tags', [...formik.values.tags, newTag]); + formik.setFieldValue('tagInput', ''); + } + } + } + }} + error_text={typeof formik.errors.tags === 'string' ? formik.errors.tags : undefined} + /> +
+ {formik.values.tags.map((tag, index) => ( +
+ {tag} + +
+ ))} +

{t('blog.tag_hint')}

diff --git a/src/pages/blog/Update.tsx b/src/pages/blog/Update.tsx new file mode 100644 index 0000000..9744aac --- /dev/null +++ b/src/pages/blog/Update.tsx @@ -0,0 +1,308 @@ +import { FC, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Input from '../../components/Input' +import Button from '../../components/Button' +import SwitchComponent from '../../components/Switch' +import { TickCircle } from 'iconsax-react' +import "quill/dist/quill.snow.css"; +// import Quill from 'quill'; +import { useFormik } from 'formik' +import { BlogCategoryType, CreateBlogType } from './types/BlogTypes' +import { useGetBlogCategories, useGetBlogDetail, useUpdateBlog } from './hooks/useBlogData' +import * as Yup from 'yup' +import CheckBoxComponent from '../../components/CheckBoxComponent' +import UploadBoxDraggble from '../../components/UploadBoxDraggble' +import Quill from 'quill' +import { useSingleUpload } from '../service/hooks/useServiceData' +import { toast } from 'react-toastify' +import { ErrorType } from '../../helpers/types' +import { Pages } from '../../config/Pages' +import { useNavigate, useParams } from 'react-router-dom' +const UpdateBlog: FC = () => { + + const { id } = useParams() + const { t } = useTranslation('global') + const [file, setFile] = useState() + const singleSlug = useSingleUpload() + const editorRef = useRef(null) + const editorRef2 = useRef(null) + const getBlogDetail = useGetBlogDetail(id || '') + useEffect(() => { + const quill = new Quill('#editor', { + theme: 'snow', + }); + + quill.on('text-change', () => { + const html = editorRef.current?.querySelector('.ql-editor')?.innerHTML || ''; + formik.setFieldValue('previewContent', html); + }); + const quill2 = new Quill('#editor2', { + theme: 'snow', + }); + + quill2.on('text-change', () => { + const html = editorRef2.current?.querySelector('.ql-editor')?.innerHTML || ''; + formik.setFieldValue('content', html); + }); + + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const navigate = useNavigate() + const updateBlog = useUpdateBlog() + const getBlogCategories = useGetBlogCategories() + const formik = useFormik({ + initialValues: { + categoryId: '', + title: '', + previewContent: '', + content: '', + imageUrl: '', + isActive: false, + isPinned: false, + metaDescription: '', + metaTitle: '', + tags: [], + tagInput: '', + slug: '', + }, + validationSchema: Yup.object({ + title: Yup.string().required(t('errors.required')), + previewContent: Yup.string().required(t('errors.required')), + content: Yup.string().required(t('errors.required')), + categoryId: Yup.string().required(t('errors.required')), + metaDescription: Yup.string().required(t('errors.required')), + metaTitle: Yup.string().required(t('errors.required')), + tags: Yup.array().of(Yup.string()).required(t('errors.required')), + slug: Yup.string().required(t('errors.required')), + }), + onSubmit: async (values) => { + if (file) { + const formData = new FormData() + formData.append('file', file) + await singleSlug.mutateAsync(formData, { + onSuccess(data) { + values.imageUrl = data.data.url + }, + }) + } + + + updateBlog.mutate({ id: id || '', params: values }, { + onSuccess() { + toast.success(t('success')) + navigate(Pages.blog.list) + }, + onError(error: ErrorType) { + toast.error(error.response?.data?.error.message[0]) + } + }) + }, + }) + + useEffect(() => { + if (getBlogDetail.data) { + formik.setValues(getBlogDetail.data?.data?.blog) + formik.setFieldValue('categoryId', getBlogDetail.data?.data?.blog.category.id) + + const editorElement = editorRef.current?.querySelector('.ql-editor'); + if (editorElement) { + editorElement.innerHTML = getBlogDetail.data?.data?.blog?.previewContent || ''; + } + + const editorElement2 = editorRef2.current?.querySelector('.ql-editor'); + if (editorElement2) { + editorElement2.innerHTML = getBlogDetail.data?.data?.blog?.content || ''; + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getBlogDetail.data]) + + + return ( +
+
+
+ {t('blog.add_blog')} +
+ +
+
+
+
+ +
+
+ +
+ +

{t('blog.blog_summary')}

+
+
+
+ + { + formik.touched.previewContent && formik.errors.previewContent && ( +

{formik.errors.previewContent}

+ ) + } + +

{t('blog.content')}

+
+
+
+ + { + formik.touched.content && formik.errors.content && ( +

{formik.errors.content}

+ ) + } + +
+ +
+ +
+ +
+ + +
+ + +
+
+

+ {t('blog.blog_status')} +

+
+ formik.setFieldValue('isActive', value)} + /> +
+
+
+

+ پین +

+
+ formik.setFieldValue('isPinned', value)} + /> +
+
+

{t('blog.category')}

+
+ { + getBlogCategories.data?.data?.categories?.map((item: BlogCategoryType) => { + return ( +
+ formik.setFieldValue('categoryId', e.target.checked ? item.id : '')} + /> +
{item.title}
+
+ ) + }) + } +
+
+

{t('blog.featured_image')}

+
+ setFile(files[0])} + /> +
+
+

{t('blog.tags')}

+
+ { + formik.setFieldValue('tagInput', e.target.value); + formik.setFieldError('tags', undefined); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const newTag = formik.values.tagInput?.trim(); + if (newTag) { + if (formik.values.tags.includes(newTag)) { + formik.setFieldError('tags', t('blog.duplicate_tag_error')); + } else { + formik.setFieldValue('tags', [...formik.values.tags, newTag]); + formik.setFieldValue('tagInput', ''); + } + } + } + }} + error_text={typeof formik.errors.tags === 'string' ? formik.errors.tags : undefined} + /> +
+ {formik.values.tags.map((tag, index) => ( +
+ {tag} + +
+ ))} +
+

+ {t('blog.tag_hint')} +

+
+ +
+
+ +
+ ) +} + +export default UpdateBlog \ No newline at end of file diff --git a/src/pages/blog/hooks/useBlogData.ts b/src/pages/blog/hooks/useBlogData.ts new file mode 100644 index 0000000..89ab8f9 --- /dev/null +++ b/src/pages/blog/hooks/useBlogData.ts @@ -0,0 +1,43 @@ +import * as api from "../service/BlogService"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { CreateBlogType } from "../types/BlogTypes"; + +export const useGetBlogCategories = () => { + return useQuery({ + queryKey: ["blog-categories"], + queryFn: api.getBlogCategories, + }); +}; + +export const useGetBlogs = (search: string) => { + return useQuery({ + queryKey: ["blogs", search], + queryFn: () => api.getBlogs(search), + }); +}; + +export const useGetBlogDetail = (id: string) => { + return useQuery({ + queryKey: ["blog", id], + queryFn: () => api.getBlogDetail(id), + }); +}; + +export const useCreateBlog = () => { + return useMutation({ + mutationFn: api.createBlog, + }); +}; + +export const useUpdateBlog = () => { + return useMutation({ + mutationFn: (data: { id: string; params: CreateBlogType }) => + api.updateBlog(data.id, data.params), + }); +}; + +export const useDeleteBlog = () => { + return useMutation({ + mutationFn: api.deleteBlog, + }); +}; diff --git a/src/pages/blog/service/BlogService.ts b/src/pages/blog/service/BlogService.ts new file mode 100644 index 0000000..1d90ff9 --- /dev/null +++ b/src/pages/blog/service/BlogService.ts @@ -0,0 +1,32 @@ +import axios from "../../../config/axios"; +import { CreateBlogType } from "../types/BlogTypes"; + +export const getBlogCategories = async () => { + const { data } = await axios.get("/blogs/categories/list"); + return data; +}; + +export const getBlogs = async (search: string) => { + const { data } = await axios.get(`/blogs/list?q=${search}`); + return data; +}; + +export const getBlogDetail = async (id: string) => { + const { data } = await axios.get(`/blogs/${id}`); + return data; +}; + +export const createBlog = async (params: CreateBlogType) => { + const { data } = await axios.post("/blogs", params); + return data; +}; + +export const updateBlog = async (id: string, params: CreateBlogType) => { + const { data } = await axios.patch(`/blogs/${id}`, params); + return data; +}; + +export const deleteBlog = async (id: string) => { + const { data } = await axios.delete(`/blogs/${id}`); + return data; +}; diff --git a/src/pages/blog/types/BlogTypes.ts b/src/pages/blog/types/BlogTypes.ts new file mode 100644 index 0000000..7931016 --- /dev/null +++ b/src/pages/blog/types/BlogTypes.ts @@ -0,0 +1,42 @@ +export type CreateBlogType = { + title: string; + previewContent: string; + content: string; + imageUrl: string; + metaTitle: string; + metaDescription: string; + tags: string[]; + tagInput: string; + categoryId: string; + isActive: boolean; + isPinned: boolean; + slug: string; +}; + +export type BlogItemType = { + id: string; + createdAt: string; + author: { + id: string; + firstName: string; + lastName: string; + }; + category: { + id: string; + title: string; + iconUrl: string; + }; + imageUrl: string; + metaTitle: string; + metaDescription: string; + previewContent: string; + slug: string; + title: string; + viewCount: number; +}; + +export type BlogCategoryType = { + id: string; + title: string; + iconUrl: string; +}; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index e239c3a..eb4de0c 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -58,6 +58,7 @@ import Reviews from '../pages/service/Reviews' import UpdateAds from '../pages/ads/Update' import UpdateService from '../pages/service/UpdateService' import UpdateCustomer from '../pages/customer/Update' +import UpdateBlog from '../pages/blog/Update' const MainRouter: FC = () => { @@ -98,6 +99,7 @@ const MainRouter: FC = () => { } /> } /> } /> + } /> } /> } /> } />