From 6bfc4259bc8b112752ef857028db0a9f6fccbfef Mon Sep 17 00:00:00 2001 From: aleksandarmicev Date: Mon, 16 Dec 2024 01:50:27 +0100 Subject: [PATCH 1/9] Made changes to include a draft status, providing different options for content managers or admins based on who is logged in. Admins now have the ability to publish blogs. Additionally, the status for API data has been changed to "Draft" because the API was returning an undefined status for fetched data. I believe this issue needs to be addressed by the backend team. --- app/content-panel/blogs/create/page.tsx | 20 ++- app/content-panel/blogs/page.tsx | 8 +- .../blog/PublishArticleForm.tsx | 24 +++- .../create-blogs/BlogListView.tsx | 127 +++++++++++++++--- .../reusable-table/ActionDropdown.tsx | 2 +- .../reusable-table/reusableTable.module.scss | 6 +- 6 files changed, 164 insertions(+), 23 deletions(-) diff --git a/app/content-panel/blogs/create/page.tsx b/app/content-panel/blogs/create/page.tsx index f0085e6d..304df713 100644 --- a/app/content-panel/blogs/create/page.tsx +++ b/app/content-panel/blogs/create/page.tsx @@ -1,15 +1,33 @@ +'use client'; + /* eslint-disable jsx-a11y/label-has-associated-control */ +import React from 'react'; +import { useSession } from 'next-auth/react'; import styles from './createArticlePage.module.scss'; import PublishArticleForm from '../../../../components/module-components/blog/PublishArticleForm'; +type UserRole = 'content-manager' | 'admin'; + const PostArticle = () => { + const { data: session, status } = useSession(); + + if (status === 'loading') { + return
Loading....
; + } + + if (!session || !session.user.role) { + return
Access denied
; + } + + const userRole = session.user.role as UserRole; + return (

Објави статија

- +
); diff --git a/app/content-panel/blogs/page.tsx b/app/content-panel/blogs/page.tsx index fb42b924..cbfdfd9f 100644 --- a/app/content-panel/blogs/page.tsx +++ b/app/content-panel/blogs/page.tsx @@ -1,8 +1,14 @@ import React from 'react'; import BlogListView from '../../../components/module-components/create-blogs/BlogListView'; +import PostArticle from './create/page'; const page = () => { - return ; + return ( +
+ + +
+ ); }; export default page; diff --git a/components/module-components/blog/PublishArticleForm.tsx b/components/module-components/blog/PublishArticleForm.tsx index cbde366a..141bbcf0 100644 --- a/components/module-components/blog/PublishArticleForm.tsx +++ b/components/module-components/blog/PublishArticleForm.tsx @@ -10,7 +10,11 @@ import TiptapEditor from '../../editor/TiptapEditor'; import TagManager from './TagManager'; import Button from '../../reusable-components/button/Button'; -const PublishArticleForm = () => { +interface PublishArticleFormProps { + userRole: 'content-manager' | 'admin'; +} + +const PublishArticleForm: React.FC = ({ userRole }) => { const addNewPostMutation = useAddNewPost(); const [selectedTags, setSelectedTags] = useState([]); @@ -40,6 +44,7 @@ const PublishArticleForm = () => { excerpt: '', content: '', tags: [], + status: 'draft', }} onSubmit={handleAddPost} > @@ -104,6 +109,23 @@ const PublishArticleForm = () => { {touched.tags && errors.tags &&
{errors.tags}
} +
+ + + + {userRole === 'content-manager' && } + {userRole === 'admin' && ( + <> + + + + )} + + {touched.status && errors.status &&
{errors.status}
} +
+ {addNewPostMutation.isPending ? ( diff --git a/components/reusable-components/reusable-table/reusableTable.module.scss b/components/reusable-components/reusable-table/reusableTable.module.scss index 100807d0..d63189be 100644 --- a/components/reusable-components/reusable-table/reusableTable.module.scss +++ b/components/reusable-components/reusable-table/reusableTable.module.scss @@ -25,14 +25,14 @@ $table-height: 400px; padding: 15px; white-space: nowrap; text-align: left; - overflow: hidden; + overflow: visible; } td { height: 60px; padding: 15px; - overflow: hidden; - white-space: nowrap; + overflow: visible; + white-space: wrap; } th:first-child, From d676c8ed0e4be95412b42e9d38c6f64542cc2dae Mon Sep 17 00:00:00 2001 From: aleksandarmicev Date: Mon, 16 Dec 2024 19:48:31 +0100 Subject: [PATCH 2/9] Correction of UserRole for Content Manager from "-" to "_", which now allows publishing blogs in Draft or InReview status. --- components/module-components/blog/PublishArticleForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/module-components/blog/PublishArticleForm.tsx b/components/module-components/blog/PublishArticleForm.tsx index 141bbcf0..82da26ac 100644 --- a/components/module-components/blog/PublishArticleForm.tsx +++ b/components/module-components/blog/PublishArticleForm.tsx @@ -11,7 +11,7 @@ import TagManager from './TagManager'; import Button from '../../reusable-components/button/Button'; interface PublishArticleFormProps { - userRole: 'content-manager' | 'admin'; + userRole: 'content_manager' | 'admin'; } const PublishArticleForm: React.FC = ({ userRole }) => { @@ -115,7 +115,7 @@ const PublishArticleForm: React.FC = ({ userRole }) => - {userRole === 'content-manager' && } + {userRole === 'content_manager' && } {userRole === 'admin' && ( <> From 8d10bb5a00956773ed3c1aebf0cbe46d4c90bdf2 Mon Sep 17 00:00:00 2001 From: aleksandarmicev Date: Tue, 17 Dec 2024 00:17:44 +0100 Subject: [PATCH 3/9] Removed component for testing --- app/content-panel/blogs/page.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/content-panel/blogs/page.tsx b/app/content-panel/blogs/page.tsx index cbfdfd9f..fb42b924 100644 --- a/app/content-panel/blogs/page.tsx +++ b/app/content-panel/blogs/page.tsx @@ -1,14 +1,8 @@ import React from 'react'; import BlogListView from '../../../components/module-components/create-blogs/BlogListView'; -import PostArticle from './create/page'; const page = () => { - return ( -
- - -
- ); + return ; }; export default page; From e01e764a042f08d3e3803a62990774fc25f70870 Mon Sep 17 00:00:00 2001 From: aleksandarmicev Date: Tue, 17 Dec 2024 00:28:36 +0100 Subject: [PATCH 4/9] Last correction of UserRole for Content Manager from "-" to "_", which now allows publishing blogs in Draft or InReview status. --- app/content-panel/blogs/create/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/content-panel/blogs/create/page.tsx b/app/content-panel/blogs/create/page.tsx index 304df713..7e11734a 100644 --- a/app/content-panel/blogs/create/page.tsx +++ b/app/content-panel/blogs/create/page.tsx @@ -7,7 +7,7 @@ import { useSession } from 'next-auth/react'; import styles from './createArticlePage.module.scss'; import PublishArticleForm from '../../../../components/module-components/blog/PublishArticleForm'; -type UserRole = 'content-manager' | 'admin'; +type UserRole = 'content_manager' | 'admin'; const PostArticle = () => { const { data: session, status } = useSession(); From 31b8aa2458b8c7c2673108fa3d12359e0b39ae89 Mon Sep 17 00:00:00 2001 From: aleksandarmicev Date: Sat, 21 Dec 2024 18:05:45 +0100 Subject: [PATCH 5/9] "With these changes, there are many updates. The User role is unified with the existing enum UserRole, and this impacts the entire project. Additionally, the use of ENDPOINTS for API data has been implemented, and the method of fetching data has been changed to using useMutation from react-query --- Types/index.ts | 5 +- app/content-panel/blogs/create/page.tsx | 16 +- .../SearchAndFilter/Filter.tsx | 7 +- .../blog/PublishArticleForm.tsx | 12 +- .../create-blogs/BlogListView.tsx | 137 +++++++----------- middleware.ts | 9 +- 6 files changed, 71 insertions(+), 115 deletions(-) diff --git a/Types/index.ts b/Types/index.ts index 9d7d1fae..38b5706d 100644 --- a/Types/index.ts +++ b/Types/index.ts @@ -3,10 +3,9 @@ import { MutationStatus, QueryStatus } from '@tanstack/react-query'; import { HTMLProps } from 'react'; -export enum Role { +export enum UserRole { admin = 'admin', content_manager = 'content_manager', - content = 'content', member = 'member', } @@ -14,7 +13,7 @@ export type UserType = { id: number | string; is_verified: boolean; email: string; - role: Role; + role: UserRole; }; export interface LoginParams { diff --git a/app/content-panel/blogs/create/page.tsx b/app/content-panel/blogs/create/page.tsx index 7e11734a..edfc6b25 100644 --- a/app/content-panel/blogs/create/page.tsx +++ b/app/content-panel/blogs/create/page.tsx @@ -6,21 +6,13 @@ import React from 'react'; import { useSession } from 'next-auth/react'; import styles from './createArticlePage.module.scss'; import PublishArticleForm from '../../../../components/module-components/blog/PublishArticleForm'; - -type UserRole = 'content_manager' | 'admin'; +import { UserRole } from '../../../../Types'; const PostArticle = () => { - const { data: session, status } = useSession(); - - if (status === 'loading') { - return
Loading....
; - } - - if (!session || !session.user.role) { - return
Access denied
; - } + const { data: session } = useSession(); - const userRole = session.user.role as UserRole; + // console.log('Session data:', session); + const userRole = session?.user.role as UserRole; return (
diff --git a/components/module-components/SearchAndFilter/Filter.tsx b/components/module-components/SearchAndFilter/Filter.tsx index 916a040c..86544a95 100644 --- a/components/module-components/SearchAndFilter/Filter.tsx +++ b/components/module-components/SearchAndFilter/Filter.tsx @@ -2,12 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import style from './filter.module.scss'; - -export enum UserRole { - Admin = 'admin', - Member = 'member', - ContentManager = 'content-manager', -} +import { UserRole } from '../../../Types'; interface FilterProps { handleRoleChange: (roles: UserRole[]) => void; diff --git a/components/module-components/blog/PublishArticleForm.tsx b/components/module-components/blog/PublishArticleForm.tsx index 82da26ac..605b4374 100644 --- a/components/module-components/blog/PublishArticleForm.tsx +++ b/components/module-components/blog/PublishArticleForm.tsx @@ -9,9 +9,10 @@ import styles from './PublishArticleForm.module.scss'; import TiptapEditor from '../../editor/TiptapEditor'; import TagManager from './TagManager'; import Button from '../../reusable-components/button/Button'; +import { UserRole } from '../../../Types'; interface PublishArticleFormProps { - userRole: 'content_manager' | 'admin'; + userRole: UserRole; } const PublishArticleForm: React.FC = ({ userRole }) => { @@ -115,13 +116,8 @@ const PublishArticleForm: React.FC = ({ userRole }) => - {userRole === 'content_manager' && } - {userRole === 'admin' && ( - <> - - - - )} + + {userRole === UserRole.admin && } {touched.status && errors.status &&
{errors.status}
}
diff --git a/components/module-components/create-blogs/BlogListView.tsx b/components/module-components/create-blogs/BlogListView.tsx index 557e1aaa..38d8a8bc 100644 --- a/components/module-components/create-blogs/BlogListView.tsx +++ b/components/module-components/create-blogs/BlogListView.tsx @@ -3,11 +3,15 @@ import React, { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useSession } from 'next-auth/react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'react-toastify'; import ReusableTable from '../../reusable-components/reusable-table/ReusableTable'; import BlogManagementControls from './BlogManagementControls'; import ActionDropdown from '../../reusable-components/reusable-table/ActionDropdown'; import style from './createBlogs.module.scss'; import { useEditor } from '../../../app/context/EditorContext'; +import { UserRole } from '../../../Types'; +import ENDPOINTS from '../../../apis/endpoints'; interface Author { first_name: string; @@ -37,19 +41,21 @@ const BlogListView = () => { const [searchTerm, setSearchTerm] = useState(''); const { editorStateChange } = useEditor(); const [data, setData] = useState([]); - const apiUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/blog-posts`; + const queryClient = useQueryClient(); const router = useRouter(); const { data: session } = useSession(); useEffect(() => { const fetchData = async () => { try { - const response = await fetch(apiUrl); + const response = await fetch(ENDPOINTS.BLOGS.GET_ALL(10, 0)); if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`); } const result = await response.json(); + // console.log('Result:', result); + const transformedData: BlogPost[] = result.data.map((item: BlogPostAPI) => ({ id: item.slug, title: item.title, @@ -66,7 +72,44 @@ const BlogListView = () => { }; fetchData(); - }, [apiUrl]); + }, []); + + const mutation = useMutation({ + mutationFn: async ({ id, newStatus }: { id: string; newStatus: string }) => { + const changeStatusUrl = new URL(`${ENDPOINTS.BLOGS.CREATE}/${id}/statuses`); + const body = { + status: newStatus, + publish_date: + newStatus === 'published' ? new Date().toISOString().split('T')[0] : undefined, + }; + + const response = await fetch(changeStatusUrl.toString(), { + method: 'PATCH', + headers: { + Authorization: `Bearer ${session?.accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to update blog status'); + } + return { id, newStatus }; + }, + onError: (error: any) => { + toast.error(error.message || 'Failed to update blog status'); + }, + onSuccess: ({ id, newStatus }: { id: string; newStatus: string }) => { + toast.success('Blog status updated successfully'); + setData((prevData) => + prevData.map((item) => (item.id === id ? { ...item, status: newStatus } : item)) + ); + queryClient.invalidateQueries({ queryKey: ['blog-posts'] }); + }, + }); const headers: (keyof BlogPost)[] = ['title', 'author', 'status']; const displayNames = { @@ -86,104 +129,34 @@ const BlogListView = () => { }; const handleChangeStatus = async (id: string, newStatus: string) => { - if (!session || !session.user) { - // eslint-disable-next-line no-console - console.error('Session is not available'); - return; - } - - try { - const changeStatusUrl = new URL( - `${process.env.NEXT_PUBLIC_API_BASE_URL}/blog-posts/${id}/statuses` - ); - - const requestHeaders = { - Authorization: `Bearer ${session.accessToken}`, - Accept: 'application/json', - 'Content-Type': 'application/json', - }; - - const body: { publish_date?: string; status: string } = { - status: newStatus, - }; - - if (newStatus === 'published') { - // eslint-disable-next-line prefer-destructuring - body.publish_date = new Date().toISOString().split('T')[0]; - } - - fetch(changeStatusUrl.toString(), { - method: 'PATCH', - headers: requestHeaders, - body: JSON.stringify(body), - }) - .then((response) => { - if (!response.ok) { - throw new Error(`${response.status} ${response.statusText}`); - } - return response.json(); - }) - .then(() => { - setData((prevData) => { - const [updatedItem] = prevData.filter((item) => item.id === id); - updatedItem.status = newStatus; - return prevData.map((item) => (item.id === id ? updatedItem : item)); - }); - }) - .catch((err) => { - if (err instanceof Error) { - // eslint-disable-next-line no-console - console.error(`Error updating status: ${err.message}`); - if (err.message.includes('404')) { - // eslint-disable-next-line no-console - console.error('The specified blog post ID was not found.'); - } - } else { - // eslint-disable-next-line no-console - console.error('An unexpected error occurred:', err); - } - }); - } catch (err) { - if (err instanceof Error) { - // eslint-disable-next-line no-console - console.error(`Error updating status: ${err.message}`); - } else { - // eslint-disable-next-line no-console - console.error('An unexpected error occurred:', err); - } - } + mutation.mutate({ id, newStatus }); }; const renderActionsDropdown = (item: BlogPost) => { - if (!session || !session.user.role) { - return null; - } - - const userRole = session.user.role as 'admin' | 'content_manager'; - const dropdownItems = [ { id: 'view', label: 'View', onClick: () => handleView(item.id) }, { id: 'edit', label: 'Edit', onClick: () => handleEdit(item.id) }, - userRole === 'admin' + UserRole.admin ? { id: 'publish', label: 'Publish', onClick: () => handleChangeStatus(item.id, 'published'), } : undefined, - userRole === 'content_manager' && item.status === 'draft' + UserRole.content_manager && item.status === 'draft' ? { id: 'in-review', label: 'Move to In Review', onClick: () => handleChangeStatus(item.id, 'in_review'), } : undefined, - ].filter( - (dropdownItem): dropdownItem is { id: string; label: string; onClick: () => void } => - dropdownItem !== undefined - ); + ].filter(Boolean); - return ; + return ( + void }[]} + /> + ); }; return ( diff --git a/middleware.ts b/middleware.ts index 4ef81a39..535e47d5 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getToken } from 'next-auth/jwt'; +import { UserRole } from './Types'; export async function middleware(req: NextRequest) { const token = await getToken({ req }); @@ -24,14 +25,14 @@ export async function middleware(req: NextRequest) { // Content Panel access if (isContentPanelRoute && !isLoginRoute) { - if (userRole !== 'admin' && userRole !== 'content_manager') { + if (userRole !== UserRole.admin && userRole !== UserRole.content_manager) { return NextResponse.redirect(new URL('/unauthorized', req.url)); } } // Admin Panel access if (isAdminPanelRoute && !isLoginRoute) { - if (userRole !== 'admin') { + if (userRole !== UserRole.admin) { return NextResponse.redirect(new URL('/unauthorized', req.url)); } } @@ -40,11 +41,11 @@ export async function middleware(req: NextRequest) { if (isLoginRoute) { if ( path.startsWith('/content-panel/login') && - (userRole === 'admin' || userRole === 'content-manager') + (userRole === UserRole.admin || userRole === UserRole.content_manager) ) { return NextResponse.redirect(new URL('/content-panel', req.url)); } - if (path.startsWith('/admin-panel/login') && userRole === 'admin') { + if (path.startsWith('/admin-panel/login') && userRole === UserRole.admin) { return NextResponse.redirect(new URL('/admin-panel', req.url)); } } From bdd50c2d0f08279feaa34310b5a6f112f9b055f7 Mon Sep 17 00:00:00 2001 From: aleksandarmicev Date: Sat, 21 Dec 2024 18:34:54 +0100 Subject: [PATCH 6/9] Fixing problem with Vercel deployment and next build --- .../module-components/SearchAndFilter/DisplayNames.tsx | 2 +- .../SearchAndFilter/SearchAndFilter.tsx | 3 ++- utils/actions/session.ts | 10 ++++++---- utils/getAuthUrl.ts | 10 +++++----- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/components/module-components/SearchAndFilter/DisplayNames.tsx b/components/module-components/SearchAndFilter/DisplayNames.tsx index 797cf160..abd7ae94 100644 --- a/components/module-components/SearchAndFilter/DisplayNames.tsx +++ b/components/module-components/SearchAndFilter/DisplayNames.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; import fetchSearchResultsFromApi from './SubmitSearchForm'; import style from './displayNames.module.scss'; -import { UserRole } from './Filter'; import Loading from '../../../app/loading'; +import { UserRole } from '../../../Types'; interface Author { id: string; diff --git a/components/module-components/SearchAndFilter/SearchAndFilter.tsx b/components/module-components/SearchAndFilter/SearchAndFilter.tsx index 5ad491f4..f7904fe6 100644 --- a/components/module-components/SearchAndFilter/SearchAndFilter.tsx +++ b/components/module-components/SearchAndFilter/SearchAndFilter.tsx @@ -4,9 +4,10 @@ import React, { useState, useEffect, useRef } from 'react'; import style from './searchAndFilter.module.scss'; -import Filter, { UserRole } from './Filter'; +import Filter from './Filter'; import Search from './Search'; import DisplayNames from './DisplayNames'; +import { UserRole } from '../../../Types'; const SearchAndFilter = () => { const [searchValue, setSearchValue] = useState(''); diff --git a/utils/actions/session.ts b/utils/actions/session.ts index 581d91d6..1697ac6f 100644 --- a/utils/actions/session.ts +++ b/utils/actions/session.ts @@ -1,12 +1,12 @@ 'use server'; import { cookies } from 'next/headers'; -import { Role } from '../../Types'; +import { UserRole } from '../../Types'; import getAuthUrl from '../getAuthUrl'; type Session = { token: string; - role: Role; + role: UserRole; }; type RefreshTokenResponse = { message: string; new_token: string }; @@ -34,7 +34,7 @@ export async function getNewToken({ role, existingToken, }: { - role: Role; + role: UserRole; existingToken: string; }): Promise { try { @@ -50,7 +50,9 @@ export async function getNewToken({ if (!response.ok) throw new Error(response.statusText); const data: RefreshTokenResponse = await response.json(); return data.new_token; - } catch (error: any) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_error: any) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars // console.error({ msg: 'Error from getNewToken', error }); return null; } diff --git a/utils/getAuthUrl.ts b/utils/getAuthUrl.ts index 8b9fd35c..463d37d2 100644 --- a/utils/getAuthUrl.ts +++ b/utils/getAuthUrl.ts @@ -1,9 +1,9 @@ -import { Role } from '../Types'; +import { UserRole } from '../Types'; -export default function getAuthUrl(baseUrl: string, role: Role): string { +export default function getAuthUrl(baseUrl: string, role: UserRole): string { let url = baseUrl; - if (role === 'member') url = `${baseUrl}`; - else if (role === 'content_manager' || role === 'content') url = `${baseUrl}/content`; - else if (role === 'admin') url = `${baseUrl}/admin`; + if (role === UserRole.member) url = `${baseUrl}`; + else if (role === UserRole.content_manager) url = `${baseUrl}/content`; + else if (role === UserRole.admin) url = `${baseUrl}/admin`; return url; } From 9d310e5259d4a47b5ce0eec876259a3f1b6c1a75 Mon Sep 17 00:00:00 2001 From: aleksandarmicev Date: Sat, 21 Dec 2024 18:35:23 +0100 Subject: [PATCH 7/9] vercel deployment fix --- utils/actions/session.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/utils/actions/session.ts b/utils/actions/session.ts index 1697ac6f..1d0b766b 100644 --- a/utils/actions/session.ts +++ b/utils/actions/session.ts @@ -51,8 +51,7 @@ export async function getNewToken({ const data: RefreshTokenResponse = await response.json(); return data.new_token; // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_error: any) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error: any) { // console.error({ msg: 'Error from getNewToken', error }); return null; } From 496cf7a459620b62c2e784eff990a0142d381928 Mon Sep 17 00:00:00 2001 From: aleksandarmicev Date: Tue, 24 Dec 2024 19:02:30 +0100 Subject: [PATCH 8/9] Made structural changes by creating a separate mutation for the blog UpdatePostStatus.ts and integrating it into the PublishArticleForm.tsx --- apis/endpoints.ts | 4 +- apis/mutations/blogs/updatePostStatus.ts | 41 +++++++++++++++++++ apis/mutations/blogs/useAddNewPost.ts | 1 + .../blog/PublishArticleForm.tsx | 15 +++++-- 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 apis/mutations/blogs/updatePostStatus.ts diff --git a/apis/endpoints.ts b/apis/endpoints.ts index 59015f12..0295cdd4 100644 --- a/apis/endpoints.ts +++ b/apis/endpoints.ts @@ -6,8 +6,10 @@ if (!API_BASE_URL) { const ENDPOINTS = { BLOGS: { - GET_ALL: (limit: number, skip: number) => `${API_BASE_URL}/posts?limit=${limit}&skip=${skip}`, + GET_ALL: (limit: number, skip: number) => + `${API_BASE_URL}/blog-posts?limit=${limit}&skip=${skip}`, CREATE: `${API_BASE_URL}/content/blog-posts`, + UPDATE_STATUS: (id: string) => `${API_BASE_URL}/content/blog-posts/${id}/statuses`, }, USERS: { GET_ALL: `${API_BASE_URL}/users`, diff --git a/apis/mutations/blogs/updatePostStatus.ts b/apis/mutations/blogs/updatePostStatus.ts new file mode 100644 index 00000000..3c817d4c --- /dev/null +++ b/apis/mutations/blogs/updatePostStatus.ts @@ -0,0 +1,41 @@ +'use client'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { toast } from 'react-toastify'; +import { useAxios } from '../../AxiosProvider'; +import ENDPOINTS from '../../endpoints'; +import QUERY_KEYS from '../../queryKeys'; + +type UpdatePostStatusPayload = { + id: string; + status: string; +}; + +type ErrorResponse = { + message: string; + statusCode?: number; +}; + +const useUpdatePostStatus = () => { + const queryClient = useQueryClient(); + const axios = useAxios(); + + return useMutation({ + mutationFn: async ({ id, status }: UpdatePostStatusPayload) => { + const response = await axios.patch(ENDPOINTS.BLOGS.UPDATE_STATUS(id), { status }); + return response.data; + }, + onError: (error: AxiosError) => { + toast.error( + error?.response?.data?.message || 'Настана грешка при ажурирање на статусот на статијата.' + ); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.BLOGS.ALL }); + toast.success('Статусот на статијата е успешно ажуриран!'); + }, + }); +}; + +export default useUpdatePostStatus; diff --git a/apis/mutations/blogs/useAddNewPost.ts b/apis/mutations/blogs/useAddNewPost.ts index 9a38666a..22a3443b 100644 --- a/apis/mutations/blogs/useAddNewPost.ts +++ b/apis/mutations/blogs/useAddNewPost.ts @@ -12,6 +12,7 @@ export type NewPost = { excerpt: string; content: string; tags: string[]; + status: string; }; type ErrorResponse = { diff --git a/components/module-components/blog/PublishArticleForm.tsx b/components/module-components/blog/PublishArticleForm.tsx index 605b4374..0ceec301 100644 --- a/components/module-components/blog/PublishArticleForm.tsx +++ b/components/module-components/blog/PublishArticleForm.tsx @@ -10,13 +10,16 @@ import TiptapEditor from '../../editor/TiptapEditor'; import TagManager from './TagManager'; import Button from '../../reusable-components/button/Button'; import { UserRole } from '../../../Types'; +import UpdatePostStatus from '../../../apis/mutations/blogs/updatePostStatus'; interface PublishArticleFormProps { userRole: UserRole; + postId?: string; } -const PublishArticleForm: React.FC = ({ userRole }) => { +const PublishArticleForm: React.FC = ({ userRole, postId }) => { const addNewPostMutation = useAddNewPost(); + const updatePostStatusMutation = UpdatePostStatus(); const [selectedTags, setSelectedTags] = useState([]); const validationSchema = Yup.object({ @@ -33,8 +36,12 @@ const PublishArticleForm: React.FC = ({ userRole }) => .min(1, 'Мора да селектираш барем еден таг.'), }); - const handleAddPost = (values: NewPost) => { - addNewPostMutation.mutate(values); + const handleSubmit = (values: NewPost) => { + if (postId) { + updatePostStatusMutation.mutate({ id: postId, status: values.status }); + } else { + addNewPostMutation.mutate(values); + } }; return ( @@ -47,7 +54,7 @@ const PublishArticleForm: React.FC = ({ userRole }) => tags: [], status: 'draft', }} - onSubmit={handleAddPost} + onSubmit={handleSubmit} > {({ values, setFieldValue, touched, errors }) => (
From be78c9244b86ce4900671fcc6692d1a157fc2b9c Mon Sep 17 00:00:00 2001 From: aleksandarmicev Date: Tue, 24 Dec 2024 19:22:35 +0100 Subject: [PATCH 9/9] Revert changes on BlogListView.tsx --- .../create-blogs/BlogListView.tsx | 100 +++--------------- 1 file changed, 16 insertions(+), 84 deletions(-) diff --git a/components/module-components/create-blogs/BlogListView.tsx b/components/module-components/create-blogs/BlogListView.tsx index 38d8a8bc..5e5c0021 100644 --- a/components/module-components/create-blogs/BlogListView.tsx +++ b/components/module-components/create-blogs/BlogListView.tsx @@ -2,16 +2,11 @@ import React, { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { useSession } from 'next-auth/react'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { toast } from 'react-toastify'; import ReusableTable from '../../reusable-components/reusable-table/ReusableTable'; import BlogManagementControls from './BlogManagementControls'; import ActionDropdown from '../../reusable-components/reusable-table/ActionDropdown'; import style from './createBlogs.module.scss'; import { useEditor } from '../../../app/context/EditorContext'; -import { UserRole } from '../../../Types'; -import ENDPOINTS from '../../../apis/endpoints'; interface Author { first_name: string; @@ -34,88 +29,42 @@ interface BlogPost { title: string; tags: Tag[]; author: string; - status: string; } const BlogListView = () => { const [searchTerm, setSearchTerm] = useState(''); const { editorStateChange } = useEditor(); const [data, setData] = useState([]); - const queryClient = useQueryClient(); + const url = `${process.env.NEXT_PUBLIC_API_BASE_URL}/blog-posts`; const router = useRouter(); - const { data: session } = useSession(); useEffect(() => { const fetchData = async () => { try { - const response = await fetch(ENDPOINTS.BLOGS.GET_ALL(10, 0)); + const response = await fetch(url); if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`); } const result = await response.json(); - - // console.log('Result:', result); - const transformedData: BlogPost[] = result.data.map((item: BlogPostAPI) => ({ id: item.slug, title: item.title, tags: item.tags, author: `${item.author.first_name} ${item.author.last_name}`, - status: 'draft', })); - setData(transformedData); } catch (error) { - // eslint-disable-next-line no-console - console.error(`Error fetching data: ${error}`); + throw new Error(`Error fetching data: ${error}`); } }; fetchData(); - }, []); + }, [url]); - const mutation = useMutation({ - mutationFn: async ({ id, newStatus }: { id: string; newStatus: string }) => { - const changeStatusUrl = new URL(`${ENDPOINTS.BLOGS.CREATE}/${id}/statuses`); - const body = { - status: newStatus, - publish_date: - newStatus === 'published' ? new Date().toISOString().split('T')[0] : undefined, - }; - - const response = await fetch(changeStatusUrl.toString(), { - method: 'PATCH', - headers: { - Authorization: `Bearer ${session?.accessToken}`, - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.message || 'Failed to update blog status'); - } - return { id, newStatus }; - }, - onError: (error: any) => { - toast.error(error.message || 'Failed to update blog status'); - }, - onSuccess: ({ id, newStatus }: { id: string; newStatus: string }) => { - toast.success('Blog status updated successfully'); - setData((prevData) => - prevData.map((item) => (item.id === id ? { ...item, status: newStatus } : item)) - ); - queryClient.invalidateQueries({ queryKey: ['blog-posts'] }); - }, - }); - - const headers: (keyof BlogPost)[] = ['title', 'author', 'status']; + const headers: (keyof BlogPost)[] = ['title', 'author']; const displayNames = { title: 'Title', author: 'Author', - status: 'Status', }; const handleView = (id: string) => { @@ -128,36 +77,19 @@ const BlogListView = () => { router.push(`/content-panel/blogs/${id}`); }; - const handleChangeStatus = async (id: string, newStatus: string) => { - mutation.mutate({ id, newStatus }); + const handleDelete = () => { + // delete logic here }; - const renderActionsDropdown = (item: BlogPost) => { - const dropdownItems = [ - { id: 'view', label: 'View', onClick: () => handleView(item.id) }, - { id: 'edit', label: 'Edit', onClick: () => handleEdit(item.id) }, - UserRole.admin - ? { - id: 'publish', - label: 'Publish', - onClick: () => handleChangeStatus(item.id, 'published'), - } - : undefined, - UserRole.content_manager && item.status === 'draft' - ? { - id: 'in-review', - label: 'Move to In Review', - onClick: () => handleChangeStatus(item.id, 'in_review'), - } - : undefined, - ].filter(Boolean); - - return ( - void }[]} - /> - ); - }; + const renderActionsDropdown = (item: BlogPost) => ( + handleView(item.id) }, + { id: 'edit', label: 'Edit', onClick: () => handleEdit(item.id) }, + { id: 'delete', label: 'Delete', onClick: () => handleDelete() }, + ]} + /> + ); return (